@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +217 -4
  3. package/dist/adapter-interface/interface.d.mts +2 -2
  4. package/dist/adapter-interface/interface.d.ts +2 -2
  5. package/dist/adapters/express.d.mts +9 -1
  6. package/dist/adapters/express.d.ts +9 -1
  7. package/dist/adapters/express.js +160 -52
  8. package/dist/adapters/express.js.map +1 -1
  9. package/dist/adapters/express.mjs +160 -52
  10. package/dist/adapters/express.mjs.map +1 -1
  11. package/dist/adapters/http-pdlss.d.mts +7 -1
  12. package/dist/adapters/http-pdlss.d.ts +7 -1
  13. package/dist/adapters/http-pdlss.js +18 -2
  14. package/dist/adapters/http-pdlss.js.map +1 -1
  15. package/dist/adapters/http-pdlss.mjs +16 -1
  16. package/dist/adapters/http-pdlss.mjs.map +1 -1
  17. package/dist/adapters/kit-express.d.mts +93 -1
  18. package/dist/adapters/kit-express.d.ts +93 -1
  19. package/dist/adapters/kit-express.js +181 -2
  20. package/dist/adapters/kit-express.js.map +1 -1
  21. package/dist/adapters/kit-express.mjs +180 -2
  22. package/dist/adapters/kit-express.mjs.map +1 -1
  23. package/dist/adapters/mcp.d.mts +102 -9
  24. package/dist/adapters/mcp.d.ts +102 -9
  25. package/dist/adapters/mcp.js +242 -19
  26. package/dist/adapters/mcp.js.map +1 -1
  27. package/dist/adapters/mcp.mjs +240 -19
  28. package/dist/adapters/mcp.mjs.map +1 -1
  29. package/dist/adapters/nextjs.d.mts +1 -1
  30. package/dist/adapters/nextjs.d.ts +1 -1
  31. package/dist/adapters/nextjs.js +97 -14
  32. package/dist/adapters/nextjs.js.map +1 -1
  33. package/dist/adapters/nextjs.mjs +97 -14
  34. package/dist/adapters/nextjs.mjs.map +1 -1
  35. package/dist/adapters/sdk.d.mts +101 -2
  36. package/dist/adapters/sdk.d.ts +101 -2
  37. package/dist/adapters/sdk.js +199 -12
  38. package/dist/adapters/sdk.js.map +1 -1
  39. package/dist/adapters/sdk.mjs +199 -12
  40. package/dist/adapters/sdk.mjs.map +1 -1
  41. package/dist/agent/index.d.mts +1 -1
  42. package/dist/agent/index.d.ts +1 -1
  43. package/dist/agent/index.js +1 -1
  44. package/dist/agent/index.js.map +1 -1
  45. package/dist/agent/index.mjs +1 -1
  46. package/dist/agent/index.mjs.map +1 -1
  47. package/dist/bin/astrasync-claude-hook.js +94 -16
  48. package/dist/bin/astrasync-codex-hook.js +94 -16
  49. package/dist/bin/astrasync-guard.js +94 -16
  50. package/dist/bin/astrasync.js +127 -16
  51. package/dist/browser/background.js +94 -16
  52. package/dist/browser/background.js.map +1 -1
  53. package/dist/browser/background.mjs +94 -16
  54. package/dist/browser/background.mjs.map +1 -1
  55. package/dist/cli/index.js +1 -1
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/index.mjs +1 -1
  58. package/dist/cli/index.mjs.map +1 -1
  59. package/dist/codex/index.js +94 -16
  60. package/dist/codex/index.js.map +1 -1
  61. package/dist/codex/index.mjs +94 -16
  62. package/dist/codex/index.mjs.map +1 -1
  63. package/dist/cursor/extension.js +94 -16
  64. package/dist/cursor/extension.js.map +1 -1
  65. package/dist/cursor/extension.mjs +94 -16
  66. package/dist/cursor/extension.mjs.map +1 -1
  67. package/dist/edge-config.d.mts +1 -1
  68. package/dist/edge-config.d.ts +1 -1
  69. package/dist/edge-config.js +1 -1
  70. package/dist/edge-config.js.map +1 -1
  71. package/dist/edge-config.mjs +1 -1
  72. package/dist/edge-config.mjs.map +1 -1
  73. package/dist/edge-core/index.d.mts +1 -1
  74. package/dist/edge-core/index.d.ts +1 -1
  75. package/dist/edge-core/index.js +69 -12
  76. package/dist/edge-core/index.js.map +1 -1
  77. package/dist/edge-core/index.mjs +69 -12
  78. package/dist/edge-core/index.mjs.map +1 -1
  79. package/dist/gateway/gateway.d.mts +2 -2
  80. package/dist/gateway/gateway.d.ts +2 -2
  81. package/dist/gateway/gateway.js +94 -16
  82. package/dist/gateway/gateway.js.map +1 -1
  83. package/dist/gateway/gateway.mjs +94 -16
  84. package/dist/gateway/gateway.mjs.map +1 -1
  85. package/dist/git-trigger/git-hooks.d.mts +1 -1
  86. package/dist/git-trigger/git-hooks.d.ts +1 -1
  87. package/dist/index.d.mts +706 -34
  88. package/dist/index.d.ts +706 -34
  89. package/dist/index.js +1391 -205
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +1374 -195
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/local-evaluator/evaluator.d.mts +2 -2
  94. package/dist/local-evaluator/evaluator.d.ts +2 -2
  95. package/dist/registration/index.d.mts +11 -2
  96. package/dist/registration/index.d.ts +11 -2
  97. package/dist/registration/index.js +1 -1
  98. package/dist/registration/index.js.map +1 -1
  99. package/dist/registration/index.mjs +1 -1
  100. package/dist/registration/index.mjs.map +1 -1
  101. package/dist/transport/index.d.mts +1 -1
  102. package/dist/transport/index.d.ts +1 -1
  103. package/dist/transport/index.js +3 -2
  104. package/dist/transport/index.js.map +1 -1
  105. package/dist/transport/index.mjs +3 -2
  106. package/dist/transport/index.mjs.map +1 -1
  107. package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
  108. package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
  109. package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
  110. package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
  111. package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
  112. package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
  113. package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
  114. package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
  115. package/dist/ui/index.d.mts +1 -1
  116. package/dist/ui/index.d.ts +1 -1
  117. package/dist/verify.d.mts +50 -4
  118. package/dist/verify.d.ts +50 -4
  119. package/dist/verify.js +93 -14
  120. package/dist/verify.js.map +1 -1
  121. package/dist/verify.mjs +91 -14
  122. package/dist/verify.mjs.map +1 -1
  123. package/dist/webhooks.d.mts +16 -3
  124. package/dist/webhooks.d.ts +16 -3
  125. package/dist/webhooks.js +24 -13
  126. package/dist/webhooks.js.map +1 -1
  127. package/dist/webhooks.mjs +24 -13
  128. package/dist/webhooks.mjs.map +1 -1
  129. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,238 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
8
8
  Full architectural context for each release lives in the platform spec at
9
9
  `docs/research/adapter-architecture-technical-requirements.md`.
10
10
 
11
+ ## [5.20.0] — 2026-10-19 — fewer backend calls, recoverable declined holds, endpoint ops from code
12
+
13
+ ### Changed (behaviour)
14
+
15
+ - **Declined kit holds are `recoverable`.** After a `step_up.denied`,
16
+ `step_up.expired` or `step_up.lapsed` event the UCP kit's
17
+ `approval_<reason>` message has severity `recoverable` and the ACP kit's
18
+ resolution is `recoverable` (both were `requires_buyer_review`): the
19
+ session is back in `ready_for_complete` / `ready_for_payment` and payable
20
+ with no further buyer step, matching the 5.19.0 `payment_failed` fix. The
21
+ code is the signal — platforms must not auto-retry on `approval_denied`.
22
+ The held response keeps `requires_buyer_review`.
23
+ - **No decision POST on a shared verdict.** The express and MCP middlewares
24
+ no longer record a second decision when the verdict came from the cache or
25
+ from a concurrent identical call (the session already has its decision).
26
+ - **Single-flight verify.** Concurrent identical cache misses share one
27
+ verify-access call; followers get a copy with their own `attemptId`.
28
+ `bypassCache` re-verifies and uncacheable requests never join a flight.
29
+
30
+ ### Added
31
+
32
+ - `createMcpMiddleware({ skipEvaluation: { tools?, methods? } })`: unenforced
33
+ listed tools / methods pass through with no verify-access call, even with
34
+ `evaluateAlwaysIfCredentialed` (handshake, listing, `resources/read`, and
35
+ observe tools that verify in their own handler).
36
+ - `createMcpMiddleware` warns once when its `counterpartyId` is not an
37
+ `mcp_server` endpoint (the routes fetch now carries the endpoint's
38
+ `counterpartyType`; `fetchRoutes(…, { onEndpointType })`). Tool rules need a
39
+ separate `mcp_server` endpoint for the site's MCP server.
40
+ - `isSharedVerdict(result)` and `onVerificationResult(listener)` (root
41
+ exports).
42
+ - **MCP middleware carries the attempt chain** (express / Next.js / edge
43
+ parity): `X-Astra-Attempt-Id`, else `params._meta.astrasync.attemptId`, is
44
+ sent as the verify `attemptId` (malformed values dropped). A tool call the
45
+ AstraSync bridge proved (acting proof bound to the attempt) is no longer
46
+ held at an `mcp_server` venue.
47
+ - `GatewayConfig.actingProof?: { connectorToken }` is sent as `actingProof`
48
+ in the verify-access body (honoured only from a first-party AstraSync
49
+ service caller; a verify carrying it is never cached).
50
+ - `VerificationGatewayClient.updateEndpoint(id, { eventsUrl, healthCheckUrl,
51
+ skillUrl, catalogUrl, checkoutUrl })` → `PATCH /api/endpoints/:id`
52
+ (operational URLs only, API key of the owner or an org member) and
53
+ `sendTestEvent(id, { type, approvalId? })` → `POST
54
+ /api/endpoints/:id/events/test` (one signed step-up event delivered now;
55
+ returns `{ delivered, status, responseCode, error, eventId, type,
56
+ approvalId }`). Types `EndpointOperationalPatch`, `TestEventType`,
57
+ `TestEventResult`.
58
+ - Kits' `pollHeldSessions` releases a `lapsed` approval (UCP and ACP);
59
+ `StepUpApprovalStatus` gains `'lapsed'`.
60
+ - ACP: `approval_required` / `approval_denied` / `approval_expired` /
61
+ `approval_lapsed` join the passthrough message codes.
62
+ - UCP profiles' `signing_keys` are accepted alongside `keys` (the kit's
63
+ platform-signature check and the profile parser read both, skipping
64
+ `use: "enc"` keys); `ucpProfileKeys(profile)` is exported from the root.
65
+ - **Kits' catalog item carries the product image.** `CatalogItem` (exported
66
+ as `UcpCatalogItem`) gains optional `image_url` (an absolute http(s) URL)
67
+ and `description`. The UCP kit keeps them on `line_items[].item`; the ACP
68
+ kit carries them on each line's `item` and on the order's line items. A
69
+ relative or invalid `image_url` is dropped when the kit prices the line.
70
+ - **`fetchRoutes` sends an `X-AstraSync-SDK-Version` header** on the routes
71
+ fetch, so the endpoint's SDK version on the dashboard updates from the
72
+ 5-minute routes refresh (MCP traffic may never send a verify-access naming
73
+ the `mcp_server` endpoint). The URL is unchanged, so mocks matching
74
+ `…/routes` keep working. Round 29.1 backends also read the version from
75
+ the `User-Agent` for older SDKs.
76
+ - `VerificationWarning` type (root export) for the result's `warnings[]`.
77
+
78
+ ### Fixed
79
+
80
+ - **The verification result carries verify-access `warnings[]`** (e.g.
81
+ `events_url_missing`). It reached the wire, but both result builders
82
+ dropped it.
83
+ - **The UCP kit verifies signed platform requests with `signing_keys` too.**
84
+ Only the mandate-key path read them, so a platform profile that published
85
+ only `signing_keys` got `key_not_found` on its request signatures.
86
+
87
+ ### Changed (types)
88
+
89
+ - `registration/types.ts`: the trust-score fields on registration and
90
+ verification responses are optional — the platform no longer returns an
91
+ agent's score to the agent or the public (counterparties still see it in
92
+ verify-access).
93
+
94
+ ## [5.19.0] — 2026-10-18 — approval events for held kit checkouts, MCP tool rules, fail-closed default
95
+
96
+ ### Changed (behaviour)
97
+
98
+ - **`failOnError` defaults to `'closed'`** in `createMcpMiddleware` and the
99
+ express `createMiddleware`. When the middleware itself throws, the request
100
+ now gets `503` + `Retry-After` + `VERIFICATION_UNAVAILABLE` (MCP: JSON-RPC
101
+ `-32003`) and the handler is not called. `'open'` (pass through unverified)
102
+ stays available as an explicit opt-in. The `[SHADOW] would-have-denied`
103
+ line is still logged on every throw. (Audit F-A1-06.)
104
+ - **ACP `complete` validates `payment_data`.** It must be one object (not an
105
+ array) whose `handler_id` is a declared handler's `id` or `name` (e.g.
106
+ `voucher` / `ai.astrasync.voucher`); a missing handler id, an unknown handler
107
+ or an array is `400 invalid` with `param: '$.payment_data.handler_id'` and
108
+ nothing is verified.
109
+ - **Checkout-kit `PUT` / update on a held session is refused** (`409
110
+ checkout_awaiting_approval`): the owner is approving exactly that cart.
111
+ - **A declined card leaves the kit session payable with a `recoverable`
112
+ message** — UCP `payment_failed` (a standard error; `requires_buyer_input`
113
+ implied a `requires_escalation` handoff) back in `ready_for_complete`, ACP
114
+ `payment_declined` back in `ready_for_payment`. Completing the same session
115
+ again (UCP: a fresh `Idempotency-Key`) verifies the same cart; the platform
116
+ records a new order beside the declined one and settles it once.
117
+ - **Held kit responses are no longer stored under an `Idempotency-Key`**, so a
118
+ re-drive with the same key after the approval reaches the kit instead of
119
+ replaying the held response.
120
+
121
+ ### Added
122
+
123
+ - **Owner approval completes a held checkout (UCP + ACP kits, item 27).** At a
124
+ hold the kit stores `_approvalId`, `_astraId`, `_attemptId`, the held
125
+ `line_items` / `totals` and the non-secret payment input. New:
126
+ - `kit.resumeApproved(sessionId)` — `validateCart` first (an invalid cart
127
+ returns before AstraSync: the approval is not redeemed, nothing charged),
128
+ then `verifyPurchase` with the same cart id and the stored held amount
129
+ (`catalog()` is never re-run), then settle → report → fulfil → order
130
+ webhook. No agent call.
131
+ - `kit.resumeDeclined(sessionId, reason)` — back to `ready_for_complete` /
132
+ `ready_for_payment` with a message.
133
+ - `handleApprovalEvents(rawBody, headers, secret | secret[], { kits, seen? })`
134
+ — verifies `X-AstraSync-Signature`, routes the event (`{ eventId, type:
135
+ 'step_up.approved' | 'step_up.denied' | 'step_up.expired' |
136
+ 'step_up.lapsed', approvalId, checkoutSessionId, kitSessionId?, protocol?,
137
+ counterpartyId, agentAstraId, transactionValue, currency, decidedAt,
138
+ redeemBy? }`) by `protocol` / `kitSessionId`, and answers `{ status, body
139
+ }`: 2xx at a terminal outcome (the event id is marked seen only then), 503
140
+ on a transient failure or a non-terminal state, 401 on a bad signature.
141
+ `MemorySeenEvents` / `SeenEvents` for dedupe.
142
+ - `approvalEventsHandler({ secret, kits, seen? })` and
143
+ `expressHandler(kit, { approvalEventsPath, webhookSecret, approvalEventKits?,
144
+ seenEvents? })` mount it on Express.
145
+ - `CheckoutStore.findByCheckoutSessionId?(id)` and `listHeld?()` (and the
146
+ ACP store equivalents); the memory stores implement them.
147
+ - `kit.pollHeldSessions({ sessionIds?, apiBaseUrl? })` — a fallback for
148
+ merchants that can't receive events.
149
+ - Card declined after redemption: the session is payable again with the
150
+ decline message; re-completing the same cart at the same amount re-sends
151
+ the same cart id and attempt id (the platform chain-passes the consumed
152
+ approval until its `redeemBy`).
153
+ - **`agentArtifacts` and `metadata` on kit confirms (item 28).**
154
+ `verifyPurchase` receives `agentArtifacts` (UCP RFC 9421-signed: the covered
155
+ headers — never `authorization` / `cookie` — the method, the public URL and
156
+ the raw-body `contentDigest`; otherwise `{ unsigned: true }`; ACP always `{
157
+ unsigned: true }`, its bearer is never forwarded) and `metadata: {
158
+ kitSessionId, protocol }`. `confirmCheckout` forwards both on verify-access.
159
+ New types `AgentArtifacts`, `CheckoutMetadata`.
160
+ - **`validateCart(session) → KitMessage[]`** on both kits (item 29) — runs on
161
+ create, update and complete before AstraSync; errors keep the session
162
+ `incomplete` (UCP, `recoverable` with `path`) / `not_ready_for_payment` (ACP,
163
+ `invalid` with `param`). `catalog(itemId, quantity)` also receives the
164
+ quantity.
165
+ - **`publicOrigin`** on both kits (item 24), used by `expressHandler` to
166
+ rebuild the request URL (UCP default: the origin of `profile.endpoint`; ACP:
167
+ `X-Forwarded-Proto` / `X-Forwarded-Host` only under Express `trust proxy`).
168
+ - **ACP `links`** option, rendered on every session (item 26).
169
+ - **`reportRetry` and `onReportError`** on both kits (item 21): settlement
170
+ reports retry on network / 429 / 5xx (3 attempts with backoff); a final
171
+ failure — or an order webhook that fails after its retries — calls
172
+ `onReportError(err, { sessionId, orderId })` (default `console.error`).
173
+ Without an `orderId` the report uses the confirm's verification `sessionId`.
174
+ - **MCP tool rules from the dashboard (item 18).** With `apiKey` +
175
+ `counterpartyId`, `createMcpMiddleware` fetches the endpoint's tool rules
176
+ (`{ tool, purpose, action, resource?, observe? }`) and refreshes them every
177
+ `routesRefreshMs` (default 5 min, shared cache helper with the express route
178
+ policy). A dashboard rule only tightens a code `toolGates` entry (`observe` /
179
+ no-purpose ignored; a differing purpose or action is checked in addition)
180
+ unless `toolGatePrecedence: 'dashboard'`; tools with no code gate follow the
181
+ dashboard rule; `legacy: true` path rules are ignored. Code-gated tool names
182
+ are sent on the fetch as `?codeGates=`. `fetchRoutes(config, id, {
183
+ codeGates })`.
184
+ - **`purposeSource`, `requestMethod`, `requestPath` on verify-access (item 20)** from the express and Next.js middlewares, so verify-access can keep a
185
+ dashboard route's purpose (e.g. `shopping.purchase`) on an MPP paid leg
186
+ after checking it against the endpoint's stored rules. `req.rawBody` (a
187
+ Buffer or string set by an `express.json({ verify })` hook) is documented as
188
+ the forwarded raw body.
189
+ - **`client.quoteCheckout()`** — the quote leg (`commercePhase: 'quote'`: peek
190
+ only, no order, no approval redeemed). `verify()` accepts `commercePhase:
191
+ 'quote'`.
192
+ - **`verifyAstraSyncWebhook` takes `secret | secret[]`** and accepts a `v1old`
193
+ signature value (the previous secret signs it for 24 h after a rotation);
194
+ `signAstraSyncWebhook(..., { previousSecret })`. `abandonIdempotent()`
195
+ releases a key without storing a non-terminal response.
196
+ - The `behaviour.*` failure family (`behaviour.limit_probing`,
197
+ `behaviour.unattributed_limit_check`) is documented on `AccessFailure`.
198
+ - **`result.agent.trustScore` is populated again** (the field existed; the
199
+ platform had stopped sending it). Rule: counterparties may see an agent's
200
+ trust score; the agent itself never does. verify-access returns the real
201
+ score (0–100) to an authenticated counterparty — merchant and service keys,
202
+ the kits, the edge adapter, AstraSync's bridge — and withholds it from a
203
+ keyless caller and from the agent itself (an agent session as that agent, or
204
+ its owner's key on a call that names no endpoint of the owner's). Never
205
+ relay it to the agent.
206
+
207
+ ### Fixed
208
+
209
+ - **A verified agent's denied call keeps `result.agent`.** The deny branch
210
+ now maps the `agent` block the platform sends on a deny (as on a grant), so
211
+ a verified-but-denied agent is still identified (the MCP bridge keys its
212
+ per-agent budget on it); before, it fell back to the per-IP budget.
213
+ - **A route's `minTrustScore` floor now gates on the real score and fails
214
+ closed when the score is absent** (express adapter). It had been passing
215
+ every agent since the platform stopped sending the score. The online-mode
216
+ decision `reason` no longer embeds the score (it is relayable text; the
217
+ number stays on `decision.trustScore`).
218
+
219
+ - **A held kit checkout could never complete without the agent:** the bridge's
220
+ re-drive got the cached held response back, and nothing resumed the session
221
+ when the owner approved. See Added (item 27).
222
+ - **`fulfil` now receives `settled`** (the processor's settled outcome),
223
+ including from `resolvePending` (item 22).
224
+ - **MPP replay protection keyed on the challenge id.** The replay key was the
225
+ body digest plus a `nonce` that mppx challenges don't carry, so two distinct
226
+ challenges over identical bodies inside the 60 s window were refused as a
227
+ replay. The key now includes the challenge's `id`; presenting the same
228
+ challenge twice is still refused.
229
+ - **`resolvePending` finishes a session once** (item 23): it claims
230
+ `complete_in_progress` → `finishing` (rendered as `complete_in_progress`);
231
+ the losing call is a no-op, and if finishing throws the session returns to
232
+ `complete_in_progress` with its pending settlement intact so the next call
233
+ retries.
234
+ - **Kit settlement reports were dropped silently** on any error (item 21).
235
+
236
+ ### Deprecated
237
+
238
+ - A `verify()` with a `transactionValue` and no `commercePhase` is a purchase
239
+ leg (it records a pending order and redeems a matching approval). 5.19.0
240
+ warns once per process; **6.0 changes the default to `'quote'`** — use
241
+ `quoteCheckout()` for checks and `confirmCheckout()` for purchases.
242
+
11
243
  ## [5.18.1] — 2026-10-17 — guard browse --open; proxy help page; browser command printed
12
244
 
13
245
  ### Added
package/README.md CHANGED
@@ -285,7 +285,7 @@ mcp.use(
285
285
  apiBaseUrl: 'https://astrasync.ai/api',
286
286
  apiKey: process.env.ASTRASYNC_API_KEY,
287
287
  counterpartyId: 'ASTRAE-mcp...',
288
- toolGates: { start_checkout: 'standard' },
288
+ toolGates: { start_checkout: { purpose: 'shopping', action: 'shopping.purchase' } },
289
289
  })
290
290
  );
291
291
 
@@ -408,6 +408,78 @@ dashboard session token authenticates. A request that offers only
408
408
  first-step sign-in tokens (before the second factor), refresh tokens and
409
409
  passkey elevation tokens are refused with 401.
410
410
 
411
+ ## Failure posture (`failOnError`, 5.19.0)
412
+
413
+ When the middleware itself throws (a parsing failure, a proxy-state surprise),
414
+ the express and MCP middlewares now **fail closed by default**: the request
415
+ gets `503` + `Retry-After` + `VERIFICATION_UNAVAILABLE` (MCP: JSON-RPC
416
+ `-32003`), never an identity-shaped 401, and your handler is not called. Set
417
+ `failOnError: 'open'` explicitly to pass the request through unverified
418
+ instead. A `[SHADOW] would-have-denied … correlationId=…` line is logged on
419
+ every throw either way.
420
+
421
+ ## MCP tool rules (5.19.0)
422
+
423
+ With `apiKey` + `counterpartyId` set, `createMcpMiddleware` fetches the
424
+ endpoint's **tool rules** from the dashboard (`{ tool, purpose, action,
425
+ resource?, observe? }`, refreshed every `routesRefreshMs`, default 5 minutes —
426
+ the same cache the express route policy uses) and turns each into a gate for
427
+ that `tools/call` tool. A rule edited in the dashboard takes effect after the
428
+ refresh, with no deploy.
429
+
430
+ - **Tighten-only by default.** For a tool you gate in code (`toolGates` object
431
+ form), a dashboard `observe` rule, or one with no purpose, is ignored; a
432
+ dashboard rule whose purpose or action differs is evaluated **in addition**
433
+ (both must pass). Tools with no code gate (or a code `'observe'`) follow the
434
+ dashboard rule.
435
+ - `toolGatePrecedence: 'dashboard'` lets the dashboard rule win outright.
436
+ - Path rules stored on an MCP endpoint before tool rules existed come back
437
+ `legacy: true` and are ignored — replace them with tool rules (a `* /mcp`
438
+ rule's `minAccessLevel` was removed in 5.0 and does nothing).
439
+ - The middleware sends its code-gated tool names on the fetch
440
+ (`?codeGates=a,b`), so the dashboard marks those tools "gated in code — a
441
+ rule here can only add checks".
442
+ - **One endpoint per surface (5.20.0).** Tool rules live on an `mcp_server`
443
+ endpoint. If your site and its MCP server share one (website) endpoint,
444
+ register a separate MCP endpoint for the MCP server — same owner; an API key
445
+ that operates your site endpoint may create it with `POST /api/endpoints` —
446
+ and use its id as the middleware's `counterpartyId`. The middleware warns
447
+ once (`console.warn`) when its `counterpartyId` is not an `mcp_server`
448
+ endpoint.
449
+
450
+ ## Fewer backend calls (5.20.0)
451
+
452
+ - **Single-flight:** concurrent identical cache misses share one
453
+ verify-access call.
454
+ - **No decision POST on a shared verdict:** a cache hit (or a single-flight
455
+ follower) no longer re-records the decision for the original session
456
+ (`isSharedVerdict(result)`).
457
+ - **`skipEvaluation: { tools?, methods? }`** (MCP middleware): unenforced
458
+ listed calls pass through with no backend call — handshake / listing /
459
+ `resources/read`, and observe tools whose handler calls verify-access
460
+ itself.
461
+ - **`onVerificationResult(listener)`** observes each verdict this process
462
+ fetched (the AstraSync bridge sizes its per-agent rate limit from it).
463
+
464
+ ## Route purpose on a paid leg (5.19.0)
465
+
466
+ The express and Next.js middlewares send `purposeSource` (`'route_config'` |
467
+ `'header'` | `'body'` | `'default'`) plus `requestMethod` / `requestPath` on
468
+ every verify-access call. When a dashboard route rule set the purpose (e.g.
469
+ `POST /api/checkout/* → shopping.purchase`), verify-access keeps it on an MPP
470
+ paid leg (`Authorization: Payment …`) instead of replacing it with
471
+ `commerce.payment.execute` — after checking that a **stored** rule on your
472
+ endpoint matches that method and path with the same purpose and action. The
473
+ claim is checked, never trusted.
474
+
475
+ The gateway forwards the exact request bytes when your JSON parser keeps them
476
+ on `req.rawBody` (a Buffer or string), so body digests match without mounting
477
+ `express.raw`:
478
+
479
+ ```ts
480
+ app.use(express.json({ verify: (req, _res, buf) => ((req as any).rawBody = buf) }));
481
+ ```
482
+
411
483
  ## Verification Response
412
484
 
413
485
  ```typescript
@@ -565,7 +637,15 @@ The first-party commerce rail: an agent shops under its ASTRA-id, holds NO
565
637
  payment credential, and the platform settles from the owner's saved
566
638
  instrument. Two phases discriminate quote from money movement:
567
639
 
568
- - `commercePhase: 'quote'` (or unphased) — verification only, never settles.
640
+ - `commercePhase: 'quote'` — a price / limit check: peek only, no order, no
641
+ approval redeemed. Use `client.quoteCheckout({ astraId, transactionValue,
642
+ currency, checkoutSessionId? })` (5.19.0). A plain `verify()` with a
643
+ `transactionValue` and no `commercePhase` is a **purchase leg** (it records
644
+ a pending order and redeems a matching approval); 5.19.0 warns once when you
645
+ call it that way, and the default becomes `'quote'` in 6.0. Quotes count
646
+ toward limit-probing detection (`behaviour.limit_probing` /
647
+ `behaviour.unattributed_limit_check`): quote what the buyer asked for, not a
648
+ ladder of values.
569
649
  - `commercePhase: 'confirm'` — the ONLY leg that can settle. Use
570
650
  `client.confirmCheckout({ astraId, transactionValue, currency,
571
651
  checkoutSessionId, checkoutItems, counterpartyUrl })`; `checkoutSessionId`
@@ -630,6 +710,7 @@ catalog, settlement and fulfilment. Both are fetch-style
630
710
  import {
631
711
  createUcpCheckoutKit,
632
712
  expressHandler,
713
+ approvalEventsHandler,
633
714
  RedisIdempotencyStore,
634
715
  } from '@astrasyncai/verification-gateway';
635
716
 
@@ -641,7 +722,9 @@ const ucp = createUcpCheckoutKit({
641
722
  orders: true, // dev.ucp.shopping.order: GET /orders/{id} + order webhooks
642
723
  },
643
724
  currency: 'USD',
644
- catalog: async (id) => db.items.find(id),
725
+ catalog: async (id, quantity) => db.items.find(id),
726
+ // 5.19.0: cart-level checks (stock, caps) on create / update / complete.
727
+ validateCart: async (session) => stockMessages(session),
645
728
  verifyPurchase: (ctx) =>
646
729
  client.confirmCheckout({
647
730
  astraId: ctx.astraId,
@@ -650,6 +733,8 @@ const ucp = createUcpCheckoutKit({
650
733
  checkoutJwt: ctx.checkoutJwt,
651
734
  transactionValue: ctx.valueMajor,
652
735
  currency: 'USD',
736
+ agentArtifacts: ctx.agentArtifacts, // 5.19.0: attribution
737
+ metadata: ctx.metadata, // 5.19.0: { kitSessionId, protocol } for approval events
653
738
  }),
654
739
  settle: async ({ result }) => chargeWithYourProcessor(result), // settled | pending | failed
655
740
  reportSettlement: (r) => client.reportSettlement(r),
@@ -667,8 +752,99 @@ const ucp = createUcpCheckoutKit({
667
752
 
668
753
  app.get('/.well-known/ucp', (_req, res) => res.json(ucp.profile));
669
754
  app.use('/ucp/v1', express.raw({ type: '*/*' }), expressHandler(ucp));
755
+ // 5.19.0: owner-approval events for held checkouts (both kits, one URL —
756
+ // set it as the endpoint's Events URL in the dashboard).
757
+ app.post(
758
+ '/astrasync/events',
759
+ express.raw({ type: '*/*' }),
760
+ approvalEventsHandler({ secret: process.env.ASTRASYNC_WEBHOOK_SECRET!, kits: [ucp, acp] })
761
+ );
762
+ ```
763
+
764
+ **Owner approval completes a held checkout (5.19.0).** When a purchase is
765
+ held, the kit stores the approval id, the agent, the attempt chain, the held
766
+ cart (`line_items` / `totals`) and the non-secret payment input (the UCP
767
+ instrument descriptor, the ACP `payment_data` fields minus anything that looks
768
+ like a credential). When the owner decides, AstraSync POSTs a signed event to
769
+ your Events URL:
770
+
771
+ ```
772
+ X-AstraSync-Signature: t=<unix>,v1=<hex>[,v1old=<hex>]
773
+ { "eventId", "type": "step_up.approved" | "step_up.denied" | "step_up.expired" | "step_up.lapsed",
774
+ "approvalId", "checkoutSessionId", "kitSessionId"?, "protocol"?: "ucp" | "acp",
775
+ "counterpartyId", "agentAstraId", "transactionValue", "currency", "decidedAt", "redeemBy"? }
670
776
  ```
671
777
 
778
+ `handleApprovalEvents(rawBody, headers, secret | secret[], { kits, seen? })`
779
+ (or `approvalEventsHandler` / `expressHandler(kit, { approvalEventsPath,
780
+ webhookSecret })`) verifies it and routes it by `protocol` / `kitSessionId`,
781
+ falling back to `store.findByCheckoutSessionId(checkoutSessionId)`:
782
+
783
+ - **approved** → `kit.resumeApproved(sessionId)`: `validateCart` first (an
784
+ invalid cart returns with its message before AstraSync, so the approval is
785
+ not redeemed and nothing is charged), then `verifyPurchase` with the same
786
+ cart id and the **stored held amount** (`catalog()` is never re-run, so a
787
+ price change can't break the exact-value redemption), then settle → report
788
+ → fulfil → order webhook. No agent call.
789
+ - **denied / expired / lapsed** → `kit.resumeDeclined(sessionId, reason)`:
790
+ back to `ready_for_complete` / `ready_for_payment` with an
791
+ `approval_denied` / `approval_expired` / `approval_lapsed` message. 5.20.0:
792
+ the message is `recoverable` (UCP severity, ACP resolution) — the session
793
+ is payable with no further buyer step. **Platforms should not auto-retry
794
+ on `approval_denied`** (the buyer said no; the code is the signal); after
795
+ `approval_expired` / `approval_lapsed` completing again requests a new
796
+ approval. The held response itself keeps `requires_buyer_review`.
797
+ - **Card declined after redemption:** the session is payable again with the
798
+ decline message and the owner sees the failed outcome. Re-completing the
799
+ same cart at the same amount before the approval's `redeemBy` passes on the
800
+ consumed approval; after that, or for another cart, a new approval is needed.
801
+ - **Retries:** `eventId` is stable; it is marked seen (`seen.add`) only when
802
+ the session reached a terminal state, answered 2xx (completed, card
803
+ declined, cart invalid, declined, already completed, canceled). A transient
804
+ failure (your store, your processor's 5xx) answers 503 so the platform
805
+ retries. Approved events retry for ~10½ minutes inside the 15-minute
806
+ `redeemBy`; an undelivered one is followed by `step_up.lapsed`.
807
+ - **Secret rotation:** for 24 h after a rotation the previous secret signs a
808
+ second value, `v1old`; pass `[current, previous]` as the secret.
809
+ - `PUT` (ACP: update) on a held session is refused (`409
810
+ checkout_awaiting_approval`), and held responses are never stored under an
811
+ `Idempotency-Key`, so a re-drive after the approval reaches the kit.
812
+ - `kit.pollHeldSessions({ sessionIds?, apiBaseUrl? })` polls held approvals
813
+ and resumes decided ones — a **fallback only** for merchants that can't
814
+ receive events (needs `store.listHeld()` when called without ids). 5.20.0:
815
+ a `lapsed` approval releases the session too.
816
+ - **Set the Events URL and test it from code (5.20.0).** With the endpoint's
817
+ API key (the owner's, or a personal key of a member of the owning
818
+ organisation):
819
+
820
+ ```ts
821
+ import { createClient } from '@astrasyncai/verification-gateway/sdk';
822
+ const client = createClient({ apiBaseUrl, apiKey, counterpartyId: 'ASTRAE-…' });
823
+ await client.updateEndpoint(undefined, { eventsUrl: 'https://shop.example/astrasync/events' });
824
+ const r = await client.sendTestEvent(undefined, { type: 'step_up.lapsed' });
825
+ // r = { delivered, status: 'delivered'|'failed'|'not_configured', responseCode, error, eventId, … }
826
+ ```
827
+
828
+ `updateEndpoint` (`PATCH /api/endpoints/:id`) changes only operational
829
+ URLs — `eventsUrl`, `healthCheckUrl`, `skillUrl`, `catalogUrl`,
830
+ `checkoutUrl` (`null` clears); rules and other policy stay in the
831
+ dashboard. The owner is alerted when the Events URL changes.
832
+ `sendTestEvent` (`POST /api/endpoints/:id/events/test`) delivers one signed
833
+ event now, outside the retrying outbox. Pass `approvalId` (a held kit
834
+ approval at this endpoint) to get that approval's real payload so your
835
+ resume path runs: `approved` re-confirms (and is held again — nothing is
836
+ charged); the others release the session.
837
+
838
+ **Agent attribution (5.19.0).** `verifyPurchase` receives `agentArtifacts`:
839
+ for a UCP request signed with RFC 9421, the covered headers (`signature`,
840
+ `signature-input`, `signature-agent`, `ucp-agent`, `content-digest`, … — never
841
+ `authorization` or `cookie`), the method, the **public** URL (`publicOrigin` +
842
+ path + query) and `contentDigest` (`sha-256=:…:` of the raw body); otherwise
843
+ `{ unsigned: true }`. ACP confirms are always `{ unsigned: true }`: the ACP
844
+ `Authorization` bearer is the platform's credential to you and is never
845
+ forwarded, so ACP confirms are unattributable to the agent. Pass
846
+ `agentArtifacts` and `metadata` straight to `confirmCheckout`.
847
+
672
848
  - **Stores.** The defaults are in-memory (one process, lost on restart); the
673
849
  kits warn when they run on them outside `NODE_ENV=test`. In production pass
674
850
  `store` and `idempotencyStore` (`RedisIdempotencyStore`).
@@ -693,7 +869,30 @@ checkout_in_progress` (ACP). Without `claim` the kit warns once and the lock
693
869
  (sending the UCP order webhook); `'failed'` reports it and returns the
694
870
  session to `ready_for_complete` / `ready_for_payment`. Calling it again is a
695
871
  no-op. The stored result never contains the settlement token, voucher or
696
- mandates.
872
+ mandates. 5.19.0: it claims the session first (`complete_in_progress` →
873
+ `finishing`, rendered as `complete_in_progress`), so concurrent webhooks
874
+ finish it once; if finishing throws, the session goes back with its pending
875
+ settlement intact and the next call retries.
876
+ - **`fulfil({ session, result, settled })`** (5.19.0) — `settled` is your
877
+ processor's settled outcome (`processorRef` / `txHash`).
878
+ - **Settlement reports retry** (5.19.0) on network errors, 429 and 5xx
879
+ (`reportRetry`, default 3 attempts with backoff); a report that still fails
880
+ goes to `onReportError(err, { sessionId, orderId })` (default
881
+ `console.error`), as does an order webhook that fails after its retries.
882
+ Without an `orderId` the report identifies the order by the confirm's
883
+ verification `sessionId`.
884
+ - **`validateCart(session) → KitMessage[]`** (5.19.0) — runs on create, update
885
+ and complete, before the session is claimed and before `verifyPurchase`. Any
886
+ error (`{ code, content, path? }`) keeps the session `incomplete` (UCP,
887
+ `recoverable` with `path`) / `not_ready_for_payment` (ACP, `invalid` with
888
+ `param`), and `complete` is refused without calling AstraSync. `catalog`
889
+ also receives the requested quantity.
890
+ - **`publicOrigin`** (5.19.0, both kits) — the origin agents reach you at.
891
+ `expressHandler` rebuilds the request URL from it, so RFC 9421
892
+ `@target-uri` / `@authority` verify behind a TLS-terminating proxy. UCP
893
+ defaults to the origin of `profile.endpoint`; ACP falls back to
894
+ `X-Forwarded-Proto` / `X-Forwarded-Host` only when Express `trust proxy` is
895
+ set.
697
896
  - **One instrument (UCP).** `complete` needs exactly one
698
897
  `payment.instruments[]` entry whose `handler_id` is one of your
699
898
  `payment_handlers`; anything else is `payment_failed`.
@@ -721,6 +920,12 @@ checkout_in_progress` (ACP). Without `claim` the kit warns once and the lock
721
920
  entity; `GET /orders/{id}` serves it; `webhook` sends it, signed, when the
722
921
  order is created, and `kit.sendOrderWebhook(sessionId)` sends it again after
723
922
  you update it.
923
+ - **ACP `complete`** (5.19.0) — `payment_data` must be **one object** (not an
924
+ array) whose `handler_id` is a declared handler's `id` or `name` (e.g.
925
+ `voucher` / `ai.astrasync.voucher`); a missing handler id, an unknown handler
926
+ or an array is `400 invalid` with `param: '$.payment_data.handler_id'`, and
927
+ nothing is verified. `links` (`{ type, url, title? }`) are returned on every
928
+ session.
724
929
  - **ACP.** An `API-Version` other than `2026-04-17` is `400
725
930
  unsupported_api_version` with `supported_versions`; `Idempotency-Key` and
726
931
  `Request-Id` are echoed; `order.line_items` mirrors the session;
@@ -1007,6 +1212,14 @@ answered by the checkpoint; the agent needs no `apiEndpoint` of its own.
1007
1212
 
1008
1213
  Newest first. Full detail for every release is in [CHANGELOG.md](./CHANGELOG.md).
1009
1214
 
1215
+ ### v5.19.0 — approval events for held kit checkouts, MCP tool rules, fail-closed default
1216
+
1217
+ - **Behaviour change:** `failOnError` defaults to `'closed'` (express + MCP); `'open'` is opt-in.
1218
+ - Kits: owner approval completes a held checkout with no agent call (`handleApprovalEvents`, `resumeApproved` / `resumeDeclined`, `pollHeldSessions`); `validateCart`; `agentArtifacts` + `metadata` on confirms; `publicOrigin`; report retries + `onReportError`; `fulfil` gets `settled`; `resolvePending` claims the session; ACP `payment_data` rule and `links`.
1219
+ - MCP: dashboard tool rules (tighten-only, `toolGatePrecedence: 'dashboard'` opt-in).
1220
+ - Express / Next.js: `purposeSource` + `requestMethod` / `requestPath` keep a route purpose on an MPP paid leg; `req.rawBody` documented.
1221
+ - `quoteCheckout()`; one-time warning for a valued `verify()` with no `commercePhase` (default flips to quote in 6.0); `verifyAstraSyncWebhook` takes `secret[]` and `v1old`.
1222
+
1010
1223
  ### v5.18.0 — checkout kit fixes, orders, Express mounting, MPP interop
1011
1224
 
1012
1225
  - UCP / ACP kits: holds never settle; `resolvePending()` finishes a pending settlement; `complete` claims the session (store `claim`) so concurrent completes can't both charge; sessions and idempotency keys are scoped to the caller.
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.mjs';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-CfBpm3w5.mjs';
3
- import '../types-BfILnheI.mjs';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-Djpz-L0w.mjs';
3
+ import '../types-CU17R4Nr.mjs';
4
4
 
5
5
  /**
6
6
  * PlatformAdapter Interface
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.js';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-BRz2U0Pn.js';
3
- import '../types-BfILnheI.js';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-B_HLhZ9n.js';
3
+ import '../types-CU17R4Nr.js';
4
4
 
5
5
  /**
6
6
  * PlatformAdapter Interface
@@ -1,5 +1,5 @@
1
1
  import { RequestHandler, Request } from 'express';
2
- import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-Cns4l8gX.mjs';
2
+ import { e as VerificationResult, E as ExpressMiddlewareOptions, b as AstraSyncCredentials } from '../types-CxjqmPl-.mjs';
3
3
 
4
4
  /**
5
5
  * AstraSync Universal Verification Gateway - Express Middleware
@@ -34,6 +34,14 @@ declare global {
34
34
  namespace Express {
35
35
  interface Request {
36
36
  agentVerification?: VerificationResult;
37
+ /**
38
+ * 5.19.0 (documented): the exact request bytes, when your JSON parser
39
+ * keeps them — `express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })`.
40
+ * The gateway forwards these (not a re-serialised `req.body`) as the
41
+ * protocol artifacts' raw body, so body digests (MPP, RFC 9421
42
+ * content-digest) match without mounting `express.raw`.
43
+ */
44
+ rawBody?: Buffer | string;
37
45
  }
38
46
  }
39
47
  }
@@ -1,5 +1,5 @@
1
1
  import { RequestHandler, Request } from 'express';
2
- import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-Cns4l8gX.js';
2
+ import { e as VerificationResult, E as ExpressMiddlewareOptions, b as AstraSyncCredentials } from '../types-CxjqmPl-.js';
3
3
 
4
4
  /**
5
5
  * AstraSync Universal Verification Gateway - Express Middleware
@@ -34,6 +34,14 @@ declare global {
34
34
  namespace Express {
35
35
  interface Request {
36
36
  agentVerification?: VerificationResult;
37
+ /**
38
+ * 5.19.0 (documented): the exact request bytes, when your JSON parser
39
+ * keeps them — `express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })`.
40
+ * The gateway forwards these (not a re-serialised `req.body`) as the
41
+ * protocol artifacts' raw body, so body digests (MPP, RFC 9421
42
+ * content-digest) match without mounting `express.raw`.
43
+ */
44
+ rawBody?: Buffer | string;
37
45
  }
38
46
  }
39
47
  }