@agent-cards/checkout 0.18.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.
- package/CHANGELOG.md +8 -0
- package/PREFLIGHT.md +4 -0
- package/README.md +91 -7
- package/dist/adyen-merchant-hosted.generated.d.ts +277 -0
- package/dist/adyen-merchant-hosted.generated.js +1902 -0
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/cdp.d.ts +4 -1
- package/dist/cdp.js +416 -217
- package/dist/client.d.ts +236 -5
- package/dist/client.js +514 -11
- package/dist/cse-body.d.ts +25 -0
- package/dist/cse-body.js +41 -0
- package/dist/fiserv.d.ts +65 -0
- package/dist/fiserv.generated.d.ts +73 -0
- package/dist/fiserv.generated.js +830 -0
- package/dist/fiserv.js +104 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -1
- package/dist/lifecycle.d.ts +15 -1
- package/dist/lifecycle.js +28 -3
- package/dist/merchant-handoff.d.ts +54 -0
- package/dist/merchant-handoff.js +100 -0
- package/dist/merchant-hosted.d.ts +140 -0
- package/dist/merchant-hosted.js +170 -0
- package/dist/merchant-total-watch.d.ts +115 -0
- package/dist/merchant-total-watch.js +268 -0
- package/dist/merchant-total.d.ts +257 -0
- package/dist/merchant-total.js +383 -0
- package/dist/pre-claim.d.ts +123 -0
- package/dist/pre-claim.js +386 -0
- package/dist/preflight-catalog.json +132 -0
- package/dist/preflight-schemas.json +14 -2
- package/dist/preflight.generated.js +15 -1
- package/dist/preparation.d.ts +7 -0
- package/dist/preparation.js +33 -6
- package/dist/prepared-processor.d.ts +36 -3
- package/dist/prepared-processor.js +53 -3
- package/dist/registry.d.ts +45 -0
- package/dist/registry.js +14 -0
- package/dist/stripe-checkout.generated.js +96 -7
- package/dist/substitutions.generated.d.ts +2 -1
- package/dist/substitutions.generated.js +758 -6
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
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.
|
|
6
14
|
|
|
7
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.
|
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
|
|
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
|
|
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`
|
|
148
|
-
|
|
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.
|
|
@@ -525,6 +562,7 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
|
|
|
525
562
|
| Recurly | `shared` | Form-encoded POST to `/js/v1/token` on `api.recurly.com` or `api.eu.recurly.com` |
|
|
526
563
|
| Spreedly | `shared` | Native iframe JSON POST to `/v1/payment_methods/restricted.json` on `core.spreedly.com` |
|
|
527
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 |
|
|
528
566
|
|
|
529
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.
|
|
530
568
|
|
|
@@ -544,6 +582,17 @@ await controller.prepare({ psp: 'adyen', environment: 'production' }); // 'sandb
|
|
|
544
582
|
|
|
545
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.
|
|
546
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
|
+
|
|
547
596
|
Prepare a Spreedly checkout before submitting the merchant's card form:
|
|
548
597
|
|
|
549
598
|
```ts
|
|
@@ -590,6 +639,41 @@ does not revoke a pending approval link or undo a processor payment. Cancellatio
|
|
|
590
639
|
after an attempt starts is therefore unknown until reconciled. A cancelled
|
|
591
640
|
attachment cannot restart.
|
|
592
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
|
+
|
|
593
677
|
### Unsupported endpoints
|
|
594
678
|
|
|
595
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 };
|