@agent-cards/checkout 0.5.0 → 0.7.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - A company can put rules on the Vault purchases it chooses (a merchant list, a currency, a spend cap, a time window) by attaching a named preset to a stored card. A purchase the rules refuse now surfaces as `PresetRefusedError`, a typed decline: at stage `create` no authorization exists and nobody was asked; at stage `pre_replay` the authorization is `declined` with the rule's reason. It carries `code`, `presetName`, `rule`, `preset`, `attachment` (the card with its last four digits) and the rule's own statement; when several presets refuse the same purchase, `refusals` names every one and the other fields are the first. The adapters quiet the page's retry as for any decline, and a create-time refusal is never treated as a permanent misconfiguration.
6
+ - Requires the matching API and Vault release.
7
+
8
+ ## 0.6.0
9
+
10
+ - Recognize Paysafe Checkout 1.8's exact hosted tokenization endpoints and preserve its native credential and correlation headers. The matching API registry and Vault deployment are required.
11
+ - Recognize Checkout.com card tokenization at `card-acquisition-gateway.checkout.com/tokens` and its sandbox counterpart alongside the existing API hosts. The matching backend registry and Vault deployment are required; this entry does not establish native merchant payment completion.
12
+ - Prepare Worldpay, Bambora and Mercado Pago checkout before the merchant's first Pay action. Use `psp: 'worldpay'` with `environment: 'production' | 'sandbox'`, or `psp: 'bambora' | 'mercado_pago'` with `environment: 'shared'`. Shared endpoints do not establish processor test mode. The merchant's credentials and checkout configuration determine that mode.
13
+ - Keep the existing selected-card, merchant, document, amount, currency and one-use consent checks. The native request begins only after approval, and approval readiness lasts at most 30 seconds. The SDK does not extend processor deadlines or automatically retry after payment uncertainty. Matching API, Vault and preparation migration are required.
14
+ - Bound Playwright and CDP setup with `attachmentTimeoutMs`, defaulting to 30 seconds. A stalled setup throws a sanitized `CheckoutAttachmentError`; late acknowledgements cannot start approval or retry setup, and concurrent setup failures report once. Close the failed checkout page and start a fresh context before trying again.
15
+ - Preserve the original setup failure across attached frames: a transport failure reports `unavailable`, a deadline reports `timeout`, and an observed page closure reports `closed`.
16
+ - Reject processor URLs with credentials or nondefault ports consistently across SDK and API discovery. Registry synchronization updates endpoint recognition; upgrading the SDK is required for the new preparation and setup behavior.
17
+
18
+ The matching Vault/API release adds Mollie card-token and supported Airwallex intent-confirmation replay through browser-owned TLS, preserves Worldpay's native session media type and Bambora's `cvd` field, and reports Nuvei's unambiguous validation error as processor refusal. Local regression and Chromium fixture results do not establish native processor acceptance or a completed merchant purchase.
19
+
3
20
  ## 0.5.0
4
21
 
5
22
  - Prepare Braintree card checkout with `controller.prepare({ psp: 'braintree', environment: 'production' | 'sandbox' })` before the merchant's first Pay action. The cardholder approves and unlocks first; one fresh native request then uses that approval without another notification. The matching API and Vault release is required.
package/README.md CHANGED
@@ -54,30 +54,28 @@ await attachToCdp(cdp, pageSessionId, {
54
54
  vault,
55
55
  user: 'usr_123', // whose card should pay
56
56
  merchant: 'vanman.shop',
57
- amountCents: 583, // what the user approves, smallest currency unit
57
+ amount: 583, // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
58
58
  currency: 'usd', // "$5.83" is derived for the approval screen
59
59
  onApprovalUrl: (url) => sendToUser(url), // iMessage, SMS, push, your call
60
60
  });
61
61
  ```
62
62
 
63
- `amount: '$5.83'` (a display string) still works on its own. Pass
64
- `amountCents` + `currency` when you want the amount enforced: on a Stripe
65
- PaymentIntent confirm, Agentcard reads the intent back from Stripe at create
66
- and again right before the cardholder's device replays, a different amount is
67
- refused with nothing charged, and after the replay the charge is reconciled
68
- against the approval (`ReplayResponse.amountVerified`, with
69
- `chargedAmountCents` and `chargedKind`: `captured` for a succeeded intent's
70
- `amount_received`, `authorized` for a manual-capture intent's
71
- `amount_capturable`, `none` when nothing is collected yet; plus the
72
- `checkout_authorization.amount_mismatch` webhook to your server when it
73
- disagrees). Amounts are Stripe minor units up to 2^53-1. `onEvent` payloads
74
- name URLs by origin and path only, so a client secret in a paused request's
75
- query string never reaches your telemetry. When you pass the pair, any
76
- `amount` string you also pass is
77
- ignored: Agentcard derives the display string from the number, following
78
- Stripe's minor units, so the approval screen, the notifications and every
79
- read show one amount. Tokenization requests carry no amount, so there the
80
- pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
63
+ `amount` is your hint: an integer in the currency's smallest unit (583 for
64
+ $5.83), or a decimal string in normal units ('5.83'), with `currency`. The
65
+ processor's own amount is the higher authority: Agentcard reads it from the
66
+ paused request where the processor puts it there, or from the Stripe intent
67
+ the request names, right before the cardholder's device replays, and the
68
+ company's caps are judged on it. A hint lets a bad purchase be refused the
69
+ moment it opens; a hint more than one smallest unit away from the processor's
70
+ amount is refused with nothing charged (`AmountMismatchError`), and after the
71
+ replay the charge is reconciled against the approval
72
+ (`ReplayResponse.amountVerified`, `chargedAmount`, and `chargedKind`:
73
+ `captured` for a succeeded intent's `amount_received`, `authorized` for a
74
+ manual-capture intent's `amount_capturable`, `none` when nothing is collected
75
+ yet; plus the `checkout_authorization.amount_mismatch` webhook to your server
76
+ when the charge disagreed). Every result carries `amountAuthority`:
77
+ `processor`, `agent`, `page`, or `none`. A display string is never sent;
78
+ Agentcard derives it.
81
79
 
82
80
  Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
83
81
  request for approval and resumes it only while the merchant request remains
@@ -129,8 +127,8 @@ Coverage is specific to the processor request format, merchant setup, browser tr
129
127
  | Processor | Status |
130
128
  |---|---|
131
129
  | Shopify | supported, verified end to end |
132
- | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms with `amountCents` + `currency` use backend amount verification. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
133
- | Braintree card tokenization | prepared checkout supported; production paid-order validation pending |
130
+ | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as `amount` + `currency` must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
131
+ | Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK `0.5.0` confirmed merchant fulfillment and SDK `completed` using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. |
134
132
  | Checkout.com | supported |
135
133
  | 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 |
136
134
  | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it |
@@ -327,6 +325,16 @@ Playwright `CDPSession` is not the `CdpLike` interface. Raw CDP must preserve th
327
325
  Initial arming errors reject `attachToCdp`; a child that cannot be armed remains
328
326
  paused and reports `browser_interception_unavailable` for operator recovery.
329
327
 
328
+ Await attachment before clicking Pay. Both adapters stop waiting for browser
329
+ setup after 30 seconds and throw `CheckoutAttachmentError` with
330
+ `code: 'checkout_attachment_failed'` and `reason: 'timeout'`, `'closed'` or
331
+ `'unavailable'`. Use `attachmentTimeoutMs` to choose a setup deadline from 1 to
332
+ 300000 milliseconds, separately from the approval's `timeoutMs`.
333
+ Close the failed checkout page and create a fresh browser context before
334
+ trying again. A late setup response cannot reopen the failed attachment or
335
+ request approval; intercepted card requests remain blocked. A setup failure
336
+ does not establish the status of any earlier purchase.
337
+
330
338
  Use a checkout context created with `serviceWorkers: 'block'`. Playwright cannot
331
339
  route requests intercepted by a service worker. The SDK rejects already active
332
340
  service workers, but that check cannot prevent a site from registering one
@@ -342,7 +350,7 @@ value continues to work. Choose `requireMerchantResult: true` for a pilot:
342
350
 
343
351
  ```ts
344
352
  const checkout = await attachToPlaywright(page, {
345
- vault, user, merchant, amountCents, currency,
353
+ vault, user, merchant, amount, currency,
346
354
  requireMerchantResult: true,
347
355
  onStateChange: state => recordState(state),
348
356
  onUserAction: action => deliverPrivatelyToUser(action),
@@ -385,7 +393,7 @@ Unrecognized merchant-server endpoints remain outside this guard unless listed
385
393
  in `paymentEndpoints`; this is not a guarantee against a merchant charging a
386
394
  saved token on its own server.
387
395
  A direct card-bearing PaymentIntent confirm remains supported with the backend's
388
- existing amount verification when `amountCents` and `currency` are supplied.
396
+ existing amount verification when `amount` and `currency` are supplied.
389
397
 
390
398
  Hosted-form submissions also always stay blocked because their payment outcome
391
399
  is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
@@ -400,24 +408,36 @@ do not reuse that token in a new attachment as a workaround. Configuration and
400
408
  unsupported-mode failures require fixing the integration. Bank flows requiring
401
409
  another confirmation and other stored-token chains remain unverified.
402
410
 
403
- Prepare a Square or Braintree checkout before the first Pay action so the cardholder can approve before the native card request starts. The API and Vault deployments must support the selected processor:
411
+ Worldpay, Bambora and Mercado Pago preparation requires SDK 0.6.0 or later and the matching API and Vault release.
412
+
413
+ Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before the first Pay action so the cardholder can approve before the native card request starts. The API and Vault deployments must support the selected processor:
404
414
 
405
415
  ```ts
406
416
  const checkout = await attachToPlaywright(page, {
407
417
  vault, user: 'your-user-id', merchant: 'Example merchant',
408
- amountCents: 100, currency: 'USD',
418
+ amount: 100, currency: 'USD',
409
419
  onApprovalUrl: deliverPrivatelyToCardholder,
410
420
  });
411
421
  const preparation = await checkout.prepare({
412
- psp: 'braintree', // use 'square' for Square
413
- environment: 'production', // explicit; use 'sandbox' for the processor's sandbox
422
+ psp: 'braintree',
423
+ environment: 'production', // Square, Braintree and Worldpay: production or sandbox
414
424
  });
415
425
  // The cardholder has consented and unlocked the same approval document.
416
426
  // No processor request or payment has started.
417
427
  await page.getByRole('button', { name: 'Pay', exact: true }).click();
418
428
  ```
419
429
 
420
- `prepare()` is available on both Playwright and raw CDP controllers. It requires `amountCents` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount remains `display_only`; a card token does not enforce the merchant's eventual charge amount.
430
+ `prepare()` is available on both Playwright and raw CDP controllers. It requires `amount` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount's authority is `agent`; a card token does not enforce the merchant's eventual charge amount.
431
+
432
+ | Processor | `environment` | Fresh native request |
433
+ | --- | --- | --- |
434
+ | Square | `production` or `sandbox` | Matching Square `/v2/card-nonce` host |
435
+ | Braintree | `production` or `sandbox` | Matching Braintree GraphQL host and guest `TokenizeCreditCard` mutation |
436
+ | Worldpay | `production` or `sandbox` | Matching Access Worldpay host and `/sessions/card` |
437
+ | Bambora | `shared` | `/scripts/tokenization/tokens` on `api.bam.shift4api.net` or `api.na.bambora.com` |
438
+ | Mercado Pago | `shared` | `api.mercadopago.com/v1/card_tokens` with a fresh card body |
439
+
440
+ Use `environment: 'shared'` for Bambora and Mercado Pago 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 and Mercado Pago 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.
421
441
 
422
442
  Braintree's native `ClientConfiguration` GraphQL query can run before, during or after preparation without using the approval. Only a single `TokenizeCreditCard` mutation can consume the prepared Braintree checkout. Prepared requests require guest card tokenization with explicit `options.validate: false`; omitted validation options, saved-card fields and `validate: true` are refused. Other GraphQL operations, batches and compound mutations are blocked. Braintree's legacy REST fallback cannot consume a prepared authorization.
423
443
 
@@ -472,9 +492,7 @@ The browser fixtures never contact a payment service. The general suite uses
472
492
  allowlisted loopback proxy and a temporary self-signed TLS stub (requires the
473
493
  `openssl` CLI). All other proxy destinations are rejected. These suites use an
474
494
  in-process Agentcard API fixture and loopback merchant pages. The preparation
475
- fixture denies all external traffic, waits more than ten seconds before any
476
- card request, and then checks one fresh request with an unchanged ten-second
477
- fixture abort timer. It exercises SDK ordering, not the native Square SDK. It proves nested-frame pause/resume, agent control during approval,
495
+ fixture denies all external traffic and waits beyond each modeled request deadline before any card request: eleven seconds for Square, sixty-one seconds for Braintree, and six and a half seconds for Worldpay, Bambora and Mercado Pago. Each fixture then checks one fresh request with an unchanged abort timer. The five-second timer matches the inspected Worldpay and Bambora source; the Mercado Pago timer is a test boundary, not a measured native deadline. The fixture exercises SDK ordering with native-shaped request bodies and synthetic responses, not processor acceptance. It proves nested-frame pause/resume, agent control during approval,
478
496
  post-payment tasks in the same page, decline/expiry/cancel, unknown-outcome retry
479
497
  blocking, explicit unsupported endpoint behavior, and blocking an immediate real-browser
480
498
  Stripe token-to-intent fetch chain, including unrelated first intents, changed
@@ -0,0 +1,11 @@
1
+ /** An attachment failure has no payment or browser credentials in its message. */
2
+ export declare class CheckoutAttachmentError extends Error {
3
+ readonly reason: 'timeout' | 'closed' | 'unavailable';
4
+ readonly code = "checkout_attachment_failed";
5
+ constructor(reason: 'timeout' | 'closed' | 'unavailable');
6
+ }
7
+ /** Preserve a known setup cause without exposing raw transport errors. */
8
+ export declare function attachmentFailure(error: unknown): CheckoutAttachmentError;
9
+ export declare function attachmentDeadline(value: number | undefined): number;
10
+ /** Stop waiting, then reject every late continuation before its next command. */
11
+ export declare function withinAttachmentDeadline(arm: (assertActive: () => void) => Promise<void>, timeoutMs: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,50 @@
1
+ /** An attachment failure has no payment or browser credentials in its message. */
2
+ export class CheckoutAttachmentError extends Error {
3
+ reason;
4
+ code = 'checkout_attachment_failed';
5
+ constructor(reason) {
6
+ super('Could not attach checkout interception. Close this checkout page and retry in a fresh browser context.');
7
+ this.reason = reason;
8
+ this.name = 'CheckoutAttachmentError';
9
+ }
10
+ }
11
+ /** Preserve a known setup cause without exposing raw transport errors. */
12
+ export function attachmentFailure(error) {
13
+ return error instanceof CheckoutAttachmentError ? error : new CheckoutAttachmentError('unavailable');
14
+ }
15
+ export function attachmentDeadline(value) {
16
+ if (value === undefined)
17
+ return 30_000;
18
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 300_000) {
19
+ throw new Error('attachmentTimeoutMs must be an integer between 1 and 300000.');
20
+ }
21
+ return value;
22
+ }
23
+ /** Stop waiting, then reject every late continuation before its next command. */
24
+ export async function withinAttachmentDeadline(arm, timeoutMs, signal) {
25
+ let stopped;
26
+ let timer;
27
+ let onAbort = () => { };
28
+ const assertActive = () => { if (stopped)
29
+ throw stopped; };
30
+ const deadline = new Promise((_resolve, reject) => {
31
+ const stop = (failure) => { stopped ??= failure; reject(stopped); };
32
+ onAbort = () => stop(attachmentFailure(signal.reason));
33
+ signal.addEventListener('abort', onAbort, { once: true });
34
+ if (signal.aborted)
35
+ onAbort();
36
+ else
37
+ timer = setTimeout(() => stop(new CheckoutAttachmentError('timeout')), timeoutMs);
38
+ });
39
+ try {
40
+ await Promise.race([deadline, Promise.resolve().then(() => { assertActive(); return arm(assertActive); })]);
41
+ }
42
+ catch (error) {
43
+ stopped ??= attachmentFailure(error);
44
+ throw stopped;
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ signal.removeEventListener('abort', onAbort);
49
+ }
50
+ }
@@ -2,6 +2,8 @@
2
2
  export type BraintreeEnvironment = 'production' | 'sandbox';
3
3
  export type BraintreeRequestKind = 'configuration' | 'tokenization' | 'invalid';
4
4
  export declare function braintreeEnvironment(url: string): BraintreeEnvironment | undefined;
5
+ /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
6
+ export declare function readTokenizationJson(body: string | null): Record<string, unknown> | null;
5
7
  /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
6
8
  export declare function classifyBraintreeRequest(url: string, method: string, body: string | null): BraintreeRequestKind | undefined;
7
9
  /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
package/dist/braintree.js CHANGED
@@ -115,6 +115,18 @@ class JsonReader {
115
115
  return value;
116
116
  }
117
117
  }
118
+ /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
119
+ export function readTokenizationJson(body) {
120
+ if (typeof body !== 'string' || body.length === 0 || body.length > MAX_BODY_LENGTH)
121
+ return null;
122
+ try {
123
+ const value = new JsonReader(body).read();
124
+ return record(value) ? value : null;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
118
130
  /** Native operations need names, punctuation and variables, never literals. */
119
131
  function lex(query) {
120
132
  const tokens = [];
package/dist/cdp.d.ts CHANGED
@@ -75,18 +75,32 @@ export interface AttachOptions extends LifecycleOptions {
75
75
  vault: VaultClient;
76
76
  user: string;
77
77
  merchant: string;
78
- /** Display string for the approval screen. Optional when amountCents + currency are given. */
79
- amount?: string;
80
78
  /**
81
- * The amount as a number (smallest currency unit) with its ISO 4217 code.
82
- * See AuthorizeInput.amountCents: on a Stripe PaymentIntent confirm this
83
- * binds the approval to the intent's amount, checked at create and again
84
- * right before replay, so a larger charge is refused with nothing charged.
79
+ * Your hint at the amount, an integer in the currency's smallest unit or a
80
+ * decimal string in normal units, with its ISO 4217 code. See
81
+ * AuthorizeInput.amount: the processor's own amount is the higher authority
82
+ * and is read right before the card is sent; a hint lets a bad purchase be
83
+ * refused the moment it opens, and one that disagrees with the processor is
84
+ * refused with nothing charged.
85
85
  */
86
- amountCents?: number;
86
+ amount?: number | string;
87
87
  currency?: string;
88
+ /**
89
+ * The total the checkout page shows, the lowest authority: used only when
90
+ * neither the processor's request nor your `amount` names one. Pass a reader
91
+ * that returns an integer in the currency's smallest unit with its code
92
+ * (`{ amount: 4210, currency: 'usd' }`), read off the page however your
93
+ * page spells it; the adapters call it when a card request pauses, give it
94
+ * one second, and send nothing when it yields nothing. Absent: no page total.
95
+ */
96
+ pageAmount?: () => Promise<{
97
+ amount: number;
98
+ currency: string;
99
+ } | undefined>;
88
100
  cardId?: string;
89
101
  timeoutMs?: number;
102
+ /** Browser interception setup deadline, separate from approval. Defaults to 30000 ms; maximum 300000 ms. */
103
+ attachmentTimeoutMs?: number;
90
104
  /** Explicit payment endpoints to block if the registry cannot handle their method/format. Unlisted traffic is untouched. */
91
105
  paymentEndpoints?: readonly PaymentEndpointGuard[];
92
106
  onApprovalUrl?: (url: string) => void;