@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/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,26 @@ 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
+ if (input.stripeCheckoutEnvironment !== undefined && !nativeCheckout)
524
+ throw new Error('Native Stripe Checkout environment requires its bound request.');
525
+ const checkoutContext = nativeCheckout ? parseStripeCheckoutContext(input.request) : null;
526
+ if (nativeCheckout) {
527
+ if (!checkoutContext || input.preparation || input.executionMode !== 'autopilot' || !input.grantId
528
+ || !Number.isSafeInteger(input.amount) || Number(input.amount) <= 0 || input.currency !== 'usd'
529
+ || input.merchantOrigin !== 'https://checkout.stripe.com')
530
+ throw new Error('Native Stripe Checkout requires an explicit mode-bound autopilot configuration.');
531
+ const phase = classifyStripeCheckoutRequest(input.request, {
532
+ environment: input.stripeCheckoutEnvironment, sessionId: checkoutContext.session_id, syntheticPaymentMethodId: checkoutContext.synthetic_payment_method_id,
533
+ amountCents: input.amount,
534
+ });
535
+ if (phase?.phase !== 'final')
536
+ throw new Error('Native Stripe Checkout request is not prepared.');
537
+ }
516
538
  const ownedShop = hasOwnedShopMarker(input.request);
517
539
  const shopOrder = ownedShop ? parseOwnedShopOrder(input.request) : null;
518
540
  if (ownedShop) {
@@ -551,7 +573,8 @@ export class VaultClient {
551
573
  throw new CheckoutCancelledError();
552
574
  if (input.merchantSignal?.aborted)
553
575
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
554
- const rec = findRecognizer(input.request.url, this.registry);
576
+ const rec = nativeCheckout ? this.registry.find(entry => entry.psp === 'stripe' && (entry.mode ?? 'token') === 'token')
577
+ : findRecognizer(input.request.url, this.registry);
555
578
  if (!rec)
556
579
  throw new Error(`not a known tokenization endpoint: ${redactUrl(input.request.url)}`);
557
580
  // A client-side-encrypted processor is only takeable when its entry says
@@ -614,8 +637,10 @@ export class VaultClient {
614
637
  request: {
615
638
  url: input.request.url,
616
639
  method: input.request.method,
617
- headers: pickHeaders(input.request.headers, rec.passthroughHeaders),
640
+ headers: { ...pickHeaders(input.request.headers, rec.passthroughHeaders),
641
+ ...(checkoutContext ? { [STRIPE_CHECKOUT_CONTEXT_HEADER]: encodeStripeCheckoutContext(checkoutContext) } : {}) },
618
642
  body: input.request.body,
643
+ ...(mercadoContext ? { mercado_checkout: mercadoContext } : {}),
619
644
  },
620
645
  };
621
646
  try {
@@ -650,10 +675,16 @@ export class VaultClient {
650
675
  if (!created || typeof created.id !== 'string' || !created.id)
651
676
  throw new PaymentOutcomeUnknownError(preparation ? await this.retireUncertainPreparation(preparation, input.onAuthorizationCreated) : null, 'authorization_create_malformed');
652
677
  const authorizationId = created.id;
678
+ // A prepared Mercado token already has the person's approval. Poll its
679
+ // result promptly while the native SDK waits: at the default interval,
680
+ // 10 seconds has at most 20 reads instead of 5 (15 additional reads).
681
+ // Then restore the caller's interval. Human approval waiting, cancellation,
682
+ // merchant deadlines, and the single processor request remain unchanged.
683
+ const preparedMercadoPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000 : 0;
653
684
  let execution = {};
654
685
  let approvalDelivered = false;
655
686
  const deliverApproval = (state) => {
656
- if (ownedShop || preparation || approvalDelivered || execution.executionMode === 'autopilot')
687
+ if (ownedShop || nativeCheckout || preparation || approvalDelivered || execution.executionMode === 'autopilot')
657
688
  return;
658
689
  const url = state.approvalUrl ?? state.approval_url;
659
690
  if (typeof url !== 'string' || !url)
@@ -679,7 +710,8 @@ export class VaultClient {
679
710
  while (Date.now() < deadline) {
680
711
  if (stopSignal?.aborted)
681
712
  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);
713
+ const pollInterval = Date.now() < preparedMercadoPollUntil ? Math.min(this.pollIntervalMs, 500) : this.pollIntervalMs;
714
+ await interruptibleSleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())), stopSignal);
683
715
  if (stopSignal?.aborted)
684
716
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
685
717
  let s;
@@ -697,6 +729,10 @@ export class VaultClient {
697
729
  throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
698
730
  }
699
731
  execution = executionMetadata(s, authorizationId, execution);
732
+ if (nativeCheckout && (s.status === 'submitted_on_device' ||
733
+ (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'
734
+ || execution.grantId !== input.grantId))))
735
+ throw new PaymentOutcomeUnknownError(authorizationId, 'checkout_receipt_unconfirmed');
700
736
  if (shopOrder && (s.status === 'submitted_on_device' ||
701
737
  (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'))))
702
738
  throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
@@ -777,11 +813,36 @@ export class VaultClient {
777
813
  response.body = JSON.stringify(receipt);
778
814
  response.headers = { 'content-type': 'application/json' };
779
815
  }
816
+ if (checkoutContext) {
817
+ let receipt;
818
+ try {
819
+ receipt = parseStripeCheckoutResponse(JSON.parse(response.body), { environment: input.stripeCheckoutEnvironment, sessionId: checkoutContext.session_id });
820
+ }
821
+ catch { /* Never return an unvalidated processor body to the page. */ }
822
+ if (response.status !== 200 || !receipt || s.amount_verified !== true
823
+ || s.charged_amount !== input.amount || s.charged_currency !== 'usd' || s.charged_kind !== 'captured')
824
+ throw new PaymentOutcomeUnknownError(authorizationId, 'checkout_receipt_unconfirmed');
825
+ response.body = JSON.stringify(receipt);
826
+ response.headers = { 'content-type': 'application/json' };
827
+ }
828
+ let processorContext;
829
+ if (mercadoContext) {
830
+ try {
831
+ processorContext = await validateMercadoProcessorContext(s.processor_context, mercadoContext, input.request.url, response.body);
832
+ }
833
+ catch {
834
+ throw new PaymentOutcomeUnknownError(authorizationId, 'mercado_checkout_metadata_invalid');
835
+ }
836
+ }
837
+ else if (s.processor_context !== undefined)
838
+ throw new PaymentOutcomeUnknownError(authorizationId, 'unexpected_processor_context');
780
839
  return {
781
840
  mode: 'token',
782
841
  authorizationId,
783
842
  ...(shopOrder ? { shopOrderId: shopOrder.order_id } : {}),
843
+ ...(checkoutContext ? { checkoutSessionId: checkoutContext.session_id } : {}),
784
844
  ...response,
845
+ ...(processorContext ? { processorContext } : {}),
785
846
  amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
786
847
  chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
787
848
  chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
@@ -824,7 +885,7 @@ export class VaultClient {
824
885
  throw error;
825
886
  }
826
887
  finally {
827
- if (input.merchantSignal?.aborted || (preparation && failed)) {
888
+ if (input.merchantSignal?.aborted || ((preparation || nativeCheckout) && failed)) {
828
889
  // Drain a create acknowledgement even after the merchant aborts so its
829
890
  // known ID can be retired. An unacknowledged create remains unknown.
830
891
  // 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
+ }
@@ -0,0 +1,52 @@
1
+ declare var MERCADO_CHECKOUT_ORIGIN: string;
2
+ declare var MERCADO_CONFIGURATION_URL: string;
3
+ declare var MERCADO_CHECKOUT_PATTERN: string;
4
+ declare function mercadoCheckoutDocument(raw: any): {
5
+ flowId: string;
6
+ preferenceId: string | null;
7
+ query: string;
8
+ };
9
+ declare function parseMercadoCheckoutContext(value: any): any;
10
+ declare function captureMercadoCheckoutContext(url: any, method: any, body: any, documentUrl: any): any;
11
+ declare function isMercadoTokenRequest(url: any, method: any): boolean;
12
+ declare function mercadoDigest(value: any): Promise<string>;
13
+ declare function mercadoContextDigest(context: any, tokenUrl: any): Promise<string>;
14
+ declare function prepareMercadoConfiguration(context: any, card: any): {
15
+ url: string;
16
+ body: string;
17
+ };
18
+ declare function parseMercadoConfiguration(value: any): {
19
+ paymentMethodId: any;
20
+ paymentTypeId: any;
21
+ paymentMethodName: any;
22
+ issuerId: string;
23
+ issuerName: any;
24
+ };
25
+ declare function createMercadoProcessorContext(configuration: any, context: any, tokenUrl: any, tokenBody: any): Promise<{
26
+ paymentMethodId: any;
27
+ paymentTypeId: any;
28
+ paymentMethodName: any;
29
+ issuerId: string;
30
+ issuerName: any;
31
+ version: number;
32
+ psp: string;
33
+ source: string;
34
+ contextDigest: string;
35
+ tokenDigest: string;
36
+ }>;
37
+ declare function validateMercadoProcessorContext(value: any, context: any, tokenUrl: any, tokenBody: any): Promise<any>;
38
+ declare function createMercadoAssociationGate({ context, tokenUrl, tokenBody, processorContext }: {
39
+ context: any;
40
+ tokenUrl: any;
41
+ tokenBody: any;
42
+ processorContext: any;
43
+ }): Promise<{
44
+ take({ url, method, body, documentUrl }: {
45
+ url: any;
46
+ method: any;
47
+ body: any;
48
+ documentUrl: any;
49
+ }): string;
50
+ readonly claimed: boolean;
51
+ }>;
52
+ export { MERCADO_CHECKOUT_ORIGIN, MERCADO_CHECKOUT_PATTERN, MERCADO_CONFIGURATION_URL, captureMercadoCheckoutContext, createMercadoAssociationGate, createMercadoProcessorContext, isMercadoTokenRequest, mercadoCheckoutDocument, mercadoContextDigest, mercadoDigest, parseMercadoCheckoutContext, parseMercadoConfiguration, prepareMercadoConfiguration, validateMercadoProcessorContext };
@@ -0,0 +1,198 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: 0ea817a75d86052699eeaa7546950e6c046bb0d7a96e4a18dfcd3a0fdd2afc99
4
+ // src/mercado-checkout.js
5
+ var MERCADO_CHECKOUT_ORIGIN = "https://www.mercadopago.com.mx";
6
+ var MERCADO_CONFIGURATION_URL = MERCADO_CHECKOUT_ORIGIN + "/checkout/v1/payment/api/bricks/card-form/tokenization";
7
+ var MERCADO_CHECKOUT_PATTERN = MERCADO_CHECKOUT_ORIGIN + "/checkout/v1/payment/api/*";
8
+ var reject = () => {
9
+ throw new Error("mercado_checkout_context_invalid");
10
+ };
11
+ var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
12
+ var only = (v, keys) => object(v) && Object.keys(v).every((k) => keys.includes(k));
13
+ var exact = (v, keys) => only(v, keys) && keys.every((k) => Object.hasOwn(v, k));
14
+ var text = (v, max = 200) => typeof v === "string" && v.length > 0 && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v);
15
+ var id = (v) => typeof v === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(v);
16
+ var numberId = (v) => typeof v === "string" && /^\d{1,20}$/.test(v);
17
+ var label = (v) => text(v, 120) && /\p{L}/u.test(v) && !/[<>]|\d(?:[ -]*\d){5}|https?:|javascript:|data:/i.test(v);
18
+ var digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
19
+ var METHODS = ["visa", "master", "amex"];
20
+ var TYPES = ["credit_card", "debit_card", "prepaid_card"];
21
+ function checkedUrl(raw) {
22
+ let url;
23
+ try {
24
+ url = new URL(raw);
25
+ }
26
+ catch {
27
+ reject();
28
+ }
29
+ if (!text(raw, 4096) || url.href !== raw || url.origin !== MERCADO_CHECKOUT_ORIGIN || url.username || url.password || url.hash)
30
+ reject();
31
+ return url;
32
+ }
33
+ function mercadoCheckoutDocument(raw) {
34
+ const url = checkedUrl(raw);
35
+ const match = /^\/checkout\/v1\/payment\/redirect\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\/card-form\/$/.exec(url.pathname);
36
+ const keys = [...url.searchParams.keys()];
37
+ if (!match || keys.length !== 3 || new Set(keys).size !== 3 || keys.some((k) => !["preference-id", "router-request-id", "p"].includes(k)) || !id(url.searchParams.get("preference-id")) || !id(url.searchParams.get("router-request-id")) || !text(url.searchParams.get("p"), 2048))
38
+ reject();
39
+ return { flowId: match[1], preferenceId: url.searchParams.get("preference-id"), query: url.search.slice(1) };
40
+ }
41
+ function initialization(value) {
42
+ const keys = [
43
+ "callerId",
44
+ "collectorId",
45
+ "allowThirdPartyCard",
46
+ "clientId",
47
+ "flowId",
48
+ "checkoutId",
49
+ "scope",
50
+ "excludedPaymentTypesAndIds",
51
+ "locale",
52
+ "siteId",
53
+ "serviceRoutePrefix",
54
+ "productId",
55
+ "marketplace",
56
+ "platform",
57
+ "cardDetails"
58
+ ];
59
+ if (!exact(value, keys) || !numberId(value.callerId) || !numberId(value.collectorId) || !numberId(value.clientId) || value.allowThirdPartyCard !== true || value.flowId !== "checkout-pro-payment" || value.checkoutId !== "cow" || value.scope !== "prod" || value.locale !== "es-MX" || value.siteId !== "MLM" || value.serviceRoutePrefix !== "/checkout/v1/payment/api" || !id(value.productId) || value.marketplace !== "NONE" || value.platform !== "MP" || !Array.isArray(value.excludedPaymentTypesAndIds) || value.excludedPaymentTypesAndIds.length !== 0 || !exact(value.cardDetails, ["fieldsSetting", "identificationTypes", "additionalSteps"]) || !Array.isArray(value.cardDetails.identificationTypes) || value.cardDetails.identificationTypes.length !== 0 || JSON.stringify(value.cardDetails.additionalSteps) !== '["name"]')
60
+ reject();
61
+ const fields = value.cardDetails.fieldsSetting;
62
+ if (!Array.isArray(fields) || fields.length !== 5 || new Set(fields.map((f) => f?.name)).size !== 5)
63
+ reject();
64
+ for (const field of fields) {
65
+ if (!only(field, ["name", "type", "title", "placeholder", "length", "validationMessages", "mask", "hintMessage", "validationPattern", "autocomplete", "tooltipMessage"]) || !["card_number", "name", "expiration", "security_code", "identification_types"].includes(field.name) || field.type !== (field.name === "name" ? "text" : "number") || !text(field.title) || field.placeholder !== void 0 && !text(field.placeholder) || ["hintMessage", "validationPattern", "tooltipMessage"].some((k) => field[k] !== void 0 && !text(field[k], 1024)) || field.autocomplete !== void 0 && typeof field.autocomplete !== "boolean" || field.length !== void 0 && (!Number.isInteger(field.length) || field.length < 1 || field.length > 40) || field.mask !== void 0 && (!text(field.mask, 30) || !/^[ $/]+$/.test(field.mask)) || !only(field.validationMessages, ["emptyField", "incompleteField", "invalidField"]) || Object.values(field.validationMessages).some((v) => !text(v)))
66
+ reject();
67
+ }
68
+ if (JSON.stringify(value).length > 12e3)
69
+ reject();
70
+ return value;
71
+ }
72
+ function parseMercadoCheckoutContext(value) {
73
+ if (!exact(value, ["version", "documentUrl", "initializationData"]) || value.version !== 1)
74
+ reject();
75
+ mercadoCheckoutDocument(value.documentUrl);
76
+ initialization(value.initializationData);
77
+ return JSON.parse(JSON.stringify(value));
78
+ }
79
+ function captureMercadoCheckoutContext(url, method, body, documentUrl) {
80
+ if (url !== MERCADO_CONFIGURATION_URL || method !== "POST" || typeof body !== "string" || body.length > 16e3)
81
+ reject();
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(body);
85
+ }
86
+ catch {
87
+ reject();
88
+ }
89
+ if (!exact(parsed, ["cardBin", "initializationData"]) || typeof parsed.cardBin !== "string" || !/^\d{8}$/.test(parsed.cardBin))
90
+ reject();
91
+ return parseMercadoCheckoutContext({ version: 1, documentUrl, initializationData: parsed.initializationData });
92
+ }
93
+ function isMercadoTokenRequest(url, method) {
94
+ try {
95
+ const value = new URL(url), names = [...value.searchParams.keys()];
96
+ return method === "POST" && value.href === url && value.origin === "https://api.mercadopago.com" && value.pathname === "/v1/card_tokens" && !value.username && !value.password && !value.hash && new Set(names).size === names.length && names.every((n) => ["public_key", "locale", "js_version", "referer"].includes(n)) && id(value.searchParams.get("public_key"));
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ async function mercadoDigest(value) {
103
+ const bytes = new TextEncoder().encode(value);
104
+ return Array.from(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
105
+ }
106
+ function canonical(value) {
107
+ return Array.isArray(value) ? value.map(canonical) : object(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) : value;
108
+ }
109
+ async function mercadoContextDigest(context, tokenUrl) {
110
+ if (!isMercadoTokenRequest(tokenUrl, "POST"))
111
+ reject();
112
+ return mercadoDigest(JSON.stringify(canonical({ context: parseMercadoCheckoutContext(context), tokenUrl })));
113
+ }
114
+ function prepareMercadoConfiguration(context, card) {
115
+ const parsed = parseMercadoCheckoutContext(context);
116
+ const number = card?.number;
117
+ if (typeof number !== "string" || !/^\d{12,19}$/.test(number))
118
+ reject();
119
+ return { url: MERCADO_CONFIGURATION_URL, body: JSON.stringify({ cardBin: number.slice(0, 8), initializationData: parsed.initializationData }) };
120
+ }
121
+ function parseMercadoConfiguration(value) {
122
+ const method = value?.paymentMethod, issuers = value?.issuersSettings;
123
+ if (!object(value) || !exact(method, ["paymentMethodId", "paymentTypeId", "name"]) || !METHODS.includes(method.paymentMethodId) || !TYPES.includes(method.paymentTypeId) || !label(method.name) || !Array.isArray(issuers) || issuers.length !== 1 || !object(issuers[0]) || !label(issuers[0].name))
124
+ reject();
125
+ const issuerId = String(issuers[0].id);
126
+ if (!/^\d{1,11}$/.test(issuerId) || typeof issuers[0].id !== "string" && !Number.isSafeInteger(issuers[0].id))
127
+ reject();
128
+ const cvc = Array.isArray(value.fieldsSetting) ? value.fieldsSetting.filter((f) => f?.name === "security_code") : [];
129
+ const amex = method.paymentMethodId === "amex";
130
+ if (cvc.length !== 1 || cvc[0].type !== "number" || cvc[0].length !== (amex ? 4 : 3) || value.securityCodeSettings?.mode !== "mandatory" || value.securityCodeSettings?.location !== (amex ? "front" : "back"))
131
+ reject();
132
+ return {
133
+ paymentMethodId: method.paymentMethodId,
134
+ paymentTypeId: method.paymentTypeId,
135
+ paymentMethodName: method.name,
136
+ issuerId,
137
+ issuerName: issuers[0].name
138
+ };
139
+ }
140
+ function tokenIdentity(body) {
141
+ let value;
142
+ try {
143
+ value = JSON.parse(body);
144
+ }
145
+ catch {
146
+ reject();
147
+ }
148
+ if (!object(value) || value.live_mode === false || typeof value.id !== "string" || !/^[A-Za-z0-9_-]{8,256}$/.test(value.id))
149
+ reject();
150
+ return value.id;
151
+ }
152
+ async function createMercadoProcessorContext(configuration, context, tokenUrl, tokenBody) {
153
+ return {
154
+ version: 1,
155
+ psp: "mercado_pago",
156
+ source: "checkout_pro_bin8",
157
+ contextDigest: await mercadoContextDigest(context, tokenUrl),
158
+ tokenDigest: await mercadoDigest(tokenIdentity(tokenBody)),
159
+ ...parseMercadoConfiguration(configuration)
160
+ };
161
+ }
162
+ async function validateMercadoProcessorContext(value, context, tokenUrl, tokenBody) {
163
+ if (!exact(value, ["version", "psp", "source", "contextDigest", "tokenDigest", "paymentMethodId", "paymentTypeId", "paymentMethodName", "issuerId", "issuerName"]) || value.version !== 1 || value.psp !== "mercado_pago" || value.source !== "checkout_pro_bin8" || !digest(value.contextDigest) || !digest(value.tokenDigest) || !METHODS.includes(value.paymentMethodId) || !TYPES.includes(value.paymentTypeId) || !label(value.paymentMethodName) || !numberId(value.issuerId) || value.issuerId.length > 11 || !label(value.issuerName) || value.contextDigest !== await mercadoContextDigest(context, tokenUrl) || value.tokenDigest !== await mercadoDigest(tokenIdentity(tokenBody)))
164
+ reject();
165
+ return { ...value };
166
+ }
167
+ async function createMercadoAssociationGate({ context, tokenUrl, tokenBody, processorContext }) {
168
+ const checked = parseMercadoCheckoutContext(context), metadata = await validateMercadoProcessorContext(processorContext, checked, tokenUrl, tokenBody);
169
+ const document = mercadoCheckoutDocument(checked.documentUrl), token = tokenIdentity(tokenBody);
170
+ let claimed = false;
171
+ return { take({ url, method, body, documentUrl }) {
172
+ if (claimed)
173
+ reject();
174
+ const target = checkedUrl(url);
175
+ if (method !== "PUT" || documentUrl !== checked.documentUrl || target.pathname !== "/checkout/v1/payment/api/flow/" + document.flowId || target.search || typeof body !== "string" || body.length > 64e3)
176
+ reject();
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(body);
180
+ }
181
+ catch {
182
+ reject();
183
+ }
184
+ if (!only(parsed, ["id", "values", "type", "featureFlags", "version", "prev_step", "urlParams"]) || parsed.id !== "next_step" || parsed.type !== "redirect" || parsed.version !== "checkout" || parsed.prev_step !== "card_form" || parsed.urlParams !== document.query || !only(parsed.values, ["payment_method", "card_token", "card_ui_settings", "captchaToken"]) || parsed.values.card_token !== token || !object(parsed.values.card_ui_settings) || !only(parsed.values.payment_method, ["id", "type", "name", "issuer"]) || parsed.values.payment_method.id !== metadata.paymentMethodId || parsed.values.payment_method.type !== metadata.paymentTypeId)
185
+ reject();
186
+ claimed = true;
187
+ parsed.values.payment_method = {
188
+ id: metadata.paymentMethodId,
189
+ type: metadata.paymentTypeId,
190
+ name: metadata.paymentMethodName,
191
+ issuer: { id: Number(metadata.issuerId), name: metadata.issuerName }
192
+ };
193
+ return JSON.stringify(parsed);
194
+ }, get claimed() {
195
+ return claimed;
196
+ } };
197
+ }
198
+ export { MERCADO_CHECKOUT_ORIGIN, MERCADO_CHECKOUT_PATTERN, MERCADO_CONFIGURATION_URL, captureMercadoCheckoutContext, createMercadoAssociationGate, createMercadoProcessorContext, isMercadoTokenRequest, mercadoCheckoutDocument, mercadoContextDigest, mercadoDigest, parseMercadoCheckoutContext, parseMercadoConfiguration, prepareMercadoConfiguration, validateMercadoProcessorContext };
@@ -0,0 +1,108 @@
1
+ import { captureMercadoCheckoutContext, createMercadoAssociationGate, isMercadoTokenRequest, MERCADO_CONFIGURATION_URL, MERCADO_CHECKOUT_ORIGIN, parseMercadoCheckoutContext } from './mercado-checkout.generated.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 class MercadoCheckoutGate {
7
+ context = null;
8
+ started = false;
9
+ stopped = false;
10
+ sequence = 0;
11
+ associatedFlow = null;
12
+ association = null;
13
+ matches(url) {
14
+ try {
15
+ const parsed = new URL(url);
16
+ return parsed.origin === MERCADO_CHECKOUT_ORIGIN
17
+ && (parsed.pathname === new URL(MERCADO_CONFIGURATION_URL).pathname || parsed.pathname.startsWith('/checkout/v1/payment/api/flow/'));
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ requiresDocument(url, method) {
24
+ try {
25
+ const parsed = new URL(url);
26
+ return method.toUpperCase() === 'POST'
27
+ && parsed.origin === 'https://api.mercadopago.com' && /^\/v1\/card_tokens$/i.test(parsed.pathname);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ invalidate() {
34
+ if (!this.context && !this.started && this.sequence === 0)
35
+ return;
36
+ this.stopped = true;
37
+ this.context = null;
38
+ this.association = null;
39
+ this.sequence++;
40
+ }
41
+ async configuration(url, method, body, readDocument) {
42
+ if (url !== MERCADO_CONFIGURATION_URL || method !== 'POST')
43
+ return false;
44
+ if (this.started || this.stopped)
45
+ throw new Error('mercado_checkout_configuration_changed');
46
+ const sequence = ++this.sequence;
47
+ this.context = null;
48
+ const documentUrl = await readDocument();
49
+ // A newer native BIN lookup owns the context; the older harmless lookup
50
+ // still continues, but cannot erase or invalidate that newer capture.
51
+ if (sequence !== this.sequence && !this.stopped)
52
+ return true;
53
+ const context = captureMercadoCheckoutContext(url, method, body, documentUrl);
54
+ if (sequence !== this.sequence || this.started || this.stopped)
55
+ throw new Error('mercado_checkout_configuration_changed');
56
+ this.context = context;
57
+ return true;
58
+ }
59
+ claimToken(url, method, documentUrl) {
60
+ if (!this.requiresDocument(url, method))
61
+ return undefined;
62
+ // Other Mercado products remain on their existing tokenization path.
63
+ if (!documentUrl.startsWith(MERCADO_CHECKOUT_ORIGIN + '/checkout/'))
64
+ return undefined;
65
+ if (!isMercadoTokenRequest(url, method) || this.started || this.stopped || !this.context || this.context.documentUrl !== documentUrl)
66
+ throw new Error('mercado_checkout_configuration_unavailable');
67
+ this.started = true;
68
+ return parseMercadoCheckoutContext(this.context);
69
+ }
70
+ async arm(replay, tokenUrl) {
71
+ if (!this.started)
72
+ return;
73
+ if (this.stopped || !this.context || replay.mode !== 'token' || replay.status !== 201 || !replay.processorContext)
74
+ throw new Error('mercado_checkout_metadata_unavailable');
75
+ const context = this.context;
76
+ const association = await createMercadoAssociationGate({ context, tokenUrl, tokenBody: replay.body, processorContext: replay.processorContext });
77
+ if (this.stopped || this.context !== context)
78
+ throw new Error('mercado_checkout_context_changed');
79
+ this.association = association;
80
+ }
81
+ associationBody(url, method, body, documentUrl) {
82
+ if (['GET', 'HEAD', 'OPTIONS'].includes(method) || !this.matches(url) || url === MERCADO_CONFIGURATION_URL)
83
+ return undefined;
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(body);
87
+ }
88
+ catch {
89
+ if (this.started)
90
+ throw new Error('mercado_checkout_association_invalid');
91
+ return undefined;
92
+ }
93
+ // The observed Pay request keeps the existing payment guards, including
94
+ // after the native document navigates to review. Every card association
95
+ // must use the checked shape; changing prev_step cannot bypass correction.
96
+ const cardAssociation = parsed?.id === 'next_step' || parsed?.prev_step === 'card_form'
97
+ || (parsed?.values && ('card_token' in parsed.values || 'payment_method' in parsed.values));
98
+ if (!cardAssociation && parsed?.id === 'pay' && parsed?.prev_step === 'review' && this.associatedFlow === url)
99
+ return undefined;
100
+ if (!cardAssociation && !this.started)
101
+ return undefined;
102
+ if (!this.started || this.stopped || !this.association)
103
+ throw new Error('mercado_checkout_association_unavailable');
104
+ const corrected = this.association.take({ url, method, body, documentUrl });
105
+ this.associatedFlow = url;
106
+ return corrected;
107
+ }
108
+ }
@@ -50,6 +50,16 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
50
50
  readonly status: "supported";
51
51
  readonly mode: "token";
52
52
  readonly operations: readonly [{
53
+ readonly id: "stripe.device_checkout_confirm.v1";
54
+ readonly path_pattern: "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$";
55
+ readonly effect: "payment_confirmation";
56
+ readonly amount_binding: "expected_amount_test_evidence_currency_unqualified";
57
+ readonly psp: "stripe";
58
+ readonly version: 1;
59
+ readonly mode: "token";
60
+ readonly autopilot_eligible: false;
61
+ readonly requires: "reviewed_final_payment_and_continuation_evidence";
62
+ }, {
53
63
  readonly id: "stripe.tokenization";
54
64
  readonly path_pattern: "^/v1/(?:payment_methods|tokens)$";
55
65
  readonly effect: "token_or_method_creation";
@@ -992,7 +1002,7 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
992
1002
  readonly requirements: readonly ["verify_merchant_billing"];
993
1003
  };
994
1004
  };
995
- readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."];
1005
+ readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.", "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."];
996
1006
  readonly unsupported_variants: readonly [{
997
1007
  readonly variant: "legacy_jsonp_get";
998
1008
  readonly status: "unsupported";
@@ -1003,6 +1013,21 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
1003
1013
  readonly status: "unsupported";
1004
1014
  readonly constraint: "direct_sdk";
1005
1015
  readonly reason: "Bank-account and alternative-payment token routes are outside the new-card path.";
1016
+ }, {
1017
+ readonly variant: "card_bound_risk_preflight";
1018
+ readonly status: "unsupported";
1019
+ readonly constraint: "direct_sdk";
1020
+ readonly reason: "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card.";
1021
+ }, {
1022
+ readonly variant: "prepared_co_badged_network_choice";
1023
+ readonly status: "unsupported";
1024
+ readonly constraint: "direct_sdk";
1025
+ readonly reason: "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks.";
1026
+ }, {
1027
+ readonly variant: "proactive_authentication";
1028
+ readonly status: "unsupported";
1029
+ readonly constraint: "direct_sdk";
1030
+ readonly reason: "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support.";
1006
1031
  }];
1007
1032
  readonly processor_amount: "none";
1008
1033
  readonly challenge_surface: "merchant_browser";
@@ -78,6 +78,17 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
78
78
  "status": "supported",
79
79
  "mode": "token",
80
80
  "operations": [
81
+ {
82
+ "id": "stripe.device_checkout_confirm.v1",
83
+ "path_pattern": "^/v1/payment_pages/cs_(?:test|live)_[A-Za-z0-9]+/confirm$",
84
+ "effect": "payment_confirmation",
85
+ "amount_binding": "expected_amount_test_evidence_currency_unqualified",
86
+ "psp": "stripe",
87
+ "version": 1,
88
+ "mode": "token",
89
+ "autopilot_eligible": false,
90
+ "requires": "reviewed_final_payment_and_continuation_evidence"
91
+ },
81
92
  {
82
93
  "id": "stripe.tokenization",
83
94
  "path_pattern": "^/v1/(?:payment_methods|tokens)$",
@@ -1534,7 +1545,8 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
1534
1545
  },
1535
1546
  "limitations": [
1536
1547
  "Only the listed request format is recognized; a recognized processor is not verified merchant coverage.",
1537
- "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints."
1548
+ "The adapter covers the normal XHR POST new-card tokenization path on Recurly US and EU endpoints.",
1549
+ "Prepare approval before submitting a timed native card form. Both regions require the shared environment because test and live keys use the same endpoints."
1538
1550
  ],
1539
1551
  "unsupported_variants": [
1540
1552
  {
@@ -1548,6 +1560,24 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
1548
1560
  "status": "unsupported",
1549
1561
  "constraint": "direct_sdk",
1550
1562
  "reason": "Bank-account and alternative-payment token routes are outside the new-card path."
1563
+ },
1564
+ {
1565
+ "variant": "card_bound_risk_preflight",
1566
+ "status": "unsupported",
1567
+ "constraint": "direct_sdk",
1568
+ "reason": "Worldpay and Cybersource risk sessions can depend on the original card. Prepared requests refuse those results until collection can use the approved card."
1569
+ },
1570
+ {
1571
+ "variant": "prepared_co_badged_network_choice",
1572
+ "status": "unsupported",
1573
+ "constraint": "direct_sdk",
1574
+ "reason": "A prepared request cannot reuse the original card’s network preference without checking the selected card’s supported networks."
1575
+ },
1576
+ {
1577
+ "variant": "proactive_authentication",
1578
+ "status": "unsupported",
1579
+ "constraint": "direct_sdk",
1580
+ "reason": "Card-bearing risk authentication and proactive 3-D Secure action tokens require separate support."
1551
1581
  }
1552
1582
  ],
1553
1583
  "processor_amount": "none",