@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.
- package/CHANGELOG.md +178 -0
- package/README.md +168 -4
- package/dist/adapters/express.d.mts +9 -1
- package/dist/adapters/express.d.ts +9 -1
- package/dist/adapters/express.js +96 -37
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +96 -37
- 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 +80 -9
- package/dist/adapters/mcp.d.ts +80 -9
- package/dist/adapters/mcp.js +153 -5
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +151 -5
- 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 +41 -4
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +41 -4
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +44 -1
- package/dist/adapters/sdk.d.ts +44 -1
- package/dist/adapters/sdk.js +76 -2
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +76 -2
- package/dist/adapters/sdk.mjs.map +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 +46 -6
- package/dist/bin/astrasync-codex-hook.js +46 -6
- package/dist/bin/astrasync-guard.js +375 -117
- package/dist/bin/astrasync.js +417 -117
- package/dist/browser/background.js +46 -6
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +46 -6
- 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 +46 -6
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +46 -6
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +46 -6
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +46 -6
- 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 +21 -2
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +21 -2
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.js +46 -6
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +46 -6
- 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 +542 -30
- package/dist/index.d.ts +542 -30
- package/dist/index.js +1171 -183
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1157 -173
- package/dist/index.mjs.map +1 -1
- package/dist/registration/index.d.mts +1 -1
- package/dist/registration/index.d.ts +1 -1
- 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.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-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
- package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
- package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
- package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +23 -2
- package/dist/verify.d.ts +23 -2
- package/dist/verify.js +23 -4
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +23 -4
- 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
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: '
|
|
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'`
|
|
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 {
|
|
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 {
|
|
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
|
}
|