diff --git a/README.md b/README.md index 4d9cb7c..37722a0 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ const _gate = agentscoreGate({ minAge: 21, allowedJurisdictions: ["US"], createSessionOnMissing: { apiKey: process.env.AGENTSCORE_API_KEY!, context: "wine-purchase" }, + // With an EMPTY policy (no KYC), add kind: "sign_in" so the minted session is a plain + // account sign-in (no identity documents) that still yields an operator token. }); // Run the gate CONDITIONALLY: only when a payment credential is already attached. diff --git a/bun.lock b/bun.lock index 7a3b0cf..fa1e314 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@agent-score/commerce", "dependencies": { - "@agent-score/sdk": "^2.7.9", + "@agent-score/sdk": "^2.7.10", }, "devDependencies": { "@a2a-js/sdk": "^1.1.0", @@ -69,7 +69,7 @@ "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], - "@agent-score/sdk": ["@agent-score/sdk@2.7.9", "", {}, "sha512-KA8zET+D0GXhkorFT6YaBAGyrE0AQF/7X8sNtllrMBC6w2g6bkpts0CZSd38QmBJYadWdbia7ZJu7mHB8hYmqg=="], + "@agent-score/sdk": ["@agent-score/sdk@2.7.10", "", {}, "sha512-QnDBdH3lqGP1JLc8AaTlXFgBNs+F5WiD+GBwOgKRaxBnojUWv0oLr3ekBDrAR2FF8O8yVQmC9bvM07uX0dht+Q=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], diff --git a/package.json b/package.json index 1bc84db..12fa942 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-score/commerce", - "version": "2.11.1", + "version": "2.12.0", "description": "Agentic commerce SDK: identity middleware (Hono, Express, Fastify, Next.js, Web Fetch) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agentic commerce.", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -148,7 +148,7 @@ "bun": ">=1.3.0 <2" }, "dependencies": { - "@agent-score/sdk": "^2.7.9" + "@agent-score/sdk": "^2.7.10" }, "overrides": { "axios": "^1.18.0", diff --git a/src/core.ts b/src/core.ts index 1eb75d3..b2ae4c8 100644 --- a/src/core.ts +++ b/src/core.ts @@ -74,6 +74,13 @@ export interface CreateSessionOnMissing { baseUrl?: string; context?: string; productName?: string; + /** Session kind sent to `POST /v1/sessions`. `'kyc'` (the API default) runs identity + * verification; `'sign_in'` is registration-only (the buyer signs in with an AgentScore + * account, no identity documents) and mints a `sign_in`-scoped credential. Use it when the + * gate runs with an EMPTY compliance policy and only needs an account to key state on + * (a prepaid balance, say): a KYC session there asks the buyer for documents nothing + * will ever check. The denial's default `error.message` follows the kind. */ + kind?: 'kyc' | 'sign_in'; /** Per-request override of `context` / `productName`. Invoked with the framework context. */ getSessionOptions?: (ctx: TCtx) => Promise<{ context?: string; productName?: string }> | { context?: string; productName?: string }; @@ -569,9 +576,10 @@ export function createAgentScoreCore(options: AgentScoreCoreOptions): AgentScore async function tryMintSessionDenial(ctx: unknown): Promise { if (!createSessionOnMissing) return undefined; try { - const sessionBody: { context?: string; product_name?: string } = {}; + const sessionBody: { context?: string; product_name?: string; kind?: 'kyc' | 'sign_in' } = {}; if (createSessionOnMissing.context != null) sessionBody.context = createSessionOnMissing.context; if (createSessionOnMissing.productName != null) sessionBody.product_name = createSessionOnMissing.productName; + if (createSessionOnMissing.kind != null) sessionBody.kind = createSessionOnMissing.kind; if (createSessionOnMissing.getSessionOptions && ctx !== undefined) { try { @@ -589,6 +597,7 @@ export function createAgentScoreCore(options: AgentScoreCoreOptions): AgentScore const data = (await sessionSdk.createSession({ ...(sessionBody.context !== undefined ? { context: sessionBody.context } : {}), ...(sessionBody.product_name !== undefined ? { product_name: sessionBody.product_name } : {}), + ...(sessionBody.kind !== undefined ? { kind: sessionBody.kind } : {}), })) as unknown as Record; // Validate required fields before trusting the response. A misbehaving (or mocked-wrong) @@ -629,6 +638,11 @@ export function createAgentScoreCore(options: AgentScoreCoreOptions): AgentScore const apiNextSteps = data.next_steps as Record | undefined; return { code: 'identity_verification_required', + // The per-code default message talks about KYC, which a sign_in session never runs; + // say what this session actually asks for so a merchant's default 403 is not a lie. + ...(sessionBody.kind === 'sign_in' && { + message: 'Sign-in is required to access this resource. Visit verify_url to sign in with an AgentScore account (no identity documents), then poll poll_url for the operator token and retry.', + }), verify_url: data.verify_url as string, session_id: data.session_id as string, poll_secret: data.poll_secret as string, diff --git a/tests/express.test.ts b/tests/express.test.ts index 6250af1..373bd40 100644 --- a/tests/express.test.ts +++ b/tests/express.test.ts @@ -1416,6 +1416,32 @@ describe('agentscoreGate middleware — createSessionOnMissing', () => { }); }); + it('sends kind in the POST body and swaps the KYC default message for the sign_in one', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValueOnce(SESSION_RESPONSE), + } as unknown as globalThis.Response); + + const mw = agentscoreGate({ + apiKey: API_KEY, + createSessionOnMissing: { apiKey: 'ask_session_key', kind: 'sign_in' }, + }); + const req = makeReq(); + const { res, status, json } = makeRes(); + const next = makeNext(); + + await mw(req, res, next); + + const fetchCall = (global.fetch as ReturnType).mock.calls[0]; + expect(JSON.parse(fetchCall[1].body as string)).toEqual({ kind: 'sign_in' }); + expect(status).toHaveBeenCalledWith(403); + const bodyArg = (json as ReturnType).mock.calls[0]![0] as { error: { code: string; message: string } }; + expect(bodyArg.error.code).toBe('identity_verification_required'); + expect(bodyArg.error.message).toContain('sign in with an AgentScore account'); + expect(bodyArg.error.message).not.toContain('KYC'); + }); + it('uses custom baseUrl for session creation', async () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: true,