From 6e252bb0a0f4c5700b989b43a0f3400bd76099e2 Mon Sep 17 00:00:00 2001 From: Nas Kavian Date: Sat, 8 Aug 2026 01:28:16 -0700 Subject: [PATCH 1/2] fix(sdk): normalize unscoped grant responses --- .changeset/calm-scopes-normalize.md | 6 +++ packages/agent/test/index.test.ts | 26 +++++++++++++ .../api-key-grant-response.schema.json | 4 +- .../schemas/basic-grant-response.schema.json | 4 +- .../oauth-bearer-grant-response.schema.json | 4 +- .../credentials/api-key/grant-response.json | 7 ++-- .../credentials/basic/grant-response.json | 6 +-- packages/core/src/protocol.ts | 39 ++++++++++++++++--- packages/core/test/index.test.ts | 17 ++++++-- 9 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 .changeset/calm-scopes-normalize.md diff --git a/.changeset/calm-scopes-normalize.md b/.changeset/calm-scopes-normalize.md new file mode 100644 index 0000000..acead18 --- /dev/null +++ b/.changeset/calm-scopes-normalize.md @@ -0,0 +1,6 @@ +--- +"@aep-foundation/agent": patch +"@aep-foundation/core": minor +--- + +Accept missing and null Grant response scopes and normalize them to an empty array. diff --git a/packages/agent/test/index.test.ts b/packages/agent/test/index.test.ts index 2b3ae38..1e1a132 100644 --- a/packages/agent/test/index.test.ts +++ b/packages/agent/test/index.test.ts @@ -844,6 +844,32 @@ describe("@aep-foundation/agent command clients", () => { }); }); + it("normalizes null API-key Grant scopes", async () => { + const result = await withFetch( + () => + jsonResponsePromise({ + api_key: "api-key", + credential_id: "cred_123", + expires_at: "2026-05-28T12:00:00Z", + header: "X-API-Key", + scopes: null + }), + () => + grantService({ + clientAssertion: "jwt.grant", + grantType: "api-key", + idempotencyKey: "9f8a4d2e-1c3b-4f5e-8b7a-grant0000001", + inspect: inspectResult(), + serviceUrl: "https://api.example.com" + }) + ); + + expect(result.body).toMatchObject({ + header: "X-API-Key", + scopes: [] + }); + }); + it("posts Revoke requests using exactly one selector", async () => { const calls: Array<{ input: URL | string; init?: RequestInit }> = []; const fetch = (input: URL | string, init?: RequestInit) => { diff --git a/packages/conformance/fixtures/aep-specs/schemas/api-key-grant-response.schema.json b/packages/conformance/fixtures/aep-specs/schemas/api-key-grant-response.schema.json index 6947b18..1b61f5e 100644 --- a/packages/conformance/fixtures/aep-specs/schemas/api-key-grant-response.schema.json +++ b/packages/conformance/fixtures/aep-specs/schemas/api-key-grant-response.schema.json @@ -3,7 +3,7 @@ "$id": "https://www.aep.foundation/schemas/api-key-grant-response.schema.json", "title": "AEP API-key Grant Response", "type": "object", - "required": ["api_key", "credential_id", "expires_at", "header", "scopes"], + "required": ["api_key", "credential_id", "expires_at", "header"], "additionalProperties": true, "properties": { "api_key": { @@ -23,7 +23,7 @@ "minLength": 1 }, "scopes": { - "type": "array", + "type": ["array", "null"], "items": { "type": "string" } diff --git a/packages/conformance/fixtures/aep-specs/schemas/basic-grant-response.schema.json b/packages/conformance/fixtures/aep-specs/schemas/basic-grant-response.schema.json index e8724bc..77dff5e 100644 --- a/packages/conformance/fixtures/aep-specs/schemas/basic-grant-response.schema.json +++ b/packages/conformance/fixtures/aep-specs/schemas/basic-grant-response.schema.json @@ -3,7 +3,7 @@ "$id": "https://www.aep.foundation/schemas/basic-grant-response.schema.json", "title": "AEP Basic Grant Response", "type": "object", - "required": ["credential_id", "expires_at", "password", "scopes", "username"], + "required": ["credential_id", "expires_at", "password", "username"], "additionalProperties": true, "properties": { "credential_id": { @@ -23,7 +23,7 @@ "minLength": 1 }, "scopes": { - "type": "array", + "type": ["array", "null"], "items": { "type": "string" } diff --git a/packages/conformance/fixtures/aep-specs/schemas/oauth-bearer-grant-response.schema.json b/packages/conformance/fixtures/aep-specs/schemas/oauth-bearer-grant-response.schema.json index df1d450..8d4a752 100644 --- a/packages/conformance/fixtures/aep-specs/schemas/oauth-bearer-grant-response.schema.json +++ b/packages/conformance/fixtures/aep-specs/schemas/oauth-bearer-grant-response.schema.json @@ -3,7 +3,7 @@ "$id": "https://www.aep.foundation/schemas/oauth-bearer-grant-response.schema.json", "title": "AEP OAuth Bearer Grant Response", "type": "object", - "required": ["access_token", "credential_id", "expires_at", "scopes", "token_type"], + "required": ["access_token", "credential_id", "expires_at", "token_type"], "additionalProperties": true, "properties": { "access_token": { @@ -19,7 +19,7 @@ "format": "date-time" }, "scopes": { - "type": "array", + "type": ["array", "null"], "items": { "type": "string" } diff --git a/packages/conformance/fixtures/aep-specs/test-vectors/credentials/api-key/grant-response.json b/packages/conformance/fixtures/aep-specs/test-vectors/credentials/api-key/grant-response.json index 273a60d..58ec2cf 100644 --- a/packages/conformance/fixtures/aep-specs/test-vectors/credentials/api-key/grant-response.json +++ b/packages/conformance/fixtures/aep-specs/test-vectors/credentials/api-key/grant-response.json @@ -1,20 +1,19 @@ { "id": "grant-response", "title": "API-key Grant response", - "description": "A successful API-key session-credential response.", + "description": "A successful API-key session-credential response with no scope-limited authorization.", "drafts": ["draft-kavian-aep-api-key-session-credential-02"], "category": "credentials/api-key", "applies_to": ["agent", "service"], "profile": "api-key", "input": { - "grant_type": "api-key", - "requested_scopes": ["read"] + "grant_type": "api-key" }, "expected": { "api_key": "aep_live_7Jm5Example", "credential_id": "key_01HZY8W7Q2F8J7D3P9G9Z1N6TT", "expires_at": "2026-12-01T00:00:00Z", "header": "x-api-key", - "scopes": ["read"] + "scopes": null } } diff --git a/packages/conformance/fixtures/aep-specs/test-vectors/credentials/basic/grant-response.json b/packages/conformance/fixtures/aep-specs/test-vectors/credentials/basic/grant-response.json index d162f99..f31da9d 100644 --- a/packages/conformance/fixtures/aep-specs/test-vectors/credentials/basic/grant-response.json +++ b/packages/conformance/fixtures/aep-specs/test-vectors/credentials/basic/grant-response.json @@ -1,21 +1,19 @@ { "id": "grant-response", "title": "Basic Grant response", - "description": "A successful HTTP Basic session-credential response.", + "description": "A successful HTTP Basic session-credential response that omits scope-limited authorization.", "drafts": ["draft-kavian-aep-basic-session-credential-02"], "category": "credentials/basic", "applies_to": ["agent", "service"], "profile": "basic", "input": { - "grant_type": "basic", - "requested_scopes": ["read"] + "grant_type": "basic" }, "expected": { "credential_id": "bas_01HZY8W7Q2F8J7D3P9G9Z1N6TT", "expires_at": "2026-12-01T00:00:00Z", "password": "s3cr3tExample", "realm": "api.example.com", - "scopes": ["read"], "username": "aep_agent_abc123" } } diff --git a/packages/core/src/protocol.ts b/packages/core/src/protocol.ts index ff768af..1147399 100644 --- a/packages/core/src/protocol.ts +++ b/packages/core/src/protocol.ts @@ -236,9 +236,12 @@ export function validateOAuthBearerGrantResponse( requireString(value, "credential_id", issues, { minLength: 1 }); requireString(value, "expires_at", issues, { minLength: 1 }); requireDateTime(value["expires_at"], "$.expires_at", issues); - requireStringArray(value["scopes"], "$.scopes", issues); + optionalNullableStringArray(value["scopes"], "$.scopes", issues); requireString(value, "token_type", issues, { allowedValues: new Set(["Bearer"]) }); - return result(value as OAuthBearerGrantResponse, issues); + return result( + { ...(value as OAuthBearerGrantResponse), scopes: normalizedScopes(value["scopes"]) }, + issues + ); } export function validateApiKeyGrantResponse(value: unknown): ValidationResult { @@ -252,8 +255,11 @@ export function validateApiKeyGrantResponse(value: unknown): ValidationResult { @@ -267,9 +273,12 @@ export function validateBasicGrantResponse(value: unknown): ValidationResult typeof scope === "string") + ? [...value] + : []; +} + function optionalNonEmptyStringArray( value: unknown, path: string, diff --git a/packages/core/test/index.test.ts b/packages/core/test/index.test.ts index 2c84a76..6cc7885 100644 --- a/packages/core/test/index.test.ts +++ b/packages/core/test/index.test.ts @@ -627,10 +627,11 @@ describe("Protocol message validation", () => { credential_id: "cred_123", expires_at: "2026-05-28T12:00:00Z", header: "X-API-Key", - scopes: [] + scopes: null }) ).toMatchObject({ - header: "X-API-Key" + header: "X-API-Key", + scopes: [] }); expect( @@ -638,13 +639,23 @@ describe("Protocol message validation", () => { credential_id: "cred_123", expires_at: "2026-05-28T12:00:00Z", password: "password", - scopes: [], username: "username" }) ).toMatchObject({ + scopes: [], username: "username" }); + expect(() => + parseBuiltInGrantResponse(AEP_GRANT_TYPE_API_KEY, { + api_key: "api-key", + credential_id: "cred_123", + expires_at: "2026-05-28T12:00:00Z", + header: "X-API-Key", + scopes: [null] + }) + ).toThrow(AepValidationError); + expect(() => parseBuiltInGrantResponse("custom", {})).toThrow(AepValidationError); }); }); From 94f8da4080e3a1c2271893c4bdad802b58d96aea Mon Sep 17 00:00:00 2001 From: Nas Kavian Date: Sat, 8 Aug 2026 01:28:32 -0700 Subject: [PATCH 2/2] test: simplify Vitest output --- packages/adapters/express/vitest.config.ts | 2 ++ packages/adapters/fastify/vitest.config.ts | 2 ++ packages/adapters/hono/vitest.config.ts | 2 ++ packages/adapters/next/vitest.config.ts | 2 ++ packages/agent/vitest.config.ts | 2 ++ packages/conformance/vitest.config.ts | 2 ++ packages/core/vitest.config.ts | 2 ++ packages/extensions/service-policy/vitest.config.ts | 2 ++ packages/platform/vitest.config.ts | 2 ++ packages/service/vitest.config.ts | 2 ++ 10 files changed, 20 insertions(+) diff --git a/packages/adapters/express/vitest.config.ts b/packages/adapters/express/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/adapters/express/vitest.config.ts +++ b/packages/adapters/express/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/adapters/fastify/vitest.config.ts b/packages/adapters/fastify/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/adapters/fastify/vitest.config.ts +++ b/packages/adapters/fastify/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/adapters/hono/vitest.config.ts b/packages/adapters/hono/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/adapters/hono/vitest.config.ts +++ b/packages/adapters/hono/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/adapters/next/vitest.config.ts b/packages/adapters/next/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/adapters/next/vitest.config.ts +++ b/packages/adapters/next/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/conformance/vitest.config.ts b/packages/conformance/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/conformance/vitest.config.ts +++ b/packages/conformance/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/extensions/service-policy/vitest.config.ts b/packages/extensions/service-policy/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/extensions/service-policy/vitest.config.ts +++ b/packages/extensions/service-policy/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/platform/vitest.config.ts b/packages/platform/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/platform/vitest.config.ts +++ b/packages/platform/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] diff --git a/packages/service/vitest.config.ts b/packages/service/vitest.config.ts index eec6de2..ddc4f5c 100644 --- a/packages/service/vitest.config.ts +++ b/packages/service/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + reporters: ["basic"], + silent: true, coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"]