@agent-cards/checkout 0.9.1 → 0.10.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - 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
+
7
+ - 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.
8
+
3
9
  ## 0.9.0
4
10
 
5
11
  - 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/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. A completed merchant payment remains 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
@@ -443,6 +470,20 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
443
470
 
444
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.
445
472
 
473
+ ### Use Checkout Pro in Mexico
474
+
475
+ Attach the SDK before entering card fields on `www.mercadopago.com.mx`, then prepare the guest card checkout:
476
+
477
+ ```ts
478
+ await controller.prepare({ psp: 'mercado_pago', environment: 'shared' });
479
+ ```
480
+
481
+ 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.
482
+
483
+ 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.
484
+
485
+ 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.
486
+
446
487
  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
488
 
448
489
  Readiness lasts up to 30 seconds (`preparation.expiresAt`) and appears as `ready_to_submit`, with `paymentStatus: 'not_started'`. Trigger the caller-owned Pay action immediately after the promise resolves. Expiry, navigation, cancellation, an early request or a changed checkout fails closed. A preparation and its attachment are single use; reconcile any bound authorization before creating a new attachment. The SDK never clicks Pay, reuses a stale request, changes native request deadlines, or automatically retries a failed prepared checkout.
@@ -529,4 +570,10 @@ When the cardholder has enabled an eligible spending rule in their vault, the sa
529
570
 
530
571
  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
572
 
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`.
574
+
575
+ 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
+
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.
578
+
532
579
  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();
@@ -633,11 +723,20 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
633
723
  lifecycle.approvalUrl(url);
634
724
  return opts.onApprovalUrl?.(url);
635
725
  } },
636
- request: { url: request.url, method: request.method, headers: request.headers, body },
726
+ request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url, method: request.method, headers: request.headers, body,
727
+ ...(mercadoCheckout.requiresDocument(request.url, request.method)
728
+ ? { mercado_checkout: mercadoCheckout.claimToken(request.url, request.method, await readDocumentUrl()) } : {}) },
637
729
  });
638
730
  attempt.assertLive();
639
731
  if (lifecycle.isCancelled())
640
732
  throw new Error('checkout cancelled locally after approval');
733
+ try {
734
+ await mercadoCheckout.arm(replay, request.url);
735
+ }
736
+ catch {
737
+ throw new PaymentOutcomeUnknownError(replay.authorizationId, 'mercado_checkout_metadata_invalid');
738
+ }
739
+ attempt.assertLive();
641
740
  lifecycle.prepareHandoff(replay, request.url);
642
741
  handoffStarted = replay.mode !== 'cse';
643
742
  if (replay.mode === 'hosted_form') {
@@ -758,6 +857,8 @@ export async function attachToPlaywright(page, opts) {
758
857
  throw new Error('Service workers are active; use a checkout context created with serviceWorkers: "block".');
759
858
  }
760
859
  const lifecycle = new CheckoutLifecycle(opts);
860
+ const stripeCheckout = new StripeCheckoutGate(opts);
861
+ const mercadoCheckout = new MercadoCheckoutGate();
761
862
  const readDocumentUrl = async () => {
762
863
  if (page.isClosed?.() || typeof page.url !== 'function')
763
864
  throw new Error('merchant_document_unavailable');
@@ -790,12 +891,17 @@ export async function attachToPlaywright(page, opts) {
790
891
  activeRequest.attempt.stop();
791
892
  });
792
893
  page.on?.('close', () => { if (!attachmentReady)
793
- setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); activeRequest?.attempt.stop(); });
894
+ setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); stripeCheckout.invalidate(); mercadoCheckout.invalidate(); activeRequest?.attempt.stop(); });
794
895
  page.on?.('crash', () => { if (!attachmentReady)
795
- setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); activeRequest?.attempt.stop(); });
896
+ setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); stripeCheckout.invalidate(); mercadoCheckout.invalidate(); activeRequest?.attempt.stop(); });
796
897
  page.on?.('framenavigated', (frame) => {
797
898
  if (frame === page.mainFrame?.()) {
798
899
  preparationGate.invalidate('merchant_document_changed');
900
+ mercadoCheckout.invalidate();
901
+ if (stripeCheckout.isPrepared()) {
902
+ stripeCheckout.invalidate();
903
+ activeRequest?.attempt.stop();
904
+ }
799
905
  if (preparationGate.isEngaged())
800
906
  activeRequest?.attempt.stop();
801
907
  }
@@ -813,8 +919,30 @@ export async function attachToPlaywright(page, opts) {
813
919
  setupStop.abort(failure);
814
920
  throw failure;
815
921
  }
816
- await page.route((url) => opts.vault.isCardRequest(url.toString()) || guards.matches(url.toString()), async (route) => {
922
+ await page.route((url) => opts.vault.isCardRequest(url.toString()) || guards.matches(url.toString()) || stripeCheckout.matches(url.toString()) || mercadoCheckout.matches(url.toString()), async (route) => {
817
923
  const request = route.request();
924
+ if (mercadoCheckout.matches(request.url()) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method())) {
925
+ try {
926
+ if (!attachmentReady || setupStop.signal.aborted || lifecycle.abort.signal.aborted || terminal)
927
+ throw new Error('checkout_interception_not_ready');
928
+ const body = request.postData() ?? '';
929
+ if (await mercadoCheckout.configuration(request.url(), request.method(), body, readDocumentUrl))
930
+ return route.fallback();
931
+ const postData = mercadoCheckout.associationBody(request.url(), request.method(), body, await readDocumentUrl());
932
+ if (postData !== undefined) {
933
+ if (setupStop.signal.aborted || lifecycle.abort.signal.aborted || page.isClosed?.())
934
+ throw new Error('checkout_interception_not_ready');
935
+ await route.continue({ postData });
936
+ return;
937
+ }
938
+ }
939
+ catch {
940
+ mercadoCheckout.invalidate();
941
+ lifecycle.failed(new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, 'mercado_checkout_continuation_stopped'));
942
+ opts.onEvent?.({ type: 'blocked', detail: 'mercado_checkout_continuation_stopped' });
943
+ return route.abort('aborted');
944
+ }
945
+ }
818
946
  const braintree = request.method().toUpperCase() === 'POST'
819
947
  ? classifyBraintreeRequest(request.url(), request.method(), request.postData() ?? '') : undefined;
820
948
  if (braintree === 'configuration')
@@ -825,7 +953,55 @@ export async function attachToPlaywright(page, opts) {
825
953
  }
826
954
  // The matcher only sees the URL; a preflight or a GET must pass through
827
955
  // untouched or the browser's CORS check fails on our synthetic answer.
828
- if (!opts.vault.isCardRequest(request.url(), request.method())) {
956
+ let stripeStep = null;
957
+ let stripeStage = 'request_read', stripeUrl = '';
958
+ try {
959
+ if (stripeCheckout.isEnabled()) {
960
+ stripeUrl = request.url();
961
+ const nativeRequest = { url: stripeUrl, method: request.method(), headers: request.headers(), body: request.postData() ?? '' };
962
+ stripeStage = 'classification';
963
+ stripeStep = stripeCheckout.claim(nativeRequest);
964
+ }
965
+ if (stripeStep) {
966
+ stripeStage = 'readiness';
967
+ if (!attachmentReady)
968
+ throw stripeCheckoutReadinessError('attachment_not_ready');
969
+ if (setupStop.signal.aborted)
970
+ throw stripeCheckoutReadinessError('cancelled');
971
+ if (terminal || lifecycle.isBlocked())
972
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
973
+ if (awaitingApproval)
974
+ throw stripeCheckoutReadinessError('approval_pending');
975
+ stripeStage = 'document';
976
+ stripeCheckout.assertDocument(await readDocumentUrl());
977
+ stripeStage = 'claim';
978
+ stripeCheckout.assertClaim(stripeStep);
979
+ stripeStage = 'readiness';
980
+ if (setupStop.signal.aborted)
981
+ throw stripeCheckoutReadinessError('cancelled');
982
+ if (lifecycle.isBlocked())
983
+ throw stripeCheckoutReadinessError(lifecycle.isCancelled() ? 'cancelled' : 'checkout_inactive');
984
+ if (page.isClosed?.() || request.failure?.())
985
+ throw stripeCheckoutReadinessError('request_inactive');
986
+ if (stripeStep.phase === 'tokenization') {
987
+ stripeStage = 'stub_response';
988
+ const response = stripeStep.response;
989
+ await route.fulfill({ ...response,
990
+ headers: withCorsHeaders(response.headers, corsHeadersFor(request.url(), request.headers())) });
991
+ opts.onEvent?.({ type: 'checkout_prepared', detail: { processor: 'stripe' } });
992
+ return;
993
+ }
994
+ }
995
+ }
996
+ catch (error) {
997
+ const detail = stripeCheckout.describeRejection(error, stripeUrl, stripeStage, stripeStep?.phase);
998
+ if (!(error instanceof StripeCheckoutClaimConflictError))
999
+ stripeCheckout.invalidate();
1000
+ opts.onEvent?.({ type: 'checkout_blocked', detail });
1001
+ opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
1002
+ return route.abort('aborted');
1003
+ }
1004
+ if (!stripeStep && !opts.vault.isCardRequest(request.url(), request.method())) {
829
1005
  if (guards.matches(request.url(), request.method())) {
830
1006
  preparationGate.invalidate('unsupported_checkout');
831
1007
  lifecycle.unsupported();
@@ -840,7 +1016,7 @@ export async function attachToPlaywright(page, opts) {
840
1016
  }
841
1017
  let preparation;
842
1018
  try {
843
- preparation = preparationGate.claim(request.url(), request.postData() ?? '');
1019
+ preparation = stripeStep ? undefined : preparationGate.claim(request.url(), request.postData() ?? '');
844
1020
  }
845
1021
  catch (error) {
846
1022
  opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
@@ -887,6 +1063,8 @@ export async function attachToPlaywright(page, opts) {
887
1063
  const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
888
1064
  const pageAmount = await pageAmountOf(opts);
889
1065
  assertRequestLive();
1066
+ if (stripeStep)
1067
+ stripeCheckout.assertClaim(stripeStep);
890
1068
  const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
891
1069
  ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
892
1070
  assertRequestLive();
@@ -910,11 +1088,20 @@ export async function attachToPlaywright(page, opts) {
910
1088
  lifecycle.approvalUrl(url);
911
1089
  return opts.onApprovalUrl?.(url);
912
1090
  } },
913
- request: { url: request.url(), method: request.method(), headers: request.headers(), body },
1091
+ request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url(), method: request.method(), headers: request.headers(), body,
1092
+ ...(mercadoCheckout.requiresDocument(request.url(), request.method())
1093
+ ? { mercado_checkout: mercadoCheckout.claimToken(request.url(), request.method(), await readDocumentUrl()) } : {}) },
914
1094
  });
915
1095
  assertRequestLive();
916
1096
  if (lifecycle.isCancelled())
917
1097
  throw new Error('checkout cancelled locally after approval');
1098
+ try {
1099
+ await mercadoCheckout.arm(replay, request.url());
1100
+ }
1101
+ catch {
1102
+ throw new PaymentOutcomeUnknownError(replay.authorizationId, 'mercado_checkout_metadata_invalid');
1103
+ }
1104
+ assertRequestLive();
918
1105
  lifecycle.prepareHandoff(replay, request.url());
919
1106
  handoffStarted = replay.mode !== 'cse';
920
1107
  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. */
package/dist/client.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, findRecognizer, } from './registry.js';
2
+ import { isMercadoTokenRequest, parseMercadoCheckoutContext, validateMercadoProcessorContext } from './mercado-checkout.generated.js';
2
3
  import { matchesPreparedRequest, validPreparationEnvironment } from './prepared-processor.js';
3
4
  import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
5
+ import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
4
6
  /**
5
7
  * The modes this SDK can finish. Asked for on syncRegistry (the API serves
6
8
  * only recognizers in these modes, so a request this build cannot complete
@@ -513,6 +515,24 @@ export class VaultClient {
513
515
  * response to replay into the browser. Your process never sees a card.
514
516
  */
515
517
  async authorize(input) {
518
+ const mercadoContext = input.request.mercado_checkout === undefined ? undefined : parseMercadoCheckoutContext(input.request.mercado_checkout);
519
+ if (mercadoContext && (!isMercadoTokenRequest(input.request.url, input.request.method)
520
+ || input.executionMode === 'autopilot' || input.grantId))
521
+ throw new Error('mercado_checkout_context_invalid');
522
+ const nativeCheckout = hasStripeCheckoutMarker(input.request);
523
+ const checkoutContext = nativeCheckout ? parseStripeCheckoutContext(input.request) : null;
524
+ if (nativeCheckout) {
525
+ if (!checkoutContext || input.preparation || input.executionMode !== 'autopilot' || !input.grantId
526
+ || !Number.isSafeInteger(input.amount) || Number(input.amount) <= 0 || input.currency !== 'usd'
527
+ || input.merchantOrigin !== 'https://checkout.stripe.com')
528
+ throw new Error('Native Stripe Checkout requires an explicit TEST autopilot configuration.');
529
+ const phase = classifyStripeCheckoutRequest(input.request, {
530
+ sessionId: checkoutContext.session_id, syntheticPaymentMethodId: checkoutContext.synthetic_payment_method_id,
531
+ amountCents: input.amount,
532
+ });
533
+ if (phase?.phase !== 'final')
534
+ throw new Error('Native Stripe Checkout request is not prepared.');
535
+ }
516
536
  const ownedShop = hasOwnedShopMarker(input.request);
517
537
  const shopOrder = ownedShop ? parseOwnedShopOrder(input.request) : null;
518
538
  if (ownedShop) {
@@ -551,7 +571,8 @@ export class VaultClient {
551
571
  throw new CheckoutCancelledError();
552
572
  if (input.merchantSignal?.aborted)
553
573
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
554
- const rec = findRecognizer(input.request.url, this.registry);
574
+ const rec = nativeCheckout ? this.registry.find(entry => entry.psp === 'stripe' && (entry.mode ?? 'token') === 'token')
575
+ : findRecognizer(input.request.url, this.registry);
555
576
  if (!rec)
556
577
  throw new Error(`not a known tokenization endpoint: ${redactUrl(input.request.url)}`);
557
578
  // A client-side-encrypted processor is only takeable when its entry says
@@ -614,8 +635,10 @@ export class VaultClient {
614
635
  request: {
615
636
  url: input.request.url,
616
637
  method: input.request.method,
617
- headers: pickHeaders(input.request.headers, rec.passthroughHeaders),
638
+ headers: { ...pickHeaders(input.request.headers, rec.passthroughHeaders),
639
+ ...(checkoutContext ? { [STRIPE_CHECKOUT_CONTEXT_HEADER]: encodeStripeCheckoutContext(checkoutContext) } : {}) },
618
640
  body: input.request.body,
641
+ ...(mercadoContext ? { mercado_checkout: mercadoContext } : {}),
619
642
  },
620
643
  };
621
644
  try {
@@ -650,10 +673,16 @@ export class VaultClient {
650
673
  if (!created || typeof created.id !== 'string' || !created.id)
651
674
  throw new PaymentOutcomeUnknownError(preparation ? await this.retireUncertainPreparation(preparation, input.onAuthorizationCreated) : null, 'authorization_create_malformed');
652
675
  const authorizationId = created.id;
676
+ // A prepared Mercado token already has the person's approval. Poll its
677
+ // result promptly while the native SDK waits: at the default interval,
678
+ // 10 seconds has at most 20 reads instead of 5 (15 additional reads).
679
+ // Then restore the caller's interval. Human approval waiting, cancellation,
680
+ // merchant deadlines, and the single processor request remain unchanged.
681
+ const preparedMercadoPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000 : 0;
653
682
  let execution = {};
654
683
  let approvalDelivered = false;
655
684
  const deliverApproval = (state) => {
656
- if (ownedShop || preparation || approvalDelivered || execution.executionMode === 'autopilot')
685
+ if (ownedShop || nativeCheckout || preparation || approvalDelivered || execution.executionMode === 'autopilot')
657
686
  return;
658
687
  const url = state.approvalUrl ?? state.approval_url;
659
688
  if (typeof url !== 'string' || !url)
@@ -679,7 +708,8 @@ export class VaultClient {
679
708
  while (Date.now() < deadline) {
680
709
  if (stopSignal?.aborted)
681
710
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
682
- await interruptibleSleep(Math.min(this.pollIntervalMs, Math.max(0, deadline - Date.now())), stopSignal);
711
+ const pollInterval = Date.now() < preparedMercadoPollUntil ? Math.min(this.pollIntervalMs, 500) : this.pollIntervalMs;
712
+ await interruptibleSleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())), stopSignal);
683
713
  if (stopSignal?.aborted)
684
714
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
685
715
  let s;
@@ -697,6 +727,10 @@ export class VaultClient {
697
727
  throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
698
728
  }
699
729
  execution = executionMetadata(s, authorizationId, execution);
730
+ if (nativeCheckout && (s.status === 'submitted_on_device' ||
731
+ (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'
732
+ || execution.grantId !== input.grantId))))
733
+ throw new PaymentOutcomeUnknownError(authorizationId, 'checkout_receipt_unconfirmed');
700
734
  if (shopOrder && (s.status === 'submitted_on_device' ||
701
735
  (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'))))
702
736
  throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
@@ -777,11 +811,36 @@ export class VaultClient {
777
811
  response.body = JSON.stringify(receipt);
778
812
  response.headers = { 'content-type': 'application/json' };
779
813
  }
814
+ if (checkoutContext) {
815
+ let receipt;
816
+ try {
817
+ receipt = parseStripeCheckoutResponse(JSON.parse(response.body), { sessionId: checkoutContext.session_id });
818
+ }
819
+ catch { /* Never return an unvalidated processor body to the page. */ }
820
+ if (response.status !== 200 || !receipt || s.amount_verified !== true
821
+ || s.charged_amount !== input.amount || s.charged_currency !== 'usd' || s.charged_kind !== 'captured')
822
+ throw new PaymentOutcomeUnknownError(authorizationId, 'checkout_receipt_unconfirmed');
823
+ response.body = JSON.stringify(receipt);
824
+ response.headers = { 'content-type': 'application/json' };
825
+ }
826
+ let processorContext;
827
+ if (mercadoContext) {
828
+ try {
829
+ processorContext = await validateMercadoProcessorContext(s.processor_context, mercadoContext, input.request.url, response.body);
830
+ }
831
+ catch {
832
+ throw new PaymentOutcomeUnknownError(authorizationId, 'mercado_checkout_metadata_invalid');
833
+ }
834
+ }
835
+ else if (s.processor_context !== undefined)
836
+ throw new PaymentOutcomeUnknownError(authorizationId, 'unexpected_processor_context');
780
837
  return {
781
838
  mode: 'token',
782
839
  authorizationId,
783
840
  ...(shopOrder ? { shopOrderId: shopOrder.order_id } : {}),
841
+ ...(checkoutContext ? { checkoutSessionId: checkoutContext.session_id } : {}),
784
842
  ...response,
843
+ ...(processorContext ? { processorContext } : {}),
785
844
  amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
786
845
  chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
787
846
  chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
@@ -824,7 +883,7 @@ export class VaultClient {
824
883
  throw error;
825
884
  }
826
885
  finally {
827
- if (input.merchantSignal?.aborted || (preparation && failed)) {
886
+ if (input.merchantSignal?.aborted || ((preparation || nativeCheckout) && failed)) {
828
887
  // Drain a create acknowledgement even after the merchant aborts so its
829
888
  // known ID can be retired. An unacknowledged create remains unknown.
830
889
  // A started/finalized replay or failed cleanup never becomes a claimed
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormR
3
3
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
4
4
  export { CheckoutAttachmentError } from './attachment.js';
5
5
  export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
6
+ export type { StripeCheckoutOptions, StripeCheckoutBlockedDetail, StripeCheckoutBlockStage, StripeCheckoutBlockReason } from './stripe-checkout.js';
6
7
  export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
7
8
  export type { Substitutions } from './substitute.js';
8
9
  export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted-form.js';
@@ -0,0 +1,20 @@
1
+ import type { ReplayResponse } from './client.js';
2
+ /** Capture only the native Checkout Pro configuration. A late configuration,
3
+ * navigation or repeated token cannot replace the context of an issued token.
4
+ * Final payment requests retain the attachment's existing payment guards.
5
+ */
6
+ export declare class MercadoCheckoutGate {
7
+ private context;
8
+ private started;
9
+ private stopped;
10
+ private sequence;
11
+ private associatedFlow;
12
+ private association;
13
+ matches(url: string): boolean;
14
+ requiresDocument(url: string, method: string): boolean;
15
+ invalidate(): void;
16
+ configuration(url: string, method: string, body: string, readDocument: () => Promise<string>): Promise<boolean>;
17
+ claimToken(url: string, method: string, documentUrl: string): unknown;
18
+ arm(replay: ReplayResponse, tokenUrl: string): Promise<void>;
19
+ associationBody(url: string, method: string, body: string, documentUrl: string): string | undefined;
20
+ }