@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 +8 -0
- package/PREFLIGHT.md +1 -1
- package/README.md +59 -1
- package/dist/cdp.d.ts +3 -0
- package/dist/cdp.js +199 -10
- package/dist/client.d.ts +8 -0
- package/dist/client.js +66 -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 +26 -1
- package/dist/preflight-capabilities.generated.js +31 -1
- package/dist/preflight-catalog.json +34 -4
- package/dist/preflight-schemas.json +4 -4
- package/dist/preflight.generated.d.ts +27 -2
- package/dist/preflight.generated.js +2 -2
- package/dist/preparation.js +2 -0
- package/dist/prepared-processor.d.ts +1 -1
- package/dist/prepared-processor.js +7 -1
- package/dist/recurly.generated.d.ts +1 -0
- package/dist/recurly.generated.js +87 -0
- package/dist/stripe-checkout.d.ts +77 -0
- package/dist/stripe-checkout.generated.d.ts +58 -0
- package/dist/stripe-checkout.generated.js +915 -0
- package/dist/stripe-checkout.js +133 -0
- package/examples/preflight/kernel-native/inventory.json +2 -2
- package/examples/preflight/kernel-profile.empty.json +1 -1
- package/examples/preflight/mollie-hosted.result.json +2 -2
- package/examples/preflight/stripe-script.direct.result.json +2 -2
- package/examples/preflight/stripe-script.result.json +2 -2
- package/package.json +2 -2
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|