@astrasyncai/verification-gateway 5.18.0 → 5.19.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 (115) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +168 -4
  3. package/dist/adapters/express.d.mts +9 -1
  4. package/dist/adapters/express.d.ts +9 -1
  5. package/dist/adapters/express.js +96 -37
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +96 -37
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/http-pdlss.d.mts +7 -1
  10. package/dist/adapters/http-pdlss.d.ts +7 -1
  11. package/dist/adapters/http-pdlss.js +18 -2
  12. package/dist/adapters/http-pdlss.js.map +1 -1
  13. package/dist/adapters/http-pdlss.mjs +16 -1
  14. package/dist/adapters/http-pdlss.mjs.map +1 -1
  15. package/dist/adapters/kit-express.d.mts +93 -1
  16. package/dist/adapters/kit-express.d.ts +93 -1
  17. package/dist/adapters/kit-express.js +181 -2
  18. package/dist/adapters/kit-express.js.map +1 -1
  19. package/dist/adapters/kit-express.mjs +180 -2
  20. package/dist/adapters/kit-express.mjs.map +1 -1
  21. package/dist/adapters/mcp.d.mts +80 -9
  22. package/dist/adapters/mcp.d.ts +80 -9
  23. package/dist/adapters/mcp.js +153 -5
  24. package/dist/adapters/mcp.js.map +1 -1
  25. package/dist/adapters/mcp.mjs +151 -5
  26. package/dist/adapters/mcp.mjs.map +1 -1
  27. package/dist/adapters/nextjs.d.mts +1 -1
  28. package/dist/adapters/nextjs.d.ts +1 -1
  29. package/dist/adapters/nextjs.js +41 -4
  30. package/dist/adapters/nextjs.js.map +1 -1
  31. package/dist/adapters/nextjs.mjs +41 -4
  32. package/dist/adapters/nextjs.mjs.map +1 -1
  33. package/dist/adapters/sdk.d.mts +44 -1
  34. package/dist/adapters/sdk.d.ts +44 -1
  35. package/dist/adapters/sdk.js +76 -2
  36. package/dist/adapters/sdk.js.map +1 -1
  37. package/dist/adapters/sdk.mjs +76 -2
  38. package/dist/adapters/sdk.mjs.map +1 -1
  39. package/dist/agent/index.js +1 -1
  40. package/dist/agent/index.js.map +1 -1
  41. package/dist/agent/index.mjs +1 -1
  42. package/dist/agent/index.mjs.map +1 -1
  43. package/dist/bin/astrasync-claude-hook.js +46 -6
  44. package/dist/bin/astrasync-codex-hook.js +46 -6
  45. package/dist/bin/astrasync-guard.js +375 -117
  46. package/dist/bin/astrasync.js +417 -117
  47. package/dist/browser/background.js +46 -6
  48. package/dist/browser/background.js.map +1 -1
  49. package/dist/browser/background.mjs +46 -6
  50. package/dist/browser/background.mjs.map +1 -1
  51. package/dist/cli/index.js +1 -1
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/index.mjs +1 -1
  54. package/dist/cli/index.mjs.map +1 -1
  55. package/dist/codex/index.js +46 -6
  56. package/dist/codex/index.js.map +1 -1
  57. package/dist/codex/index.mjs +46 -6
  58. package/dist/codex/index.mjs.map +1 -1
  59. package/dist/cursor/extension.js +46 -6
  60. package/dist/cursor/extension.js.map +1 -1
  61. package/dist/cursor/extension.mjs +46 -6
  62. package/dist/cursor/extension.mjs.map +1 -1
  63. package/dist/edge-config.d.mts +1 -1
  64. package/dist/edge-config.d.ts +1 -1
  65. package/dist/edge-config.js +1 -1
  66. package/dist/edge-config.js.map +1 -1
  67. package/dist/edge-config.mjs +1 -1
  68. package/dist/edge-config.mjs.map +1 -1
  69. package/dist/edge-core/index.d.mts +1 -1
  70. package/dist/edge-core/index.d.ts +1 -1
  71. package/dist/edge-core/index.js +21 -2
  72. package/dist/edge-core/index.js.map +1 -1
  73. package/dist/edge-core/index.mjs +21 -2
  74. package/dist/edge-core/index.mjs.map +1 -1
  75. package/dist/gateway/gateway.js +46 -6
  76. package/dist/gateway/gateway.js.map +1 -1
  77. package/dist/gateway/gateway.mjs +46 -6
  78. package/dist/gateway/gateway.mjs.map +1 -1
  79. package/dist/git-trigger/git-hooks.d.mts +1 -1
  80. package/dist/git-trigger/git-hooks.d.ts +1 -1
  81. package/dist/index.d.mts +542 -30
  82. package/dist/index.d.ts +542 -30
  83. package/dist/index.js +1171 -183
  84. package/dist/index.js.map +1 -1
  85. package/dist/index.mjs +1157 -173
  86. package/dist/index.mjs.map +1 -1
  87. package/dist/registration/index.d.mts +1 -1
  88. package/dist/registration/index.d.ts +1 -1
  89. package/dist/registration/index.js +1 -1
  90. package/dist/registration/index.js.map +1 -1
  91. package/dist/registration/index.mjs +1 -1
  92. package/dist/registration/index.mjs.map +1 -1
  93. package/dist/transport/index.js +3 -2
  94. package/dist/transport/index.js.map +1 -1
  95. package/dist/transport/index.mjs +3 -2
  96. package/dist/transport/index.mjs.map +1 -1
  97. package/dist/{types-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
  98. package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
  99. package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
  100. package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
  101. package/dist/ui/index.d.mts +1 -1
  102. package/dist/ui/index.d.ts +1 -1
  103. package/dist/verify.d.mts +23 -2
  104. package/dist/verify.d.ts +23 -2
  105. package/dist/verify.js +23 -4
  106. package/dist/verify.js.map +1 -1
  107. package/dist/verify.mjs +23 -4
  108. package/dist/verify.mjs.map +1 -1
  109. package/dist/webhooks.d.mts +16 -3
  110. package/dist/webhooks.d.ts +16 -3
  111. package/dist/webhooks.js +24 -13
  112. package/dist/webhooks.js.map +1 -1
  113. package/dist/webhooks.mjs +24 -13
  114. package/dist/webhooks.mjs.map +1 -1
  115. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,184 @@ 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.19.0] — 2026-10-18 — approval events for held kit checkouts, MCP tool rules, fail-closed default
12
+
13
+ ### Changed (behaviour)
14
+
15
+ - **`failOnError` defaults to `'closed'`** in `createMcpMiddleware` and the
16
+ express `createMiddleware`. When the middleware itself throws, the request
17
+ now gets `503` + `Retry-After` + `VERIFICATION_UNAVAILABLE` (MCP: JSON-RPC
18
+ `-32003`) and the handler is not called. `'open'` (pass through unverified)
19
+ stays available as an explicit opt-in. The `[SHADOW] would-have-denied`
20
+ line is still logged on every throw. (Audit F-A1-06.)
21
+ - **ACP `complete` validates `payment_data`.** It must be one object (not an
22
+ array) whose `handler_id` is a declared handler's `id` or `name` (e.g.
23
+ `voucher` / `ai.astrasync.voucher`); a missing handler id, an unknown handler
24
+ or an array is `400 invalid` with `param: '$.payment_data.handler_id'` and
25
+ nothing is verified.
26
+ - **Checkout-kit `PUT` / update on a held session is refused** (`409
27
+ checkout_awaiting_approval`): the owner is approving exactly that cart.
28
+ - **A declined card leaves the kit session payable with a `recoverable`
29
+ message** — UCP `payment_failed` (a standard error; `requires_buyer_input`
30
+ implied a `requires_escalation` handoff) back in `ready_for_complete`, ACP
31
+ `payment_declined` back in `ready_for_payment`. Completing the same session
32
+ again (UCP: a fresh `Idempotency-Key`) verifies the same cart; the platform
33
+ records a new order beside the declined one and settles it once.
34
+ - **Held kit responses are no longer stored under an `Idempotency-Key`**, so a
35
+ re-drive with the same key after the approval reaches the kit instead of
36
+ replaying the held response.
37
+
38
+ ### Added
39
+
40
+ - **Owner approval completes a held checkout (UCP + ACP kits, item 27).** At a
41
+ hold the kit stores `_approvalId`, `_astraId`, `_attemptId`, the held
42
+ `line_items` / `totals` and the non-secret payment input. New:
43
+ - `kit.resumeApproved(sessionId)` — `validateCart` first (an invalid cart
44
+ returns before AstraSync: the approval is not redeemed, nothing charged),
45
+ then `verifyPurchase` with the same cart id and the stored held amount
46
+ (`catalog()` is never re-run), then settle → report → fulfil → order
47
+ webhook. No agent call.
48
+ - `kit.resumeDeclined(sessionId, reason)` — back to `ready_for_complete` /
49
+ `ready_for_payment` with a message.
50
+ - `handleApprovalEvents(rawBody, headers, secret | secret[], { kits, seen? })`
51
+ — verifies `X-AstraSync-Signature`, routes the event (`{ eventId, type:
52
+ 'step_up.approved' | 'step_up.denied' | 'step_up.expired' |
53
+ 'step_up.lapsed', approvalId, checkoutSessionId, kitSessionId?, protocol?,
54
+ counterpartyId, agentAstraId, transactionValue, currency, decidedAt,
55
+ redeemBy? }`) by `protocol` / `kitSessionId`, and answers `{ status, body
56
+ }`: 2xx at a terminal outcome (the event id is marked seen only then), 503
57
+ on a transient failure or a non-terminal state, 401 on a bad signature.
58
+ `MemorySeenEvents` / `SeenEvents` for dedupe.
59
+ - `approvalEventsHandler({ secret, kits, seen? })` and
60
+ `expressHandler(kit, { approvalEventsPath, webhookSecret, approvalEventKits?,
61
+ seenEvents? })` mount it on Express.
62
+ - `CheckoutStore.findByCheckoutSessionId?(id)` and `listHeld?()` (and the
63
+ ACP store equivalents); the memory stores implement them.
64
+ - `kit.pollHeldSessions({ sessionIds?, apiBaseUrl? })` — a fallback for
65
+ merchants that can't receive events.
66
+ - Card declined after redemption: the session is payable again with the
67
+ decline message; re-completing the same cart at the same amount re-sends
68
+ the same cart id and attempt id (the platform chain-passes the consumed
69
+ approval until its `redeemBy`).
70
+ - **`agentArtifacts` and `metadata` on kit confirms (item 28).**
71
+ `verifyPurchase` receives `agentArtifacts` (UCP RFC 9421-signed: the covered
72
+ headers — never `authorization` / `cookie` — the method, the public URL and
73
+ the raw-body `contentDigest`; otherwise `{ unsigned: true }`; ACP always `{
74
+ unsigned: true }`, its bearer is never forwarded) and `metadata: {
75
+ kitSessionId, protocol }`. `confirmCheckout` forwards both on verify-access.
76
+ New types `AgentArtifacts`, `CheckoutMetadata`.
77
+ - **`validateCart(session) → KitMessage[]`** on both kits (item 29) — runs on
78
+ create, update and complete before AstraSync; errors keep the session
79
+ `incomplete` (UCP, `recoverable` with `path`) / `not_ready_for_payment` (ACP,
80
+ `invalid` with `param`). `catalog(itemId, quantity)` also receives the
81
+ quantity.
82
+ - **`publicOrigin`** on both kits (item 24), used by `expressHandler` to
83
+ rebuild the request URL (UCP default: the origin of `profile.endpoint`; ACP:
84
+ `X-Forwarded-Proto` / `X-Forwarded-Host` only under Express `trust proxy`).
85
+ - **ACP `links`** option, rendered on every session (item 26).
86
+ - **`reportRetry` and `onReportError`** on both kits (item 21): settlement
87
+ reports retry on network / 429 / 5xx (3 attempts with backoff); a final
88
+ failure — or an order webhook that fails after its retries — calls
89
+ `onReportError(err, { sessionId, orderId })` (default `console.error`).
90
+ Without an `orderId` the report uses the confirm's verification `sessionId`.
91
+ - **MCP tool rules from the dashboard (item 18).** With `apiKey` +
92
+ `counterpartyId`, `createMcpMiddleware` fetches the endpoint's tool rules
93
+ (`{ tool, purpose, action, resource?, observe? }`) and refreshes them every
94
+ `routesRefreshMs` (default 5 min, shared cache helper with the express route
95
+ policy). A dashboard rule only tightens a code `toolGates` entry (`observe` /
96
+ no-purpose ignored; a differing purpose or action is checked in addition)
97
+ unless `toolGatePrecedence: 'dashboard'`; tools with no code gate follow the
98
+ dashboard rule; `legacy: true` path rules are ignored. Code-gated tool names
99
+ are sent on the fetch as `?codeGates=`. `fetchRoutes(config, id, {
100
+ codeGates })`.
101
+ - **`purposeSource`, `requestMethod`, `requestPath` on verify-access (item 20)** from the express and Next.js middlewares, so verify-access can keep a
102
+ dashboard route's purpose (e.g. `shopping.purchase`) on an MPP paid leg
103
+ after checking it against the endpoint's stored rules. `req.rawBody` (a
104
+ Buffer or string set by an `express.json({ verify })` hook) is documented as
105
+ the forwarded raw body.
106
+ - **`client.quoteCheckout()`** — the quote leg (`commercePhase: 'quote'`: peek
107
+ only, no order, no approval redeemed). `verify()` accepts `commercePhase:
108
+ 'quote'`.
109
+ - **`verifyAstraSyncWebhook` takes `secret | secret[]`** and accepts a `v1old`
110
+ signature value (the previous secret signs it for 24 h after a rotation);
111
+ `signAstraSyncWebhook(..., { previousSecret })`. `abandonIdempotent()`
112
+ releases a key without storing a non-terminal response.
113
+ - The `behaviour.*` failure family (`behaviour.limit_probing`,
114
+ `behaviour.unattributed_limit_check`) is documented on `AccessFailure`.
115
+ - **`result.agent.trustScore` is populated again** (the field existed; the
116
+ platform had stopped sending it). Rule: counterparties may see an agent's
117
+ trust score; the agent itself never does. verify-access returns the real
118
+ score (0–100) to an authenticated counterparty — merchant and service keys,
119
+ the kits, the edge adapter, AstraSync's bridge — and withholds it from a
120
+ keyless caller and from the agent itself (an agent session as that agent, or
121
+ its owner's key on a call that names no endpoint of the owner's). Never
122
+ relay it to the agent.
123
+
124
+ ### Fixed
125
+
126
+ - **A verified agent's denied call keeps `result.agent`.** The deny branch
127
+ now maps the `agent` block the platform sends on a deny (as on a grant), so
128
+ a verified-but-denied agent is still identified (the MCP bridge keys its
129
+ per-agent budget on it); before, it fell back to the per-IP budget.
130
+ - **A route's `minTrustScore` floor now gates on the real score and fails
131
+ closed when the score is absent** (express adapter). It had been passing
132
+ every agent since the platform stopped sending the score. The online-mode
133
+ decision `reason` no longer embeds the score (it is relayable text; the
134
+ number stays on `decision.trustScore`).
135
+
136
+ - **A held kit checkout could never complete without the agent:** the bridge's
137
+ re-drive got the cached held response back, and nothing resumed the session
138
+ when the owner approved. See Added (item 27).
139
+ - **`fulfil` now receives `settled`** (the processor's settled outcome),
140
+ including from `resolvePending` (item 22).
141
+ - **MPP replay protection keyed on the challenge id.** The replay key was the
142
+ body digest plus a `nonce` that mppx challenges don't carry, so two distinct
143
+ challenges over identical bodies inside the 60 s window were refused as a
144
+ replay. The key now includes the challenge's `id`; presenting the same
145
+ challenge twice is still refused.
146
+ - **`resolvePending` finishes a session once** (item 23): it claims
147
+ `complete_in_progress` → `finishing` (rendered as `complete_in_progress`);
148
+ the losing call is a no-op, and if finishing throws the session returns to
149
+ `complete_in_progress` with its pending settlement intact so the next call
150
+ retries.
151
+ - **Kit settlement reports were dropped silently** on any error (item 21).
152
+
153
+ ### Deprecated
154
+
155
+ - A `verify()` with a `transactionValue` and no `commercePhase` is a purchase
156
+ leg (it records a pending order and redeems a matching approval). 5.19.0
157
+ warns once per process; **6.0 changes the default to `'quote'`** — use
158
+ `quoteCheckout()` for checks and `confirmCheckout()` for purchases.
159
+
160
+ ## [5.18.1] — 2026-10-17 — guard browse --open; proxy help page; browser command printed
161
+
162
+ ### Added
163
+
164
+ - **`astrasync guard browse --open`** starts Chrome, Edge or Chromium through
165
+ the device proxy (its own profile, and
166
+ `--ignore-certificate-errors-spki-list` pinned to the proxy's local
167
+ certificate, so every other site is checked as usual; Chrome still shows an
168
+ "unsupported command-line flag" bar, which the output says to close) on the one-time sign-in link as soon as the code
169
+ arrives. On WSL a native Linux Chromium (on `PATH` or Playwright's) is
170
+ preferred over the Windows browser, which may not reach the WSL loopback
171
+ proxy. With no browser found it prints the command to start one.
172
+
173
+ ### Changed
174
+
175
+ - **`guard browse` prints a ready-to-paste browser command** (PowerShell on
176
+ Windows and WSL, a shell line elsewhere) before the Playwright hint, with a
177
+ tip to re-run with `--open`.
178
+ - **The proxy address explains itself.** Opening `http://127.0.0.1:<port>`
179
+ directly now shows a short page — it only works as a browser proxy, and how
180
+ to open the sign-in link — instead of `400 absolute URL required`.
181
+
182
+ ### Fixed
183
+
184
+ - **Ctrl-C stops `guard browse` while a browser is still open.** Stopping waited
185
+ for the browser to close its proxy connections, which it never does. The
186
+ proxy now ends them, and a second Ctrl-C (or a stop taking over 2 s) exits
187
+ anyway.
188
+
11
189
  ## [5.18.0] — 2026-10-16 — checkout kit fixes, orders, Express mounting, MPP interop
12
190
 
13
191
  ### Fixed
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,57 @@ 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
+
443
+ ## Route purpose on a paid leg (5.19.0)
444
+
445
+ The express and Next.js middlewares send `purposeSource` (`'route_config'` |
446
+ `'header'` | `'body'` | `'default'`) plus `requestMethod` / `requestPath` on
447
+ every verify-access call. When a dashboard route rule set the purpose (e.g.
448
+ `POST /api/checkout/* → shopping.purchase`), verify-access keeps it on an MPP
449
+ paid leg (`Authorization: Payment …`) instead of replacing it with
450
+ `commerce.payment.execute` — after checking that a **stored** rule on your
451
+ endpoint matches that method and path with the same purpose and action. The
452
+ claim is checked, never trusted.
453
+
454
+ The gateway forwards the exact request bytes when your JSON parser keeps them
455
+ on `req.rawBody` (a Buffer or string), so body digests match without mounting
456
+ `express.raw`:
457
+
458
+ ```ts
459
+ app.use(express.json({ verify: (req, _res, buf) => ((req as any).rawBody = buf) }));
460
+ ```
461
+
411
462
  ## Verification Response
412
463
 
413
464
  ```typescript
@@ -565,7 +616,15 @@ The first-party commerce rail: an agent shops under its ASTRA-id, holds NO
565
616
  payment credential, and the platform settles from the owner's saved
566
617
  instrument. Two phases discriminate quote from money movement:
567
618
 
568
- - `commercePhase: 'quote'` (or unphased) — verification only, never settles.
619
+ - `commercePhase: 'quote'` — a price / limit check: peek only, no order, no
620
+ approval redeemed. Use `client.quoteCheckout({ astraId, transactionValue,
621
+ currency, checkoutSessionId? })` (5.19.0). A plain `verify()` with a
622
+ `transactionValue` and no `commercePhase` is a **purchase leg** (it records
623
+ a pending order and redeems a matching approval); 5.19.0 warns once when you
624
+ call it that way, and the default becomes `'quote'` in 6.0. Quotes count
625
+ toward limit-probing detection (`behaviour.limit_probing` /
626
+ `behaviour.unattributed_limit_check`): quote what the buyer asked for, not a
627
+ ladder of values.
569
628
  - `commercePhase: 'confirm'` — the ONLY leg that can settle. Use
570
629
  `client.confirmCheckout({ astraId, transactionValue, currency,
571
630
  checkoutSessionId, checkoutItems, counterpartyUrl })`; `checkoutSessionId`
@@ -630,6 +689,7 @@ catalog, settlement and fulfilment. Both are fetch-style
630
689
  import {
631
690
  createUcpCheckoutKit,
632
691
  expressHandler,
692
+ approvalEventsHandler,
633
693
  RedisIdempotencyStore,
634
694
  } from '@astrasyncai/verification-gateway';
635
695
 
@@ -641,7 +701,9 @@ const ucp = createUcpCheckoutKit({
641
701
  orders: true, // dev.ucp.shopping.order: GET /orders/{id} + order webhooks
642
702
  },
643
703
  currency: 'USD',
644
- catalog: async (id) => db.items.find(id),
704
+ catalog: async (id, quantity) => db.items.find(id),
705
+ // 5.19.0: cart-level checks (stock, caps) on create / update / complete.
706
+ validateCart: async (session) => stockMessages(session),
645
707
  verifyPurchase: (ctx) =>
646
708
  client.confirmCheckout({
647
709
  astraId: ctx.astraId,
@@ -650,6 +712,8 @@ const ucp = createUcpCheckoutKit({
650
712
  checkoutJwt: ctx.checkoutJwt,
651
713
  transactionValue: ctx.valueMajor,
652
714
  currency: 'USD',
715
+ agentArtifacts: ctx.agentArtifacts, // 5.19.0: attribution
716
+ metadata: ctx.metadata, // 5.19.0: { kitSessionId, protocol } for approval events
653
717
  }),
654
718
  settle: async ({ result }) => chargeWithYourProcessor(result), // settled | pending | failed
655
719
  reportSettlement: (r) => client.reportSettlement(r),
@@ -667,8 +731,71 @@ const ucp = createUcpCheckoutKit({
667
731
 
668
732
  app.get('/.well-known/ucp', (_req, res) => res.json(ucp.profile));
669
733
  app.use('/ucp/v1', express.raw({ type: '*/*' }), expressHandler(ucp));
734
+ // 5.19.0: owner-approval events for held checkouts (both kits, one URL —
735
+ // set it as the endpoint's Events URL in the dashboard).
736
+ app.post(
737
+ '/astrasync/events',
738
+ express.raw({ type: '*/*' }),
739
+ approvalEventsHandler({ secret: process.env.ASTRASYNC_WEBHOOK_SECRET!, kits: [ucp, acp] })
740
+ );
741
+ ```
742
+
743
+ **Owner approval completes a held checkout (5.19.0).** When a purchase is
744
+ held, the kit stores the approval id, the agent, the attempt chain, the held
745
+ cart (`line_items` / `totals`) and the non-secret payment input (the UCP
746
+ instrument descriptor, the ACP `payment_data` fields minus anything that looks
747
+ like a credential). When the owner decides, AstraSync POSTs a signed event to
748
+ your Events URL:
749
+
750
+ ```
751
+ X-AstraSync-Signature: t=<unix>,v1=<hex>[,v1old=<hex>]
752
+ { "eventId", "type": "step_up.approved" | "step_up.denied" | "step_up.expired" | "step_up.lapsed",
753
+ "approvalId", "checkoutSessionId", "kitSessionId"?, "protocol"?: "ucp" | "acp",
754
+ "counterpartyId", "agentAstraId", "transactionValue", "currency", "decidedAt", "redeemBy"? }
670
755
  ```
671
756
 
757
+ `handleApprovalEvents(rawBody, headers, secret | secret[], { kits, seen? })`
758
+ (or `approvalEventsHandler` / `expressHandler(kit, { approvalEventsPath,
759
+ webhookSecret })`) verifies it and routes it by `protocol` / `kitSessionId`,
760
+ falling back to `store.findByCheckoutSessionId(checkoutSessionId)`:
761
+
762
+ - **approved** → `kit.resumeApproved(sessionId)`: `validateCart` first (an
763
+ invalid cart returns with its message before AstraSync, so the approval is
764
+ not redeemed and nothing is charged), then `verifyPurchase` with the same
765
+ cart id and the **stored held amount** (`catalog()` is never re-run, so a
766
+ price change can't break the exact-value redemption), then settle → report
767
+ → fulfil → order webhook. No agent call.
768
+ - **denied / expired / lapsed** → `kit.resumeDeclined(sessionId, reason)`:
769
+ back to `ready_for_complete` / `ready_for_payment` with a message.
770
+ - **Card declined after redemption:** the session is payable again with the
771
+ decline message and the owner sees the failed outcome. Re-completing the
772
+ same cart at the same amount before the approval's `redeemBy` passes on the
773
+ consumed approval; after that, or for another cart, a new approval is needed.
774
+ - **Retries:** `eventId` is stable; it is marked seen (`seen.add`) only when
775
+ the session reached a terminal state, answered 2xx (completed, card
776
+ declined, cart invalid, declined, already completed, canceled). A transient
777
+ failure (your store, your processor's 5xx) answers 503 so the platform
778
+ retries. Approved events retry for ~10½ minutes inside the 15-minute
779
+ `redeemBy`; an undelivered one is followed by `step_up.lapsed`.
780
+ - **Secret rotation:** for 24 h after a rotation the previous secret signs a
781
+ second value, `v1old`; pass `[current, previous]` as the secret.
782
+ - `PUT` (ACP: update) on a held session is refused (`409
783
+ checkout_awaiting_approval`), and held responses are never stored under an
784
+ `Idempotency-Key`, so a re-drive after the approval reaches the kit.
785
+ - `kit.pollHeldSessions({ sessionIds?, apiBaseUrl? })` polls held approvals
786
+ and resumes decided ones — a **fallback only** for merchants that can't
787
+ receive events (needs `store.listHeld()` when called without ids).
788
+
789
+ **Agent attribution (5.19.0).** `verifyPurchase` receives `agentArtifacts`:
790
+ for a UCP request signed with RFC 9421, the covered headers (`signature`,
791
+ `signature-input`, `signature-agent`, `ucp-agent`, `content-digest`, … — never
792
+ `authorization` or `cookie`), the method, the **public** URL (`publicOrigin` +
793
+ path + query) and `contentDigest` (`sha-256=:…:` of the raw body); otherwise
794
+ `{ unsigned: true }`. ACP confirms are always `{ unsigned: true }`: the ACP
795
+ `Authorization` bearer is the platform's credential to you and is never
796
+ forwarded, so ACP confirms are unattributable to the agent. Pass
797
+ `agentArtifacts` and `metadata` straight to `confirmCheckout`.
798
+
672
799
  - **Stores.** The defaults are in-memory (one process, lost on restart); the
673
800
  kits warn when they run on them outside `NODE_ENV=test`. In production pass
674
801
  `store` and `idempotencyStore` (`RedisIdempotencyStore`).
@@ -693,7 +820,30 @@ checkout_in_progress` (ACP). Without `claim` the kit warns once and the lock
693
820
  (sending the UCP order webhook); `'failed'` reports it and returns the
694
821
  session to `ready_for_complete` / `ready_for_payment`. Calling it again is a
695
822
  no-op. The stored result never contains the settlement token, voucher or
696
- mandates.
823
+ mandates. 5.19.0: it claims the session first (`complete_in_progress` →
824
+ `finishing`, rendered as `complete_in_progress`), so concurrent webhooks
825
+ finish it once; if finishing throws, the session goes back with its pending
826
+ settlement intact and the next call retries.
827
+ - **`fulfil({ session, result, settled })`** (5.19.0) — `settled` is your
828
+ processor's settled outcome (`processorRef` / `txHash`).
829
+ - **Settlement reports retry** (5.19.0) on network errors, 429 and 5xx
830
+ (`reportRetry`, default 3 attempts with backoff); a report that still fails
831
+ goes to `onReportError(err, { sessionId, orderId })` (default
832
+ `console.error`), as does an order webhook that fails after its retries.
833
+ Without an `orderId` the report identifies the order by the confirm's
834
+ verification `sessionId`.
835
+ - **`validateCart(session) → KitMessage[]`** (5.19.0) — runs on create, update
836
+ and complete, before the session is claimed and before `verifyPurchase`. Any
837
+ error (`{ code, content, path? }`) keeps the session `incomplete` (UCP,
838
+ `recoverable` with `path`) / `not_ready_for_payment` (ACP, `invalid` with
839
+ `param`), and `complete` is refused without calling AstraSync. `catalog`
840
+ also receives the requested quantity.
841
+ - **`publicOrigin`** (5.19.0, both kits) — the origin agents reach you at.
842
+ `expressHandler` rebuilds the request URL from it, so RFC 9421
843
+ `@target-uri` / `@authority` verify behind a TLS-terminating proxy. UCP
844
+ defaults to the origin of `profile.endpoint`; ACP falls back to
845
+ `X-Forwarded-Proto` / `X-Forwarded-Host` only when Express `trust proxy` is
846
+ set.
697
847
  - **One instrument (UCP).** `complete` needs exactly one
698
848
  `payment.instruments[]` entry whose `handler_id` is one of your
699
849
  `payment_handlers`; anything else is `payment_failed`.
@@ -721,6 +871,12 @@ checkout_in_progress` (ACP). Without `claim` the kit warns once and the lock
721
871
  entity; `GET /orders/{id}` serves it; `webhook` sends it, signed, when the
722
872
  order is created, and `kit.sendOrderWebhook(sessionId)` sends it again after
723
873
  you update it.
874
+ - **ACP `complete`** (5.19.0) — `payment_data` must be **one object** (not an
875
+ array) whose `handler_id` is a declared handler's `id` or `name` (e.g.
876
+ `voucher` / `ai.astrasync.voucher`); a missing handler id, an unknown handler
877
+ or an array is `400 invalid` with `param: '$.payment_data.handler_id'`, and
878
+ nothing is verified. `links` (`{ type, url, title? }`) are returned on every
879
+ session.
724
880
  - **ACP.** An `API-Version` other than `2026-04-17` is `400
725
881
  unsupported_api_version` with `supported_versions`; `Idempotency-Key` and
726
882
  `Request-Id` are echoed; `order.line_items` mirrors the session;
@@ -1007,6 +1163,14 @@ answered by the checkpoint; the agent needs no `apiEndpoint` of its own.
1007
1163
 
1008
1164
  Newest first. Full detail for every release is in [CHANGELOG.md](./CHANGELOG.md).
1009
1165
 
1166
+ ### v5.19.0 — approval events for held kit checkouts, MCP tool rules, fail-closed default
1167
+
1168
+ - **Behaviour change:** `failOnError` defaults to `'closed'` (express + MCP); `'open'` is opt-in.
1169
+ - 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`.
1170
+ - MCP: dashboard tool rules (tighten-only, `toolGatePrecedence: 'dashboard'` opt-in).
1171
+ - Express / Next.js: `purposeSource` + `requestMethod` / `requestPath` keep a route purpose on an MPP paid leg; `req.rawBody` documented.
1172
+ - `quoteCheckout()`; one-time warning for a valued `verify()` with no `commercePhase` (default flips to quote in 6.0); `verifyAstraSyncWebhook` takes `secret[]` and `v1old`.
1173
+
1010
1174
  ### v5.18.0 — checkout kit fixes, orders, Express mounting, MPP interop
1011
1175
 
1012
1176
  - 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,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-DqWPkiYl.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-DqWPkiYl.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
  }