@astrasyncai/verification-gateway 5.18.1 → 5.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +232 -0
- package/README.md +217 -4
- package/dist/adapter-interface/interface.d.mts +2 -2
- package/dist/adapter-interface/interface.d.ts +2 -2
- package/dist/adapters/express.d.mts +9 -1
- package/dist/adapters/express.d.ts +9 -1
- package/dist/adapters/express.js +160 -52
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +160 -52
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/http-pdlss.d.mts +7 -1
- package/dist/adapters/http-pdlss.d.ts +7 -1
- package/dist/adapters/http-pdlss.js +18 -2
- package/dist/adapters/http-pdlss.js.map +1 -1
- package/dist/adapters/http-pdlss.mjs +16 -1
- package/dist/adapters/http-pdlss.mjs.map +1 -1
- package/dist/adapters/kit-express.d.mts +93 -1
- package/dist/adapters/kit-express.d.ts +93 -1
- package/dist/adapters/kit-express.js +181 -2
- package/dist/adapters/kit-express.js.map +1 -1
- package/dist/adapters/kit-express.mjs +180 -2
- package/dist/adapters/kit-express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +102 -9
- package/dist/adapters/mcp.d.ts +102 -9
- package/dist/adapters/mcp.js +242 -19
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +240 -19
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +1 -1
- package/dist/adapters/nextjs.d.ts +1 -1
- package/dist/adapters/nextjs.js +97 -14
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +97 -14
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +101 -2
- package/dist/adapters/sdk.d.ts +101 -2
- package/dist/adapters/sdk.js +199 -12
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +199 -12
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.d.mts +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +94 -16
- package/dist/bin/astrasync-codex-hook.js +94 -16
- package/dist/bin/astrasync-guard.js +94 -16
- package/dist/bin/astrasync.js +127 -16
- package/dist/browser/background.js +94 -16
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +94 -16
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.js +94 -16
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +94 -16
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +94 -16
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +94 -16
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +1 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +1 -1
- package/dist/edge-core/index.d.ts +1 -1
- package/dist/edge-core/index.js +69 -12
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +69 -12
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.d.mts +2 -2
- package/dist/gateway/gateway.d.ts +2 -2
- package/dist/gateway/gateway.js +94 -16
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +94 -16
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +1 -1
- package/dist/git-trigger/git-hooks.d.ts +1 -1
- package/dist/index.d.mts +706 -34
- package/dist/index.d.ts +706 -34
- package/dist/index.js +1391 -205
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1374 -195
- package/dist/index.mjs.map +1 -1
- package/dist/local-evaluator/evaluator.d.mts +2 -2
- package/dist/local-evaluator/evaluator.d.ts +2 -2
- package/dist/registration/index.d.mts +11 -2
- package/dist/registration/index.d.ts +11 -2
- package/dist/registration/index.js +1 -1
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/transport/index.d.mts +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.js +3 -2
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +3 -2
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
- package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
- package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
- package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
- package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
- package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
- package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
- package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +50 -4
- package/dist/verify.d.ts +50 -4
- package/dist/verify.js +93 -14
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +91 -14
- package/dist/verify.mjs.map +1 -1
- package/dist/webhooks.d.mts +16 -3
- package/dist/webhooks.d.ts +16 -3
- package/dist/webhooks.js +24 -13
- package/dist/webhooks.js.map +1 -1
- package/dist/webhooks.mjs +24 -13
- package/dist/webhooks.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/kit-express.ts","../../src/ucp/kit-shared.ts"],"sourcesContent":["/**\n * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,\n * `createAcpCheckoutKit`) on Express.\n *\n * app.use('/ucp/v1', express.raw({ type: '*\\/*' }), expressHandler(ucpKit));\n *\n * The kit checks RFC 9421 signatures and Content-Digest / idempotency\n * fingerprints over the EXACT request bytes, so give it the raw body: mount\n * `express.raw()` on the kit's routes (req.body is a Buffer), or keep a copy\n * on `req.rawBody` (Buffer or string) in your JSON parser's `verify` hook.\n * A body that was already parsed to an object is re-serialised with\n * JSON.stringify — fine for unsigned traffic, but signatures won't verify.\n *\n * When the AstraSync middleware ran first, the caller's verified ASTRA-id\n * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency\n * keys; override with `callerId`.\n */\nimport type { KitRequestContext } from '../ucp/kit-shared';\nimport { warnOnce } from '../ucp/kit-shared';\n\n/** Anything with a fetch-style `handle` (both checkout kits). */\nexport interface FetchKit {\n handle(req: Request, ctx?: KitRequestContext): Promise<Response>;\n}\n\n/** The parts of an Express request this adapter reads. */\nexport interface KitExpressRequest {\n method: string;\n protocol?: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n rawBody?: unknown;\n get?(name: string): string | undefined;\n agentVerification?: {\n identityVerified?: boolean;\n agent?: { astraId?: string };\n };\n}\n\n/** The parts of an Express response this adapter writes. */\nexport interface KitExpressResponse {\n status(code: number): unknown;\n setHeader(name: string, value: string | string[]): unknown;\n end(body?: unknown): unknown;\n}\n\nexport interface ExpressHandlerOptions {\n /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */\n callerId?: (req: KitExpressRequest) => string | undefined;\n}\n\nconst bytes = (b: Buffer): ArrayBuffer =>\n b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer;\n\nfunction rawBodyOf(req: KitExpressRequest): string | ArrayBuffer | undefined {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined;\n const raw = req.rawBody;\n if (Buffer.isBuffer(raw)) return bytes(raw);\n if (typeof raw === 'string') return raw;\n if (Buffer.isBuffer(req.body)) return bytes(req.body);\n if (typeof req.body === 'string') return req.body;\n if (req.body !== undefined && req.body !== null && typeof req.body === 'object') {\n if (Object.keys(req.body as object).length === 0) return undefined;\n warnOnce(\n 'kit-express-parsed-body',\n '[astrasync] expressHandler: the request body was already parsed — re-serialising it. ' +\n 'Mount express.raw() on the checkout routes (or keep req.rawBody) so signatures and ' +\n 'idempotency fingerprints see the exact bytes.'\n );\n return JSON.stringify(req.body);\n }\n return undefined;\n}\n\n/** An Express handler that runs a checkout kit and writes its Response back. */\nexport function expressHandler(kit: FetchKit, options: ExpressHandlerOptions = {}) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n const host = req.get?.('host') ?? (req.headers.host as string | undefined) ?? 'localhost';\n const url = `${req.protocol ?? 'http'}://${host}${req.originalUrl ?? req.url}`;\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));\n else headers.set(k, v);\n }\n const body = rawBodyOf(req);\n const request = new Request(url, {\n method: req.method,\n headers,\n ...(body !== undefined && { body }),\n });\n const verified = req.agentVerification?.identityVerified\n ? req.agentVerification.agent?.astraId\n : undefined;\n const callerId = options.callerId ? options.callerId(req) : verified;\n const response = await kit.handle(request, callerId ? { callerId } : undefined);\n res.status(response.status);\n response.headers.forEach((value, name) => {\n res.setHeader(name, value);\n });\n res.end(Buffer.from(await response.arrayBuffer()));\n } catch (err) {\n next(err);\n }\n };\n}\n","/**\n * 5.18.0 — logic the UCP and ACP checkout kits share: the hold test, caller\n * scoping, the result kept for a pending settlement, and one-time warnings.\n */\nimport { createHash } from 'crypto';\nimport type { VerificationResult } from '../types';\n\n/** Per-request context a host adapter (e.g. `expressHandler`) passes to `kit.handle`. */\nexport interface KitRequestContext {\n /**\n * The caller's VERIFIED ASTRA-id (e.g. from `req.agentVerification` after\n * the AstraSync middleware ran). Sessions and idempotency keys are scoped\n * to it; without it the kit falls back to Authorization, then X-Astra-Id.\n */\n callerId?: string;\n}\n\n/**\n * True when AstraSync held the purchase for the buyer's approval. Any step-up\n * signal counts; an approval that was just redeemed (`status: 'consumed'`)\n * does not. A held purchase must never reach settle or fulfil.\n */\nexport function isHeld(result: VerificationResult): boolean {\n const r = result as VerificationResult & { recommendation?: string };\n return Boolean(\n r.requiresStepUp ||\n r.requiresApproval ||\n r.recommendation === 'step_up_required' ||\n r.settlementOutcome?.status === 'requires_approval' ||\n (r.stepUpApproval && r.stepUpApproval.status !== 'consumed')\n );\n}\n\nconst scopeHash = (kind: string, value: string) =>\n `${kind}:${createHash('sha256').update(`${kind}\\n${value}`).digest('hex').slice(0, 32)}`;\n\n/**\n * The scope a checkout session and its idempotency keys belong to: the\n * verified ASTRA-id when known, else the Authorization header, else the\n * X-Astra-Id header. Hashed, so no bearer token is ever written to a store.\n * Undefined when the request carries none of them.\n */\nexport async function resolveCallerScope(\n req: Request,\n ctx: KitRequestContext | undefined,\n resolve?: (req: Request) => string | undefined | Promise<string | undefined>\n): Promise<string | undefined> {\n const verified = ctx?.callerId ?? (resolve ? await resolve(req) : undefined);\n if (verified) return scopeHash('astra', verified);\n const auth = req.headers.get('authorization');\n if (auth) return scopeHash('auth', auth);\n const astraId = req.headers.get('x-astra-id');\n if (astraId) return scopeHash('astra-header', astraId);\n return undefined;\n}\n\n/** A scope for a fallback identity (e.g. the UCP platform profile). */\nexport const fallbackScope = (kind: string, value: string) => scopeHash(kind, value);\n\n/**\n * The verification result kept with a pending settlement, minus the\n * merchant-lane secrets (card settlement token, voucher, mandates, KYAPay\n * token, AEP payloads): a store must never hold payment material.\n */\nexport function pendingResult(result: VerificationResult): VerificationResult {\n const {\n settlementToken: _t,\n settlement: _s,\n mandates: _m,\n kyaToken: _k,\n aep: _a,\n ...rest\n } = result;\n return rest;\n}\n\nconst warned = new Set<string>();\n\n/** console.warn once per process per key (never under NODE_ENV=test). */\nexport function warnOnce(key: string, message: string): void {\n if (warned.has(key) || process.env.NODE_ENV === 'test') return;\n warned.add(key);\n // eslint-disable-next-line no-console\n console.warn(message);\n}\n\n/** Warn when a kit runs on its in-memory defaults outside tests. */\nexport function warnInMemoryStores(kit: 'UCP' | 'ACP', missing: string[]): void {\n if (!missing.length || process.env.NODE_ENV === 'test') return;\n // eslint-disable-next-line no-console\n console.warn(\n `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(' and ')} ` +\n '(sessions are lost on restart and not shared across instances). ' +\n `Pass ${missing.map((m) => (m.startsWith('idempotency') ? 'idempotencyStore' : 'store')).join(' and ')} in production.`\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC4EA,IAAM,SAAS,oBAAI,IAAY;AAGxB,SAAS,SAAS,KAAa,SAAuB;AAC3D,MAAI,OAAO,IAAI,GAAG,KAAK,QAAQ,IAAI,aAAa,OAAQ;AACxD,SAAO,IAAI,GAAG;AAEd,UAAQ,KAAK,OAAO;AACtB;;;AD/BA,IAAM,QAAQ,CAAC,MACb,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU;AAE1D,SAAS,UAAU,KAA0D;AAC3E,MAAI,IAAI,WAAW,SAAS,IAAI,WAAW,OAAQ,QAAO;AAC1D,QAAM,MAAM,IAAI;AAChB,MAAI,OAAO,SAAS,GAAG,EAAG,QAAO,MAAM,GAAG;AAC1C,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,SAAS,IAAI,IAAI,EAAG,QAAO,MAAM,IAAI,IAAI;AACpD,MAAI,OAAO,IAAI,SAAS,SAAU,QAAO,IAAI;AAC7C,MAAI,IAAI,SAAS,UAAa,IAAI,SAAS,QAAQ,OAAO,IAAI,SAAS,UAAU;AAC/E,QAAI,OAAO,KAAK,IAAI,IAAc,EAAE,WAAW,EAAG,QAAO;AACzD;AAAA,MACE;AAAA,MACA;AAAA,IAGF;AACA,WAAO,KAAK,UAAU,IAAI,IAAI;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,eAAe,KAAe,UAAiC,CAAC,GAAG;AACjF,SAAO,OACL,KACA,KACA,SACkB;AAClB,QAAI;AACF,YAAM,OAAO,IAAI,MAAM,MAAM,KAAM,IAAI,QAAQ,QAA+B;AAC9E,YAAM,MAAM,GAAG,IAAI,YAAY,MAAM,MAAM,IAAI,GAAG,IAAI,eAAe,IAAI,GAAG;AAC5E,YAAM,UAAU,IAAI,QAAQ;AAC5B,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,OAAO,GAAG;AAChD,YAAI,MAAM,OAAW;AACrB,YAAI,MAAM,QAAQ,CAAC,EAAG,GAAE,QAAQ,CAAC,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC;AAAA,YACtD,SAAQ,IAAI,GAAG,CAAC;AAAA,MACvB;AACA,YAAM,OAAO,UAAU,GAAG;AAC1B,YAAM,UAAU,IAAI,QAAQ,KAAK;AAAA,QAC/B,QAAQ,IAAI;AAAA,QACZ;AAAA,QACA,GAAI,SAAS,UAAa,EAAE,KAAK;AAAA,MACnC,CAAC;AACD,YAAM,WAAW,IAAI,mBAAmB,mBACpC,IAAI,kBAAkB,OAAO,UAC7B;AACJ,YAAM,WAAW,QAAQ,WAAW,QAAQ,SAAS,GAAG,IAAI;AAC5D,YAAM,WAAW,MAAM,IAAI,OAAO,SAAS,WAAW,EAAE,SAAS,IAAI,MAAS;AAC9E,UAAI,OAAO,SAAS,MAAM;AAC1B,eAAS,QAAQ,QAAQ,CAAC,OAAO,SAAS;AACxC,YAAI,UAAU,MAAM,KAAK;AAAA,MAC3B,CAAC;AACD,UAAI,IAAI,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IACnD,SAAS,KAAK;AACZ,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/kit-express.ts","../../src/http-signatures/sign.ts","../../src/ucp/kit-shared.ts","../../src/webhooks.ts","../../src/ucp/approval-events.ts"],"sourcesContent":["/**\n * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,\n * `createAcpCheckoutKit`) on Express.\n *\n * app.use('/ucp/v1', express.raw({ type: '*\\/*' }), expressHandler(ucpKit));\n *\n * The kit checks RFC 9421 signatures and Content-Digest / idempotency\n * fingerprints over the EXACT request bytes, so give it the raw body: mount\n * `express.raw()` on the kit's routes (req.body is a Buffer), or keep a copy\n * on `req.rawBody` (Buffer or string) in your JSON parser's `verify` hook.\n * A body that was already parsed to an object is re-serialised with\n * JSON.stringify — fine for unsigned traffic, but signatures won't verify.\n *\n * When the AstraSync middleware ran first, the caller's verified ASTRA-id\n * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency\n * keys; override with `callerId`.\n *\n * 5.19.0:\n * - the request URL the kit sees uses the kit's `publicOrigin` (UCP default:\n * the origin of `profile.endpoint`), so RFC 9421 `@target-uri` /\n * `@authority` and the forwarded agent artifacts match what the agent\n * signed behind a TLS-terminating proxy. Without one (ACP default), the\n * origin is `X-Forwarded-Proto` / `X-Forwarded-Host` when Express\n * `trust proxy` is set, else `req.protocol` + Host.\n * - `approvalEventsPath` + `webhookSecret` mount the owner-approval events\n * receiver on the same handler (`handleApprovalEvents`); or mount\n * `approvalEventsHandler({ secret, kits: [ucpKit, acpKit] })` on its own\n * path to serve both kits from one Events URL.\n */\nimport type { ApprovalEventKit, KitRequestContext } from '../ucp/kit-shared';\nimport { warnOnce } from '../ucp/kit-shared';\nimport { handleApprovalEvents, type SeenEvents } from '../ucp/approval-events';\n\n/** Anything with a fetch-style `handle` (both checkout kits). */\nexport interface FetchKit {\n handle(req: Request, ctx?: KitRequestContext): Promise<Response>;\n /** 5.19.0: the public origin to rebuild request URLs with (both kits expose it). */\n readonly publicOrigin?: string;\n /** 5.19.0: present on both kits — makes the kit an approval-event target. */\n readonly protocol?: 'ucp' | 'acp';\n applyApprovalEvent?: ApprovalEventKit['applyApprovalEvent'];\n}\n\n/** The parts of an Express request this adapter reads. */\nexport interface KitExpressRequest {\n method: string;\n protocol?: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n rawBody?: unknown;\n get?(name: string): string | undefined;\n /** Express app (to read the `trust proxy` setting). */\n app?: { get(name: string): unknown };\n agentVerification?: {\n identityVerified?: boolean;\n agent?: { astraId?: string };\n };\n}\n\n/** The parts of an Express response this adapter writes. */\nexport interface KitExpressResponse {\n status(code: number): unknown;\n setHeader(name: string, value: string | string[]): unknown;\n end(body?: unknown): unknown;\n}\n\nexport interface ExpressHandlerOptions {\n /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */\n callerId?: (req: KitExpressRequest) => string | undefined;\n /**\n * 5.19.0: serve owner-approval events on this path (POST; matched against\n * the full request path or the path under the mount point), e.g.\n * `'/astrasync/events'`. Set the endpoint's Events URL to it. Needs\n * `webhookSecret`.\n */\n approvalEventsPath?: string;\n /** 5.19.0: the endpoint's webhook secret — or `[current, previous]` during a rotation. */\n webhookSecret?: string | readonly string[];\n /** 5.19.0: the kits events may name (default: this kit). */\n approvalEventKits?: ApprovalEventKit[];\n /** 5.19.0: dedupe store for event ids (default: process-local memory). */\n seenEvents?: SeenEvents | ((eventId: string) => Promise<boolean>);\n}\n\nconst bytes = (b: Buffer): ArrayBuffer =>\n b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer;\n\nfunction rawBodyOf(req: KitExpressRequest): string | ArrayBuffer | undefined {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined;\n const raw = req.rawBody;\n if (Buffer.isBuffer(raw)) return bytes(raw);\n if (typeof raw === 'string') return raw;\n if (Buffer.isBuffer(req.body)) return bytes(req.body);\n if (typeof req.body === 'string') return req.body;\n if (req.body !== undefined && req.body !== null && typeof req.body === 'object') {\n if (Object.keys(req.body as object).length === 0) return undefined;\n warnOnce(\n 'kit-express-parsed-body',\n '[astrasync] expressHandler: the request body was already parsed — re-serialising it. ' +\n 'Mount express.raw() on the checkout routes (or keep req.rawBody) so signatures and ' +\n 'idempotency fingerprints see the exact bytes.'\n );\n return JSON.stringify(req.body);\n }\n return undefined;\n}\n\nconst first = (v: string | string[] | undefined) =>\n (Array.isArray(v) ? v[0] : v)?.split(',')[0]?.trim() || undefined;\n\n/** The origin a request reached us at (see the module comment). */\nfunction originOf(req: KitExpressRequest, publicOrigin?: string): string {\n if (publicOrigin) return publicOrigin.replace(/\\/+$/, '');\n const trustProxy = Boolean(req.app?.get('trust proxy'));\n const host =\n (trustProxy && first(req.headers['x-forwarded-host'])) ||\n req.get?.('host') ||\n (req.headers.host as string | undefined) ||\n 'localhost';\n const proto = (trustProxy && first(req.headers['x-forwarded-proto'])) || req.protocol || 'http';\n return `${proto}://${host}`;\n}\n\nconst isKit = (k: FetchKit): k is FetchKit & ApprovalEventKit =>\n typeof k.applyApprovalEvent === 'function' && (k.protocol === 'ucp' || k.protocol === 'acp');\n\nasync function writeEvents(\n req: KitExpressRequest,\n res: KitExpressResponse,\n secret: string | readonly string[] | undefined,\n kits: ApprovalEventKit[],\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>)\n) {\n const raw = rawBodyOf(req);\n const text =\n raw === undefined ? '' : typeof raw === 'string' ? raw : Buffer.from(raw).toString('utf8');\n const out = secret\n ? await handleApprovalEvents(text, req.headers, secret, { kits, ...(seen && { seen }) })\n : { status: 500, body: { ok: false, error: 'webhook_secret_not_configured' } };\n res.status(out.status);\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(out.body));\n}\n\nexport interface ApprovalEventsHandlerOptions {\n /** The endpoint's webhook secret — or `[current, previous]` during a rotation. */\n secret: string | readonly string[];\n /** The kits events may name, e.g. `[ucpKit, acpKit]`. */\n kits: ApprovalEventKit[];\n /** Dedupe store (default: process-local memory). */\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>);\n}\n\n/**\n * 5.19.0: an Express handler for the endpoint's Events URL, serving both\n * kits: `app.post('/astrasync/events', express.raw({ type: '*\\/*' }),\n * approvalEventsHandler({ secret, kits: [ucpKit, acpKit] }))`. Answers 2xx\n * when done, 401 on a bad signature, 503 to have the platform retry.\n */\nexport function approvalEventsHandler(options: ApprovalEventsHandlerOptions) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n await writeEvents(req, res, options.secret, options.kits, options.seen);\n } catch (err) {\n next(err);\n }\n };\n}\n\n/** An Express handler that runs a checkout kit and writes its Response back. */\nexport function expressHandler(kit: FetchKit, options: ExpressHandlerOptions = {}) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n if (options.approvalEventsPath && req.method === 'POST') {\n const p = options.approvalEventsPath.replace(/\\/+$/, '');\n const full = (req.originalUrl ?? req.url).split('?')[0].replace(/\\/+$/, '');\n const local = req.url.split('?')[0].replace(/\\/+$/, '');\n if (full === p || local === p) {\n const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);\n await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);\n return;\n }\n }\n const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));\n else headers.set(k, v);\n }\n const body = rawBodyOf(req);\n const request = new Request(url, {\n method: req.method,\n headers,\n ...(body !== undefined && { body }),\n });\n const verified = req.agentVerification?.identityVerified\n ? req.agentVerification.agent?.astraId\n : undefined;\n const callerId = options.callerId ? options.callerId(req) : verified;\n const response = await kit.handle(request, callerId ? { callerId } : undefined);\n res.status(response.status);\n response.headers.forEach((value, name) => {\n res.setHeader(name, value);\n });\n res.end(Buffer.from(await response.arrayBuffer()));\n } catch (err) {\n next(err);\n }\n };\n}\n","/**\n * 5.15.0 — RFC 9421 HTTP Message Signatures: signing.\n *\n * Signs an outbound request (agent → merchant, platform → merchant webhook)\n * with an ES256 or Ed25519 private key. Covered components default to\n * `@method`, `@authority`, `@path` (+ `content-digest` when there's a body,\n * computed per RFC 9530). Protocol profiles (Web Bot Auth, Visa TAP,\n * Mastercard Agent Pay, UCP) pass their own `components`, `tag` and\n * parameters. Verification is transport/rfc9421-verify.\n */\nimport { createHash, randomBytes, type KeyObject } from 'crypto';\nimport { httpbis, createSigner } from 'http-message-signatures';\n\nexport type HttpSignatureAlgorithm = 'ecdsa-p256-sha256' | 'ed25519';\n\nexport interface SignHttpRequestOptions {\n /** Private key (Node KeyObject / PEM). */\n privateKey: KeyObject | string;\n keyId: string;\n alg?: HttpSignatureAlgorithm;\n /** Covered components. Default @method @authority @path (+ content-digest with a body). */\n components?: string[];\n /** Signature label. Default `sig1`. */\n label?: string;\n /** `tag` parameter (profile name, e.g. a Web Bot Auth or TAP tag). */\n tag?: string;\n /** `nonce` parameter: a value, or true for 32 random bytes (base64url). */\n nonce?: string | true;\n /** Lifetime in seconds (expires = created + this). Default 300. */\n expiresInSec?: number;\n /** Include the `alg` parameter. Default false (key type implies it). */\n includeAlg?: boolean;\n /** Include `expires`. Default true (UCP: keyid only, created optional). */\n includeExpires?: boolean;\n now?: () => Date;\n}\n\nexport interface SignableRequest {\n method: string;\n url: string;\n headers: Record<string, string>;\n body?: string;\n}\n\n/** RFC 9530 Content-Digest (sha-256) of a body. */\nexport function contentDigest(body: string): string {\n return `sha-256=:${createHash('sha256').update(body, 'utf8').digest('base64')}:`;\n}\n\n/**\n * Returns the request's headers plus `Signature-Input`, `Signature` (and\n * `Content-Digest` when a body is covered). Never mutates the input.\n */\nexport async function signHttpRequest(\n req: SignableRequest,\n opts: SignHttpRequestOptions\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) headers[k.toLowerCase()] = v;\n const hasBody = req.body !== undefined && req.body !== '';\n if (hasBody && !headers['content-digest']) headers['content-digest'] = contentDigest(req.body!);\n const components = opts.components ?? [\n '@method',\n '@authority',\n '@path',\n ...(hasBody ? ['content-digest'] : []),\n ];\n const alg = opts.alg ?? 'ecdsa-p256-sha256';\n const created = (opts.now ?? (() => new Date()))();\n const expires = new Date(created.getTime() + (opts.expiresInSec ?? 300) * 1000);\n const nonce =\n opts.nonce === true ? randomBytes(32).toString('base64url') : (opts.nonce ?? undefined);\n const params = [\n 'created',\n ...(opts.includeExpires === false ? [] : ['expires']),\n 'keyid',\n ...(opts.includeAlg ? ['alg'] : []),\n ...(nonce ? ['nonce'] : []),\n ...(opts.tag ? ['tag'] : []),\n ];\n const signer = createSigner(opts.privateKey, alg, opts.keyId);\n const signed = await httpbis.signMessage(\n {\n key: signer,\n name: opts.label ?? 'sig1',\n fields: components,\n params,\n paramValues: {\n created,\n expires,\n keyid: opts.keyId,\n ...(nonce && { nonce }),\n ...(opts.tag && { tag: opts.tag }),\n },\n },\n { method: req.method.toUpperCase(), url: req.url, headers }\n );\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(signed.headers)) {\n out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : String(v);\n }\n return out;\n}\n","/**\n * 5.18.0 — logic the UCP and ACP checkout kits share: the hold test, caller\n * scoping, the result kept for a pending settlement, and one-time warnings.\n */\nimport { createHash } from 'crypto';\nimport { contentDigest } from '../http-signatures/sign';\nimport type { VerificationResult } from '../types';\n\n/** Per-request context a host adapter (e.g. `expressHandler`) passes to `kit.handle`. */\nexport interface KitRequestContext {\n /**\n * The caller's VERIFIED ASTRA-id (e.g. from `req.agentVerification` after\n * the AstraSync middleware ran). Sessions and idempotency keys are scoped\n * to it; without it the kit falls back to Authorization, then X-Astra-Id.\n */\n callerId?: string;\n}\n\n/**\n * True when AstraSync held the purchase for the buyer's approval. Any step-up\n * signal counts; an approval that was just redeemed (`status: 'consumed'`)\n * does not. A held purchase must never reach settle or fulfil.\n */\nexport function isHeld(result: VerificationResult): boolean {\n const r = result as VerificationResult & { recommendation?: string };\n return Boolean(\n r.requiresStepUp ||\n r.requiresApproval ||\n r.recommendation === 'step_up_required' ||\n r.settlementOutcome?.status === 'requires_approval' ||\n (r.stepUpApproval && r.stepUpApproval.status !== 'consumed')\n );\n}\n\nconst scopeHash = (kind: string, value: string) =>\n `${kind}:${createHash('sha256').update(`${kind}\\n${value}`).digest('hex').slice(0, 32)}`;\n\n/**\n * The scope a checkout session and its idempotency keys belong to: the\n * verified ASTRA-id when known, else the Authorization header, else the\n * X-Astra-Id header. Hashed, so no bearer token is ever written to a store.\n * Undefined when the request carries none of them.\n */\nexport async function resolveCallerScope(\n req: Request,\n ctx: KitRequestContext | undefined,\n resolve?: (req: Request) => string | undefined | Promise<string | undefined>\n): Promise<string | undefined> {\n const verified = ctx?.callerId ?? (resolve ? await resolve(req) : undefined);\n if (verified) return scopeHash('astra', verified);\n const auth = req.headers.get('authorization');\n if (auth) return scopeHash('auth', auth);\n const astraId = req.headers.get('x-astra-id');\n if (astraId) return scopeHash('astra-header', astraId);\n return undefined;\n}\n\n/** A scope for a fallback identity (e.g. the UCP platform profile). */\nexport const fallbackScope = (kind: string, value: string) => scopeHash(kind, value);\n\n/**\n * The verification result kept with a pending settlement, minus the\n * merchant-lane secrets (card settlement token, voucher, mandates, KYAPay\n * token, AEP payloads): a store must never hold payment material.\n */\nexport function pendingResult(result: VerificationResult): VerificationResult {\n const {\n settlementToken: _t,\n settlement: _s,\n mandates: _m,\n kyaToken: _k,\n aep: _a,\n ...rest\n } = result;\n return rest;\n}\n\nconst warned = new Set<string>();\n\n/** console.warn once per process per key (never under NODE_ENV=test). */\nexport function warnOnce(key: string, message: string): void {\n if (warned.has(key) || process.env.NODE_ENV === 'test') return;\n warned.add(key);\n // eslint-disable-next-line no-console\n console.warn(message);\n}\n\n/** Warn when a kit runs on its in-memory defaults outside tests. */\nexport function warnInMemoryStores(kit: 'UCP' | 'ACP', missing: string[]): void {\n if (!missing.length || process.env.NODE_ENV === 'test') return;\n // eslint-disable-next-line no-console\n console.warn(\n `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(' and ')} ` +\n '(sessions are lost on restart and not shared across instances). ' +\n `Pass ${missing.map((m) => (m.startsWith('idempotency') ? 'idempotencyStore' : 'store')).join(' and ')} in production.`\n );\n}\n\n// ── 5.19.0 ────────────────────────────────────────────────────────────────\n\n/**\n * 5.19.0: a cart-level message from your `validateCart` hook. `type`\n * defaults to `'error'`; any error keeps the session from being completed\n * (UCP `incomplete`, ACP `not_ready_for_payment`) and `complete` is refused\n * before AstraSync is called. `path` points at the offending part of the\n * cart (e.g. `$.line_items[0].quantity`) — UCP `path`, ACP `param`.\n */\nexport interface KitMessage {\n type?: 'error' | 'warning' | 'info';\n code: string;\n content: string;\n path?: string;\n}\n\n/** Keys that never leave the request: payment secrets in a payment input. */\nconst SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;\n\n/**\n * 5.19.0: the non-secret part of a payment input (UCP instrument descriptor,\n * ACP `payment_data`) kept with a held checkout so an approved hold can\n * settle without the agent: primitive top-level fields only, minus anything\n * that looks like payment material.\n */\nexport function scrubPaymentInput(\n input: Record<string, unknown> | null | undefined\n): Record<string, unknown> | undefined {\n if (!input || typeof input !== 'object') return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(input)) {\n if (SECRET_KEY.test(k)) continue;\n if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) out[k] = v;\n }\n return out;\n}\n\n/** Headers that are never forwarded as agent artifacts (credentials). */\nconst NEVER_FORWARD = new Set(['authorization', 'cookie', 'x-api-key', 'proxy-authorization']);\n\n/** The header names an RFC 9421 `Signature-Input` covers (derived components excluded). */\nfunction coveredHeaders(signatureInput: string): string[] {\n const names = new Set<string>();\n for (const m of signatureInput.matchAll(/\\(([^)]*)\\)/g)) {\n for (const q of m[1].matchAll(/\"([^\"]+)\"/g)) {\n const name = q[1].toLowerCase();\n if (!name.startsWith('@')) names.add(name);\n }\n }\n return [...names];\n}\n\n/**\n * 5.19.0: the agent artifacts a kit forwards on its confirm (see\n * `AgentArtifacts` in types.ts). UCP: an RFC 9421-signed request becomes its\n * covered headers + `signature` / `signature-input` / `signature-agent` /\n * `ucp-agent`, the method, the PUBLIC url (`publicOrigin` + path + query)\n * and the raw-body digest; an unsigned one is `{ unsigned: true }`. ACP is\n * always `{ unsigned: true }` — its `Authorization` bearer is the platform's\n * credential to you and is never forwarded.\n */\nexport function buildAgentArtifacts(\n req: Request,\n rawBody: string,\n opts: { protocol: 'ucp' | 'acp'; publicOrigin?: string }\n): import('../types').AgentArtifacts {\n const signature = req.headers.get('signature');\n const signatureInput = req.headers.get('signature-input');\n if (opts.protocol === 'acp' || !signature || !signatureInput) {\n return { unsigned: true, protocol: opts.protocol };\n }\n const headers: Record<string, string> = {};\n const names = [\n 'signature',\n 'signature-input',\n 'signature-agent',\n 'ucp-agent',\n 'content-digest',\n ...coveredHeaders(signatureInput),\n ];\n for (const name of names) {\n if (NEVER_FORWARD.has(name)) continue;\n const v = req.headers.get(name);\n if (v !== null) headers[name] = v;\n }\n const u = new URL(req.url);\n const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\\/+$/, '') : u.origin;\n return {\n protocol: opts.protocol,\n method: req.method,\n url: `${origin}${u.pathname}${u.search}`,\n headers,\n ...(rawBody && { contentDigest: contentDigest(rawBody) }),\n };\n}\n\n/** 5.19.0: retry policy for settlement reports (`reportRetry` kit option). */\nexport interface ReportRetryOptions {\n /** Total attempts. Default 3. */\n attempts?: number;\n /** First backoff in ms, doubled per retry. Default 250. */\n backoffMs?: number;\n}\n\n/**\n * Run a settlement report with retries on retryable failures (network,\n * 429, 5xx — `isRetryableSettlementError`); a 4xx is a decision about the\n * report and is not retried. Throws the last error.\n */\nexport async function withReportRetries<T>(\n fn: () => Promise<T>,\n opts: ReportRetryOptions = {},\n isRetryable: (err: unknown) => boolean\n): Promise<T> {\n const attempts = Math.max(1, opts.attempts ?? 3);\n const backoff = opts.backoffMs ?? 250;\n let last: unknown;\n for (let i = 0; i < attempts; i++) {\n try {\n return await fn();\n } catch (err) {\n last = err;\n if (i === attempts - 1 || !isRetryable(err)) break;\n await new Promise((r) => setTimeout(r, backoff * 2 ** i));\n }\n }\n throw last;\n}\n\n/** Default `onReportError`: log it (the order is still the merchant's to reconcile). */\nexport function defaultOnReportError(\n err: unknown,\n ctx: { sessionId: string; orderId?: string; kind?: 'settlement_report' | 'order_webhook' }\n): void {\n // eslint-disable-next-line no-console\n console.error(\n `[astrasync] checkout kit: ${ctx.kind === 'order_webhook' ? 'order webhook' : 'settlement report'} ` +\n `failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ''}:`,\n err\n );\n}\n\n/**\n * 5.19.0: what an approval-event resume did. `terminal` outcomes are final\n * for the event (the dispatcher answers 2xx and marks the event seen);\n * non-terminal ones (`busy`, `still_held`) answer 5xx so the platform\n * retries.\n */\nexport type ResumeOutcome =\n | 'completed'\n | 'pending'\n | 'payment_failed'\n | 'cart_invalid'\n | 'not_authorized'\n | 'declined'\n | 'already_completed'\n | 'canceled'\n | 'not_held'\n | 'stale'\n | 'not_found'\n | 'busy'\n | 'still_held';\n\nconst NON_TERMINAL: ReadonlySet<ResumeOutcome> = new Set(['busy', 'still_held']);\n\nexport function isTerminalOutcome(outcome: ResumeOutcome): boolean {\n return !NON_TERMINAL.has(outcome);\n}\n\nexport interface ResumeResult<S> {\n outcome: ResumeOutcome;\n terminal: boolean;\n session: S | null;\n}\n\nexport const resumeResult = <S>(outcome: ResumeOutcome, session: S | null): ResumeResult<S> => ({\n outcome,\n terminal: isTerminalOutcome(outcome),\n session,\n});\n\n/** 5.19.0: the approval event AstraSync POSTs to your endpoint's Events URL. */\nexport interface ApprovalEvent {\n /** Stable across retries — dedupe on it. */\n eventId: string;\n type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired' | 'step_up.lapsed';\n approvalId: string;\n /** The cart id the confirm carried (the platform's, else the kit's session id). */\n checkoutSessionId: string;\n /** The kit session id the confirm carried in `metadata`. */\n kitSessionId?: string;\n protocol?: 'ucp' | 'acp';\n counterpartyId: string;\n agentAstraId: string;\n transactionValue: number;\n currency: string;\n decidedAt: string;\n /** Approved only: the approval can be redeemed until this instant (ISO). */\n redeemBy?: string;\n}\n\n/** A kit the approval-event dispatcher can route to (both checkout kits). */\nexport interface ApprovalEventKit {\n readonly protocol: 'ucp' | 'acp';\n /** Apply one verified event to the kit session it names. */\n applyApprovalEvent(event: ApprovalEvent): Promise<ResumeResult<unknown>>;\n}\n","/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's\n * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a\n * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when\n * ANY value matches ANY secret you pass, so pass both while you roll over:\n * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued\n * events signed before the rotation still verify on either side.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; signatures: string[] } {\n let t: string | undefined;\n const signatures: string[] = [];\n for (const segment of header.split(',')) {\n const eq = segment.indexOf('=');\n if (eq < 0) continue;\n const k = segment.slice(0, eq).trim();\n const v = segment.slice(eq + 1).trim();\n if (k === 't') t = v;\n // 5.19.0: `v1old` = the same HMAC under the previous secret (rotation).\n else if ((k === 'v1' || k === 'v1old') && v) signatures.push(v);\n }\n return { t, signatures };\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration,\n * or (5.19.0) several — e.g. `[current, previous]` during a\n * rotation. Any secret matching any `v1` / `v1old` value passes.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string | readonly string[],\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n const secrets = (Array.isArray(secret) ? secret : [secret]).filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (!secrets.length) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, signatures } = parseSignatureHeader(headerValue);\n if (!timestamp || !signatures.length) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n for (const key of secrets) {\n const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);\n if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {\n return { ok: true };\n }\n }\n return { ok: false, reason: 'signature_mismatch' };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now(),\n /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */\n options: { previousSecret?: string } = {}\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n const old = options.previousSecret\n ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}`\n : '';\n return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };\n}\n","/**\n * 5.19.0 — owner-approval events for held checkout-kit sessions.\n *\n * When an owner approves, denies, or lets a held purchase expire, AstraSync\n * POSTs a signed event to the endpoint's Events URL (dashboard → endpoint):\n *\n * X-AstraSync-Signature: t=<unix>,v1=<hex>[,v1old=<hex>]\n * { eventId, type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired'\n * | 'step_up.lapsed', approvalId, checkoutSessionId, kitSessionId?,\n * protocol?: 'ucp' | 'acp', counterpartyId, agentAstraId,\n * transactionValue, currency, decidedAt, redeemBy? }\n *\n * HMAC-SHA256(secret, `${t}.${rawBody}`) — `verifyAstraSyncWebhook`. One\n * Events URL serves both kits: the event names its protocol and kit session.\n *\n * Retries and dedupe: `eventId` is stable across deliveries. The dispatcher\n * marks it seen only once the session reached a TERMINAL state (completed,\n * card declined, cart invalid, declined, already completed, canceled, …) and\n * answers 2xx; a transient failure (your store, your processor's 5xx) or a\n * non-terminal state answers 5xx so the platform retries. An approved event\n * is retried for about 10½ minutes, inside the approval's 15-minute\n * `redeemBy`; if it never lands, a `step_up.lapsed` event follows.\n */\nimport { verifyAstraSyncWebhook } from '../webhooks';\nimport type { ApprovalEvent, ApprovalEventKit, ResumeOutcome } from './kit-shared';\n\n/** Where the dispatcher remembers events it finished (dedupe). */\nexport interface SeenEvents {\n has(eventId: string): Promise<boolean>;\n add(eventId: string): Promise<void>;\n}\n\n/** Process-local {@link SeenEvents} (single instance / tests). Bounded. */\nexport class MemorySeenEvents implements SeenEvents {\n private ids = new Set<string>();\n constructor(private max = 10_000) {}\n async has(eventId: string) {\n return this.ids.has(eventId);\n }\n async add(eventId: string) {\n if (this.ids.size >= this.max) {\n const first = this.ids.values().next().value;\n if (first !== undefined) this.ids.delete(first);\n }\n this.ids.add(eventId);\n }\n}\n\nconst defaultSeen = new MemorySeenEvents();\n\nexport interface HandleApprovalEventsOptions {\n /** The kits the events can name (e.g. `[ucpKit, acpKit]`). */\n kits: ApprovalEventKit[];\n /**\n * Dedupe store. An object gets `add(eventId)` at a terminal outcome; a\n * function is a check only (you record seen ids yourself). Default: a\n * process-local memory set — use a shared store across instances.\n */\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>);\n /** Signature timestamp tolerance (s). Default 300. */\n toleranceSeconds?: number;\n /** Override \"now\" (tests). */\n nowMs?: number;\n /** Called for every applied event (logging / metrics). */\n onEvent?: (event: ApprovalEvent, outcome: ResumeOutcome | 'duplicate' | 'ignored') => void;\n}\n\nexport interface ApprovalEventsResponse {\n /** 2xx: done (don't retry). 401/400: refused. 503: retry later. */\n status: number;\n body: {\n ok: boolean;\n eventId?: string;\n outcome?: ResumeOutcome | 'duplicate' | 'ignored';\n error?: string;\n };\n}\n\nconst TYPES = new Set(['step_up.approved', 'step_up.denied', 'step_up.expired', 'step_up.lapsed']);\n\nfunction toHeaderRecord(\n headers: Record<string, string | string[] | undefined> | Headers\n): Record<string, string | string[] | undefined> {\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n const out: Record<string, string> = {};\n headers.forEach((v, k) => (out[k.toLowerCase()] = v));\n return out;\n }\n const out: Record<string, string | string[] | undefined> = {};\n for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;\n return out;\n}\n\n/**\n * 5.19.0: verify and apply one approval event. Approved → the kit's\n * `resumeApproved` (settle, report, fulfil — no agent call); denied /\n * expired / lapsed → `resumeDeclined`. Routes by `protocol` and\n * `kitSessionId`, falling back to each kit's cart-id lookup. Never throws:\n * answer the returned `status` and `body`.\n */\nexport async function handleApprovalEvents(\n rawBody: string | Buffer | Uint8Array,\n headers: Record<string, string | string[] | undefined> | Headers,\n secret: string | readonly string[],\n opts: HandleApprovalEventsOptions\n): Promise<ApprovalEventsResponse> {\n const raw = typeof rawBody === 'string' ? rawBody : Buffer.from(rawBody).toString('utf8');\n const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {\n ...(opts.toleranceSeconds !== undefined && { toleranceSeconds: opts.toleranceSeconds }),\n ...(opts.nowMs !== undefined && { nowMs: opts.nowMs }),\n });\n if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };\n\n let event: ApprovalEvent;\n try {\n event = JSON.parse(raw) as ApprovalEvent;\n } catch {\n return { status: 400, body: { ok: false, error: 'invalid_json' } };\n }\n if (!event || typeof event.eventId !== 'string' || typeof event.type !== 'string') {\n return { status: 400, body: { ok: false, error: 'invalid_event' } };\n }\n if (!TYPES.has(event.type)) {\n // A newer event type this SDK doesn't know: acknowledge, don't retry.\n opts.onEvent?.(event, 'ignored');\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: 'ignored' } };\n }\n\n const seen = opts.seen ?? defaultSeen;\n const has = typeof seen === 'function' ? seen : (id: string) => seen.has(id);\n try {\n if (await has(event.eventId)) {\n opts.onEvent?.(event, 'duplicate');\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: 'duplicate' } };\n }\n const candidates = event.protocol\n ? opts.kits.filter((k) => k.protocol === event.protocol)\n : opts.kits;\n let last: ResumeOutcome = 'not_found';\n let terminal = true;\n for (const kit of candidates) {\n const r = await kit.applyApprovalEvent(event);\n last = r.outcome;\n terminal = r.terminal;\n if (r.outcome !== 'not_found') break;\n }\n opts.onEvent?.(event, last);\n if (!terminal) {\n return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };\n }\n if (typeof seen !== 'function') await seen.add(event.eventId);\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };\n } catch (err) {\n // Transient (your store, your processor): the platform retries; the\n // event is NOT marked seen, so the redelivery runs again.\n return {\n status: 503,\n body: {\n ok: false,\n eventId: event.eventId,\n error: err instanceof Error ? err.message : 'transient_error',\n },\n };\n }\n}\n"],"mappings":"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","names":["first","out"]}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/http-signatures/sign.ts
|
|
2
|
+
import { httpbis, createSigner } from "http-message-signatures";
|
|
3
|
+
|
|
1
4
|
// src/ucp/kit-shared.ts
|
|
2
5
|
var warned = /* @__PURE__ */ new Set();
|
|
3
6
|
function warnOnce(key, message) {
|
|
@@ -6,6 +9,145 @@ function warnOnce(key, message) {
|
|
|
6
9
|
console.warn(message);
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
// src/webhooks.ts
|
|
13
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
14
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
15
|
+
function parseSignatureHeader(header) {
|
|
16
|
+
let t;
|
|
17
|
+
const signatures = [];
|
|
18
|
+
for (const segment of header.split(",")) {
|
|
19
|
+
const eq = segment.indexOf("=");
|
|
20
|
+
if (eq < 0) continue;
|
|
21
|
+
const k = segment.slice(0, eq).trim();
|
|
22
|
+
const v = segment.slice(eq + 1).trim();
|
|
23
|
+
if (k === "t") t = v;
|
|
24
|
+
else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
|
|
25
|
+
}
|
|
26
|
+
return { t, signatures };
|
|
27
|
+
}
|
|
28
|
+
function computeHmac(secret, signedPayload) {
|
|
29
|
+
return createHmac("sha256", secret).update(signedPayload, "utf8").digest("hex");
|
|
30
|
+
}
|
|
31
|
+
function constantTimeEquals(a, b) {
|
|
32
|
+
const aBuf = Buffer.from(a, "utf8");
|
|
33
|
+
const bBuf = Buffer.from(b, "utf8");
|
|
34
|
+
if (aBuf.length !== bBuf.length) return false;
|
|
35
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
36
|
+
}
|
|
37
|
+
function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
|
|
38
|
+
const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
|
|
39
|
+
(x) => typeof x === "string" && x.length > 0
|
|
40
|
+
);
|
|
41
|
+
if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
|
|
42
|
+
const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
|
|
43
|
+
if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
|
|
44
|
+
const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
45
|
+
const { t: timestamp, signatures } = parseSignatureHeader(headerValue);
|
|
46
|
+
if (!timestamp || !signatures.length) {
|
|
47
|
+
return { ok: false, reason: "malformed_signature_header" };
|
|
48
|
+
}
|
|
49
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
50
|
+
const now = options.nowMs ?? Date.now();
|
|
51
|
+
const tsSeconds = Number(timestamp);
|
|
52
|
+
if (!Number.isFinite(tsSeconds)) {
|
|
53
|
+
return { ok: false, reason: "invalid_timestamp" };
|
|
54
|
+
}
|
|
55
|
+
const ageSeconds = Math.abs(now / 1e3 - tsSeconds);
|
|
56
|
+
if (ageSeconds > tolerance) {
|
|
57
|
+
return { ok: false, reason: "timestamp_outside_tolerance" };
|
|
58
|
+
}
|
|
59
|
+
for (const key of secrets) {
|
|
60
|
+
const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);
|
|
61
|
+
if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, reason: "signature_mismatch" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/ucp/approval-events.ts
|
|
69
|
+
var MemorySeenEvents = class {
|
|
70
|
+
constructor(max = 1e4) {
|
|
71
|
+
this.max = max;
|
|
72
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
73
|
+
}
|
|
74
|
+
async has(eventId) {
|
|
75
|
+
return this.ids.has(eventId);
|
|
76
|
+
}
|
|
77
|
+
async add(eventId) {
|
|
78
|
+
if (this.ids.size >= this.max) {
|
|
79
|
+
const first2 = this.ids.values().next().value;
|
|
80
|
+
if (first2 !== void 0) this.ids.delete(first2);
|
|
81
|
+
}
|
|
82
|
+
this.ids.add(eventId);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var defaultSeen = new MemorySeenEvents();
|
|
86
|
+
var TYPES = /* @__PURE__ */ new Set(["step_up.approved", "step_up.denied", "step_up.expired", "step_up.lapsed"]);
|
|
87
|
+
function toHeaderRecord(headers) {
|
|
88
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
89
|
+
const out2 = {};
|
|
90
|
+
headers.forEach((v, k) => out2[k.toLowerCase()] = v);
|
|
91
|
+
return out2;
|
|
92
|
+
}
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
async function handleApprovalEvents(rawBody, headers, secret, opts) {
|
|
98
|
+
const raw = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
|
|
99
|
+
const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {
|
|
100
|
+
...opts.toleranceSeconds !== void 0 && { toleranceSeconds: opts.toleranceSeconds },
|
|
101
|
+
...opts.nowMs !== void 0 && { nowMs: opts.nowMs }
|
|
102
|
+
});
|
|
103
|
+
if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };
|
|
104
|
+
let event;
|
|
105
|
+
try {
|
|
106
|
+
event = JSON.parse(raw);
|
|
107
|
+
} catch {
|
|
108
|
+
return { status: 400, body: { ok: false, error: "invalid_json" } };
|
|
109
|
+
}
|
|
110
|
+
if (!event || typeof event.eventId !== "string" || typeof event.type !== "string") {
|
|
111
|
+
return { status: 400, body: { ok: false, error: "invalid_event" } };
|
|
112
|
+
}
|
|
113
|
+
if (!TYPES.has(event.type)) {
|
|
114
|
+
opts.onEvent?.(event, "ignored");
|
|
115
|
+
return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "ignored" } };
|
|
116
|
+
}
|
|
117
|
+
const seen = opts.seen ?? defaultSeen;
|
|
118
|
+
const has = typeof seen === "function" ? seen : (id) => seen.has(id);
|
|
119
|
+
try {
|
|
120
|
+
if (await has(event.eventId)) {
|
|
121
|
+
opts.onEvent?.(event, "duplicate");
|
|
122
|
+
return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "duplicate" } };
|
|
123
|
+
}
|
|
124
|
+
const candidates = event.protocol ? opts.kits.filter((k) => k.protocol === event.protocol) : opts.kits;
|
|
125
|
+
let last = "not_found";
|
|
126
|
+
let terminal = true;
|
|
127
|
+
for (const kit of candidates) {
|
|
128
|
+
const r = await kit.applyApprovalEvent(event);
|
|
129
|
+
last = r.outcome;
|
|
130
|
+
terminal = r.terminal;
|
|
131
|
+
if (r.outcome !== "not_found") break;
|
|
132
|
+
}
|
|
133
|
+
opts.onEvent?.(event, last);
|
|
134
|
+
if (!terminal) {
|
|
135
|
+
return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };
|
|
136
|
+
}
|
|
137
|
+
if (typeof seen !== "function") await seen.add(event.eventId);
|
|
138
|
+
return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return {
|
|
141
|
+
status: 503,
|
|
142
|
+
body: {
|
|
143
|
+
ok: false,
|
|
144
|
+
eventId: event.eventId,
|
|
145
|
+
error: err instanceof Error ? err.message : "transient_error"
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
9
151
|
// src/adapters/kit-express.ts
|
|
10
152
|
var bytes = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
|
11
153
|
function rawBodyOf(req) {
|
|
@@ -25,11 +167,46 @@ function rawBodyOf(req) {
|
|
|
25
167
|
}
|
|
26
168
|
return void 0;
|
|
27
169
|
}
|
|
170
|
+
var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
|
|
171
|
+
function originOf(req, publicOrigin) {
|
|
172
|
+
if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
|
|
173
|
+
const trustProxy = Boolean(req.app?.get("trust proxy"));
|
|
174
|
+
const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
|
|
175
|
+
const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
|
|
176
|
+
return `${proto}://${host}`;
|
|
177
|
+
}
|
|
178
|
+
var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
|
|
179
|
+
async function writeEvents(req, res, secret, kits, seen) {
|
|
180
|
+
const raw = rawBodyOf(req);
|
|
181
|
+
const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
182
|
+
const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
|
|
183
|
+
res.status(out.status);
|
|
184
|
+
res.setHeader("content-type", "application/json");
|
|
185
|
+
res.end(JSON.stringify(out.body));
|
|
186
|
+
}
|
|
187
|
+
function approvalEventsHandler(options) {
|
|
188
|
+
return async (req, res, next) => {
|
|
189
|
+
try {
|
|
190
|
+
await writeEvents(req, res, options.secret, options.kits, options.seen);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
next(err);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
28
196
|
function expressHandler(kit, options = {}) {
|
|
29
197
|
return async (req, res, next) => {
|
|
30
198
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
199
|
+
if (options.approvalEventsPath && req.method === "POST") {
|
|
200
|
+
const p = options.approvalEventsPath.replace(/\/+$/, "");
|
|
201
|
+
const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
|
|
202
|
+
const local = req.url.split("?")[0].replace(/\/+$/, "");
|
|
203
|
+
if (full === p || local === p) {
|
|
204
|
+
const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
|
|
205
|
+
await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
|
|
33
210
|
const headers = new Headers();
|
|
34
211
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
35
212
|
if (v === void 0) continue;
|
|
@@ -56,6 +233,7 @@ function expressHandler(kit, options = {}) {
|
|
|
56
233
|
};
|
|
57
234
|
}
|
|
58
235
|
export {
|
|
236
|
+
approvalEventsHandler,
|
|
59
237
|
expressHandler
|
|
60
238
|
};
|
|
61
239
|
//# sourceMappingURL=kit-express.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ucp/kit-shared.ts","../../src/adapters/kit-express.ts"],"sourcesContent":["/**\n * 5.18.0 — logic the UCP and ACP checkout kits share: the hold test, caller\n * scoping, the result kept for a pending settlement, and one-time warnings.\n */\nimport { createHash } from 'crypto';\nimport type { VerificationResult } from '../types';\n\n/** Per-request context a host adapter (e.g. `expressHandler`) passes to `kit.handle`. */\nexport interface KitRequestContext {\n /**\n * The caller's VERIFIED ASTRA-id (e.g. from `req.agentVerification` after\n * the AstraSync middleware ran). Sessions and idempotency keys are scoped\n * to it; without it the kit falls back to Authorization, then X-Astra-Id.\n */\n callerId?: string;\n}\n\n/**\n * True when AstraSync held the purchase for the buyer's approval. Any step-up\n * signal counts; an approval that was just redeemed (`status: 'consumed'`)\n * does not. A held purchase must never reach settle or fulfil.\n */\nexport function isHeld(result: VerificationResult): boolean {\n const r = result as VerificationResult & { recommendation?: string };\n return Boolean(\n r.requiresStepUp ||\n r.requiresApproval ||\n r.recommendation === 'step_up_required' ||\n r.settlementOutcome?.status === 'requires_approval' ||\n (r.stepUpApproval && r.stepUpApproval.status !== 'consumed')\n );\n}\n\nconst scopeHash = (kind: string, value: string) =>\n `${kind}:${createHash('sha256').update(`${kind}\\n${value}`).digest('hex').slice(0, 32)}`;\n\n/**\n * The scope a checkout session and its idempotency keys belong to: the\n * verified ASTRA-id when known, else the Authorization header, else the\n * X-Astra-Id header. Hashed, so no bearer token is ever written to a store.\n * Undefined when the request carries none of them.\n */\nexport async function resolveCallerScope(\n req: Request,\n ctx: KitRequestContext | undefined,\n resolve?: (req: Request) => string | undefined | Promise<string | undefined>\n): Promise<string | undefined> {\n const verified = ctx?.callerId ?? (resolve ? await resolve(req) : undefined);\n if (verified) return scopeHash('astra', verified);\n const auth = req.headers.get('authorization');\n if (auth) return scopeHash('auth', auth);\n const astraId = req.headers.get('x-astra-id');\n if (astraId) return scopeHash('astra-header', astraId);\n return undefined;\n}\n\n/** A scope for a fallback identity (e.g. the UCP platform profile). */\nexport const fallbackScope = (kind: string, value: string) => scopeHash(kind, value);\n\n/**\n * The verification result kept with a pending settlement, minus the\n * merchant-lane secrets (card settlement token, voucher, mandates, KYAPay\n * token, AEP payloads): a store must never hold payment material.\n */\nexport function pendingResult(result: VerificationResult): VerificationResult {\n const {\n settlementToken: _t,\n settlement: _s,\n mandates: _m,\n kyaToken: _k,\n aep: _a,\n ...rest\n } = result;\n return rest;\n}\n\nconst warned = new Set<string>();\n\n/** console.warn once per process per key (never under NODE_ENV=test). */\nexport function warnOnce(key: string, message: string): void {\n if (warned.has(key) || process.env.NODE_ENV === 'test') return;\n warned.add(key);\n // eslint-disable-next-line no-console\n console.warn(message);\n}\n\n/** Warn when a kit runs on its in-memory defaults outside tests. */\nexport function warnInMemoryStores(kit: 'UCP' | 'ACP', missing: string[]): void {\n if (!missing.length || process.env.NODE_ENV === 'test') return;\n // eslint-disable-next-line no-console\n console.warn(\n `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(' and ')} ` +\n '(sessions are lost on restart and not shared across instances). ' +\n `Pass ${missing.map((m) => (m.startsWith('idempotency') ? 'idempotencyStore' : 'store')).join(' and ')} in production.`\n );\n}\n","/**\n * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,\n * `createAcpCheckoutKit`) on Express.\n *\n * app.use('/ucp/v1', express.raw({ type: '*\\/*' }), expressHandler(ucpKit));\n *\n * The kit checks RFC 9421 signatures and Content-Digest / idempotency\n * fingerprints over the EXACT request bytes, so give it the raw body: mount\n * `express.raw()` on the kit's routes (req.body is a Buffer), or keep a copy\n * on `req.rawBody` (Buffer or string) in your JSON parser's `verify` hook.\n * A body that was already parsed to an object is re-serialised with\n * JSON.stringify — fine for unsigned traffic, but signatures won't verify.\n *\n * When the AstraSync middleware ran first, the caller's verified ASTRA-id\n * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency\n * keys; override with `callerId`.\n */\nimport type { KitRequestContext } from '../ucp/kit-shared';\nimport { warnOnce } from '../ucp/kit-shared';\n\n/** Anything with a fetch-style `handle` (both checkout kits). */\nexport interface FetchKit {\n handle(req: Request, ctx?: KitRequestContext): Promise<Response>;\n}\n\n/** The parts of an Express request this adapter reads. */\nexport interface KitExpressRequest {\n method: string;\n protocol?: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n rawBody?: unknown;\n get?(name: string): string | undefined;\n agentVerification?: {\n identityVerified?: boolean;\n agent?: { astraId?: string };\n };\n}\n\n/** The parts of an Express response this adapter writes. */\nexport interface KitExpressResponse {\n status(code: number): unknown;\n setHeader(name: string, value: string | string[]): unknown;\n end(body?: unknown): unknown;\n}\n\nexport interface ExpressHandlerOptions {\n /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */\n callerId?: (req: KitExpressRequest) => string | undefined;\n}\n\nconst bytes = (b: Buffer): ArrayBuffer =>\n b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer;\n\nfunction rawBodyOf(req: KitExpressRequest): string | ArrayBuffer | undefined {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined;\n const raw = req.rawBody;\n if (Buffer.isBuffer(raw)) return bytes(raw);\n if (typeof raw === 'string') return raw;\n if (Buffer.isBuffer(req.body)) return bytes(req.body);\n if (typeof req.body === 'string') return req.body;\n if (req.body !== undefined && req.body !== null && typeof req.body === 'object') {\n if (Object.keys(req.body as object).length === 0) return undefined;\n warnOnce(\n 'kit-express-parsed-body',\n '[astrasync] expressHandler: the request body was already parsed — re-serialising it. ' +\n 'Mount express.raw() on the checkout routes (or keep req.rawBody) so signatures and ' +\n 'idempotency fingerprints see the exact bytes.'\n );\n return JSON.stringify(req.body);\n }\n return undefined;\n}\n\n/** An Express handler that runs a checkout kit and writes its Response back. */\nexport function expressHandler(kit: FetchKit, options: ExpressHandlerOptions = {}) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n const host = req.get?.('host') ?? (req.headers.host as string | undefined) ?? 'localhost';\n const url = `${req.protocol ?? 'http'}://${host}${req.originalUrl ?? req.url}`;\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));\n else headers.set(k, v);\n }\n const body = rawBodyOf(req);\n const request = new Request(url, {\n method: req.method,\n headers,\n ...(body !== undefined && { body }),\n });\n const verified = req.agentVerification?.identityVerified\n ? req.agentVerification.agent?.astraId\n : undefined;\n const callerId = options.callerId ? options.callerId(req) : verified;\n const response = await kit.handle(request, callerId ? { callerId } : undefined);\n res.status(response.status);\n response.headers.forEach((value, name) => {\n res.setHeader(name, value);\n });\n res.end(Buffer.from(await response.arrayBuffer()));\n } catch (err) {\n next(err);\n }\n };\n}\n"],"mappings":";AA4EA,IAAM,SAAS,oBAAI,IAAY;AAGxB,SAAS,SAAS,KAAa,SAAuB;AAC3D,MAAI,OAAO,IAAI,GAAG,KAAK,QAAQ,IAAI,aAAa,OAAQ;AACxD,SAAO,IAAI,GAAG;AAEd,UAAQ,KAAK,OAAO;AACtB;;;AC/BA,IAAM,QAAQ,CAAC,MACb,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU;AAE1D,SAAS,UAAU,KAA0D;AAC3E,MAAI,IAAI,WAAW,SAAS,IAAI,WAAW,OAAQ,QAAO;AAC1D,QAAM,MAAM,IAAI;AAChB,MAAI,OAAO,SAAS,GAAG,EAAG,QAAO,MAAM,GAAG;AAC1C,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,SAAS,IAAI,IAAI,EAAG,QAAO,MAAM,IAAI,IAAI;AACpD,MAAI,OAAO,IAAI,SAAS,SAAU,QAAO,IAAI;AAC7C,MAAI,IAAI,SAAS,UAAa,IAAI,SAAS,QAAQ,OAAO,IAAI,SAAS,UAAU;AAC/E,QAAI,OAAO,KAAK,IAAI,IAAc,EAAE,WAAW,EAAG,QAAO;AACzD;AAAA,MACE;AAAA,MACA;AAAA,IAGF;AACA,WAAO,KAAK,UAAU,IAAI,IAAI;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,eAAe,KAAe,UAAiC,CAAC,GAAG;AACjF,SAAO,OACL,KACA,KACA,SACkB;AAClB,QAAI;AACF,YAAM,OAAO,IAAI,MAAM,MAAM,KAAM,IAAI,QAAQ,QAA+B;AAC9E,YAAM,MAAM,GAAG,IAAI,YAAY,MAAM,MAAM,IAAI,GAAG,IAAI,eAAe,IAAI,GAAG;AAC5E,YAAM,UAAU,IAAI,QAAQ;AAC5B,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,OAAO,GAAG;AAChD,YAAI,MAAM,OAAW;AACrB,YAAI,MAAM,QAAQ,CAAC,EAAG,GAAE,QAAQ,CAAC,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC;AAAA,YACtD,SAAQ,IAAI,GAAG,CAAC;AAAA,MACvB;AACA,YAAM,OAAO,UAAU,GAAG;AAC1B,YAAM,UAAU,IAAI,QAAQ,KAAK;AAAA,QAC/B,QAAQ,IAAI;AAAA,QACZ;AAAA,QACA,GAAI,SAAS,UAAa,EAAE,KAAK;AAAA,MACnC,CAAC;AACD,YAAM,WAAW,IAAI,mBAAmB,mBACpC,IAAI,kBAAkB,OAAO,UAC7B;AACJ,YAAM,WAAW,QAAQ,WAAW,QAAQ,SAAS,GAAG,IAAI;AAC5D,YAAM,WAAW,MAAM,IAAI,OAAO,SAAS,WAAW,EAAE,SAAS,IAAI,MAAS;AAC9E,UAAI,OAAO,SAAS,MAAM;AAC1B,eAAS,QAAQ,QAAQ,CAAC,OAAO,SAAS;AACxC,YAAI,UAAU,MAAM,KAAK;AAAA,MAC3B,CAAC;AACD,UAAI,IAAI,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IACnD,SAAS,KAAK;AACZ,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-signatures/sign.ts","../../src/ucp/kit-shared.ts","../../src/webhooks.ts","../../src/ucp/approval-events.ts","../../src/adapters/kit-express.ts"],"sourcesContent":["/**\n * 5.15.0 — RFC 9421 HTTP Message Signatures: signing.\n *\n * Signs an outbound request (agent → merchant, platform → merchant webhook)\n * with an ES256 or Ed25519 private key. Covered components default to\n * `@method`, `@authority`, `@path` (+ `content-digest` when there's a body,\n * computed per RFC 9530). Protocol profiles (Web Bot Auth, Visa TAP,\n * Mastercard Agent Pay, UCP) pass their own `components`, `tag` and\n * parameters. Verification is transport/rfc9421-verify.\n */\nimport { createHash, randomBytes, type KeyObject } from 'crypto';\nimport { httpbis, createSigner } from 'http-message-signatures';\n\nexport type HttpSignatureAlgorithm = 'ecdsa-p256-sha256' | 'ed25519';\n\nexport interface SignHttpRequestOptions {\n /** Private key (Node KeyObject / PEM). */\n privateKey: KeyObject | string;\n keyId: string;\n alg?: HttpSignatureAlgorithm;\n /** Covered components. Default @method @authority @path (+ content-digest with a body). */\n components?: string[];\n /** Signature label. Default `sig1`. */\n label?: string;\n /** `tag` parameter (profile name, e.g. a Web Bot Auth or TAP tag). */\n tag?: string;\n /** `nonce` parameter: a value, or true for 32 random bytes (base64url). */\n nonce?: string | true;\n /** Lifetime in seconds (expires = created + this). Default 300. */\n expiresInSec?: number;\n /** Include the `alg` parameter. Default false (key type implies it). */\n includeAlg?: boolean;\n /** Include `expires`. Default true (UCP: keyid only, created optional). */\n includeExpires?: boolean;\n now?: () => Date;\n}\n\nexport interface SignableRequest {\n method: string;\n url: string;\n headers: Record<string, string>;\n body?: string;\n}\n\n/** RFC 9530 Content-Digest (sha-256) of a body. */\nexport function contentDigest(body: string): string {\n return `sha-256=:${createHash('sha256').update(body, 'utf8').digest('base64')}:`;\n}\n\n/**\n * Returns the request's headers plus `Signature-Input`, `Signature` (and\n * `Content-Digest` when a body is covered). Never mutates the input.\n */\nexport async function signHttpRequest(\n req: SignableRequest,\n opts: SignHttpRequestOptions\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) headers[k.toLowerCase()] = v;\n const hasBody = req.body !== undefined && req.body !== '';\n if (hasBody && !headers['content-digest']) headers['content-digest'] = contentDigest(req.body!);\n const components = opts.components ?? [\n '@method',\n '@authority',\n '@path',\n ...(hasBody ? ['content-digest'] : []),\n ];\n const alg = opts.alg ?? 'ecdsa-p256-sha256';\n const created = (opts.now ?? (() => new Date()))();\n const expires = new Date(created.getTime() + (opts.expiresInSec ?? 300) * 1000);\n const nonce =\n opts.nonce === true ? randomBytes(32).toString('base64url') : (opts.nonce ?? undefined);\n const params = [\n 'created',\n ...(opts.includeExpires === false ? [] : ['expires']),\n 'keyid',\n ...(opts.includeAlg ? ['alg'] : []),\n ...(nonce ? ['nonce'] : []),\n ...(opts.tag ? ['tag'] : []),\n ];\n const signer = createSigner(opts.privateKey, alg, opts.keyId);\n const signed = await httpbis.signMessage(\n {\n key: signer,\n name: opts.label ?? 'sig1',\n fields: components,\n params,\n paramValues: {\n created,\n expires,\n keyid: opts.keyId,\n ...(nonce && { nonce }),\n ...(opts.tag && { tag: opts.tag }),\n },\n },\n { method: req.method.toUpperCase(), url: req.url, headers }\n );\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(signed.headers)) {\n out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : String(v);\n }\n return out;\n}\n","/**\n * 5.18.0 — logic the UCP and ACP checkout kits share: the hold test, caller\n * scoping, the result kept for a pending settlement, and one-time warnings.\n */\nimport { createHash } from 'crypto';\nimport { contentDigest } from '../http-signatures/sign';\nimport type { VerificationResult } from '../types';\n\n/** Per-request context a host adapter (e.g. `expressHandler`) passes to `kit.handle`. */\nexport interface KitRequestContext {\n /**\n * The caller's VERIFIED ASTRA-id (e.g. from `req.agentVerification` after\n * the AstraSync middleware ran). Sessions and idempotency keys are scoped\n * to it; without it the kit falls back to Authorization, then X-Astra-Id.\n */\n callerId?: string;\n}\n\n/**\n * True when AstraSync held the purchase for the buyer's approval. Any step-up\n * signal counts; an approval that was just redeemed (`status: 'consumed'`)\n * does not. A held purchase must never reach settle or fulfil.\n */\nexport function isHeld(result: VerificationResult): boolean {\n const r = result as VerificationResult & { recommendation?: string };\n return Boolean(\n r.requiresStepUp ||\n r.requiresApproval ||\n r.recommendation === 'step_up_required' ||\n r.settlementOutcome?.status === 'requires_approval' ||\n (r.stepUpApproval && r.stepUpApproval.status !== 'consumed')\n );\n}\n\nconst scopeHash = (kind: string, value: string) =>\n `${kind}:${createHash('sha256').update(`${kind}\\n${value}`).digest('hex').slice(0, 32)}`;\n\n/**\n * The scope a checkout session and its idempotency keys belong to: the\n * verified ASTRA-id when known, else the Authorization header, else the\n * X-Astra-Id header. Hashed, so no bearer token is ever written to a store.\n * Undefined when the request carries none of them.\n */\nexport async function resolveCallerScope(\n req: Request,\n ctx: KitRequestContext | undefined,\n resolve?: (req: Request) => string | undefined | Promise<string | undefined>\n): Promise<string | undefined> {\n const verified = ctx?.callerId ?? (resolve ? await resolve(req) : undefined);\n if (verified) return scopeHash('astra', verified);\n const auth = req.headers.get('authorization');\n if (auth) return scopeHash('auth', auth);\n const astraId = req.headers.get('x-astra-id');\n if (astraId) return scopeHash('astra-header', astraId);\n return undefined;\n}\n\n/** A scope for a fallback identity (e.g. the UCP platform profile). */\nexport const fallbackScope = (kind: string, value: string) => scopeHash(kind, value);\n\n/**\n * The verification result kept with a pending settlement, minus the\n * merchant-lane secrets (card settlement token, voucher, mandates, KYAPay\n * token, AEP payloads): a store must never hold payment material.\n */\nexport function pendingResult(result: VerificationResult): VerificationResult {\n const {\n settlementToken: _t,\n settlement: _s,\n mandates: _m,\n kyaToken: _k,\n aep: _a,\n ...rest\n } = result;\n return rest;\n}\n\nconst warned = new Set<string>();\n\n/** console.warn once per process per key (never under NODE_ENV=test). */\nexport function warnOnce(key: string, message: string): void {\n if (warned.has(key) || process.env.NODE_ENV === 'test') return;\n warned.add(key);\n // eslint-disable-next-line no-console\n console.warn(message);\n}\n\n/** Warn when a kit runs on its in-memory defaults outside tests. */\nexport function warnInMemoryStores(kit: 'UCP' | 'ACP', missing: string[]): void {\n if (!missing.length || process.env.NODE_ENV === 'test') return;\n // eslint-disable-next-line no-console\n console.warn(\n `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(' and ')} ` +\n '(sessions are lost on restart and not shared across instances). ' +\n `Pass ${missing.map((m) => (m.startsWith('idempotency') ? 'idempotencyStore' : 'store')).join(' and ')} in production.`\n );\n}\n\n// ── 5.19.0 ────────────────────────────────────────────────────────────────\n\n/**\n * 5.19.0: a cart-level message from your `validateCart` hook. `type`\n * defaults to `'error'`; any error keeps the session from being completed\n * (UCP `incomplete`, ACP `not_ready_for_payment`) and `complete` is refused\n * before AstraSync is called. `path` points at the offending part of the\n * cart (e.g. `$.line_items[0].quantity`) — UCP `path`, ACP `param`.\n */\nexport interface KitMessage {\n type?: 'error' | 'warning' | 'info';\n code: string;\n content: string;\n path?: string;\n}\n\n/** Keys that never leave the request: payment secrets in a payment input. */\nconst SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;\n\n/**\n * 5.19.0: the non-secret part of a payment input (UCP instrument descriptor,\n * ACP `payment_data`) kept with a held checkout so an approved hold can\n * settle without the agent: primitive top-level fields only, minus anything\n * that looks like payment material.\n */\nexport function scrubPaymentInput(\n input: Record<string, unknown> | null | undefined\n): Record<string, unknown> | undefined {\n if (!input || typeof input !== 'object') return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(input)) {\n if (SECRET_KEY.test(k)) continue;\n if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) out[k] = v;\n }\n return out;\n}\n\n/** Headers that are never forwarded as agent artifacts (credentials). */\nconst NEVER_FORWARD = new Set(['authorization', 'cookie', 'x-api-key', 'proxy-authorization']);\n\n/** The header names an RFC 9421 `Signature-Input` covers (derived components excluded). */\nfunction coveredHeaders(signatureInput: string): string[] {\n const names = new Set<string>();\n for (const m of signatureInput.matchAll(/\\(([^)]*)\\)/g)) {\n for (const q of m[1].matchAll(/\"([^\"]+)\"/g)) {\n const name = q[1].toLowerCase();\n if (!name.startsWith('@')) names.add(name);\n }\n }\n return [...names];\n}\n\n/**\n * 5.19.0: the agent artifacts a kit forwards on its confirm (see\n * `AgentArtifacts` in types.ts). UCP: an RFC 9421-signed request becomes its\n * covered headers + `signature` / `signature-input` / `signature-agent` /\n * `ucp-agent`, the method, the PUBLIC url (`publicOrigin` + path + query)\n * and the raw-body digest; an unsigned one is `{ unsigned: true }`. ACP is\n * always `{ unsigned: true }` — its `Authorization` bearer is the platform's\n * credential to you and is never forwarded.\n */\nexport function buildAgentArtifacts(\n req: Request,\n rawBody: string,\n opts: { protocol: 'ucp' | 'acp'; publicOrigin?: string }\n): import('../types').AgentArtifacts {\n const signature = req.headers.get('signature');\n const signatureInput = req.headers.get('signature-input');\n if (opts.protocol === 'acp' || !signature || !signatureInput) {\n return { unsigned: true, protocol: opts.protocol };\n }\n const headers: Record<string, string> = {};\n const names = [\n 'signature',\n 'signature-input',\n 'signature-agent',\n 'ucp-agent',\n 'content-digest',\n ...coveredHeaders(signatureInput),\n ];\n for (const name of names) {\n if (NEVER_FORWARD.has(name)) continue;\n const v = req.headers.get(name);\n if (v !== null) headers[name] = v;\n }\n const u = new URL(req.url);\n const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\\/+$/, '') : u.origin;\n return {\n protocol: opts.protocol,\n method: req.method,\n url: `${origin}${u.pathname}${u.search}`,\n headers,\n ...(rawBody && { contentDigest: contentDigest(rawBody) }),\n };\n}\n\n/** 5.19.0: retry policy for settlement reports (`reportRetry` kit option). */\nexport interface ReportRetryOptions {\n /** Total attempts. Default 3. */\n attempts?: number;\n /** First backoff in ms, doubled per retry. Default 250. */\n backoffMs?: number;\n}\n\n/**\n * Run a settlement report with retries on retryable failures (network,\n * 429, 5xx — `isRetryableSettlementError`); a 4xx is a decision about the\n * report and is not retried. Throws the last error.\n */\nexport async function withReportRetries<T>(\n fn: () => Promise<T>,\n opts: ReportRetryOptions = {},\n isRetryable: (err: unknown) => boolean\n): Promise<T> {\n const attempts = Math.max(1, opts.attempts ?? 3);\n const backoff = opts.backoffMs ?? 250;\n let last: unknown;\n for (let i = 0; i < attempts; i++) {\n try {\n return await fn();\n } catch (err) {\n last = err;\n if (i === attempts - 1 || !isRetryable(err)) break;\n await new Promise((r) => setTimeout(r, backoff * 2 ** i));\n }\n }\n throw last;\n}\n\n/** Default `onReportError`: log it (the order is still the merchant's to reconcile). */\nexport function defaultOnReportError(\n err: unknown,\n ctx: { sessionId: string; orderId?: string; kind?: 'settlement_report' | 'order_webhook' }\n): void {\n // eslint-disable-next-line no-console\n console.error(\n `[astrasync] checkout kit: ${ctx.kind === 'order_webhook' ? 'order webhook' : 'settlement report'} ` +\n `failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ''}:`,\n err\n );\n}\n\n/**\n * 5.19.0: what an approval-event resume did. `terminal` outcomes are final\n * for the event (the dispatcher answers 2xx and marks the event seen);\n * non-terminal ones (`busy`, `still_held`) answer 5xx so the platform\n * retries.\n */\nexport type ResumeOutcome =\n | 'completed'\n | 'pending'\n | 'payment_failed'\n | 'cart_invalid'\n | 'not_authorized'\n | 'declined'\n | 'already_completed'\n | 'canceled'\n | 'not_held'\n | 'stale'\n | 'not_found'\n | 'busy'\n | 'still_held';\n\nconst NON_TERMINAL: ReadonlySet<ResumeOutcome> = new Set(['busy', 'still_held']);\n\nexport function isTerminalOutcome(outcome: ResumeOutcome): boolean {\n return !NON_TERMINAL.has(outcome);\n}\n\nexport interface ResumeResult<S> {\n outcome: ResumeOutcome;\n terminal: boolean;\n session: S | null;\n}\n\nexport const resumeResult = <S>(outcome: ResumeOutcome, session: S | null): ResumeResult<S> => ({\n outcome,\n terminal: isTerminalOutcome(outcome),\n session,\n});\n\n/** 5.19.0: the approval event AstraSync POSTs to your endpoint's Events URL. */\nexport interface ApprovalEvent {\n /** Stable across retries — dedupe on it. */\n eventId: string;\n type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired' | 'step_up.lapsed';\n approvalId: string;\n /** The cart id the confirm carried (the platform's, else the kit's session id). */\n checkoutSessionId: string;\n /** The kit session id the confirm carried in `metadata`. */\n kitSessionId?: string;\n protocol?: 'ucp' | 'acp';\n counterpartyId: string;\n agentAstraId: string;\n transactionValue: number;\n currency: string;\n decidedAt: string;\n /** Approved only: the approval can be redeemed until this instant (ISO). */\n redeemBy?: string;\n}\n\n/** A kit the approval-event dispatcher can route to (both checkout kits). */\nexport interface ApprovalEventKit {\n readonly protocol: 'ucp' | 'acp';\n /** Apply one verified event to the kit session it names. */\n applyApprovalEvent(event: ApprovalEvent): Promise<ResumeResult<unknown>>;\n}\n","/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's\n * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a\n * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when\n * ANY value matches ANY secret you pass, so pass both while you roll over:\n * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued\n * events signed before the rotation still verify on either side.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; signatures: string[] } {\n let t: string | undefined;\n const signatures: string[] = [];\n for (const segment of header.split(',')) {\n const eq = segment.indexOf('=');\n if (eq < 0) continue;\n const k = segment.slice(0, eq).trim();\n const v = segment.slice(eq + 1).trim();\n if (k === 't') t = v;\n // 5.19.0: `v1old` = the same HMAC under the previous secret (rotation).\n else if ((k === 'v1' || k === 'v1old') && v) signatures.push(v);\n }\n return { t, signatures };\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration,\n * or (5.19.0) several — e.g. `[current, previous]` during a\n * rotation. Any secret matching any `v1` / `v1old` value passes.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string | readonly string[],\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n const secrets = (Array.isArray(secret) ? secret : [secret]).filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (!secrets.length) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, signatures } = parseSignatureHeader(headerValue);\n if (!timestamp || !signatures.length) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n for (const key of secrets) {\n const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);\n if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {\n return { ok: true };\n }\n }\n return { ok: false, reason: 'signature_mismatch' };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now(),\n /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */\n options: { previousSecret?: string } = {}\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n const old = options.previousSecret\n ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}`\n : '';\n return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };\n}\n","/**\n * 5.19.0 — owner-approval events for held checkout-kit sessions.\n *\n * When an owner approves, denies, or lets a held purchase expire, AstraSync\n * POSTs a signed event to the endpoint's Events URL (dashboard → endpoint):\n *\n * X-AstraSync-Signature: t=<unix>,v1=<hex>[,v1old=<hex>]\n * { eventId, type: 'step_up.approved' | 'step_up.denied' | 'step_up.expired'\n * | 'step_up.lapsed', approvalId, checkoutSessionId, kitSessionId?,\n * protocol?: 'ucp' | 'acp', counterpartyId, agentAstraId,\n * transactionValue, currency, decidedAt, redeemBy? }\n *\n * HMAC-SHA256(secret, `${t}.${rawBody}`) — `verifyAstraSyncWebhook`. One\n * Events URL serves both kits: the event names its protocol and kit session.\n *\n * Retries and dedupe: `eventId` is stable across deliveries. The dispatcher\n * marks it seen only once the session reached a TERMINAL state (completed,\n * card declined, cart invalid, declined, already completed, canceled, …) and\n * answers 2xx; a transient failure (your store, your processor's 5xx) or a\n * non-terminal state answers 5xx so the platform retries. An approved event\n * is retried for about 10½ minutes, inside the approval's 15-minute\n * `redeemBy`; if it never lands, a `step_up.lapsed` event follows.\n */\nimport { verifyAstraSyncWebhook } from '../webhooks';\nimport type { ApprovalEvent, ApprovalEventKit, ResumeOutcome } from './kit-shared';\n\n/** Where the dispatcher remembers events it finished (dedupe). */\nexport interface SeenEvents {\n has(eventId: string): Promise<boolean>;\n add(eventId: string): Promise<void>;\n}\n\n/** Process-local {@link SeenEvents} (single instance / tests). Bounded. */\nexport class MemorySeenEvents implements SeenEvents {\n private ids = new Set<string>();\n constructor(private max = 10_000) {}\n async has(eventId: string) {\n return this.ids.has(eventId);\n }\n async add(eventId: string) {\n if (this.ids.size >= this.max) {\n const first = this.ids.values().next().value;\n if (first !== undefined) this.ids.delete(first);\n }\n this.ids.add(eventId);\n }\n}\n\nconst defaultSeen = new MemorySeenEvents();\n\nexport interface HandleApprovalEventsOptions {\n /** The kits the events can name (e.g. `[ucpKit, acpKit]`). */\n kits: ApprovalEventKit[];\n /**\n * Dedupe store. An object gets `add(eventId)` at a terminal outcome; a\n * function is a check only (you record seen ids yourself). Default: a\n * process-local memory set — use a shared store across instances.\n */\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>);\n /** Signature timestamp tolerance (s). Default 300. */\n toleranceSeconds?: number;\n /** Override \"now\" (tests). */\n nowMs?: number;\n /** Called for every applied event (logging / metrics). */\n onEvent?: (event: ApprovalEvent, outcome: ResumeOutcome | 'duplicate' | 'ignored') => void;\n}\n\nexport interface ApprovalEventsResponse {\n /** 2xx: done (don't retry). 401/400: refused. 503: retry later. */\n status: number;\n body: {\n ok: boolean;\n eventId?: string;\n outcome?: ResumeOutcome | 'duplicate' | 'ignored';\n error?: string;\n };\n}\n\nconst TYPES = new Set(['step_up.approved', 'step_up.denied', 'step_up.expired', 'step_up.lapsed']);\n\nfunction toHeaderRecord(\n headers: Record<string, string | string[] | undefined> | Headers\n): Record<string, string | string[] | undefined> {\n if (typeof Headers !== 'undefined' && headers instanceof Headers) {\n const out: Record<string, string> = {};\n headers.forEach((v, k) => (out[k.toLowerCase()] = v));\n return out;\n }\n const out: Record<string, string | string[] | undefined> = {};\n for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;\n return out;\n}\n\n/**\n * 5.19.0: verify and apply one approval event. Approved → the kit's\n * `resumeApproved` (settle, report, fulfil — no agent call); denied /\n * expired / lapsed → `resumeDeclined`. Routes by `protocol` and\n * `kitSessionId`, falling back to each kit's cart-id lookup. Never throws:\n * answer the returned `status` and `body`.\n */\nexport async function handleApprovalEvents(\n rawBody: string | Buffer | Uint8Array,\n headers: Record<string, string | string[] | undefined> | Headers,\n secret: string | readonly string[],\n opts: HandleApprovalEventsOptions\n): Promise<ApprovalEventsResponse> {\n const raw = typeof rawBody === 'string' ? rawBody : Buffer.from(rawBody).toString('utf8');\n const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {\n ...(opts.toleranceSeconds !== undefined && { toleranceSeconds: opts.toleranceSeconds }),\n ...(opts.nowMs !== undefined && { nowMs: opts.nowMs }),\n });\n if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };\n\n let event: ApprovalEvent;\n try {\n event = JSON.parse(raw) as ApprovalEvent;\n } catch {\n return { status: 400, body: { ok: false, error: 'invalid_json' } };\n }\n if (!event || typeof event.eventId !== 'string' || typeof event.type !== 'string') {\n return { status: 400, body: { ok: false, error: 'invalid_event' } };\n }\n if (!TYPES.has(event.type)) {\n // A newer event type this SDK doesn't know: acknowledge, don't retry.\n opts.onEvent?.(event, 'ignored');\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: 'ignored' } };\n }\n\n const seen = opts.seen ?? defaultSeen;\n const has = typeof seen === 'function' ? seen : (id: string) => seen.has(id);\n try {\n if (await has(event.eventId)) {\n opts.onEvent?.(event, 'duplicate');\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: 'duplicate' } };\n }\n const candidates = event.protocol\n ? opts.kits.filter((k) => k.protocol === event.protocol)\n : opts.kits;\n let last: ResumeOutcome = 'not_found';\n let terminal = true;\n for (const kit of candidates) {\n const r = await kit.applyApprovalEvent(event);\n last = r.outcome;\n terminal = r.terminal;\n if (r.outcome !== 'not_found') break;\n }\n opts.onEvent?.(event, last);\n if (!terminal) {\n return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };\n }\n if (typeof seen !== 'function') await seen.add(event.eventId);\n return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };\n } catch (err) {\n // Transient (your store, your processor): the platform retries; the\n // event is NOT marked seen, so the redelivery runs again.\n return {\n status: 503,\n body: {\n ok: false,\n eventId: event.eventId,\n error: err instanceof Error ? err.message : 'transient_error',\n },\n };\n }\n}\n","/**\n * 5.18.0 — mount a fetch-style checkout kit (`createUcpCheckoutKit`,\n * `createAcpCheckoutKit`) on Express.\n *\n * app.use('/ucp/v1', express.raw({ type: '*\\/*' }), expressHandler(ucpKit));\n *\n * The kit checks RFC 9421 signatures and Content-Digest / idempotency\n * fingerprints over the EXACT request bytes, so give it the raw body: mount\n * `express.raw()` on the kit's routes (req.body is a Buffer), or keep a copy\n * on `req.rawBody` (Buffer or string) in your JSON parser's `verify` hook.\n * A body that was already parsed to an object is re-serialised with\n * JSON.stringify — fine for unsigned traffic, but signatures won't verify.\n *\n * When the AstraSync middleware ran first, the caller's verified ASTRA-id\n * (`req.agentVerification.agent.astraId`) scopes sessions and idempotency\n * keys; override with `callerId`.\n *\n * 5.19.0:\n * - the request URL the kit sees uses the kit's `publicOrigin` (UCP default:\n * the origin of `profile.endpoint`), so RFC 9421 `@target-uri` /\n * `@authority` and the forwarded agent artifacts match what the agent\n * signed behind a TLS-terminating proxy. Without one (ACP default), the\n * origin is `X-Forwarded-Proto` / `X-Forwarded-Host` when Express\n * `trust proxy` is set, else `req.protocol` + Host.\n * - `approvalEventsPath` + `webhookSecret` mount the owner-approval events\n * receiver on the same handler (`handleApprovalEvents`); or mount\n * `approvalEventsHandler({ secret, kits: [ucpKit, acpKit] })` on its own\n * path to serve both kits from one Events URL.\n */\nimport type { ApprovalEventKit, KitRequestContext } from '../ucp/kit-shared';\nimport { warnOnce } from '../ucp/kit-shared';\nimport { handleApprovalEvents, type SeenEvents } from '../ucp/approval-events';\n\n/** Anything with a fetch-style `handle` (both checkout kits). */\nexport interface FetchKit {\n handle(req: Request, ctx?: KitRequestContext): Promise<Response>;\n /** 5.19.0: the public origin to rebuild request URLs with (both kits expose it). */\n readonly publicOrigin?: string;\n /** 5.19.0: present on both kits — makes the kit an approval-event target. */\n readonly protocol?: 'ucp' | 'acp';\n applyApprovalEvent?: ApprovalEventKit['applyApprovalEvent'];\n}\n\n/** The parts of an Express request this adapter reads. */\nexport interface KitExpressRequest {\n method: string;\n protocol?: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n rawBody?: unknown;\n get?(name: string): string | undefined;\n /** Express app (to read the `trust proxy` setting). */\n app?: { get(name: string): unknown };\n agentVerification?: {\n identityVerified?: boolean;\n agent?: { astraId?: string };\n };\n}\n\n/** The parts of an Express response this adapter writes. */\nexport interface KitExpressResponse {\n status(code: number): unknown;\n setHeader(name: string, value: string | string[]): unknown;\n end(body?: unknown): unknown;\n}\n\nexport interface ExpressHandlerOptions {\n /** The caller's verified ASTRA-id (default: from `req.agentVerification` when identity-verified). */\n callerId?: (req: KitExpressRequest) => string | undefined;\n /**\n * 5.19.0: serve owner-approval events on this path (POST; matched against\n * the full request path or the path under the mount point), e.g.\n * `'/astrasync/events'`. Set the endpoint's Events URL to it. Needs\n * `webhookSecret`.\n */\n approvalEventsPath?: string;\n /** 5.19.0: the endpoint's webhook secret — or `[current, previous]` during a rotation. */\n webhookSecret?: string | readonly string[];\n /** 5.19.0: the kits events may name (default: this kit). */\n approvalEventKits?: ApprovalEventKit[];\n /** 5.19.0: dedupe store for event ids (default: process-local memory). */\n seenEvents?: SeenEvents | ((eventId: string) => Promise<boolean>);\n}\n\nconst bytes = (b: Buffer): ArrayBuffer =>\n b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer;\n\nfunction rawBodyOf(req: KitExpressRequest): string | ArrayBuffer | undefined {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined;\n const raw = req.rawBody;\n if (Buffer.isBuffer(raw)) return bytes(raw);\n if (typeof raw === 'string') return raw;\n if (Buffer.isBuffer(req.body)) return bytes(req.body);\n if (typeof req.body === 'string') return req.body;\n if (req.body !== undefined && req.body !== null && typeof req.body === 'object') {\n if (Object.keys(req.body as object).length === 0) return undefined;\n warnOnce(\n 'kit-express-parsed-body',\n '[astrasync] expressHandler: the request body was already parsed — re-serialising it. ' +\n 'Mount express.raw() on the checkout routes (or keep req.rawBody) so signatures and ' +\n 'idempotency fingerprints see the exact bytes.'\n );\n return JSON.stringify(req.body);\n }\n return undefined;\n}\n\nconst first = (v: string | string[] | undefined) =>\n (Array.isArray(v) ? v[0] : v)?.split(',')[0]?.trim() || undefined;\n\n/** The origin a request reached us at (see the module comment). */\nfunction originOf(req: KitExpressRequest, publicOrigin?: string): string {\n if (publicOrigin) return publicOrigin.replace(/\\/+$/, '');\n const trustProxy = Boolean(req.app?.get('trust proxy'));\n const host =\n (trustProxy && first(req.headers['x-forwarded-host'])) ||\n req.get?.('host') ||\n (req.headers.host as string | undefined) ||\n 'localhost';\n const proto = (trustProxy && first(req.headers['x-forwarded-proto'])) || req.protocol || 'http';\n return `${proto}://${host}`;\n}\n\nconst isKit = (k: FetchKit): k is FetchKit & ApprovalEventKit =>\n typeof k.applyApprovalEvent === 'function' && (k.protocol === 'ucp' || k.protocol === 'acp');\n\nasync function writeEvents(\n req: KitExpressRequest,\n res: KitExpressResponse,\n secret: string | readonly string[] | undefined,\n kits: ApprovalEventKit[],\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>)\n) {\n const raw = rawBodyOf(req);\n const text =\n raw === undefined ? '' : typeof raw === 'string' ? raw : Buffer.from(raw).toString('utf8');\n const out = secret\n ? await handleApprovalEvents(text, req.headers, secret, { kits, ...(seen && { seen }) })\n : { status: 500, body: { ok: false, error: 'webhook_secret_not_configured' } };\n res.status(out.status);\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(out.body));\n}\n\nexport interface ApprovalEventsHandlerOptions {\n /** The endpoint's webhook secret — or `[current, previous]` during a rotation. */\n secret: string | readonly string[];\n /** The kits events may name, e.g. `[ucpKit, acpKit]`. */\n kits: ApprovalEventKit[];\n /** Dedupe store (default: process-local memory). */\n seen?: SeenEvents | ((eventId: string) => Promise<boolean>);\n}\n\n/**\n * 5.19.0: an Express handler for the endpoint's Events URL, serving both\n * kits: `app.post('/astrasync/events', express.raw({ type: '*\\/*' }),\n * approvalEventsHandler({ secret, kits: [ucpKit, acpKit] }))`. Answers 2xx\n * when done, 401 on a bad signature, 503 to have the platform retry.\n */\nexport function approvalEventsHandler(options: ApprovalEventsHandlerOptions) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n await writeEvents(req, res, options.secret, options.kits, options.seen);\n } catch (err) {\n next(err);\n }\n };\n}\n\n/** An Express handler that runs a checkout kit and writes its Response back. */\nexport function expressHandler(kit: FetchKit, options: ExpressHandlerOptions = {}) {\n return async (\n req: KitExpressRequest,\n res: KitExpressResponse,\n next: (err?: unknown) => void\n ): Promise<void> => {\n try {\n if (options.approvalEventsPath && req.method === 'POST') {\n const p = options.approvalEventsPath.replace(/\\/+$/, '');\n const full = (req.originalUrl ?? req.url).split('?')[0].replace(/\\/+$/, '');\n const local = req.url.split('?')[0].replace(/\\/+$/, '');\n if (full === p || local === p) {\n const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);\n await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);\n return;\n }\n }\n const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v === undefined) continue;\n if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));\n else headers.set(k, v);\n }\n const body = rawBodyOf(req);\n const request = new Request(url, {\n method: req.method,\n headers,\n ...(body !== undefined && { body }),\n });\n const verified = req.agentVerification?.identityVerified\n ? req.agentVerification.agent?.astraId\n : undefined;\n const callerId = options.callerId ? options.callerId(req) : verified;\n const response = await kit.handle(request, callerId ? { callerId } : undefined);\n res.status(response.status);\n response.headers.forEach((value, name) => {\n res.setHeader(name, value);\n });\n res.end(Buffer.from(await response.arrayBuffer()));\n } catch (err) {\n next(err);\n }\n };\n}\n"],"mappings":"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","names":["first","out"]}
|
package/dist/adapters/mcp.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, RequestHandler } from 'express';
|
|
2
|
-
import {
|
|
2
|
+
import { e as VerificationResult, G as GatewayConfig } from '../types-CxjqmPl-.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
|
|
@@ -378,23 +378,116 @@ interface McpMiddlewareOptions extends GatewayConfig {
|
|
|
378
378
|
recordDecisions?: boolean;
|
|
379
379
|
/** Forward runtime challenge (default `true`). */
|
|
380
380
|
enableRuntimeChallenge?: boolean;
|
|
381
|
+
/**
|
|
382
|
+
* 5.19.0: how a dashboard tool rule combines with a code `toolGates`
|
|
383
|
+
* entry for the same tool. With `apiKey` + `counterpartyId` set, the
|
|
384
|
+
* middleware fetches the endpoint's tool rules (`{tool, purpose, action,
|
|
385
|
+
* resource?, observe?}`, edited in the AstraSync dashboard; refreshed every
|
|
386
|
+
* `routesRefreshMs`) and turns each into a gate for that tool.
|
|
387
|
+
*
|
|
388
|
+
* - `'code'` (default): a dashboard rule can TIGHTEN a code gate but never
|
|
389
|
+
* loosen it. For a tool gated in code (object form), a dashboard
|
|
390
|
+
* `observe` rule, or one with no purpose, is ignored; a dashboard rule
|
|
391
|
+
* whose purpose or action differs is evaluated IN ADDITION — both must
|
|
392
|
+
* pass. Tools with no code gate (or a code `'observe'`) follow the
|
|
393
|
+
* dashboard rule.
|
|
394
|
+
* - `'dashboard'`: the dashboard rule wins outright for every tool it
|
|
395
|
+
* names (including `observe`).
|
|
396
|
+
*
|
|
397
|
+
* Rules flagged `legacy: true` (path rules stored on an MCP endpoint before
|
|
398
|
+
* tool rules existed) are ignored. The middleware sends its code-gated tool
|
|
399
|
+
* names on the policy fetch (`?codeGates=`) so the dashboard can mark them.
|
|
400
|
+
*/
|
|
401
|
+
toolGatePrecedence?: 'code' | 'dashboard';
|
|
402
|
+
/** 5.19.0: refresh interval (ms) for the dashboard tool rules. Default 5 minutes. */
|
|
403
|
+
routesRefreshMs?: number;
|
|
404
|
+
/**
|
|
405
|
+
* 5.20.0: tools and JSON-RPC methods that pass through with NO verify-access
|
|
406
|
+
* call at all — not even the audit evaluation `evaluateAlwaysIfCredentialed`
|
|
407
|
+
* adds for credentialed callers. Applies only while the call is not
|
|
408
|
+
* enforced (an `'observe'` gate, or a handshake/introspection method with
|
|
409
|
+
* no `methodGates` entry); a dashboard rule that enforces a listed tool
|
|
410
|
+
* still verifies.
|
|
411
|
+
*
|
|
412
|
+
* Use it for:
|
|
413
|
+
* - `methods`: handshake and introspection (`initialize`, `tools/list`,
|
|
414
|
+
* `resources/list`, `resources/read`) — nothing to attribute, and a
|
|
415
|
+
* backend round trip slows the host's first card fetch;
|
|
416
|
+
* - `tools`: observe tools whose handler calls verify-access itself, so
|
|
417
|
+
* the gate's evaluation would be a second backend verify for the same
|
|
418
|
+
* call (the MCP bridge measured ~3 backend calls per `verify_agent`).
|
|
419
|
+
*
|
|
420
|
+
* Listed calls leave `req.agentVerification` unset.
|
|
421
|
+
*/
|
|
422
|
+
skipEvaluation?: {
|
|
423
|
+
tools?: readonly string[];
|
|
424
|
+
methods?: readonly string[];
|
|
425
|
+
};
|
|
381
426
|
/**
|
|
382
427
|
* Posture when the MCP middleware itself throws an internal error.
|
|
383
|
-
* Default `'open'` for backward compatibility in SDK 2.4.13. Shadow logs
|
|
384
|
-
* record what WOULD be denied with `failOnError: 'closed'` so merchants
|
|
385
|
-
* can grep correlationId for impact analysis during the observation
|
|
386
|
-
* window. Default flips to `'closed'` in a follow-up release.
|
|
387
428
|
*
|
|
388
|
-
* 5.
|
|
389
|
-
* JSON-RPC error (HTTP 503 +
|
|
390
|
-
* identity-shaped 401
|
|
429
|
+
* 5.19.0 (behaviour change): default `'closed'` — an internal throw answers
|
|
430
|
+
* the retryable verification-unavailable JSON-RPC error (HTTP 503 +
|
|
431
|
+
* Retry-After, code -32003; never an identity-shaped 401) and the tool
|
|
432
|
+
* handler is not called. `'open'` (pass the call through unverified) is
|
|
433
|
+
* an explicit opt-in. The `[SHADOW] would-have-denied` log line is still
|
|
434
|
+
* written on every throw, with the correlationId.
|
|
391
435
|
*/
|
|
392
436
|
failOnError?: 'open' | 'closed';
|
|
393
437
|
}
|
|
438
|
+
/** 5.19.0: a dashboard tool rule as the MCP middleware evaluates it. */
|
|
439
|
+
interface DashboardToolRule {
|
|
440
|
+
tool: string;
|
|
441
|
+
purpose?: string;
|
|
442
|
+
action?: string;
|
|
443
|
+
resource?: string;
|
|
444
|
+
observe?: boolean;
|
|
445
|
+
}
|
|
446
|
+
/** The usable tool rules in a fetched policy (tool key set, not legacy), by tool name. */
|
|
447
|
+
declare function dashboardToolRules(routes: ReadonlyArray<{
|
|
448
|
+
tool?: string;
|
|
449
|
+
legacy?: boolean;
|
|
450
|
+
purpose?: string;
|
|
451
|
+
action?: string;
|
|
452
|
+
resource?: string;
|
|
453
|
+
observe?: boolean;
|
|
454
|
+
}>): Map<string, DashboardToolRule>;
|
|
455
|
+
type GateSource = 'toolGate' | 'methodGate' | 'dashboard' | 'tier' | 'discovery_default';
|
|
456
|
+
/**
|
|
457
|
+
* Resolve whether a parsed MCP request is enforced, and the gate config to
|
|
458
|
+
* apply. SDK 5.0.0 removed the access-level band — a gate's only decision is
|
|
459
|
+
* enforce vs observe.
|
|
460
|
+
*
|
|
461
|
+
* Precedence:
|
|
462
|
+
* 1. Non-tools/call without `methodGates` entry → pass through
|
|
463
|
+
* (`discovery_default`). Discovery is open — agents must list tools
|
|
464
|
+
* before invoking them.
|
|
465
|
+
* 2. `methodGates[method]` — per-method override (`'observe'` or a config)
|
|
466
|
+
* 3. tools/call: the code `toolGates[toolName]` and the dashboard tool rule
|
|
467
|
+
* (5.19.0) — see `toolGatePrecedence`. With the default `'code'`, a
|
|
468
|
+
* dashboard rule only tightens a code gate: it is ignored when it is
|
|
469
|
+
* `observe` or has no purpose, and evaluated as an `additional` gate
|
|
470
|
+
* (both must pass) when its purpose/action differ.
|
|
471
|
+
* 4. `mcpDefaultEnforced` — built-in default (tools/call → enforced)
|
|
472
|
+
*
|
|
473
|
+
* The `source` field is logged for forensics / activity-feed attribution.
|
|
474
|
+
*/
|
|
475
|
+
declare function resolveGate(parsed: ParsedMcpRequest, opts: {
|
|
476
|
+
toolGates?: Record<string, ToolGate>;
|
|
477
|
+
methodGates?: Record<string, ToolGate>;
|
|
478
|
+
dashboardRules?: Map<string, DashboardToolRule>;
|
|
479
|
+
toolGatePrecedence?: 'code' | 'dashboard';
|
|
480
|
+
}): {
|
|
481
|
+
enforce: boolean;
|
|
482
|
+
config?: ToolGateConfig;
|
|
483
|
+
/** 5.19.0: a dashboard gate evaluated in addition to `config` (both must pass). */
|
|
484
|
+
additional?: ToolGateConfig;
|
|
485
|
+
source: GateSource;
|
|
486
|
+
};
|
|
394
487
|
/**
|
|
395
488
|
* Create the MCP middleware. Attach AFTER `express.json()` — the body must
|
|
396
489
|
* already be a parsed object.
|
|
397
490
|
*/
|
|
398
491
|
declare function createMcpMiddleware(options: McpMiddlewareOptions): RequestHandler;
|
|
399
492
|
|
|
400
|
-
export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGate, type ToolGateConfig, createMcpMiddleware, isVerifiedHopValidFor, mcpDefaultEnforced, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
|
|
493
|
+
export { type DashboardToolRule, MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGate, type ToolGateConfig, createMcpMiddleware, dashboardToolRules, isVerifiedHopValidFor, mcpDefaultEnforced, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, resolveGate, serializeVerifiedHop };
|