@agent-cards/checkout 0.10.0 → 0.12.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
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Complete a merchant-confirmed payment when the merchant supplies no order or receipt ID. Return `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }` from the merchant resolver using the current checkout authorization. The resolver must verify the original payment request, amount, currency, selected card and merchant success; HTTP 200 or a success page alone is insufficient. The completed state carries `confirmation` without inventing `orderId`, clears stale reasons and continues to block another payment. Existing completion results with a genuine `orderId` keep working.
6
+
7
+ - Approve a Recurly checkout before its native card request starts. Call `prepare` with `psp: 'recurly'` and `environment: 'shared'`, then submit the merchant's form once. The same selected card and approved purchase bind a new-card POST on the US or EU endpoint. The matching API, database migration and Vault release are required. Token issuance and merchant payment completion remain separate results.
8
+
5
9
  - Keep the selected card's issuer in guest Mercado Pago Checkout Pro in Mexico. The Vault resolves the card's eight-digit prefix against the merchant's captured checkout configuration over browser TLS. The SDK returns the original card-token response and corrects one native card association. Missing configuration, ambiguous issuers, changed checkout context, and a different card brand or type stop continuation. The matching API, Vault and encrypted relay release is required. Other Mercado Pago checkout families and merchant payment completion need separate validation.
6
10
 
7
11
  - Prepare controlled hosted Stripe Checkout in TEST mode with an explicit `stripeCheckout` attachment option and Autopilot grant. Dummy tokenization stays local; the browser's native final confirmation uses the generated shared core and enclave execution path. The matching backend and executor flags default off. This does not enable production Autopilot or establish a completed merchant order.
package/PREFLIGHT.md CHANGED
@@ -208,7 +208,7 @@ Declare processor support and flow support separately. A flow entry cannot estab
208
208
 
209
209
  Take PSP identifiers, payment modes, flow identifiers, operation identifiers, and operation revisions from the packaged catalog. Verify each combination against the named adapter version before adding a `supported` entry. Use `unsupported` only for a known exclusion. A profile with a wrong schema, incompatible catalog, mismatched adapter version, or expired timestamp preserves `unknown`.
210
210
 
211
- The JSON contract uses `schema_version: 1`. The catalog bundled with this release uses `catalog_version: "2026-09-11.2"`. Keep the snapshot, classifier, and profile on the same catalog version. Re-normalize observations with the installed package after changing the catalog. Review the adapter declarations before issuing a compatible profile; changing the version string alone does not validate new behavior.
211
+ The JSON contract uses `schema_version: 1`. The catalog bundled with this release uses `catalog_version: "2026-09-14.1"`. Keep the snapshot, classifier, and profile on the same catalog version. Re-normalize observations with the installed package after changing the catalog. Review the adapter declarations before issuing a compatible profile; changing the version string alone does not validate new behavior.
212
212
 
213
213
  Kernel can integrate the JSON contract immediately and supply its validated native capabilities later. No deployed Agentcard endpoint or payment request is required for that work.
214
214
 
package/README.md CHANGED
@@ -134,7 +134,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
134
134
  | 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 |
135
135
  | 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. |
136
136
  | Checkout.com | supported |
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. A completed merchant payment remains unverified. |
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
139
  | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it |
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 |
@@ -367,17 +367,56 @@ const checkout = await attachToPlaywright(page, {
367
367
 
368
368
  // Your existing agent dispatches checkout. Later, once the paused request resumes:
369
369
  const state = await checkout.reconcile();
370
- if (state.status === 'completed' && state.orderId) await finishAgentTask(state.orderId);
370
+ if (state.status === 'completed') await finishAgentTask(state);
371
371
  ```
372
372
 
373
- `resolveMerchantResult` must read an authoritative merchant order/receipt tied
374
- to this attempt. It returns one of:
373
+ `resolveMerchantResult` must verify the merchant's result for the original
374
+ payment attempt. It returns one of:
375
375
 
376
- - `{ status: 'completed', orderId }`: merchant-confirmed success.
376
+ - `{ status: 'completed', orderId }`: merchant-confirmed success with a genuine order or receipt ID. Existing integrations keep this form.
377
+ - `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }`: merchant-confirmed payment when no order or receipt ID is available. The ID must match the current checkout authorization.
377
378
  - `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
378
379
  - `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
379
380
  - `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
380
381
 
382
+ For a merchant that confirms payment without returning an order ID, your
383
+ resolver can return the explicit payment confirmation:
384
+
385
+ ```ts
386
+ const checkout = await attachToPlaywright(page, {
387
+ vault, user, merchant, amount, currency,
388
+ requireMerchantResult: true,
389
+ resolveMerchantResult: async state => {
390
+ const payment = await readOriginalMerchantPayment(state);
391
+ if (!payment.confirmed || !state.authorizationId) return { status: 'unknown' };
392
+ return {
393
+ status: 'completed',
394
+ confirmation: {
395
+ kind: 'merchant_payment',
396
+ authorizationId: state.authorizationId,
397
+ },
398
+ };
399
+ },
400
+ });
401
+ ```
402
+
403
+ `readOriginalMerchantPayment` is your merchant-specific check. The check must
404
+ match the original payment request, amount, currency and selected card, and
405
+ verify authoritative merchant success for that attempt. HTTP 200 alone, a card
406
+ token, a success URL or text that anyone can open does not establish payment.
407
+ Copying `state.authorizationId` without checking the payment is insufficient.
408
+ The SDK checks the authorization binding; it does not independently authenticate
409
+ the merchant evidence supplied by your resolver.
410
+
411
+ Return one completion form at a time. The payment-confirmation form leaves
412
+ `state.orderId` absent and exposes `state.confirmation` with the exported
413
+ `MerchantPaymentConfirmation` type. Your consumer should finish on
414
+ `state.status === 'completed'` and treat `orderId` as optional. A missing or
415
+ mismatched authorization, or `{ status: 'completed' }` without either completion
416
+ form, leaves the outcome unknown. Confirmed completion clears stale failure or
417
+ authentication reasons and continues to block further payment submissions.
418
+ Never invent an order ID from a token or authorization ID.
419
+
381
420
  The SDK does not infer order success from `authorized` or a tokenization reply,
382
421
  and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
383
422
  resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
@@ -388,22 +427,29 @@ isolated from the payment handoff.
388
427
  Native Stripe Checkout emits `checkout_blocked` before the existing `blocked`
389
428
  event when its local preparation rejects a request. Its
390
429
  `StripeCheckoutBlockedDetail` contains only fixed codes: `version: 1`,
391
- `processor: 'stripe'`, endpoint family, phase, stage, reason, gate state, and
392
- disposition. It contains no URLs, identifiers, request fields, or exception text.
430
+ `processor: 'stripe'`, endpoint family, phase, stage, reason, optional validation code, gate state, and
431
+ disposition. It contains no URLs, identifiers, request-derived field names or values, or exception text.
393
432
 
394
433
  | Field | Values |
395
434
  | --- | --- |
396
435
  | `endpoint_family` | `payment_methods`, `payment_page_confirm`, `other` |
397
436
  | `phase` | `tokenization`, `final`, `unknown` |
398
437
  | `stage` | `request_read`, `classification`, `claim`, `readiness`, `document`, `stub_response` |
438
+ | `validation_code` | Shared-core `StripeCheckoutValidationCode`; present only for `request_validation_failed` |
399
439
  | `gate_state` | `fresh`, `stubbed`, `submitted`, `stopped` |
400
440
  | `disposition` | `active_claim_preserved`, `checkout_stopped` |
401
441
 
402
442
  `reason` is the exported `StripeCheckoutBlockReason` union. It identifies SDK
403
443
  claim and readiness failures, such as `duplicate_confirmation`, `document_changed`,
404
- or `attachment_not_ready`. The shared core's rejection is
405
- `request_validation_failed` with phase `unknown`; this event does not infer which
406
- core predicate failed. `gate_state` records the state before the adapter handles
444
+ or `attachment_not_ready`. A shared-core rejection reports
445
+ `request_validation_failed` and a fixed `validation_code` identifying the failed
446
+ check, or `unclassified` when no recognized code is available. Initial request
447
+ classification uses phase `unknown`; a billing capture rejection uses
448
+ `tokenization`, and a billing attachment rejection uses `final`.
449
+ For example, `form_field_unknown` identifies an allowlist rejection without
450
+ revealing the field name or value. Identifying that field requires a separate
451
+ reviewed synthetic fixture; the code alone does not establish or fix a processor
452
+ schema mismatch. `gate_state` records the state before the adapter handles
407
453
  the failure, though document validation may already have stopped the gate.
408
454
 
409
455
  `active_claim_preserved` means a valid duplicate was refused without invalidating
@@ -467,8 +513,17 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
467
513
  | Worldpay | `production` or `sandbox` | Matching Access Worldpay host and `/sessions/card` |
468
514
  | Bambora | `shared` | `/scripts/tokenization/tokens` on `api.bam.shift4api.net` or `api.na.bambora.com` |
469
515
  | Mercado Pago | `shared` | `api.mercadopago.com/v1/card_tokens` with a fresh card body |
516
+ | Recurly | `shared` | Form-encoded POST to `/js/v1/token` on `api.recurly.com` or `api.eu.recurly.com` |
517
+
518
+ Use `environment: 'shared'` for Bambora, Mercado Pago and Recurly 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.
470
519
 
471
- 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.
520
+ Prepare Recurly before clicking the merchant's payment button:
521
+
522
+ ```ts
523
+ await controller.prepare({ psp: 'recurly', environment: 'shared' });
524
+ ```
525
+
526
+ After approval, submit the merchant's form once. Recurly's native request timer starts with that submission. The SDK preserves the captured US or EU endpoint and accepts a new-card form only. Legacy JSONP, saved tokens, bank accounts, alternative payments and proactive authentication requests require separate support. Prepared requests also refuse nonempty Worldpay or Cybersource risk results because those sessions can depend on the original card. A nonempty co-badged network preference is also refused until the selected card’s supported networks can be checked. A card token does not confirm a donation, subscription or purchase; confirm the merchant's result before reporting payment success.
472
527
 
473
528
  ### Use Checkout Pro in Mexico
474
529
 
@@ -570,10 +625,12 @@ When the cardholder has enabled an eligible spending rule in their vault, the sa
570
625
 
571
626
  Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
572
627
 
573
- For the controlled hosted Stripe Checkout TEST integration, both attachment functions accept `stripeCheckout: { sessionId, publishableKey }`. This requires a `cs_test_` Session, its `pk_test_` key, `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that Session on `checkout.stripe.com`.
628
+ For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that exact Session on `https://checkout.stripe.com`, at `/c/pay/{sessionId}`, `/pay/{sessionId}`, `/g/pay/{sessionId}`, or `/f/pay/{sessionId}`. The SDK validates the existing URL without rewriting or navigating it. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
574
629
 
575
630
  The SDK answers dummy-card tokenization locally, then sends the browser's final native confirmation through the shared core and enclave. The local response is preparation only: it creates no authorization and makes no processor request. The final result includes `checkoutSessionId` only after the selected grant, restricted terminal response, and captured amount have been checked. Continue to confirm the merchant order through the checkout controller.
576
631
 
577
- This integration requires matching backend and measured executor releases, with separate TEST flags explicitly enabled. An unavailable prepared request is declined or held for reconciliation; it cannot switch to human approval. LIVE Sessions, subscriptions, saved-card flows, and authentication continuations are not supported by this option. It cannot enable production Autopilot.
632
+ The shared core carries a validated billing email from that tokenization request into the final confirmation before authorization. The email stays bound to the same Session and local PaymentMethod reference. Missing email stays missing; duplicate or conflicting values are refused. The SDK does not fill an email from the Agentcard account or invent one for the merchant.
633
+
634
+ This integration requires matching backend and measured executor releases. TEST and LIVE have separate admission controls. LIVE also requires a newly authorized production grant on the selected real card, a fixed merchant account/profile with exactly one complete fixed-price USD line item and an independently reviewed operation qualification reference; the SDK option supplies none of these. LIVE deployment remains unqualified and disabled until that evidence exists. Exact observed totals do not establish atomic amount enforcement by Stripe. Subscriptions, saved-card flows and authentication continuations remain excluded. An unavailable preparation or uncertain result is declined or held for reconciliation; it cannot switch to a competing human-approval payment.
578
635
 
579
636
  The adapters also send the observed top-level HTTPS `merchantOrigin`, such as `https://shop.example`, without a path, query or trailing slash. Direct `authorize()` callers can supply that origin explicitly. It is a routing hint; the protected adapter independently verifies the processor account, payee and amount. If a browser cannot provide its top-level URL, an explicit `merchantOrigin` option can supply the hint; otherwise the regular approval path remains available.
package/dist/cdp.js CHANGED
@@ -711,6 +711,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
711
711
  cardId: preparation?.cardId ?? opts.cardId,
712
712
  executionMode: opts.executionMode,
713
713
  grantId: opts.grantId,
714
+ stripeCheckoutEnvironment: opts.stripeCheckout?.environment,
714
715
  merchantOrigin,
715
716
  pageOrigin,
716
717
  pageAmount,
@@ -1076,6 +1077,7 @@ export async function attachToPlaywright(page, opts) {
1076
1077
  cardId: preparation?.cardId ?? opts.cardId,
1077
1078
  executionMode: opts.executionMode,
1078
1079
  grantId: opts.grantId,
1080
+ stripeCheckoutEnvironment: opts.stripeCheckout?.environment,
1079
1081
  merchantOrigin,
1080
1082
  pageOrigin,
1081
1083
  pageAmount,
package/dist/client.d.ts CHANGED
@@ -166,6 +166,8 @@ export declare class CheckoutPreparationError extends Error {
166
166
  constructor(preparationId: string | null, reason: string);
167
167
  }
168
168
  export interface AuthorizeInput extends ExecutionMetadata {
169
+ /** Native Checkout defaults to TEST; LIVE requires this explicit opt-in. */
170
+ stripeCheckoutEnvironment?: 'test' | 'production';
169
171
  /** Observed top-level HTTPS merchant origin; a routing hint, never payment authority. */
170
172
  merchantOrigin?: string;
171
173
  /** Your identifier for the person whose card should pay. */
package/dist/client.js CHANGED
@@ -520,14 +520,16 @@ export class VaultClient {
520
520
  || input.executionMode === 'autopilot' || input.grantId))
521
521
  throw new Error('mercado_checkout_context_invalid');
522
522
  const nativeCheckout = hasStripeCheckoutMarker(input.request);
523
+ if (input.stripeCheckoutEnvironment !== undefined && !nativeCheckout)
524
+ throw new Error('Native Stripe Checkout environment requires its bound request.');
523
525
  const checkoutContext = nativeCheckout ? parseStripeCheckoutContext(input.request) : null;
524
526
  if (nativeCheckout) {
525
527
  if (!checkoutContext || input.preparation || input.executionMode !== 'autopilot' || !input.grantId
526
528
  || !Number.isSafeInteger(input.amount) || Number(input.amount) <= 0 || input.currency !== 'usd'
527
529
  || input.merchantOrigin !== 'https://checkout.stripe.com')
528
- throw new Error('Native Stripe Checkout requires an explicit TEST autopilot configuration.');
530
+ throw new Error('Native Stripe Checkout requires an explicit mode-bound autopilot configuration.');
529
531
  const phase = classifyStripeCheckoutRequest(input.request, {
530
- sessionId: checkoutContext.session_id, syntheticPaymentMethodId: checkoutContext.synthetic_payment_method_id,
532
+ environment: input.stripeCheckoutEnvironment, sessionId: checkoutContext.session_id, syntheticPaymentMethodId: checkoutContext.synthetic_payment_method_id,
531
533
  amountCents: input.amount,
532
534
  });
533
535
  if (phase?.phase !== 'final')
@@ -814,7 +816,7 @@ export class VaultClient {
814
816
  if (checkoutContext) {
815
817
  let receipt;
816
818
  try {
817
- receipt = parseStripeCheckoutResponse(JSON.parse(response.body), { sessionId: checkoutContext.session_id });
819
+ receipt = parseStripeCheckoutResponse(JSON.parse(response.body), { environment: input.stripeCheckoutEnvironment, sessionId: checkoutContext.session_id });
818
820
  }
819
821
  catch { /* Never return an unvalidated processor body to the page. */ }
820
822
  if (response.status !== 200 || !receipt || s.amount_verified !== true
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted
10
10
  export type { HostedFormSubmittedPageInput, SyntheticPage } from './hosted-form.js';
11
11
  export { BUILTIN_REGISTRY, cardUrlPatterns, findRecognizer } from './registry.js';
12
12
  export type { Recognizer, CheckoutMode } from './registry.js';
13
- export type { CheckoutController, CheckoutState, MerchantResult, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
13
+ export type { CheckoutController, CheckoutState, MerchantResult, MerchantPaymentConfirmation, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
@@ -1,9 +1,19 @@
1
1
  import { CheckoutPreparationError, type PrepareCheckoutOptions, type PreparedCheckout, type ReplayResponse } from './client.js';
2
2
  import type { CheckoutMode } from './registry.js';
3
- /** A processor approval is not an order. Only the merchant can confirm this result. */
3
+ /** The application's resolver confirmed the payment for this checkout authorization. */
4
+ export interface MerchantPaymentConfirmation {
5
+ kind: 'merchant_payment';
6
+ authorizationId: string;
7
+ }
8
+ /** Only authoritative merchant evidence can confirm completion; a processor token is insufficient. */
4
9
  export type MerchantResult = {
5
10
  status: 'completed';
6
11
  orderId: string;
12
+ confirmation?: never;
13
+ } | {
14
+ status: 'completed';
15
+ confirmation: MerchantPaymentConfirmation;
16
+ orderId?: never;
7
17
  } | {
8
18
  status: 'failed';
9
19
  } | {
@@ -18,6 +28,7 @@ export interface CheckoutState {
18
28
  preparationId?: string;
19
29
  mode?: CheckoutMode;
20
30
  orderId?: string;
31
+ confirmation?: MerchantPaymentConfirmation;
21
32
  /** Stable SDK category; never includes a request body, processor response, or approval link. */
22
33
  reason?: string;
23
34
  }
@@ -30,7 +41,7 @@ export interface UserAction {
30
41
  export interface LifecycleOptions {
31
42
  onStateChange?: (state: Readonly<CheckoutState>) => void;
32
43
  onUserAction?: (action: UserAction) => void | Promise<void>;
33
- /** Read authoritative merchant order state; do not click Pay or initiate a new charge here. */
44
+ /** Read authoritative merchant payment or order state; do not click Pay or initiate a new charge here. */
34
45
  resolveMerchantResult?: (state: Readonly<CheckoutState>) => Promise<MerchantResult>;
35
46
  /** Hold further card requests after handoff until the merchant result is reconciled. Default false for compatibility. */
36
47
  requireMerchantResult?: boolean;
package/dist/lifecycle.js CHANGED
@@ -17,7 +17,9 @@ export class CheckoutLifecycle {
17
17
  constructor(options) {
18
18
  this.options = options;
19
19
  }
20
- getState() { return { ...this.state }; }
20
+ getState() {
21
+ return { ...this.state, ...(this.state.confirmation ? { confirmation: { ...this.state.confirmation } } : {}) };
22
+ }
21
23
  setPreparationHandler(handler) { this.preparationHandler = handler; }
22
24
  prepare(options) {
23
25
  if (!this.preparationHandler)
@@ -195,14 +197,28 @@ export class CheckoutLifecycle {
195
197
  catch {
196
198
  result = { status: 'unknown' };
197
199
  }
198
- if (!result || typeof result !== 'object')
200
+ if (!result || typeof result !== 'object' || Array.isArray(result))
199
201
  result = { status: 'unknown' };
200
- if (result.status === 'completed' && typeof result.orderId === 'string' && result.orderId.length > 0) {
202
+ // The resolver is trusted application code: it must check the original
203
+ // request, amount, currency, selected card and authoritative merchant
204
+ // result. The SDK binds its explicit confirmation to this authorization;
205
+ // it does not turn an HTTP response or a success URL into payment proof.
206
+ const orderConfirmed = result.status === 'completed' && result.confirmation === undefined
207
+ && typeof result.orderId === 'string' && result.orderId.length > 0;
208
+ const paymentConfirmed = result.status === 'completed' && result.orderId === undefined
209
+ && result.confirmation && typeof result.confirmation === 'object' && !Array.isArray(result.confirmation)
210
+ && result.confirmation.kind === 'merchant_payment'
211
+ && typeof this.state.authorizationId === 'string' && this.state.authorizationId.length > 0
212
+ && result.confirmation.authorizationId === this.state.authorizationId;
213
+ if (result.status === 'completed' && (orderConfirmed || paymentConfirmed)) {
201
214
  this.held = true;
202
- // Merchant completion supersedes the earlier failure or user-action
203
- // reason; keep the authorization and preparation context for observers.
204
- const { reason: _previousReason, ...context } = this.state;
205
- this.set({ ...context, status: 'completed', orderId: result.orderId });
215
+ // Completion supersedes an earlier failure/challenge, while retaining
216
+ // preparation and authorization context. Never copy arbitrary evidence
217
+ // into public state or release the hold on additional payment requests.
218
+ const { reason: _previousReason, orderId: _previousOrder, confirmation: _previousConfirmation, ...context } = this.state;
219
+ this.set({ ...context, status: 'completed', ...(orderConfirmed
220
+ ? { orderId: result.orderId }
221
+ : { confirmation: { kind: 'merchant_payment', authorizationId: this.state.authorizationId } }) });
206
222
  }
207
223
  else if (result.status === 'failed') {
208
224
  // Keep the guard armed until the application deliberately starts another attempt.
@@ -51,7 +51,7 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
51
51
  readonly mode: "token";
52
52
  readonly operations: readonly [{
53
53
  readonly id: "stripe.device_checkout_confirm.v1";
54
- readonly path_pattern: "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$";
54
+ readonly path_pattern: "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$";
55
55
  readonly effect: "payment_confirmation";
56
56
  readonly amount_binding: "expected_amount_test_evidence_currency_unqualified";
57
57
  readonly psp: "stripe";
@@ -1002,7 +1002,7 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
1002
1002
  readonly requirements: readonly ["verify_merchant_billing"];
1003
1003
  };
1004
1004
  };
1005
- readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."];
1005
+ readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.", "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."];
1006
1006
  readonly unsupported_variants: readonly [{
1007
1007
  readonly variant: "legacy_jsonp_get";
1008
1008
  readonly status: "unsupported";
@@ -1013,6 +1013,21 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
1013
1013
  readonly status: "unsupported";
1014
1014
  readonly constraint: "direct_sdk";
1015
1015
  readonly reason: "Bank-account and alternative-payment token routes are outside the new-card path.";
1016
+ }, {
1017
+ readonly variant: "card_bound_risk_preflight";
1018
+ readonly status: "unsupported";
1019
+ readonly constraint: "direct_sdk";
1020
+ readonly reason: "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card.";
1021
+ }, {
1022
+ readonly variant: "prepared_co_badged_network_choice";
1023
+ readonly status: "unsupported";
1024
+ readonly constraint: "direct_sdk";
1025
+ readonly reason: "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks.";
1026
+ }, {
1027
+ readonly variant: "proactive_authentication";
1028
+ readonly status: "unsupported";
1029
+ readonly constraint: "direct_sdk";
1030
+ readonly reason: "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support.";
1016
1031
  }];
1017
1032
  readonly processor_amount: "none";
1018
1033
  readonly challenge_surface: "merchant_browser";
@@ -80,7 +80,7 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
80
80
  "operations": [
81
81
  {
82
82
  "id": "stripe.device_checkout_confirm.v1",
83
- "path_pattern": "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$",
83
+ "path_pattern": "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$",
84
84
  "effect": "payment_confirmation",
85
85
  "amount_binding": "expected_amount_test_evidence_currency_unqualified",
86
86
  "psp": "stripe",
@@ -1545,7 +1545,8 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
1545
1545
  },
1546
1546
  "limitations": [
1547
1547
  "Only the listed request format is recognized; a recognized processor is not verified merchant coverage.",
1548
- "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."
1548
+ "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.",
1549
+ "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."
1549
1550
  ],
1550
1551
  "unsupported_variants": [
1551
1552
  {
@@ -1559,6 +1560,24 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
1559
1560
  "status": "unsupported",
1560
1561
  "constraint": "direct_sdk",
1561
1562
  "reason": "Bank-account and alternative-payment token routes are outside the new-card path."
1563
+ },
1564
+ {
1565
+ "variant": "card_bound_risk_preflight",
1566
+ "status": "unsupported",
1567
+ "constraint": "direct_sdk",
1568
+ "reason": "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card."
1569
+ },
1570
+ {
1571
+ "variant": "prepared_co_badged_network_choice",
1572
+ "status": "unsupported",
1573
+ "constraint": "direct_sdk",
1574
+ "reason": "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks."
1575
+ },
1576
+ {
1577
+ "variant": "proactive_authentication",
1578
+ "status": "unsupported",
1579
+ "constraint": "direct_sdk",
1580
+ "reason": "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support."
1562
1581
  }
1563
1582
  ],
1564
1583
  "processor_amount": "none",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "catalog_version": "2026-09-11.2",
3
+ "catalog_version": "2026-09-14.1",
4
4
  "scope": "preflight_hint",
5
5
  "integration_version_note": "bundled describes the capabilities accompanying this helper, not a claim about another installed SDK version.",
6
6
  "limits": {
@@ -1966,7 +1966,7 @@
1966
1966
  "operations": [
1967
1967
  {
1968
1968
  "id": "stripe.device_checkout_confirm.v1",
1969
- "path_pattern": "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$",
1969
+ "path_pattern": "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$",
1970
1970
  "effect": "payment_confirmation",
1971
1971
  "amount_binding": "expected_amount_test_evidence_currency_unqualified",
1972
1972
  "psp": "stripe",
@@ -3431,7 +3431,8 @@
3431
3431
  },
3432
3432
  "limitations": [
3433
3433
  "Only the listed request format is recognized; a recognized processor is not verified merchant coverage.",
3434
- "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."
3434
+ "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.",
3435
+ "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."
3435
3436
  ],
3436
3437
  "unsupported_variants": [
3437
3438
  {
@@ -3445,6 +3446,24 @@
3445
3446
  "status": "unsupported",
3446
3447
  "constraint": "direct_sdk",
3447
3448
  "reason": "Bank-account and alternative-payment token routes are outside the new-card path."
3449
+ },
3450
+ {
3451
+ "variant": "card_bound_risk_preflight",
3452
+ "status": "unsupported",
3453
+ "constraint": "direct_sdk",
3454
+ "reason": "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card."
3455
+ },
3456
+ {
3457
+ "variant": "prepared_co_badged_network_choice",
3458
+ "status": "unsupported",
3459
+ "constraint": "direct_sdk",
3460
+ "reason": "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks."
3461
+ },
3462
+ {
3463
+ "variant": "proactive_authentication",
3464
+ "status": "unsupported",
3465
+ "constraint": "direct_sdk",
3466
+ "reason": "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support."
3448
3467
  }
3449
3468
  ],
3450
3469
  "processor_amount": "none",
@@ -3722,7 +3741,7 @@
3722
3741
  "direct_sdk_profile": {
3723
3742
  "schema_version": 1,
3724
3743
  "profile_version": "preflight-2",
3725
- "catalog_version": "2026-09-11.2",
3744
+ "catalog_version": "2026-09-14.1",
3726
3745
  "integration": {
3727
3746
  "id": "direct_sdk",
3728
3747
  "version": "bundled"
@@ -3881,7 +3900,7 @@
3881
3900
  "kernel_profile_template": {
3882
3901
  "schema_version": 1,
3883
3902
  "profile_version": "kernel-template-1",
3884
- "catalog_version": "2026-09-11.2",
3903
+ "catalog_version": "2026-09-14.1",
3885
3904
  "integration": {
3886
3905
  "id": "kernel_native",
3887
3906
  "version": "REPLACE-WITH-ADAPTER-VERSION"
@@ -162,7 +162,7 @@
162
162
  "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]{0,119}$"
163
163
  },
164
164
  "catalog_version": {
165
- "const": "2026-09-11.2"
165
+ "const": "2026-09-14.1"
166
166
  },
167
167
  "integration": {
168
168
  "type": "object",
@@ -358,7 +358,7 @@
358
358
  "const": 1
359
359
  },
360
360
  "catalog_version": {
361
- "const": "2026-09-11.2"
361
+ "const": "2026-09-14.1"
362
362
  },
363
363
  "signals": {
364
364
  "type": "array",
@@ -550,7 +550,7 @@
550
550
  "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]{0,119}$"
551
551
  },
552
552
  "catalog_version": {
553
- "const": "2026-09-11.2"
553
+ "const": "2026-09-14.1"
554
554
  },
555
555
  "integration": {
556
556
  "type": "object",
@@ -753,7 +753,7 @@
753
753
  "const": 1
754
754
  },
755
755
  "catalog_version": {
756
- "const": "2026-09-11.2"
756
+ "const": "2026-09-14.1"
757
757
  },
758
758
  "integration": {
759
759
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  /** Advisory page discovery. These rules never admit a payment destination. */
2
2
  export declare const CHECKOUT_PREFLIGHT_VERSION = "1";
3
- export declare const CHECKOUT_PREFLIGHT_CATALOG_VERSION = "2026-09-11.2";
3
+ export declare const CHECKOUT_PREFLIGHT_CATALOG_VERSION = "2026-09-14.1";
4
4
  export declare const CHECKOUT_PREFLIGHT_LIMITS: Readonly<{
5
5
  frames: 64;
6
6
  signals: 2048;
@@ -196,7 +196,7 @@ export declare function getCheckoutPreflightCatalog(): {
196
196
  readonly mode: "token";
197
197
  readonly operations: readonly [{
198
198
  readonly id: "stripe.device_checkout_confirm.v1";
199
- readonly path_pattern: "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$";
199
+ readonly path_pattern: "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$";
200
200
  readonly effect: "payment_confirmation";
201
201
  readonly amount_binding: "expected_amount_test_evidence_currency_unqualified";
202
202
  readonly psp: "stripe";
@@ -1147,7 +1147,7 @@ export declare function getCheckoutPreflightCatalog(): {
1147
1147
  readonly requirements: readonly ["verify_merchant_billing"];
1148
1148
  };
1149
1149
  };
1150
- readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."];
1150
+ readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.", "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."];
1151
1151
  readonly unsupported_variants: readonly [{
1152
1152
  readonly variant: "legacy_jsonp_get";
1153
1153
  readonly status: "unsupported";
@@ -1158,6 +1158,21 @@ export declare function getCheckoutPreflightCatalog(): {
1158
1158
  readonly status: "unsupported";
1159
1159
  readonly constraint: "direct_sdk";
1160
1160
  readonly reason: "Bank-account and alternative-payment token routes are outside the new-card path.";
1161
+ }, {
1162
+ readonly variant: "card_bound_risk_preflight";
1163
+ readonly status: "unsupported";
1164
+ readonly constraint: "direct_sdk";
1165
+ readonly reason: "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card.";
1166
+ }, {
1167
+ readonly variant: "prepared_co_badged_network_choice";
1168
+ readonly status: "unsupported";
1169
+ readonly constraint: "direct_sdk";
1170
+ readonly reason: "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks.";
1171
+ }, {
1172
+ readonly variant: "proactive_authentication";
1173
+ readonly status: "unsupported";
1174
+ readonly constraint: "direct_sdk";
1175
+ readonly reason: "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support.";
1161
1176
  }];
1162
1177
  readonly processor_amount: "none";
1163
1178
  readonly challenge_surface: "merchant_browser";
@@ -1,9 +1,9 @@
1
1
  // Generated from @agent-cards/payment-core. Do not edit.
2
- // source-sha256: ced5aecd22684c680a53b9e33dff1c5333cd36823f6fe3b4b8e84f10f6f73c53
2
+ // source-sha256: 69919c5fe4133b090eddae027fb12c5ad7c51a8c63e8719325c174aeee78c30a
3
3
  import { CHECKOUT_PREFLIGHT_CAPABILITIES } from './preflight-capabilities.generated.js';
4
4
  /** Advisory page discovery. These rules never admit a payment destination. */
5
5
  export const CHECKOUT_PREFLIGHT_VERSION = '1';
6
- export const CHECKOUT_PREFLIGHT_CATALOG_VERSION = '2026-09-11.2';
6
+ export const CHECKOUT_PREFLIGHT_CATALOG_VERSION = '2026-09-14.1';
7
7
  export const CHECKOUT_PREFLIGHT_LIMITS = Object.freeze({ frames: 64, signals: 2048, snapshot_bytes: 262144, url_length: 4096 });
8
8
  // Identification-only IDs, checked against the canonical catalog by the core tests.
9
9
  // No card destination, request method, or request body pattern lives here.
@@ -1,4 +1,4 @@
1
- export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago';
1
+ export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly';
2
2
  export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
3
3
  /** Shared endpoint processors cannot attest test/live mode from their URL or key prefix. */
4
4
  export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
@@ -1,7 +1,8 @@
1
1
  import { braintreeEnvironment, isPreparedBraintreeRequest, readTokenizationJson } from './braintree.js';
2
+ import { isPreparedRecurlyRequest } from './recurly.generated.js';
2
3
  /** Shared endpoint processors cannot attest test/live mode from their URL or key prefix. */
3
4
  export function validPreparationEnvironment(psp, environment) {
4
- if (psp === 'bambora' || psp === 'mercado_pago')
5
+ if (psp === 'bambora' || psp === 'mercado_pago' || psp === 'recurly')
5
6
  return environment === 'shared';
6
7
  return ['square', 'braintree', 'worldpay'].includes(psp) && ['production', 'sandbox'].includes(environment);
7
8
  }
@@ -12,6 +13,8 @@ export function preparationEndpoint(psp, environment) {
12
13
  return 'https://api.bam.shift4api.net/scripts/tokenization/tokens';
13
14
  if (psp === 'mercado_pago')
14
15
  return 'https://api.mercadopago.com/v1/card_tokens';
16
+ if (psp === 'recurly')
17
+ return 'https://api.recurly.com/js/v1/token';
15
18
  if (psp === 'worldpay')
16
19
  return environment === 'production'
17
20
  ? 'https://access.worldpay.com/sessions/card' : 'https://try.access.worldpay.com/sessions/card';
@@ -70,6 +73,9 @@ export function matchesPreparedRequest(psp, environment, requestUrl, method, bod
70
73
  const request = new URL(requestUrl), endpoint = new URL(preparationEndpoint(psp, environment));
71
74
  if (request.username || request.password || request.hash)
72
75
  return false;
76
+ if (psp === 'recurly')
77
+ return [endpoint.href, 'https://api.eu.recurly.com/js/v1/token'].includes(requestUrl)
78
+ && isPreparedRecurlyRequest(body ?? null);
73
79
  if (psp === 'bambora') {
74
80
  if (![endpoint.href, 'https://api.na.bambora.com/scripts/tokenization/tokens'].includes(requestUrl))
75
81
  return false;
@@ -0,0 +1 @@
1
+ export declare function isPreparedRecurlyRequest(body: string | null): boolean;