@agent-cards/checkout 0.9.1 → 0.11.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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - 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.
6
+
7
+ - 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.
8
+
9
+ - 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.
10
+
3
11
  ## 0.9.0
4
12
 
5
13
  - Inspect checkout assets before entering card details with `collectCheckoutSignals` and `inspectCheckout`, or assess sanitized observations with `assessCheckoutSupport`. Results identify the PSP, retain evidence and report `supported`, `unsupported` or `unknown` for the selected integration.
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,6 +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. 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. |
137
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 |
138
139
  | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it |
139
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 |
@@ -384,6 +385,32 @@ drives that hook. Deliver `onUserAction` approval URLs privately: they are
384
385
  capabilities and never belong in general telemetry. Observer exceptions are
385
386
  isolated from the payment handoff.
386
387
 
388
+ Native Stripe Checkout emits `checkout_blocked` before the existing `blocked`
389
+ event when its local preparation rejects a request. Its
390
+ `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.
393
+
394
+ | Field | Values |
395
+ | --- | --- |
396
+ | `endpoint_family` | `payment_methods`, `payment_page_confirm`, `other` |
397
+ | `phase` | `tokenization`, `final`, `unknown` |
398
+ | `stage` | `request_read`, `classification`, `claim`, `readiness`, `document`, `stub_response` |
399
+ | `gate_state` | `fresh`, `stubbed`, `submitted`, `stopped` |
400
+ | `disposition` | `active_claim_preserved`, `checkout_stopped` |
401
+
402
+ `reason` is the exported `StripeCheckoutBlockReason` union. It identifies SDK
403
+ 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
407
+ the failure, though document validation may already have stopped the gate.
408
+
409
+ `active_claim_preserved` means a valid duplicate was refused without invalidating
410
+ the original request's claim. Do not cancel that checkout in response to the duplicate.
411
+ `checkout_stopped` means the local preparation was retired; reconcile any existing
412
+ authorization before another attempt. Neither disposition proves a payment outcome.
413
+
387
414
  With `requireMerchantResult`, subsequent card requests stay blocked after
388
415
  handoff. Stripe `/v1/payment_methods` and `/v1/tokens` handoffs always hold further
389
416
  recognized card requests, even when that option is false. A tokenization approval has no
@@ -440,8 +467,31 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
440
467
  | Worldpay | `production` or `sandbox` | Matching Access Worldpay host and `/sessions/card` |
441
468
  | Bambora | `shared` | `/scripts/tokenization/tokens` on `api.bam.shift4api.net` or `api.na.bambora.com` |
442
469
  | Mercado Pago | `shared` | `api.mercadopago.com/v1/card_tokens` with a fresh card body |
470
+ | Recurly | `shared` | Form-encoded POST to `/js/v1/token` on `api.recurly.com` or `api.eu.recurly.com` |
471
+
472
+ 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.
473
+
474
+ Prepare Recurly before clicking the merchant's payment button:
475
+
476
+ ```ts
477
+ await controller.prepare({ psp: 'recurly', environment: 'shared' });
478
+ ```
479
+
480
+ 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.
481
+
482
+ ### Use Checkout Pro in Mexico
483
+
484
+ Attach the SDK before entering card fields on `www.mercadopago.com.mx`, then prepare the guest card checkout:
443
485
 
444
- 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.
486
+ ```ts
487
+ await controller.prepare({ psp: 'mercado_pago', environment: 'shared' });
488
+ ```
489
+
490
+ The Vault checks the selected card's issuer using the merchant's current checkout configuration. Your browser receives the processor's original card-token response, and the SDK replaces the issuer in one native card association. The card number, security code and eight-digit prefix stay out of your SDK process. The encrypted relay carries the configuration lookup between the approval device and Mercado Pago.
491
+
492
+ The selected card must have the same brand and card type as the native form. Missing or ambiguous configuration, changed checkout details, and a repeated card association stop continuation. Start a fresh checkout and approval after resolving the mismatch. Final Pay and merchant confirmation still follow your existing checkout integration.
493
+
494
+ Use the matching SDK, API, Vault and relay releases together. The issuer correction covers the observed guest Checkout Pro flow in Mexico. CardForm or Bricks on merchant websites, other countries, installment changes and Autopilot need separate validation. Successful tokenization and issuer association do not establish a paid order; keep `requireMerchantResult` and a merchant receipt resolver when validating a purchase.
445
495
 
446
496
  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.
447
497
 
@@ -529,4 +579,12 @@ When the cardholder has enabled an eligible spending rule in their vault, the sa
529
579
 
530
580
  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.
531
581
 
582
+ 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 Session on `checkout.stripe.com`. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
583
+
584
+ 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.
585
+
586
+ 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.
587
+
588
+ 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.
589
+
532
590
  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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type VaultClient, type ExecutionMetadata } from './client.js';
2
+ import { type StripeCheckoutOptions } from './stripe-checkout.js';
2
3
  import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard } from './lifecycle.js';
3
4
  /**
4
5
  * The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
@@ -72,6 +73,8 @@ export interface CdpLike {
72
73
  on(handler: (method: string, params: any, sessionId?: string) => void): void;
73
74
  }
74
75
  export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
76
+ /** Explicit native hosted Stripe Checkout TEST integration; requires an Autopilot grant. */
77
+ stripeCheckout?: StripeCheckoutOptions;
75
78
  /** Fallback when the browser cannot expose its top-level origin. Never an amount/payee authority. */
76
79
  merchantOrigin?: string;
77
80
  vault: VaultClient;
package/dist/cdp.js CHANGED
@@ -2,6 +2,9 @@ import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
2
2
  import { ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, PaymentOutcomeUnknownError, ProcessorRefusedError, UnsupportedModeError, redactUrl, } from './client.js';
3
3
  import { PreparationGate } from './preparation.js';
4
4
  import { classifyBraintreeRequest } from './braintree.js';
5
+ import { StripeCheckoutGate, StripeCheckoutClaimConflictError, stripeCheckoutReadinessError } from './stripe-checkout.js';
6
+ import { MercadoCheckoutGate } from './mercado-checkout.js';
7
+ import { MERCADO_CHECKOUT_PATTERN } from './mercado-checkout.generated.js';
5
8
  import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
6
9
  import { substituteEncryptedFields } from './substitute.js';
7
10
  import { hostedFormSubmittedPage } from './hosted-form.js';
@@ -407,6 +410,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
407
410
  let attachmentReady = false;
408
411
  let setupFailureReported = false;
409
412
  const lifecycle = new CheckoutLifecycle(opts);
413
+ const stripeCheckout = new StripeCheckoutGate(opts);
414
+ const mercadoCheckout = new MercadoCheckoutGate();
410
415
  let preparationFrameId;
411
416
  const readDocumentUrl = async () => {
412
417
  const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
@@ -431,7 +436,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
431
436
  const repeatQuietMs = opts.hostedFormRepeatQuietMs ?? HOSTED_FORM_REPEAT_QUIET_MS;
432
437
  let lastSubmitted = null;
433
438
  const derived = typeof opts.vault?.cardUrlPatterns === 'function' ? opts.vault.cardUrlPatterns() : [];
434
- const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns])];
439
+ const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
440
+ ...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN])];
435
441
  const arm = async (sessionId, resume = false) => {
436
442
  const key = sessionId ?? '__root__';
437
443
  if (armed.has(key))
@@ -478,6 +484,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
478
484
  cdp.on(async (method, params, sessionId) => {
479
485
  if (((method === 'Page.frameNavigated' && !params.frame?.parentId) || (method === 'Page.navigatedWithinDocument' && preparationFrameId && params.frameId === preparationFrameId)) && sessionId === pageSessionId) {
480
486
  preparationGate.invalidate('merchant_document_changed');
487
+ mercadoCheckout.invalidate();
488
+ if (stripeCheckout.isPrepared()) {
489
+ stripeCheckout.invalidate();
490
+ activeRequest?.attempt.stop();
491
+ }
481
492
  if (preparationGate.isEngaged())
482
493
  activeRequest?.attempt.stop();
483
494
  return;
@@ -486,6 +497,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
486
497
  || (method === 'Target.detachedFromTarget' && params.sessionId === pageSessionId)) {
487
498
  setupStop.abort(new CheckoutAttachmentError('closed'));
488
499
  preparationGate.invalidate('merchant_document_closed');
500
+ stripeCheckout.invalidate();
501
+ mercadoCheckout.invalidate();
489
502
  // The root page owns every attached OOPIF; its loss ends child requests too.
490
503
  activeRequest?.attempt.stop();
491
504
  }
@@ -526,6 +539,31 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
526
539
  if (method !== 'Fetch.requestPaused')
527
540
  return;
528
541
  const { requestId, request, resourceType, networkId, frameId } = params;
542
+ if (mercadoCheckout.matches(request.url) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
543
+ try {
544
+ if (!attachmentReady || setupStop.signal.aborted || lifecycle.abort.signal.aborted || terminal)
545
+ throw new Error('checkout_interception_not_ready');
546
+ const body = pausedBody(request) ?? '';
547
+ if (await mercadoCheckout.configuration(request.url, request.method, body, readDocumentUrl)) {
548
+ await cdp.send('Fetch.continueRequest', { requestId }, sessionId);
549
+ return;
550
+ }
551
+ const postData = mercadoCheckout.associationBody(request.url, request.method, body, await readDocumentUrl());
552
+ if (postData !== undefined) {
553
+ if (setupStop.signal.aborted || lifecycle.abort.signal.aborted)
554
+ throw new Error('checkout_interception_not_ready');
555
+ await cdp.send('Fetch.continueRequest', { requestId, postData: Buffer.from(postData).toString('base64') }, sessionId);
556
+ return;
557
+ }
558
+ }
559
+ catch {
560
+ mercadoCheckout.invalidate();
561
+ lifecycle.failed(new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, 'mercado_checkout_continuation_stopped'));
562
+ opts.onEvent?.({ type: 'blocked', detail: 'mercado_checkout_continuation_stopped' });
563
+ await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
564
+ return;
565
+ }
566
+ }
529
567
  // Braintree shares /graphql between configuration and card mutations.
530
568
  // Classify before URL-only recognition or claiming a prepared request.
531
569
  const braintree = request.method.toUpperCase() === 'POST'
@@ -539,7 +577,57 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
539
577
  await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
540
578
  return;
541
579
  }
542
- if (!opts.vault.isCardRequest(request.url, request.method)) {
580
+ let stripeStep = null;
581
+ let stripeStage = 'request_read', stripeUrl = '';
582
+ try {
583
+ if (stripeCheckout.isEnabled()) {
584
+ stripeUrl = request.url;
585
+ const nativeRequest = { url: stripeUrl, method: request.method,
586
+ headers: request.headers, body: pausedBody(request) ?? '' };
587
+ stripeStage = 'classification';
588
+ stripeStep = stripeCheckout.claim(nativeRequest);
589
+ }
590
+ if (stripeStep) {
591
+ stripeStage = 'readiness';
592
+ if (!attachmentReady || arming.has(sessionId ?? '__root__'))
593
+ throw stripeCheckoutReadinessError('attachment_not_ready');
594
+ if (setupStop.signal.aborted)
595
+ throw stripeCheckoutReadinessError('cancelled');
596
+ if (terminal || lifecycle.isBlocked())
597
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
598
+ if (awaitingApproval)
599
+ throw stripeCheckoutReadinessError('approval_pending');
600
+ stripeStage = 'document';
601
+ stripeCheckout.assertDocument(await readDocumentUrl());
602
+ stripeStage = 'claim';
603
+ stripeCheckout.assertClaim(stripeStep);
604
+ stripeStage = 'readiness';
605
+ if (setupStop.signal.aborted)
606
+ throw stripeCheckoutReadinessError('cancelled');
607
+ if (lifecycle.isBlocked())
608
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
609
+ if (stripeStep.phase === 'tokenization') {
610
+ stripeStage = 'stub_response';
611
+ const response = stripeStep.response;
612
+ await cdp.send('Fetch.fulfillRequest', { requestId, responseCode: response.status,
613
+ responseHeaders: Object.entries(withCorsHeaders(response.headers, corsHeadersFor(request.url, request.headers)))
614
+ .map(([name, value]) => ({ name, value })),
615
+ body: Buffer.from(response.body).toString('base64') }, sessionId);
616
+ opts.onEvent?.({ type: 'checkout_prepared', detail: { processor: 'stripe' } });
617
+ return;
618
+ }
619
+ }
620
+ }
621
+ catch (error) {
622
+ const detail = stripeCheckout.describeRejection(error, stripeUrl, stripeStage, stripeStep?.phase);
623
+ if (!(error instanceof StripeCheckoutClaimConflictError))
624
+ stripeCheckout.invalidate();
625
+ opts.onEvent?.({ type: 'checkout_blocked', detail });
626
+ opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
627
+ await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
628
+ return;
629
+ }
630
+ if (!stripeStep && !opts.vault.isCardRequest(request.url, request.method)) {
543
631
  if (guards.matches(request.url, request.method)) {
544
632
  preparationGate.invalidate('unsupported_checkout');
545
633
  lifecycle.unsupported();
@@ -557,7 +645,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
557
645
  }
558
646
  let preparation;
559
647
  try {
560
- preparation = preparationGate.claim(request.url, pausedBody(request));
648
+ preparation = stripeStep ? undefined : preparationGate.claim(request.url, pausedBody(request));
561
649
  }
562
650
  catch (error) {
563
651
  opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
@@ -610,6 +698,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
610
698
  const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
611
699
  const pageAmount = await pageAmountOf(opts);
612
700
  attempt.assertLive();
701
+ if (stripeStep)
702
+ stripeCheckout.assertClaim(stripeStep);
613
703
  const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
614
704
  ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
615
705
  attempt.assertLive();
@@ -621,6 +711,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
621
711
  cardId: preparation?.cardId ?? opts.cardId,
622
712
  executionMode: opts.executionMode,
623
713
  grantId: opts.grantId,
714
+ stripeCheckoutEnvironment: opts.stripeCheckout?.environment,
624
715
  merchantOrigin,
625
716
  pageOrigin,
626
717
  pageAmount,
@@ -633,11 +724,20 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
633
724
  lifecycle.approvalUrl(url);
634
725
  return opts.onApprovalUrl?.(url);
635
726
  } },
636
- request: { url: request.url, method: request.method, headers: request.headers, body },
727
+ request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url, method: request.method, headers: request.headers, body,
728
+ ...(mercadoCheckout.requiresDocument(request.url, request.method)
729
+ ? { mercado_checkout: mercadoCheckout.claimToken(request.url, request.method, await readDocumentUrl()) } : {}) },
637
730
  });
638
731
  attempt.assertLive();
639
732
  if (lifecycle.isCancelled())
640
733
  throw new Error('checkout cancelled locally after approval');
734
+ try {
735
+ await mercadoCheckout.arm(replay, request.url);
736
+ }
737
+ catch {
738
+ throw new PaymentOutcomeUnknownError(replay.authorizationId, 'mercado_checkout_metadata_invalid');
739
+ }
740
+ attempt.assertLive();
641
741
  lifecycle.prepareHandoff(replay, request.url);
642
742
  handoffStarted = replay.mode !== 'cse';
643
743
  if (replay.mode === 'hosted_form') {
@@ -758,6 +858,8 @@ export async function attachToPlaywright(page, opts) {
758
858
  throw new Error('Service workers are active; use a checkout context created with serviceWorkers: "block".');
759
859
  }
760
860
  const lifecycle = new CheckoutLifecycle(opts);
861
+ const stripeCheckout = new StripeCheckoutGate(opts);
862
+ const mercadoCheckout = new MercadoCheckoutGate();
761
863
  const readDocumentUrl = async () => {
762
864
  if (page.isClosed?.() || typeof page.url !== 'function')
763
865
  throw new Error('merchant_document_unavailable');
@@ -790,12 +892,17 @@ export async function attachToPlaywright(page, opts) {
790
892
  activeRequest.attempt.stop();
791
893
  });
792
894
  page.on?.('close', () => { if (!attachmentReady)
793
- setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); activeRequest?.attempt.stop(); });
895
+ setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); stripeCheckout.invalidate(); mercadoCheckout.invalidate(); activeRequest?.attempt.stop(); });
794
896
  page.on?.('crash', () => { if (!attachmentReady)
795
- setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); activeRequest?.attempt.stop(); });
897
+ setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); stripeCheckout.invalidate(); mercadoCheckout.invalidate(); activeRequest?.attempt.stop(); });
796
898
  page.on?.('framenavigated', (frame) => {
797
899
  if (frame === page.mainFrame?.()) {
798
900
  preparationGate.invalidate('merchant_document_changed');
901
+ mercadoCheckout.invalidate();
902
+ if (stripeCheckout.isPrepared()) {
903
+ stripeCheckout.invalidate();
904
+ activeRequest?.attempt.stop();
905
+ }
799
906
  if (preparationGate.isEngaged())
800
907
  activeRequest?.attempt.stop();
801
908
  }
@@ -813,8 +920,30 @@ export async function attachToPlaywright(page, opts) {
813
920
  setupStop.abort(failure);
814
921
  throw failure;
815
922
  }
816
- await page.route((url) => opts.vault.isCardRequest(url.toString()) || guards.matches(url.toString()), async (route) => {
923
+ await page.route((url) => opts.vault.isCardRequest(url.toString()) || guards.matches(url.toString()) || stripeCheckout.matches(url.toString()) || mercadoCheckout.matches(url.toString()), async (route) => {
817
924
  const request = route.request();
925
+ if (mercadoCheckout.matches(request.url()) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method())) {
926
+ try {
927
+ if (!attachmentReady || setupStop.signal.aborted || lifecycle.abort.signal.aborted || terminal)
928
+ throw new Error('checkout_interception_not_ready');
929
+ const body = request.postData() ?? '';
930
+ if (await mercadoCheckout.configuration(request.url(), request.method(), body, readDocumentUrl))
931
+ return route.fallback();
932
+ const postData = mercadoCheckout.associationBody(request.url(), request.method(), body, await readDocumentUrl());
933
+ if (postData !== undefined) {
934
+ if (setupStop.signal.aborted || lifecycle.abort.signal.aborted || page.isClosed?.())
935
+ throw new Error('checkout_interception_not_ready');
936
+ await route.continue({ postData });
937
+ return;
938
+ }
939
+ }
940
+ catch {
941
+ mercadoCheckout.invalidate();
942
+ lifecycle.failed(new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, 'mercado_checkout_continuation_stopped'));
943
+ opts.onEvent?.({ type: 'blocked', detail: 'mercado_checkout_continuation_stopped' });
944
+ return route.abort('aborted');
945
+ }
946
+ }
818
947
  const braintree = request.method().toUpperCase() === 'POST'
819
948
  ? classifyBraintreeRequest(request.url(), request.method(), request.postData() ?? '') : undefined;
820
949
  if (braintree === 'configuration')
@@ -825,7 +954,55 @@ export async function attachToPlaywright(page, opts) {
825
954
  }
826
955
  // The matcher only sees the URL; a preflight or a GET must pass through
827
956
  // untouched or the browser's CORS check fails on our synthetic answer.
828
- if (!opts.vault.isCardRequest(request.url(), request.method())) {
957
+ let stripeStep = null;
958
+ let stripeStage = 'request_read', stripeUrl = '';
959
+ try {
960
+ if (stripeCheckout.isEnabled()) {
961
+ stripeUrl = request.url();
962
+ const nativeRequest = { url: stripeUrl, method: request.method(), headers: request.headers(), body: request.postData() ?? '' };
963
+ stripeStage = 'classification';
964
+ stripeStep = stripeCheckout.claim(nativeRequest);
965
+ }
966
+ if (stripeStep) {
967
+ stripeStage = 'readiness';
968
+ if (!attachmentReady)
969
+ throw stripeCheckoutReadinessError('attachment_not_ready');
970
+ if (setupStop.signal.aborted)
971
+ throw stripeCheckoutReadinessError('cancelled');
972
+ if (terminal || lifecycle.isBlocked())
973
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
974
+ if (awaitingApproval)
975
+ throw stripeCheckoutReadinessError('approval_pending');
976
+ stripeStage = 'document';
977
+ stripeCheckout.assertDocument(await readDocumentUrl());
978
+ stripeStage = 'claim';
979
+ stripeCheckout.assertClaim(stripeStep);
980
+ stripeStage = 'readiness';
981
+ if (setupStop.signal.aborted)
982
+ throw stripeCheckoutReadinessError('cancelled');
983
+ if (lifecycle.isBlocked())
984
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
985
+ if (page.isClosed?.() || request.failure?.())
986
+ throw stripeCheckoutReadinessError('request_inactive');
987
+ if (stripeStep.phase === 'tokenization') {
988
+ stripeStage = 'stub_response';
989
+ const response = stripeStep.response;
990
+ await route.fulfill({ ...response,
991
+ headers: withCorsHeaders(response.headers, corsHeadersFor(request.url(), request.headers())) });
992
+ opts.onEvent?.({ type: 'checkout_prepared', detail: { processor: 'stripe' } });
993
+ return;
994
+ }
995
+ }
996
+ }
997
+ catch (error) {
998
+ const detail = stripeCheckout.describeRejection(error, stripeUrl, stripeStage, stripeStep?.phase);
999
+ if (!(error instanceof StripeCheckoutClaimConflictError))
1000
+ stripeCheckout.invalidate();
1001
+ opts.onEvent?.({ type: 'checkout_blocked', detail });
1002
+ opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
1003
+ return route.abort('aborted');
1004
+ }
1005
+ if (!stripeStep && !opts.vault.isCardRequest(request.url(), request.method())) {
829
1006
  if (guards.matches(request.url(), request.method())) {
830
1007
  preparationGate.invalidate('unsupported_checkout');
831
1008
  lifecycle.unsupported();
@@ -840,7 +1017,7 @@ export async function attachToPlaywright(page, opts) {
840
1017
  }
841
1018
  let preparation;
842
1019
  try {
843
- preparation = preparationGate.claim(request.url(), request.postData() ?? '');
1020
+ preparation = stripeStep ? undefined : preparationGate.claim(request.url(), request.postData() ?? '');
844
1021
  }
845
1022
  catch (error) {
846
1023
  opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
@@ -887,6 +1064,8 @@ export async function attachToPlaywright(page, opts) {
887
1064
  const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
888
1065
  const pageAmount = await pageAmountOf(opts);
889
1066
  assertRequestLive();
1067
+ if (stripeStep)
1068
+ stripeCheckout.assertClaim(stripeStep);
890
1069
  const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
891
1070
  ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
892
1071
  assertRequestLive();
@@ -898,6 +1077,7 @@ export async function attachToPlaywright(page, opts) {
898
1077
  cardId: preparation?.cardId ?? opts.cardId,
899
1078
  executionMode: opts.executionMode,
900
1079
  grantId: opts.grantId,
1080
+ stripeCheckoutEnvironment: opts.stripeCheckout?.environment,
901
1081
  merchantOrigin,
902
1082
  pageOrigin,
903
1083
  pageAmount,
@@ -910,11 +1090,20 @@ export async function attachToPlaywright(page, opts) {
910
1090
  lifecycle.approvalUrl(url);
911
1091
  return opts.onApprovalUrl?.(url);
912
1092
  } },
913
- request: { url: request.url(), method: request.method(), headers: request.headers(), body },
1093
+ request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url(), method: request.method(), headers: request.headers(), body,
1094
+ ...(mercadoCheckout.requiresDocument(request.url(), request.method())
1095
+ ? { mercado_checkout: mercadoCheckout.claimToken(request.url(), request.method(), await readDocumentUrl()) } : {}) },
914
1096
  });
915
1097
  assertRequestLive();
916
1098
  if (lifecycle.isCancelled())
917
1099
  throw new Error('checkout cancelled locally after approval');
1100
+ try {
1101
+ await mercadoCheckout.arm(replay, request.url());
1102
+ }
1103
+ catch {
1104
+ throw new PaymentOutcomeUnknownError(replay.authorizationId, 'mercado_checkout_metadata_invalid');
1105
+ }
1106
+ assertRequestLive();
918
1107
  lifecycle.prepareHandoff(replay, request.url());
919
1108
  handoffStarted = replay.mode !== 'cse';
920
1109
  if (replay.mode === 'hosted_form') {
package/dist/client.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface PausedRequest {
6
6
  method: string;
7
7
  headers: Record<string, string>;
8
8
  body: string;
9
+ /** Captured native Checkout Pro initialization, validated before use. */
10
+ mercado_checkout?: unknown;
9
11
  }
10
12
  /**
11
13
  * The modes this SDK can finish. Asked for on syncRegistry (the API serves
@@ -39,6 +41,10 @@ export interface ExecutionMetadata {
39
41
  * this is the processor's answer to replay into the paused request.
40
42
  */
41
43
  export interface TokenReplay extends ExecutionMetadata {
44
+ /** Authenticated selected-card metadata; the processor response body stays unchanged. */
45
+ processorContext?: unknown;
46
+ /** Present only after validating the native TEST Checkout completion. */
47
+ checkoutSessionId?: string;
42
48
  /** Present only after validating the explicit owned-shop purchase receipt. */
43
49
  shopOrderId?: string;
44
50
  /** Absent on older API versions; always 'token' here. */
@@ -160,6 +166,8 @@ export declare class CheckoutPreparationError extends Error {
160
166
  constructor(preparationId: string | null, reason: string);
161
167
  }
162
168
  export interface AuthorizeInput extends ExecutionMetadata {
169
+ /** Native Checkout defaults to TEST; LIVE requires this explicit opt-in. */
170
+ stripeCheckoutEnvironment?: 'test' | 'production';
163
171
  /** Observed top-level HTTPS merchant origin; a routing hint, never payment authority. */
164
172
  merchantOrigin?: string;
165
173
  /** Your identifier for the person whose card should pay. */