@agent-cards/checkout 0.9.0 → 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/PREFLIGHT.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/README.md +112 -0
- package/examples/preflight/kernel-native/documented-adapters.json +113 -0
- package/examples/preflight/kernel-native/inventory.json +224 -0
- package/examples/preflight/kernel-native/qualification.mjs +182 -0
- package/package.json +5 -4
|
@@ -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_[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";
|
|
@@ -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_[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)$",
|
|
@@ -1964,6 +1964,17 @@
|
|
|
1964
1964
|
"status": "supported",
|
|
1965
1965
|
"mode": "token",
|
|
1966
1966
|
"operations": [
|
|
1967
|
+
{
|
|
1968
|
+
"id": "stripe.device_checkout_confirm.v1",
|
|
1969
|
+
"path_pattern": "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$",
|
|
1970
|
+
"effect": "payment_confirmation",
|
|
1971
|
+
"amount_binding": "expected_amount_test_evidence_currency_unqualified",
|
|
1972
|
+
"psp": "stripe",
|
|
1973
|
+
"version": 1,
|
|
1974
|
+
"mode": "token",
|
|
1975
|
+
"autopilot_eligible": false,
|
|
1976
|
+
"requires": "reviewed_final_payment_and_continuation_evidence"
|
|
1977
|
+
},
|
|
1967
1978
|
{
|
|
1968
1979
|
"id": "stripe.tokenization",
|
|
1969
1980
|
"path_pattern": "^/v1/(?:payment_methods|tokens)$",
|
|
@@ -195,6 +195,16 @@ export declare function getCheckoutPreflightCatalog(): {
|
|
|
195
195
|
readonly status: "supported";
|
|
196
196
|
readonly mode: "token";
|
|
197
197
|
readonly operations: readonly [{
|
|
198
|
+
readonly id: "stripe.device_checkout_confirm.v1";
|
|
199
|
+
readonly path_pattern: "^/v1/payment_pages/cs_test_[A-Za-z0-9]+/confirm$";
|
|
200
|
+
readonly effect: "payment_confirmation";
|
|
201
|
+
readonly amount_binding: "expected_amount_test_evidence_currency_unqualified";
|
|
202
|
+
readonly psp: "stripe";
|
|
203
|
+
readonly version: 1;
|
|
204
|
+
readonly mode: "token";
|
|
205
|
+
readonly autopilot_eligible: false;
|
|
206
|
+
readonly requires: "reviewed_final_payment_and_continuation_evidence";
|
|
207
|
+
}, {
|
|
198
208
|
readonly id: "stripe.tokenization";
|
|
199
209
|
readonly path_pattern: "^/v1/(?:payment_methods|tokens)$";
|
|
200
210
|
readonly effect: "token_or_method_creation";
|
package/dist/preparation.js
CHANGED
|
@@ -20,6 +20,8 @@ export class PreparationGate {
|
|
|
20
20
|
lifecycle.abort.signal.addEventListener('abort', () => this.invalidate('cancelled'), { once: true });
|
|
21
21
|
}
|
|
22
22
|
async prepare(options) {
|
|
23
|
+
if (this.opts.stripeCheckout)
|
|
24
|
+
throw new CheckoutPreparationError(null, 'unsupported_processor');
|
|
23
25
|
options = { ...options };
|
|
24
26
|
if (this.state !== 'unused' || this.observedRequest || this.lifecycle.getState().status !== 'idle' || this.lifecycle.isBlocked()) {
|
|
25
27
|
// A late opt-in cannot turn the next native retry into ordinary approval.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PausedRequest } from './client.js';
|
|
2
|
+
/** Explicit development enrollment; this option cannot enable production execution. */
|
|
3
|
+
export interface StripeCheckoutOptions {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
publishableKey: string;
|
|
6
|
+
}
|
|
7
|
+
type StubResponse = {
|
|
8
|
+
status: number;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
body: string;
|
|
11
|
+
};
|
|
12
|
+
type Step = {
|
|
13
|
+
phase: 'tokenization';
|
|
14
|
+
response: StubResponse;
|
|
15
|
+
} | {
|
|
16
|
+
phase: 'final';
|
|
17
|
+
request: PausedRequest;
|
|
18
|
+
};
|
|
19
|
+
type Phase = Step['phase'] | 'unknown';
|
|
20
|
+
type GateState = 'fresh' | 'stubbed' | 'submitted' | 'stopped';
|
|
21
|
+
export type StripeCheckoutBlockStage = 'request_read' | 'classification' | 'claim' | 'readiness' | 'document' | 'stub_response';
|
|
22
|
+
type ReadinessReason = 'attachment_not_ready' | 'cancelled' | 'checkout_inactive' | 'approval_pending' | 'request_inactive';
|
|
23
|
+
export type StripeCheckoutBlockReason = ReadinessReason | 'request_unavailable' | 'request_validation_failed' | 'context_already_present' | 'duplicate_tokenization' | 'duplicate_confirmation' | 'preparation_unavailable' | 'preparation_missing' | 'claim_invalidated' | 'document_changed' | 'document_unavailable' | 'readiness_unavailable' | 'stub_response_failed';
|
|
24
|
+
/** Native pre-authorization refusal metadata. Never contains request or exception text.
|
|
25
|
+
* Classification failures intentionally do not guess which shared-core predicate rejected.
|
|
26
|
+
* gate_state is observed before the adapter retires a failed claim. A preserved duplicate
|
|
27
|
+
* belongs to a competing request: consumers must not cancel the active checkout for it.
|
|
28
|
+
*/
|
|
29
|
+
export interface StripeCheckoutBlockedDetail {
|
|
30
|
+
version: 1;
|
|
31
|
+
processor: 'stripe';
|
|
32
|
+
endpoint_family: 'payment_methods' | 'payment_page_confirm' | 'other';
|
|
33
|
+
phase: Phase;
|
|
34
|
+
stage: StripeCheckoutBlockStage;
|
|
35
|
+
reason: StripeCheckoutBlockReason;
|
|
36
|
+
gate_state: GateState;
|
|
37
|
+
disposition: 'active_claim_preserved' | 'checkout_stopped';
|
|
38
|
+
}
|
|
39
|
+
declare class StripeCheckoutRejectionError extends Error {
|
|
40
|
+
readonly stage: StripeCheckoutBlockStage;
|
|
41
|
+
readonly reason: StripeCheckoutBlockReason;
|
|
42
|
+
readonly phase?: Phase | undefined;
|
|
43
|
+
constructor(message: string, stage: StripeCheckoutBlockStage, reason: StripeCheckoutBlockReason, phase?: Phase | undefined);
|
|
44
|
+
}
|
|
45
|
+
export declare function stripeCheckoutReadinessError(reason: ReadinessReason): Error;
|
|
46
|
+
/** A valid competing request must not retire the request that already claimed this step. */
|
|
47
|
+
export declare class StripeCheckoutClaimConflictError extends StripeCheckoutRejectionError {
|
|
48
|
+
constructor(phase: Step['phase']);
|
|
49
|
+
}
|
|
50
|
+
/** The two native browser requests belong to one attachment and one grant.
|
|
51
|
+
* Processor rules and form transformations live in the generated shared core.
|
|
52
|
+
* The local stub contains no selected-card data and starts no authorization.
|
|
53
|
+
*/
|
|
54
|
+
export declare class StripeCheckoutGate {
|
|
55
|
+
private state;
|
|
56
|
+
private readonly binding?;
|
|
57
|
+
constructor(options: {
|
|
58
|
+
stripeCheckout?: StripeCheckoutOptions;
|
|
59
|
+
executionMode?: string;
|
|
60
|
+
grantId?: string;
|
|
61
|
+
amount?: number | string;
|
|
62
|
+
currency?: string;
|
|
63
|
+
});
|
|
64
|
+
matches(url: string): boolean;
|
|
65
|
+
isEnabled(): boolean;
|
|
66
|
+
isPrepared(): boolean;
|
|
67
|
+
invalidate(): void;
|
|
68
|
+
/** Observability only: this endpoint label never participates in a payment decision. */
|
|
69
|
+
describeRejection(error: unknown, rawUrl: string, stage: StripeCheckoutBlockStage, phase?: Phase): StripeCheckoutBlockedDetail;
|
|
70
|
+
assertClaim(step: Step): void;
|
|
71
|
+
/** Check the actual top-level hosted document before preparing either step. */
|
|
72
|
+
assertDocument(raw: string): void;
|
|
73
|
+
claim(request: PausedRequest): Step | null;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
declare var STRIPE_CHECKOUT_OPERATION: string;
|
|
2
|
+
declare var STRIPE_CHECKOUT_CONTEXT_HEADER: string;
|
|
3
|
+
declare function isStripeCheckoutHostedMode(value: any): boolean;
|
|
4
|
+
declare function encodeStripeCheckoutContext(value: any): string;
|
|
5
|
+
declare function hasStripeCheckoutMarker(request: any): boolean;
|
|
6
|
+
declare function parseStripeCheckoutContext(request: any): {
|
|
7
|
+
version: number;
|
|
8
|
+
session_id: any;
|
|
9
|
+
synthetic_payment_method_id: any;
|
|
10
|
+
} | null | undefined;
|
|
11
|
+
declare function isStripeCheckoutEndpoint(raw: any): boolean;
|
|
12
|
+
declare function classifyStripeCheckoutRequest(request: any, options?: {}): {
|
|
13
|
+
phase: string;
|
|
14
|
+
session_id: any;
|
|
15
|
+
publishable_key: any;
|
|
16
|
+
synthetic_payment_method_id?: undefined;
|
|
17
|
+
amount_cents?: undefined;
|
|
18
|
+
currency?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
phase: string;
|
|
21
|
+
session_id: any;
|
|
22
|
+
publishable_key: any;
|
|
23
|
+
synthetic_payment_method_id: string | undefined;
|
|
24
|
+
amount_cents: number;
|
|
25
|
+
currency: string;
|
|
26
|
+
} | null;
|
|
27
|
+
declare function createStripeCheckoutTokenizationStub(request: any, options: any): {
|
|
28
|
+
status: number;
|
|
29
|
+
headers: {
|
|
30
|
+
"content-type": string;
|
|
31
|
+
};
|
|
32
|
+
body: string;
|
|
33
|
+
};
|
|
34
|
+
declare function parseStripeCheckoutResponse(raw: any, options: any): any;
|
|
35
|
+
declare function prepareStripeCheckoutReplay(request: any, card: any, options?: {}): {
|
|
36
|
+
psp: string;
|
|
37
|
+
request: {
|
|
38
|
+
url: any;
|
|
39
|
+
method: string;
|
|
40
|
+
headers: {
|
|
41
|
+
"content-type": string;
|
|
42
|
+
};
|
|
43
|
+
body: string;
|
|
44
|
+
};
|
|
45
|
+
where: string[];
|
|
46
|
+
};
|
|
47
|
+
export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay };
|