@agent-cards/checkout 0.17.0 → 0.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 (49) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +106 -14
  4. package/dist/adyen-merchant-hosted.generated.d.ts +277 -0
  5. package/dist/adyen-merchant-hosted.generated.js +1902 -0
  6. package/dist/builtin-registry.generated.js +1 -1
  7. package/dist/card-fields.generated.d.ts +3 -0
  8. package/dist/card-fields.generated.js +46 -0
  9. package/dist/cdp.d.ts +6 -1
  10. package/dist/cdp.js +574 -220
  11. package/dist/client.d.ts +285 -5
  12. package/dist/client.js +590 -12
  13. package/dist/cse-body.d.ts +25 -0
  14. package/dist/cse-body.js +41 -0
  15. package/dist/fiserv.d.ts +65 -0
  16. package/dist/fiserv.generated.d.ts +73 -0
  17. package/dist/fiserv.generated.js +830 -0
  18. package/dist/fiserv.js +104 -0
  19. package/dist/index.d.ts +7 -2
  20. package/dist/index.js +5 -1
  21. package/dist/lifecycle.d.ts +38 -1
  22. package/dist/lifecycle.js +77 -5
  23. package/dist/merchant-handoff.d.ts +54 -0
  24. package/dist/merchant-handoff.js +100 -0
  25. package/dist/merchant-hosted.d.ts +140 -0
  26. package/dist/merchant-hosted.js +170 -0
  27. package/dist/merchant-total-watch.d.ts +115 -0
  28. package/dist/merchant-total-watch.js +268 -0
  29. package/dist/merchant-total.d.ts +257 -0
  30. package/dist/merchant-total.js +383 -0
  31. package/dist/pre-claim.d.ts +123 -0
  32. package/dist/pre-claim.js +386 -0
  33. package/dist/preflight-capabilities.generated.d.ts +1 -1
  34. package/dist/preflight-capabilities.generated.js +1 -1
  35. package/dist/preflight-catalog.json +133 -1
  36. package/dist/preflight-schemas.json +14 -2
  37. package/dist/preflight.generated.d.ts +1 -1
  38. package/dist/preflight.generated.js +15 -1
  39. package/dist/preparation.d.ts +13 -0
  40. package/dist/preparation.js +46 -9
  41. package/dist/prepared-processor.d.ts +36 -3
  42. package/dist/prepared-processor.js +53 -3
  43. package/dist/registry.d.ts +60 -0
  44. package/dist/registry.js +14 -0
  45. package/dist/stripe-checkout.generated.js +140 -20
  46. package/dist/substitutions.generated.d.ts +2 -1
  47. package/dist/substitutions.generated.js +758 -6
  48. package/examples/preflight/kernel-native/inventory.json +1 -1
  49. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Approve a Fiserv Commerce Hub checkout before Pay with `prepare({ psp: 'fiserv', environment: 'production' | 'sandbox', merchantProfile })`, where `merchantProfile` names the key Agentcard reviewed for the merchant (`agentcard_sandbox` in the sandbox) and the checkout's `merchant` is that key's merchant (`Agentcard sandbox`; any other name is refused with reason `merchant_profile_mismatch`). The sync asks the API for `capabilities=fiserv_card_capture`, and the API serves Fiserv's recognizer only for a company Agentcard turned Fiserv on for, so only that company's SDK arms Fiserv's card capture. The SDK pauses the card frame's capture, and it continues from your browser with the envelope the cardholder's device sealed around the approved card, written at `source.encryptionData` by the new `substituteFiservEnvelope`. A card capture with no preparation is refused as `preparation_required` before anyone is asked. Fastlane, ACH, gift and EBT captures continue untouched; every other body on the endpoint counts as a card capture, one the SDK cannot prove card-free included. `authorize()` requires a preparation for any Fiserv request, whatever the served registry says. Autopilot never pays Fiserv. Fiserv stays off until Agentcard turns it on for your company: `prepare()` then fails with reason `processor_unavailable`. A Fiserv preparation the API refuses before it exists names the refusal as the error's reason (every other processor keeps `preparation_unconfirmed`), and an envelope read after its window raises `PaymentOutcomeUnknownError` with reason `cse_substitutions_expired`. Matching API and Vault releases are required.
6
+
7
+ - Run an Adyen merchant-hosted checkout against your own Adyen TEST account from a test-mode client. `sandboxMerchants` on `VaultClient` declares your own HTTPS endpoint as taking the card request of a profile this build reviewed, under your own `test_` client key; both adapters pause that endpoint whatever the profile's status, `prepare({ psp: 'adyen', environment: 'sandbox', merchantProfile })` and the create carry the declaration and name its host as the merchant, and the continuation keeps every body rule of the profile. A declared profile id takes the place of the reviewed profile on that client. The endpoint must be HTTPS on a public domain name outside every processor's host, Adyen's and Agentcard's domains and every reviewed merchant's domain, or the constructor throws a `TypeError`. The API accepts a declaration only from a test-mode client of an organization Agentcard has turned declarations on for, and reads its key from Adyen's TEST platform, and the Vault encrypts only Adyen's documented test cards under it. `prepare()` throws `sandbox_declaration_refused` and `sandbox_declaration_invalid` as `CheckoutPreparationError` reasons. The matching API and Vault releases are required.
8
+
9
+ - Pause Adyen merchants that take the card on their own server, through the merchant profiles Agentcard reviewed (Dunelm and Cinemark). Every profile this build reviewed is armed from the start in `observe`, so a sync that has not run or failed never lets the agent's dummy card through; `syncRegistry()` asks for each profile's status on your client, and a profile turns enabled only when the API serves it enabled with the same rules this build reviewed. Both adapters pause the profile's endpoint and its siblings from attach. Both profiles ship in `observe`: a card request there is aborted before it reaches the merchant and reported as `merchant_profile_observed` (the SHA-256 of the body's key paths, never a value) and `unsupported_checkout`, and no authorization is created. A body neither adapter can read counts as a card request there. Once Agentcard enables a profile for your client and your SDK build, `prepare({ psp: 'adyen', environment, merchantProfile })` pays its card request: the create names only the profile id, the API keeps only the SHA-256 of the paused body it checks, the request continues from your browser with the Vault's ciphertext only when the live body hashes to that one (a refused continuation retires the approval), and a `5xx` final answer, a network failure, the page closing or the page's retry after hand-off reports `payment_outcome_unknown`. A card request the profile cannot finish is aborted with the reason, and one without a preparation is aborted as `preparation_required` before any pause, prompt or authorization (the controller reads `failed` with that reason). Events name these endpoints by their templated URL. The matching API release serves the profiles; an older API serves none and nothing changes. `ReplayResponse` gains `MerchantHostedReplay` (`mode: 'cse'`, `kind: 'merchant_hosted'`), so code that reads `substitutions.at` on every `cse` replay now branches on `kind`.
10
+
11
+ - Refuse Adyen test-platform payments with a typed error, for Adyen Sessions checkouts too. `AdyenTestPlatformRefusedError` (an `ApprovalDeclinedError`) carries `adyen_test_environment_refused` at create or before the card is sent, or `adyen_test_platform_requires_documented_test_card`, where the SDK used to throw a generic error. The controller state changes with it: refused at create, it reads `failed` with the code as its reason (it read `checkout_failed`), and the `failed` event reads `AdyenTestPlatformRefusedError: adyen_test_environment_refused` (it read `CheckoutApiError: adyen_test_environment_refused`); declined before the card is sent, it reads `declined` with the code as its reason (it read `approval_declined`), and the event names the code. A refused preparation throws `CheckoutPreparationError` with the API's code as its reason instead of `preparation_unconfirmed`.
12
+
13
+ - Pay a Stripe Checkout Elements page with a vaulted card. A page built with `stripe.initCheckout` (Checkout Elements) sends the card inline in the Checkout Session confirm, `/v1/payment_pages/{cs_...}/confirm`, which the SDK never recognized, so the placeholder card reached Stripe and the purchase failed. The confirm now pauses for the cardholder's approval like a PaymentIntent confirm, and the phone sends the real card. The API refuses it unless the amount you pass equals the total the Session will charge (`expected_amount`). Hosted and embedded Checkout confirm the same endpoint with the payment method they created first; that confirm carries no card and continues untouched, as before, even after the approval. `syncRegistry()` asks for `features=checkout_sessions`: the API serves the Checkout Session endpoint only to an SDK that lets such a request through ahead of its holds.
14
+
15
+ - Let a page pay with the Stripe card token the cardholder just approved. A page that tokenizes first (`stripe.createPaymentMethod`, `createToken`, `createSource`, `createConfirmationToken`) and then confirms the payment in the browser with what came back (`confirmCardPayment` with `payment_method: 'pm_...'`, `confirmPayment` with the confirmation token) used to fail: the SDK held that card-free confirmation, because the token alone could pay any amount. The SDK now asks the API (`VaultClient.checkStripeContinuation`), which reads the payment from Stripe and allows it only for the approved amount and currency, on the same Stripe account, paid with exactly the approved token. Allowed, the page's own request continues untouched, the SDK emits `stripe_payment_continued`, and the controller reads `awaiting_merchant` with reason `stripe_payment_continued`. One confirmation continues per approval; later ones stay held, and a refused one is `blocked` with the API's code (at most three checks per approval). The matching API release is required; an older API refuses, which leaves the hold as it was.
16
+
17
+ - Pause Stripe confirmation tokens and card sources. A page that calls `stripe.createConfirmationToken` (collect the card, confirm on your own server) or the older `stripe.createSource` sends the card to `/v1/confirmation_tokens` or `/v1/sources`; the SDK never recognized either, so the placeholder card reached Stripe and the purchase failed. Both now pause for the cardholder's approval, and the page receives a real confirmation token or card source. A confirmation token for a saved method, or a bank source, carries no card and continues untouched. The matching API serves these endpoints only to an SDK that asks for `features=card_fields`, which this release does. They are not eligible for auto-approval yet.
18
+
19
+ - Refuse a Stripe confirmation that carries no card at once. A page that pays with a method it created itself confirms with `payment_method=pm_...`; the SDK used to pause that request and ask the cardholder to approve something their card could not complete. The SDK now reads the body after its holds: a Stripe request without `card[number]` or `payment_method_data[card][number]` is refused at once with a `blocked` event (`request_without_card`) and never reaches the API. A confirmation that follows an approved card token keeps its existing hold. `VaultClient.withoutCard(url, body)` exposes the judgement, and `syncRegistry()` asks for `features=card_fields` (an older API ignores it). The matching API refuses a Stripe request with no card as `400 card_fields_missing`.
20
+
5
21
  - Approve a Paysafe Checkout 1.8.0 fresh-card checkout before Pay with `prepare({ psp: 'paysafe', environment: 'production' | 'sandbox' })`. Phone approval finishes before the hosted checkout starts its card request. One matching request consumes the approval; saved cards and other Paysafe APIs do not use preparation. Matching API, Vault and database releases are required. A token does not confirm merchant payment.
6
22
 
7
23
  - Keep the cardholder's approval when the merchant page gives up on its own card request. A page whose script times out its tokenization while the person is still deciding (Braintree after 60 seconds, Square after about 10) no longer cancels the approval: the SDK reports `merchant_request_lost`, answers the page's next request for the same purchase from the same authorization, and reads `ready_to_submit` with reason `awaiting_merchant_retry` when the approval lands before the page asks again, so one more Pay click completes the purchase with no second prompt. The wait for that request is bounded by `merchantRetryWaitMs` (two minutes by default, never past the approval window); an approval the page never asks for again is retired through the API as `merchant_never_retried` and the controller reads `declined` with that reason. Applies to `token` and `cse` checkouts; hosted forms, prepared checkouts and native Stripe Checkout keep cancelling. `VaultClient.cancelAuthorization` takes the reason and `checkoutModeOf` reads a request's mode. The matching API and Vault releases are required for the new reason; an older API answers its retirement as unknown, which the SDK holds.
package/PREFLIGHT.md CHANGED
@@ -176,10 +176,14 @@ The catalog includes reviewed discovery rules and direct SDK implementation capa
176
176
  | Bambora | `token` |
177
177
  | Global Payments | `token` |
178
178
 
179
+ Fiserv Commerce Hub is not in the table. Agentcard keeps Fiserv off until it turns it on for a company, so the bundled profile declares no Fiserv support. Fiserv's assets still identify `fiserv`: `payment-fields.js` and `checkout.js` on `commercehub-checkout.fiservapps.com` and the card frame on `commercehub-secure-data-capture.fiservapps.com`, with their `-cert` hosts. The result stays `unknown`, and a page that loads a Fiserv asset beside another processor's reads as `ambiguous`. A company that Agentcard turned Fiserv on for pays it through `prepare()`, as the [README](./README.md) describes.
180
+
179
181
  The bundled direct SDK profile declares processor support for `one_time` purchases. The catalog carries each processor's request limitations and known exclusions. A detection rule does not cover every product or regional variant sold under the processor's name. For example, Moneris Hosted Tokenization and Moneris Checkout use different payment paths; the implemented adapter covers Hosted Tokenization.
180
182
 
181
183
  The catalog also carries reviewed hosted, regional, and versioned variants observed on live checkouts: Adyen secured fields on the regional live hosts and any release, the Square card element frame, dLocal Smart Fields releases, Mercado Pago secure fields and the guest Checkout Pro card form, and the Rapyd hosted checkout page. A variant the implemented adapter does not admit, such as the Tranzila terminal-root page, the Authorize.Net hosted payment form, or a Worldpay hosted payment page, identifies its processor and returns `unknown` with `processor_variant_unverified`. A new fingerprint never declares payment support by itself.
182
184
 
185
+ Adyen's `supported` covers the Sessions payment request on Adyen's own hosts. Some Adyen merchants post the four encrypted fields to their own server instead, and their pages load the same secured fields, so an inspection identifies Adyen on both and cannot tell the two flows apart. The SDK pays a merchant's own endpoint only through a merchant profile Agentcard reviewed for it; the reviewed profiles (Dunelm and Cinemark) are in `observe`, so the SDK aborts their card requests and nothing is charged. The `actual_payment_request_validation` requirement is where that difference shows: check the paused request's host before treating an Adyen result as payable.
186
+
183
187
  | Observed checkout | Classification |
184
188
  | --- | --- |
185
189
  | A compatible Stripe script, `one_time`, direct SDK | `supported` at `processor_integration` scope; `checkout_flow_status` remains `unknown`. |
package/README.md CHANGED
@@ -136,16 +136,21 @@ Coverage is specific to the processor request format, merchant setup, browser tr
136
136
  | Checkout.com | supported |
137
137
  | Mercado Pago | Card tokenization and prepared checkout are implemented. Guest Checkout Pro in Mexico also corrects the issuer for one native card association when the selected card has the same brand and type. One live MXN 40 Lotería Chida purchase with published SDK 0.10.0 completed automatically through Pay; the merchant confirmed paid status and PDF fulfillment. The historical SDK result remains unknown because the private receipt adapter rejected a relative download URL; a separate read with the corrected adapter confirms that same paid receipt. Independent processor capture/settlement and other country or integration paths remain unverified. |
138
138
  | VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
139
- | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it. Sessions flow only: the paused request is `/checkoutshopper/v1/sessions/{id}/payments` on Adyen's own hosts; a merchant that posts the encrypted fields to its own server is not recognized, so nothing pauses there. Without a preparation, approval starts at Pay and must land before Adyen Web's own request timeout (observed at 60 seconds on Adyen Web 6.41 and 6.44; not enforced by the SDK); `prepare({ psp: 'adyen' })` moves the approval before Pay |
139
+ | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it. Sessions flow: the paused request is `/checkoutshopper/v1/sessions/{id}/payments` on Adyen's own hosts. A merchant that posts the encrypted fields to its own server pauses only when Agentcard reviewed it as a merchant profile (Dunelm and Cinemark so far); both profiles are in `observe`, so their card requests are reported and aborted and nothing pays there yet (see "Adyen merchants that take the card on their own server"). Without a preparation, approval starts at Pay and must land before Adyen Web's own request timeout (observed at 60 seconds on Adyen Web 6.41 and 6.44; not enforced by the SDK); `prepare({ psp: 'adyen' })` moves the approval before Pay |
140
140
  | Tranzila | supported (mode `hosted_form`): the cardholder finishes on Tranzila's own page; the paused form navigation resolves to a synthetic page, and you poll the merchant's order state |
141
+ | Fiserv Commerce Hub | implemented (mode `cse`, prepared checkout only) and off until Agentcard turns it on for your company: the card frame's card capture on `connect.fiservapis.com` (or `connect-cert.fiservapis.com` in the sandbox), with the approved card encrypted on the cardholder's device under a key Agentcard reviewed for the merchant. A merchant whose frame posts to Fiserv's `payment-link` endpoint is not recognized. No live merchant purchase is verified yet |
141
142
 
142
- The recognizer list is fetched from the API at runtime (`vault.syncRegistry()`),
143
+ The recognizer list, and the status of each reviewed Adyen merchant profile on
144
+ your client, are fetched from the API at runtime (`vault.syncRegistry()`),
143
145
  so new processors work without you shipping a release. `attachToCdp` derives the
144
146
  `Fetch.enable` url patterns from that same list rather than a constant, which is
145
147
  why the sync call belongs before the attach. `vault.cardUrlPatterns()` returns
146
148
  those patterns if you arm a CDP connection yourself. Call
147
- `GET /v2/checkout/recognizers?modes=token,cse,hosted_form` for the list that is
148
- live right now.
149
+ `GET /v2/checkout/recognizers?modes=token,cse,hosted_form&capabilities=fiserv_card_capture`
150
+ for the list this build syncs. The API serves Fiserv's card capture only to a
151
+ build that lists `fiserv_card_capture`, and only for a company Agentcard turned
152
+ Fiserv on for. An older SDK never pauses a Fiserv request it could not finish,
153
+ and no SDK pauses one for a company Fiserv is off for.
149
154
 
150
155
  ## Modes
151
156
 
@@ -154,7 +159,7 @@ carries the mode it was handled in. The adapters do the right thing for all
154
159
  three; the difference matters if you drive `authorize()` yourself.
155
160
  `ReplayResponse` is a union, so branch on `mode`.
156
161
 
157
- - **`token`** (every processor but Adyen). The cardholder's device calls the
162
+ - **`token`** (every processor but Adyen and Fiserv). The cardholder's device calls the
158
163
  processor and reports its answer; `authorize()` resolves with `status`,
159
164
  `headers` and `body` to fulfill the paused request with. The browser checks
160
165
  a fulfilled answer exactly as it checks a real one, so when the page called
@@ -170,7 +175,7 @@ three; the difference matters if you drive `authorize()` yourself.
170
175
  the page could not read the answer). Only what a browser serializes is
171
176
  echoed: one canonical http(s) origin, or the opaque `null`. Shopify's
172
177
  card iframe posts to its own origin, so it never needed them.
173
- - **`cse`** (Adyen). Adyen's own page SDK encrypts the card before the request
178
+ - **`cse`** (Adyen and Fiserv). Adyen's own page SDK encrypts the card before the request
174
179
  leaves the browser, so the paused body carries ciphertext. The cardholder's
175
180
  device produces the same ciphertext under the merchant's Adyen public key
176
181
  (fetched by Agentcard from Adyen's host when the request is parked) and
@@ -186,7 +191,19 @@ three; the difference matters if you drive `authorize()` yourself.
186
191
  absent, Adyen reads the brand off the card it decrypts). A body that lacks
187
192
  the fields throws `SubstitutionError`, which is not terminal. Adyen answers the browser,
188
193
  so `charged_kind` is null on the approval and the merchant's order state is
189
- the outcome to poll.
194
+ the outcome to poll. A cse replay with `kind: 'merchant_hosted'` is an Adyen
195
+ merchant's own endpoint instead: its `substitutions` name the profile, a JSON
196
+ path (`[]` is the body root) and the merchant's field names, and
197
+ `substituteMerchantHostedBody(body, replay)` writes it after checking the body
198
+ against `bodySha256` (see "Adyen merchants that take the card on their own
199
+ server"). A Fiserv card capture (a prepared checkout only) comes back with its
200
+ envelope's four members at `source.encryptionData`: write them with
201
+ `substituteFiservEnvelope(body, substitutions)`, never
202
+ `substituteEncryptedFields`, and continue the request the same way.
203
+ `substituteFiservEnvelope` throws, writing nothing, when the body is not the
204
+ paused capture or the envelope names another key than the capture does. The
205
+ API serves a Fiserv envelope only in the moments after the approval; a later
206
+ read raises `PaymentOutcomeUnknownError` with reason `cse_substitutions_expired`.
190
207
  - **`hosted_form`** (Tranzila). The processor's hosted card form submits the
191
208
  card as a TOP-LEVEL form post, so the paused request is a page navigation
192
209
  (the adapters arm `Fetch.enable` with no resource-type filter and attach the
@@ -290,12 +307,32 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
290
307
  page's re-posts for `approvalCooldownMs` (no second notification for one
291
308
  payment) and judge the next request afresh, since the prior authorization
292
309
  declines or expires and the same form is then a new question.
310
+ - `AdyenTestPlatformRefusedError`: a payment on Adyen's TEST platform was
311
+ refused, since a test account can read whatever is encrypted under its key.
312
+ Nothing was encrypted or charged. An `ApprovalDeclinedError` with `code`
313
+ `adyen_test_environment_refused` (a live checkout names Adyen's test host or
314
+ a `test_` client key; at `stage: 'create'` nothing was created and the
315
+ adapters stop intercepting for that page) or
316
+ `adyen_test_platform_requires_documented_test_card` (a test-mode checkout
317
+ where the cardholder's card is not one of Adyen's documented test cards).
318
+ The controller reads `failed` at `stage: 'create'` and `declined` at
319
+ `stage: 'pre_replay'`, with the code as its reason, and the adapters' event
320
+ names the code (`AdyenTestPlatformRefusedError: <code>`).
321
+ A preparation refused for Adyen's test platform, or for a merchant profile
322
+ that is not turned on, not offered to your account, or whose key or checkout
323
+ page changed, throws `CheckoutPreparationError` with the API's code as its
324
+ `reason`.
293
325
  - `CardEncryptedError`: this processor encrypts the card in-page and its
294
326
  registry entry does not (yet) say the vault can produce that ciphertext;
295
327
  route the purchase to an Agentcard-issued card instead.
296
328
  - `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
297
329
  before an authorization exists. Upgrade. An unexpected approved mode instead
298
330
  raises `PaymentOutcomeUnknownError` and requires reconciliation.
331
+ - `PreparationRequiredError`: a direct `authorize()` call for a processor that
332
+ pays only through a prepared checkout (Fiserv) passed no preparation. Nothing
333
+ was created and nothing was charged; call `prepareCheckout()` first. The
334
+ adapters refuse such a request before it pauses, with the controller reason
335
+ `preparation_required`.
299
336
  - `SubstitutionError`: a `cse` approval could not be written into the paused
300
337
  body (the four encrypted fields were not there). The request is failed and
301
338
  the next one is judged afresh.
@@ -458,14 +495,22 @@ the original request's claim. Do not cancel that checkout in response to the dup
458
495
  authorization before another attempt. Neither disposition proves a payment outcome.
459
496
 
460
497
  With `requireMerchantResult`, subsequent card requests stay blocked after
461
- handoff. Stripe `/v1/payment_methods` and `/v1/tokens` handoffs always hold further
462
- recognized card requests, even when that option is false. A tokenization approval has no
463
- authoritative binding to a specific PaymentIntent, amount or currency. The first
464
- observed confirm cannot supply that binding. The SDK therefore blocks every
465
- follow-up confirm on that attachment, including the same token, an unrelated
466
- intent, changed amounts and retries. It reports `awaiting_merchant` with reason
498
+ handoff. Stripe tokenization handoffs (`/v1/payment_methods`, `/v1/tokens`,
499
+ `/v1/sources`, `/v1/confirmation_tokens`) always hold further recognized card
500
+ requests, even when that option is false. A tokenization approval has no
501
+ authoritative binding to a specific PaymentIntent, amount or currency, and the
502
+ page cannot supply one. The SDK reports `awaiting_merchant` with reason
467
503
  `stripe_tokenization_unbound`, while ordinary browser traffic stays available.
468
- There is no automatic token-to-intent continuation or merchant-continuation hook.
504
+
505
+ One follow-up can continue: the page's own card-free PaymentIntent confirm that
506
+ pays with the approved token (`confirmCardPayment` with `payment_method: 'pm_...'`,
507
+ `confirmPayment` with the confirmation token). The SDK asks the API, which reads
508
+ the payment from Stripe and allows it only for the approved amount and
509
+ currency, on the same Stripe account, paid with exactly the approved token.
510
+ Allowed, the request continues untouched, the SDK emits `stripe_payment_continued`
511
+ and the reason becomes `stripe_payment_continued`. One confirm continues per
512
+ approval; every other follow-up, including an unrelated intent, a changed
513
+ amount and a retry, stays blocked.
469
514
  Unrecognized merchant-server endpoints remain outside this guard unless listed
470
515
  in `paymentEndpoints`; this is not a guarantee against a merchant charging a
471
516
  saved token on its own server.
@@ -517,6 +562,7 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
517
562
  | Recurly | `shared` | Form-encoded POST to `/js/v1/token` on `api.recurly.com` or `api.eu.recurly.com` |
518
563
  | Spreedly | `shared` | Native iframe JSON POST to `/v1/payment_methods/restricted.json` on `core.spreedly.com` |
519
564
  | Adyen | `production` or `sandbox` | The Sessions `/checkoutshopper/v1/sessions/{id}/payments` POST on the matching Adyen host family (`sandbox` is `checkoutshopper-test.adyen.com` with a `test_` client key; `production` is the live hosts with a `live_` key), carrying a fresh card's four encrypted fields |
565
+ | Fiserv | `production` or `sandbox`, with `merchantProfile` | The card frame's `/ch/payments-vas/v1/card-capture` POST on `connect.fiservapis.com` (`production`) or `connect-cert.fiservapis.com` (`sandbox`), carrying a new card's envelope under the key `merchantProfile` names |
520
566
 
521
567
  Use `environment: 'shared'` for Bambora, Mercado Pago, Recurly and Spreedly because the same endpoint serves test and live requests. Agentcard cannot establish the processor's test mode from that URL or a credential prefix. Configure test mode through the merchant's processor account when testing. Agentcard's own `sandbox` flag remains separate. Prepared Worldpay, Bambora, Mercado Pago and Recurly requests reject saved-card and recurring request bodies; a refused request retires the local preparation. Reconcile any existing merchant attempt before creating a new attachment.
522
568
 
@@ -536,6 +582,17 @@ await controller.prepare({ psp: 'adyen', environment: 'production' }); // 'sandb
536
582
 
537
583
  After approval, click Pay once. adyen-web encrypts the agent's placeholder digits and posts its Sessions `/payments` request; the SDK pauses it and binds it to the approval, the cardholder's device (still on the approval page) encrypts the approved card under the merchant's Adyen key, and the request continues from your browser with only the four encrypted fields swapped and `brand` dropped. Adyen answers your browser, so the merchant's order state is the outcome to poll. The bound request must be a fresh `scheme` card on the approved host family with a client key of that environment; a stored-card, single-blob or store-the-card request never uses the approval. Adyen Web's own 60-second request timeout then covers only the pause, the bind and the device's encryption.
538
584
 
585
+ Prepare a Fiserv checkout before the agent clicks Pay:
586
+
587
+ ```ts
588
+ // The attachment names the key's merchant: attachToCdp(cdp, sessionId, { ..., merchant: 'Agentcard sandbox' })
589
+ await controller.prepare({ psp: 'fiserv', environment: 'sandbox', merchantProfile: 'agentcard_sandbox' });
590
+ ```
591
+
592
+ `merchantProfile` names the key Agentcard reviewed for the merchant, and the cardholder's approval page names that key's merchant. The checkout's `merchant` must be that merchant's name (`Agentcard sandbox` for `agentcard_sandbox`), so the approval, the preparation and the authorization name one merchant; any other name is refused with reason `merchant_profile_mismatch` before any network call. After approval, click Pay once. Fiserv's card frame encrypts the agent's placeholder and posts its card capture; the SDK pauses the capture and binds it to the approval, the cardholder's device encrypts the approved card under that key, and the capture continues from your browser with only the envelope's four members swapped. Fiserv answers your browser, so confirm the order with the merchant.
593
+
594
+ A card capture with no preparation is refused before anyone is asked: the controller reads `failed` with reason `preparation_required`, nothing reaches Fiserv, and a later `prepare()` on that attachment is refused, so attach again. Fastlane, ACH, gift and EBT captures on the same endpoint continue untouched and leave the preparation for the card capture. Every other body on that endpoint counts as a card capture, including one the SDK cannot prove card-free (not JSON, over 64 KiB, or a plaintext card): with no preparation it is refused the same way, and with one the bind refuses it as `checkout_changed`. So does a capture carrying a header that neither Fiserv's card frame nor the browser writes, such as the `traceparent` header tracing tools add: it is not the request the cardholder approved. Every Fiserv payment needs the cardholder's approval: the envelope carries no expiry, so Autopilot never pays one. Fiserv is off for every company until Agentcard turns it on for yours; until then the API serves your SDK no Fiserv recognizer, so nothing pauses on a Fiserv checkout, and `prepare()` fails with reason `processor_unavailable`. A merchant whose key Agentcard has not turned on yet fails with `unsupported_checkout`.
595
+
539
596
  Prepare a Spreedly checkout before submitting the merchant's card form:
540
597
 
541
598
  ```ts
@@ -582,6 +639,41 @@ does not revoke a pending approval link or undo a processor payment. Cancellatio
582
639
  after an attempt starts is therefore unknown until reconciled. A cancelled
583
640
  attachment cannot restart.
584
641
 
642
+ ### Adyen merchants that take the card on their own server
643
+
644
+ Some Adyen merchants have adyen-web encrypt the card into four fields and post them to their own server, which then charges the card through Adyen. No processor host is involved, so no recognizer matches that request. Agentcard reviews each such merchant as a merchant profile: its endpoint, the body rules of its card request, and the Adyen key the card must be encrypted under. The SDK arms every profile this build reviewed from the start, in `observe`, so the agent's dummy card never reaches a reviewed merchant, even before `syncRegistry()` runs or when it fails. `syncRegistry()` asks the API for each profile's status on your client (`merchant_profiles=1`), and a profile turns `enabled` only when the API serves it enabled with the same rules this SDK build reviewed and this build ships it enabled. Both adapters pause the profile's endpoint and any sibling of it (the same path under another query) from attach, and judge each request there against the profile's status at that moment.
645
+
646
+ - A request that carries no encrypted card (a basket query, a preflight) continues as before.
647
+ - A body the browser did not hand over (a blob or a file, which neither adapter can read) is not proven card-free, so any request but a `GET`, `HEAD` or `OPTIONS` that carries one is treated as a card request the profile cannot finish. Playwright reports a missing body the same way, so an empty body is treated alike in both adapters.
648
+ - A profile in `observe` never pays. Its card request is aborted before it reaches the merchant, and the adapters report `merchant_profile_observed` with the profile, the templated URL, the verdict and `keyPathSha256`, the SHA-256 of the body's sorted key paths (never a value; `null` for a body that was not read), then `unsupported_checkout`. The controller reads `unsupported` with reason `merchant_profile_observe`, and no authorization exists. Both reviewed profiles, `dunelm_was_graphql` and `cinemark_initiate`, ship in `observe`.
649
+ - A card request the profile cannot finish (storing the card, a stored card, another operation, three fields instead of four, a sibling endpoint, an unreadable body) is aborted with `unsupported_checkout` and the reason, and reads `unsupported` with reason `merchant_request_unsupported`.
650
+ - The profile's own card request, once Agentcard enables the profile for your client and this build, pays only through a preparation for that profile. Without one it is aborted before any pause, prompt or authorization: the adapters report `blocked` with `preparation_required`, the controller reads `failed` with that reason, and `prepare()` is refused on that attachment, as after any first card request. Create a new attachment and prepare before Pay.
651
+
652
+ ```ts
653
+ await controller.prepare({ psp: 'adyen', environment: 'production', merchantProfile: 'dunelm_was_graphql' });
654
+ ```
655
+
656
+ The environment is the profile's (`production` for a live Adyen key). After approval, click Pay once. The SDK sends the API the profile id with the paused request (`merchant_hosted: { profile }`, `content-type` as the only header, never Autopilot), and the cardholder's device encrypts the approved card under the key Agentcard reviewed for that merchant. The paused request's URL and body reach the API with that create, as every processor's do: the API checks the body against the profile and the amount, then keeps only its SHA-256 and the templated URL, so anything else in the body (a gift card number, a session token, an idempotency key) passes through Agentcard once and is not stored. The approval comes back as a `cse` replay with `kind: 'merchant_hosted'`, and the request continues from your browser with the four fields swapped and adyen-web's `brand` dropped, but only if the live body still hashes to the body the API checked at create (`bodySha256`). A continuation the SDK refuses sends nothing, and the SDK retires the approval (`merchant_never_retried`) so the API stops serving its ciphertext. The merchant's server charges the card and answers its own page; confirm the order with the merchant.
657
+
658
+ After hand-off, a `5xx` final answer (after any redirect), a network failure, or the page closing before an answer arrived reports `payment_outcome_unknown` and holds the checkout (`outcome_unknown`). So does the page sending the card request again before an answer arrived. Every later card request to the same profile is aborted, since it carries the agent's dummy card and a prepared approval is single use. Events name a merchant profile's endpoint by its templated URL (origin, path template and the profile's own query), so a user id, cart id or email in the merchant's URL never reaches telemetry.
659
+
660
+ #### Testing against your own Adyen TEST account
661
+
662
+ A test-mode client can run the same checkout against its own server and its own Adyen TEST account, before any reviewed profile is enabled. Declare your endpoint on the client, before any attach, as taking the card request of a profile this build reviewed:
663
+
664
+ ```ts
665
+ const vault = new VaultClient({ clientId, clientSecret, sandboxMerchants: [
666
+ { profile: 'dunelm_was_graphql', endpoint: 'https://pay.your-test-shop.example/graphql', clientKey: 'test_...' },
667
+ ] });
668
+ await controller.prepare({ psp: 'adyen', environment: 'sandbox', merchantProfile: 'dunelm_was_graphql' });
669
+ ```
670
+
671
+ The adapters then pause that exact endpoint (and its siblings) and judge its requests with the profile's body rules, whatever the profile's status. The preparation and the create carry `sandbox_declaration: { endpoint, client_key, environment: 'test' }`, and both name your endpoint's host as the merchant. The API accepts a declaration only from a test-mode client of an organization Agentcard has turned declarations on for. Your endpoint must be HTTPS on a public domain name, outside every payment processor's host, Adyen's and Agentcard's domains, and the domain of every merchant Agentcard reviewed (so `www.dunelm.com` is refused along with `was.dunelm.com`); the client checks the same rule when it is constructed. The API reads the key's public key from Adyen's TEST platform itself. The Vault page names your host, holds the bound request's key to the one the preparation read, and encrypts only Adyen's documented test cards under a `test_` key: a real card is refused on the device. A live client, or an organization without declarations turned on, gets `sandbox_declaration_refused`, and a declaration the API cannot pay gets `sandbox_declaration_invalid`, both as `CheckoutPreparationError` reasons at `prepare()`.
672
+
673
+ On a client that declares a profile, that profile id names the declaration: `prepare({ merchantProfile })` with that id pays your endpoint, never the reviewed merchant. A test-mode client can never pay a reviewed merchant on Adyen's live platform anyway; use a separate live client for that.
674
+
675
+ A raw CDP runtime arms `vault.merchantProfileUrlPatterns()` after `syncRegistry()`, beside `vault.cardUrlPatterns()`, reads `vault.merchantProfileOf(url)` and `classifyMerchantRequest(profile, url, method, body)` for each paused request, and writes a merchant-hosted approval with `substituteMerchantHostedBody(body, replay)`. Never continue a card body, or a body you could not read, to a profile's endpoint that `authorize()` did not pay.
676
+
585
677
  ### Unsupported endpoints
586
678
 
587
679
  `paymentEndpoints` is an explicit list supplied by the integrator after observing
@@ -0,0 +1,277 @@
1
+ declare var MERCHANT_EXCHANGES_MAX: number;
2
+ declare var MERCHANT_PROFILE_RECOGNIZERS: any;
3
+ declare function declaredEndpointRefusal(endpoint: any): string | null;
4
+ declare function sha256Hex(input: any): string;
5
+ declare function merchantAmountFromResponses(profile: any, input: any): {
6
+ ok: boolean;
7
+ code: any;
8
+ reason: any;
9
+ } | {
10
+ ok: boolean;
11
+ merchant: any;
12
+ origin: string;
13
+ placeholders: {};
14
+ body: any;
15
+ pausedAt: number;
16
+ page: any;
17
+ checkoutOrigins: unknown[];
18
+ } | {
19
+ ok: boolean;
20
+ ctx: {
21
+ ok: boolean;
22
+ code: any;
23
+ reason: any;
24
+ } | {
25
+ ok: boolean;
26
+ merchant: any;
27
+ origin: string;
28
+ placeholders: {};
29
+ body: any;
30
+ pausedAt: number;
31
+ page: any;
32
+ checkoutOrigins: unknown[];
33
+ };
34
+ responses: any[];
35
+ origin: any;
36
+ } | {
37
+ ok: boolean;
38
+ amountCents: number;
39
+ currency: any;
40
+ } | {
41
+ ok: boolean;
42
+ amount: {
43
+ amountCents: any;
44
+ currency: any;
45
+ };
46
+ source: {
47
+ kind: string;
48
+ id: any;
49
+ method: any;
50
+ path: any;
51
+ completedAt: number;
52
+ ageMs: number;
53
+ bodyAmount: string;
54
+ };
55
+ };
56
+ declare function merchantChargedFromResponses(profile: any, input: any): {
57
+ ok: boolean;
58
+ code: any;
59
+ reason: any;
60
+ } | {
61
+ ok: boolean;
62
+ merchant: any;
63
+ origin: string;
64
+ placeholders: {};
65
+ body: any;
66
+ pausedAt: number;
67
+ page: any;
68
+ checkoutOrigins: unknown[];
69
+ } | {
70
+ ok: boolean;
71
+ ctx: {
72
+ ok: boolean;
73
+ code: any;
74
+ reason: any;
75
+ } | {
76
+ ok: boolean;
77
+ merchant: any;
78
+ origin: string;
79
+ placeholders: {};
80
+ body: any;
81
+ pausedAt: number;
82
+ page: any;
83
+ checkoutOrigins: unknown[];
84
+ };
85
+ responses: any[];
86
+ origin: any;
87
+ } | {
88
+ ok: boolean;
89
+ charged: {
90
+ amountCents: number;
91
+ currency: any;
92
+ };
93
+ source: {
94
+ kind: string;
95
+ id: string;
96
+ method: any;
97
+ path: any;
98
+ completedAt: any;
99
+ };
100
+ readings: number;
101
+ };
102
+ declare function merchantAmountStagesOf(profile: any, request: any, scope: any): string[];
103
+ declare function merchantAmountPending(profile: any, stage: any, card: any, request: any): boolean;
104
+ declare function merchantAmountExchanges(profile: any, stage: any, input: any): {
105
+ ok: boolean;
106
+ code: any;
107
+ reason: any;
108
+ } | {
109
+ ok: boolean;
110
+ merchant: any;
111
+ origin: string;
112
+ placeholders: {};
113
+ body: any;
114
+ pausedAt: number;
115
+ page: any;
116
+ checkoutOrigins: unknown[];
117
+ } | {
118
+ ok: boolean;
119
+ stage: any;
120
+ origin: any;
121
+ } | {
122
+ ok: boolean;
123
+ responses: any[];
124
+ };
125
+ declare function merchantTotalFromResponses(profileId: any, input: any): {
126
+ ok: boolean;
127
+ code: any;
128
+ reason: any;
129
+ } | {
130
+ ok: boolean;
131
+ merchant: any;
132
+ origin: string;
133
+ placeholders: {};
134
+ body: any;
135
+ pausedAt: number;
136
+ page: any;
137
+ checkoutOrigins: unknown[];
138
+ } | {
139
+ ok: boolean;
140
+ ctx: {
141
+ ok: boolean;
142
+ code: any;
143
+ reason: any;
144
+ } | {
145
+ ok: boolean;
146
+ merchant: any;
147
+ origin: string;
148
+ placeholders: {};
149
+ body: any;
150
+ pausedAt: number;
151
+ page: any;
152
+ checkoutOrigins: unknown[];
153
+ };
154
+ responses: any[];
155
+ origin: any;
156
+ } | {
157
+ ok: boolean;
158
+ amountCents: number;
159
+ currency: any;
160
+ } | {
161
+ ok: boolean;
162
+ amount: {
163
+ amountCents: any;
164
+ currency: any;
165
+ };
166
+ source: {
167
+ kind: string;
168
+ id: any;
169
+ method: any;
170
+ path: any;
171
+ completedAt: number;
172
+ ageMs: number;
173
+ bodyAmount: string;
174
+ };
175
+ } | {
176
+ amount: {
177
+ amount: any;
178
+ currency: any;
179
+ };
180
+ ok: boolean;
181
+ code: any;
182
+ reason: any;
183
+ } | {
184
+ amount: {
185
+ amount: any;
186
+ currency: any;
187
+ };
188
+ ok: boolean;
189
+ merchant: any;
190
+ origin: string;
191
+ placeholders: {};
192
+ body: any;
193
+ pausedAt: number;
194
+ page: any;
195
+ checkoutOrigins: unknown[];
196
+ } | {
197
+ amount: {
198
+ amount: any;
199
+ currency: any;
200
+ };
201
+ ok: boolean;
202
+ ctx: {
203
+ ok: boolean;
204
+ code: any;
205
+ reason: any;
206
+ } | {
207
+ ok: boolean;
208
+ merchant: any;
209
+ origin: string;
210
+ placeholders: {};
211
+ body: any;
212
+ pausedAt: number;
213
+ page: any;
214
+ checkoutOrigins: unknown[];
215
+ };
216
+ responses: any[];
217
+ origin: any;
218
+ } | {
219
+ amount: {
220
+ amount: any;
221
+ currency: any;
222
+ };
223
+ ok: boolean;
224
+ amountCents: number;
225
+ currency: any;
226
+ } | {
227
+ amount: {
228
+ amount: any;
229
+ currency: any;
230
+ };
231
+ ok: boolean;
232
+ source: {
233
+ kind: string;
234
+ id: any;
235
+ method: any;
236
+ path: any;
237
+ completedAt: number;
238
+ ageMs: number;
239
+ bodyAmount: string;
240
+ };
241
+ };
242
+ declare function merchantProfileUrlRelation(profileId: any, rawUrl: any): "endpoint" | "sibling" | null;
243
+ declare function carriesAdyenCardCiphertext(body: any): any;
244
+ declare function classifyMerchantBody(profileId: any, body: any): Readonly<{
245
+ verdict: "pass";
246
+ }> | Readonly<{
247
+ verdict: "abort";
248
+ reason: any;
249
+ }> | Readonly<{
250
+ verdict: "pause";
251
+ }>;
252
+ declare function classifyMerchantRequest(profileId: any, rawUrl: any, method: any, body: any): Readonly<{
253
+ verdict: "pass";
254
+ }> | Readonly<{
255
+ verdict: "abort";
256
+ reason: any;
257
+ }> | Readonly<{
258
+ verdict: "pause";
259
+ }>;
260
+ declare function declaredUrlRelation(endpoint: any, rawUrl: any): "endpoint" | "sibling" | null;
261
+ declare function classifyDeclaredMerchantRequest(profileId: any, endpoint: any, rawUrl: any, method: any, body: any): Readonly<{
262
+ verdict: "pass";
263
+ }> | Readonly<{
264
+ verdict: "abort";
265
+ reason: any;
266
+ }> | Readonly<{
267
+ verdict: "pause";
268
+ }>;
269
+ declare function declaredTemplatedUrl(endpoint: any): string;
270
+ declare function merchantProfileTemplatedUrl(profileId: any, rawUrl: any): string;
271
+ declare function merchantBodyKeyPathSha256(body: any): string | null;
272
+ declare function merchantProfileUrlGlobs(profileId: any): unknown[];
273
+ declare function declaredUrlGlobs(endpoint: any): string[];
274
+ declare function merchantProfileRulesMatch(profileId: any, served: any): boolean;
275
+ declare function merchantProfileEnvironment(profileId: any): "production" | "sandbox";
276
+ declare function merchantProfileEndpointFor(profileId: any, checkoutOrigin: any): string | null;
277
+ export { MERCHANT_EXCHANGES_MAX, MERCHANT_PROFILE_RECOGNIZERS, carriesAdyenCardCiphertext, classifyDeclaredMerchantRequest, classifyMerchantBody, classifyMerchantRequest, declaredEndpointRefusal, declaredTemplatedUrl, declaredUrlGlobs, declaredUrlRelation, merchantAmountExchanges, merchantAmountFromResponses, merchantAmountPending, merchantAmountStagesOf, merchantBodyKeyPathSha256, merchantChargedFromResponses, merchantProfileEndpointFor, merchantProfileEnvironment, merchantProfileRulesMatch, merchantProfileTemplatedUrl, merchantProfileUrlGlobs, merchantProfileUrlRelation, merchantTotalFromResponses, sha256Hex };