@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 +6 -0
- package/README.md +47 -0
- package/dist/cdp.d.ts +3 -0
- package/dist/cdp.js +197 -10
- package/dist/client.d.ts +6 -0
- package/dist/client.js +64 -5
- package/dist/index.d.ts +1 -0
- package/dist/mercado-checkout.d.ts +20 -0
- package/dist/mercado-checkout.generated.d.ts +52 -0
- package/dist/mercado-checkout.generated.js +198 -0
- package/dist/mercado-checkout.js +108 -0
- package/dist/preflight-capabilities.generated.d.ts +10 -0
- package/dist/preflight-capabilities.generated.js +11 -0
- package/dist/preflight-catalog.json +11 -0
- package/dist/preflight.generated.d.ts +10 -0
- package/dist/preparation.js +2 -0
- package/dist/stripe-checkout.d.ts +75 -0
- package/dist/stripe-checkout.generated.d.ts +47 -0
- package/dist/stripe-checkout.generated.js +843 -0
- package/dist/stripe-checkout.js +122 -0
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|