Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 15 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export interface CreateSessionOnMissing<TCtx = unknown> {
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 };
Expand Down Expand Up @@ -569,9 +576,10 @@ export function createAgentScoreCore(options: AgentScoreCoreOptions): AgentScore
async function tryMintSessionDenial(ctx: unknown): Promise<DenialReason | undefined> {
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 {
Expand All @@ -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<string, unknown>;

// Validate required fields before trusting the response. A misbehaving (or mocked-wrong)
Expand Down Expand Up @@ -629,6 +638,11 @@ export function createAgentScoreCore(options: AgentScoreCoreOptions): AgentScore
const apiNextSteps = data.next_steps as Record<string, unknown> | 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,
Expand Down
26 changes: 26 additions & 0 deletions tests/express.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof vi.fn>).mock.calls[0];
expect(JSON.parse(fetchCall[1].body as string)).toEqual({ kind: 'sign_in' });
expect(status).toHaveBeenCalledWith(403);
const bodyArg = (json as ReturnType<typeof vi.fn>).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,
Expand Down