@agent-cards/checkout 0.6.0 → 0.8.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/braintree.js CHANGED
@@ -1,302 +1,2 @@
1
- const PRODUCTION = 'https://payments.braintree-api.com/graphql';
2
- const SANDBOX = 'https://payments.sandbox.braintree-api.com/graphql';
3
- const MAX_BODY_LENGTH = 128 * 1024;
4
- const MAX_DEPTH = 32;
5
- const MAX_NODES = 10_000;
6
- export function braintreeEnvironment(url) {
7
- return url === PRODUCTION ? 'production' : url === SANDBOX ? 'sandbox' : undefined;
8
- }
9
- function invalid() { throw new Error('invalid_braintree_request'); }
10
- function record(value) {
11
- return value !== null && typeof value === 'object' && !Array.isArray(value);
12
- }
13
- /** JSON.parse discards duplicate keys, so validate keys before using any value. */
14
- class JsonReader {
15
- text;
16
- at = 0;
17
- nodes = 0;
18
- constructor(text) {
19
- this.text = text;
20
- }
21
- read() {
22
- const value = this.value(0);
23
- this.space();
24
- if (this.at !== this.text.length)
25
- invalid();
26
- return value;
27
- }
28
- space() {
29
- while (' \t\r\n'.includes(this.text[this.at] ?? '\0'))
30
- this.at++;
31
- }
32
- take(expected) {
33
- this.space();
34
- if (this.text[this.at++] !== expected)
35
- invalid();
36
- }
37
- string() {
38
- this.space();
39
- const start = this.at;
40
- if (this.text[this.at++] !== '"')
41
- invalid();
42
- while (this.at < this.text.length) {
43
- const char = this.text[this.at++];
44
- if (char === '\\') {
45
- this.at++;
46
- continue;
47
- }
48
- if (char === '"')
49
- return JSON.parse(this.text.slice(start, this.at));
50
- }
51
- return invalid();
52
- }
53
- value(depth) {
54
- if (depth > MAX_DEPTH || ++this.nodes > MAX_NODES)
55
- invalid();
56
- this.space();
57
- const char = this.text[this.at];
58
- if (char === '"')
59
- return this.string();
60
- if (char === '{') {
61
- this.at++;
62
- const result = Object.create(null);
63
- const keys = new Set();
64
- this.space();
65
- if (this.text[this.at] === '}') {
66
- this.at++;
67
- return result;
68
- }
69
- while (true) {
70
- const key = this.string();
71
- if (keys.has(key))
72
- invalid();
73
- keys.add(key);
74
- this.take(':');
75
- result[key] = this.value(depth + 1);
76
- this.space();
77
- const next = this.text[this.at++];
78
- if (next === '}')
79
- return result;
80
- if (next !== ',')
81
- invalid();
82
- }
83
- }
84
- if (char === '[') {
85
- this.at++;
86
- const result = [];
87
- this.space();
88
- if (this.text[this.at] === ']') {
89
- this.at++;
90
- return result;
91
- }
92
- while (true) {
93
- result.push(this.value(depth + 1));
94
- this.space();
95
- const next = this.text[this.at++];
96
- if (next === ']')
97
- return result;
98
- if (next !== ',')
99
- invalid();
100
- }
101
- }
102
- for (const [literal, value] of [['true', true], ['false', false], ['null', null]]) {
103
- if (this.text.startsWith(literal, this.at)) {
104
- this.at += literal.length;
105
- return value;
106
- }
107
- }
108
- const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.at));
109
- if (!number)
110
- invalid();
111
- this.at += number[0].length;
112
- const value = Number(number[0]);
113
- if (!Number.isFinite(value))
114
- invalid();
115
- return value;
116
- }
117
- }
118
- /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
119
- export function readTokenizationJson(body) {
120
- if (typeof body !== 'string' || body.length === 0 || body.length > MAX_BODY_LENGTH)
121
- return null;
122
- try {
123
- const value = new JsonReader(body).read();
124
- return record(value) ? value : null;
125
- }
126
- catch {
127
- return null;
128
- }
129
- }
130
- /** Native operations need names, punctuation and variables, never literals. */
131
- function lex(query) {
132
- const tokens = [];
133
- let at = 0;
134
- while (at < query.length) {
135
- const char = query[at];
136
- if (' \t\r\n,'.includes(char)) {
137
- at++;
138
- continue;
139
- }
140
- if ('!$():{}'.includes(char)) {
141
- tokens.push(char);
142
- at++;
143
- }
144
- else if (/[A-Za-z_]/.test(char)) {
145
- const start = at++;
146
- while (at < query.length && /[A-Za-z_0-9]/.test(query[at]))
147
- at++;
148
- tokens.push(query.slice(start, at));
149
- }
150
- else
151
- invalid(); // Includes comments, strings, fragments and directives.
152
- if (tokens.length > MAX_NODES)
153
- invalid();
154
- }
155
- return tokens;
156
- }
157
- class GraphqlReader {
158
- tokens;
159
- at = 0;
160
- constructor(tokens) {
161
- this.tokens = tokens;
162
- }
163
- peek() { return this.tokens[this.at]; }
164
- take(expected) {
165
- if (this.tokens[this.at++] !== expected)
166
- invalid();
167
- }
168
- name() {
169
- const value = this.tokens[this.at++];
170
- if (!value || !/^[A-Za-z_][A-Za-z_0-9]*$/.test(value))
171
- invalid();
172
- return value;
173
- }
174
- read() {
175
- const kind = this.name();
176
- const name = this.name();
177
- let inputDefinition = false;
178
- if (this.peek() === '(') {
179
- // No defaults, alternate types, unused variables or duplicate definitions.
180
- for (const token of ['(', '$', 'input', ':', 'TokenizeCreditCardInput', '!', ')'])
181
- this.take(token);
182
- inputDefinition = true;
183
- }
184
- const fields = this.selection(0);
185
- if (this.at !== this.tokens.length)
186
- invalid();
187
- return { kind, name, inputDefinition, fields };
188
- }
189
- selection(depth) {
190
- if (depth > MAX_DEPTH)
191
- invalid();
192
- this.take('{');
193
- const fields = [];
194
- const names = new Set();
195
- while (this.peek() !== '}') {
196
- const name = this.name();
197
- if (names.has(name))
198
- invalid();
199
- names.add(name);
200
- const args = [];
201
- if (this.peek() === '(') {
202
- if (depth !== 0)
203
- invalid();
204
- this.take('(');
205
- const argument = this.name();
206
- this.take(':');
207
- this.take('$');
208
- args.push({ name: argument, variable: this.name() });
209
- this.take(')');
210
- }
211
- const children = this.peek() === '{' ? this.selection(depth + 1) : [];
212
- fields.push({ name, arguments: args, fields: children });
213
- // Aliases, directives and other punctuation cannot begin the next field.
214
- }
215
- this.take('}');
216
- if (fields.length === 0)
217
- invalid();
218
- return fields;
219
- }
220
- }
221
- /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
222
- export function classifyBraintreeRequest(url, method, body) {
223
- let hostname;
224
- try {
225
- hostname = new URL(url).hostname;
226
- }
227
- catch {
228
- return undefined;
229
- }
230
- if (hostname !== 'payments.braintree-api.com' && hostname !== 'payments.sandbox.braintree-api.com')
231
- return undefined;
232
- if (!braintreeEnvironment(url) || method !== 'POST' || typeof body !== 'string' || body.length === 0 || body.length > MAX_BODY_LENGTH)
233
- return 'invalid';
234
- try {
235
- const envelope = new JsonReader(body).read();
236
- if (!record(envelope) || typeof envelope.query !== 'string' || typeof envelope.operationName !== 'string')
237
- return 'invalid';
238
- if (Object.keys(envelope).some(key => !['query', 'operationName', 'variables', 'clientSdkMetadata'].includes(key)))
239
- return 'invalid';
240
- if (envelope.clientSdkMetadata !== undefined && !record(envelope.clientSdkMetadata))
241
- return 'invalid';
242
- const operation = new GraphqlReader(lex(envelope.query)).read();
243
- if (operation.name !== envelope.operationName || operation.fields.length !== 1)
244
- return 'invalid';
245
- const root = operation.fields[0];
246
- if (root.fields.length === 0)
247
- return 'invalid';
248
- const variables = envelope.variables === undefined ? Object.create(null) : envelope.variables;
249
- if (!record(variables))
250
- return 'invalid';
251
- if (operation.kind === 'query' && ['ClientConfiguration', 'ClientConfigurationQuery'].includes(operation.name)
252
- && !operation.inputDefinition && root.name === 'clientConfiguration' && root.arguments.length === 0
253
- && Object.keys(variables).length === 0)
254
- return 'configuration';
255
- if (operation.kind !== 'mutation' || operation.name !== 'TokenizeCreditCard' || !operation.inputDefinition
256
- || root.name !== 'tokenizeCreditCard' || root.arguments.length !== 1
257
- || root.arguments[0].name !== 'input' || root.arguments[0].variable !== 'input'
258
- || Object.keys(variables).length !== 1 || !record(variables.input) || !record(variables.input.creditCard))
259
- return 'invalid';
260
- const card = variables.input.creditCard;
261
- if (!['number', 'expirationMonth', 'expirationYear'].every(key => typeof card[key] === 'string' && card[key].trim().length > 0))
262
- return 'invalid';
263
- if (card.cvv !== undefined && (typeof card.cvv !== 'string' || card.cvv.trim().length === 0))
264
- return 'invalid';
265
- return 'tokenization';
266
- }
267
- catch {
268
- return 'invalid';
269
- }
270
- }
271
- /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
272
- export function isPreparedBraintreeRequest(body) {
273
- if (classifyBraintreeRequest(PRODUCTION, 'POST', body) !== 'tokenization')
274
- return false;
275
- try {
276
- const envelope = new JsonReader(body).read();
277
- const variables = envelope.variables;
278
- const input = variables.input;
279
- if (Object.keys(input).some(key => key !== 'creditCard' && key !== 'options'))
280
- return false;
281
- const card = input.creditCard;
282
- if (Object.keys(card).some(key => !['number', 'expirationMonth', 'expirationYear', 'cvv', 'cardholderName', 'billingAddress'].includes(key)))
283
- return false;
284
- if (Object.entries(card).some(([key, value]) => key !== 'billingAddress' && typeof value !== 'string'))
285
- return false;
286
- if (card.billingAddress !== undefined) {
287
- if (!record(card.billingAddress))
288
- return false;
289
- const addressKeys = ['postalCode', 'firstName', 'lastName', 'company', 'streetAddress', 'extendedAddress',
290
- 'locality', 'region', 'countryCodeNumeric', 'countryCodeAlpha2', 'countryCodeAlpha3', 'countryName'];
291
- if (Object.entries(card.billingAddress).some(([key, value]) => !addressKeys.includes(key) || typeof value !== 'string'))
292
- return false;
293
- }
294
- // Fingerprint authorization defaults validation on when the option is absent.
295
- // Preparation permits only an explicit request for a transient card token.
296
- return record(input.options) && Object.keys(input.options).every(key => key === 'validate')
297
- && input.options.validate === false;
298
- }
299
- catch {
300
- return false;
301
- }
302
- }
1
+ // The generated projection keeps the public SDK free of runtime dependencies.
2
+ export { braintreeEnvironment, classifyBraintreeRequest, isPreparedBraintreeRequest, readTokenizationJson } from './braintree.generated.js';
@@ -0,0 +1,2 @@
1
+ import type { Recognizer } from './registry.js';
2
+ export declare const BUILTIN_REGISTRY: Recognizer[];
@@ -0,0 +1 @@
1
+ export const BUILTIN_REGISTRY = [{ "psp": "shopify", "match": /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i, "hosts": ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"], "encoding": "json", "passthroughHeaders": [/^shopify-identification-signature$/i] }, { "psp": "stripe", "match": /api\.stripe\.com\/v1\/(payment_methods|tokens|setup_intents\/seti_\w+\/confirm|payment_intents\/pi_\w+\/confirm)/i, "hosts": ["^api\\.stripe\\.com$"], "encoding": "form", "passthroughHeaders": [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i] }, { "psp": "braintree", "match": /payments(\.sandbox)?\.braintree-api\.com\/graphql/i, "hosts": ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^braintree-version$/i] }, { "psp": "checkout_com", "match": /(api|card-acquisition-gateway)(\.sandbox)?\.checkout\.com\/tokens/i, "hosts": ["^api\\.checkout\\.com$", "^api\\.sandbox\\.checkout\\.com$", "^card-acquisition-gateway\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "adyen", "match": /(checkoutshopper-(test|live(-[a-z]+)?)\.adyen\.com|([a-z0-9-]+\.)*adyenpayments\.com)\/checkoutshopper\/v1\/sessions\/[A-Za-z0-9_-]+\/payments/i, "hosts": ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"], "encoding": "json", "passthroughHeaders": [], "clientSideEncrypted": true, "mode": "cse" }, { "psp": "tranzila", "match": /(direct\.tranzila\.com\/process\/n\/|directng\.tranzila\.com\/process\/?)/i, "hosts": ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"], "encoding": "form", "passthroughHeaders": [], "mode": "hosted_form" }, { "psp": "square", "match": /pci-connect\.squareup(?:sandbox)?\.com\/v2\/card-nonce/i, "hosts": ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "authorize_net", "match": /(api2|apitest)\.authorize\.net\/xml\/v1\/request\.api/i, "hosts": ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "worldpay", "match": /(try\.)?access\.worldpay\.com\/(sessions\/card|verifiedTokens\/sessions)/i, "hosts": ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^accept$/i] }, { "psp": "nuvei", "match": /(secure|ppp-test)\.safecharge\.com\/ppp\/api\/v1\/(?:cardTokenization|clientPayment|websdk\/initPaymentWithCardTokenization)\.do/i, "hosts": ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "airwallex", "match": /(checkout(\.sandbox)?\.airwallex\.com|pci-api(\.sandbox)?\.airwallex\.com)\/api\/v1\/pa\/(payment_intents\/[^/?#]+\/confirm|payment_consents\/[^/?#]+\/verify|payment_methods\/create)(\?|$)/i, "hosts": ["^checkout\\.airwallex\\.com$", "^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"], "encoding": "json", "passthroughHeaders": [/^client-secret$/i, /^authorization$/i, /^x-auth-token$/i, /^x-on-behalf-of$/i, /^x-api-version$/i] }, { "psp": "rapyd", "match": /(api|sandboxapi)\.rapyd\.net\/v1\/(?:hosted\/collect\/card\/[^/?#]+\/payment_method|checkout\/checkout_[A-Za-z0-9]+)(?:[?#]|$)/i, "hosts": ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "dlocal", "match": /ppmcc(-sandbox)?\.dlocal\.com\/cvault\/credit-card\/temporal/i, "hosts": ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-fields-api-key$/i, /^x-uow$/i, /^x-dlocal-infrav2$/i] }, { "psp": "ebanx", "match": /(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\.ebanx\.com|(?:api-diamond|api|sandbox)\.ebanxpay\.com)\/ws\/token/i, "hosts": ["^customer\\.ebanx\\.com$", "^customer-frontier\\.ebanx\\.com$", "^api\\.ebanx\\.com$", "^sandbox\\.ebanx\\.com$", "^api-local-latam\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^api-diamond\\.ebanxpay\\.com$", "^api\\.ebanxpay\\.com$", "^sandbox\\.ebanxpay\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "mercado_pago", "match": /api\.mercadopago\.com\/v1\/card_tokens(?![\w/-])/i, "hosts": ["^api\\.mercadopago\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-product-id$/i] }, { "psp": "payu", "match": /(secure\.payu\.com|(merch-prod|secure)\.snd\.payu\.com)\/api\/front\/tokens/i, "hosts": ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "razorpay", "match": /api\.razorpay\.com\/v1\/(?:standard_checkout\/)?payments\/create\/(?:ajax|checkout|fees)/i, "hosts": ["^api\\.razorpay\\.com$"], "encoding": "form", "passthroughHeaders": [/^x-razorpay-sessionid$/i, /^x-customer-access-token$/i] }, { "psp": "mollie", "match": /api\.cc\.mollie\.com\/v1\/card-tokens/i, "hosts": ["^api\\.cc\\.mollie\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "paysafe", "match": /(?:api(\.test)?\.paysafe\.com\/(?:paymenthub\/v1\/singleusepaymenthandles|js\/api\/v1\/tokenize)|hosted(\.test)?\.paysafe\.com\/checkout\/api\/v1\/tokenize)(?![\w/-])/i, "hosts": ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-paysafe-credentials$/i, /^correlationid$/i] }, { "psp": "recurly", "match": /api(\.eu)?\.recurly\.com\/js\/v1\/token(?![\w-])/i, "hosts": ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"], "encoding": "form", "passthroughHeaders": [/^recurly-credential-checkout-hostname$/i] }, { "psp": "moneris", "match": /(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\.moneris\.com\/HPPtoken\/request\.php/i, "hosts": ["^www3\\.moneris\\.com$", "^esqa\\.moneris\\.com$", "^gateway\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"], "encoding": "form", "passthroughHeaders": [] }, { "psp": "bambora", "match": /(api\.na\.bambora\.com|api\.bam\.shift4api\.net)\/scripts\/tokenization\/tokens/i, "hosts": ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "global_payments", "match": /(?:api\.heartlandportico\.com\/SecureSubmit\.v1\/api\/token|cert\.api2\.heartlandportico\.com\/Hps\.Exchange\.PosGateway\.Hpf\.v1\/api\/token|apis(?:\.sandbox)?(?:\.eu)?\.globalpay\.com\/ucp\/(?:merchants\/[^/]+\/)?payment-methods)/i, "hosts": ["^api\\.heartlandportico\\.com$", "^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.globalpay\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.eu\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-gp-version$/i] }];
package/dist/cdp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type VaultClient } from './client.js';
1
+ import { type VaultClient, type ExecutionMetadata } from './client.js';
2
2
  import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard } from './lifecycle.js';
3
3
  /**
4
4
  * The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
@@ -71,20 +71,34 @@ export interface CdpLike {
71
71
  send(method: string, params?: any, sessionId?: string): Promise<any>;
72
72
  on(handler: (method: string, params: any, sessionId?: string) => void): void;
73
73
  }
74
- export interface AttachOptions extends LifecycleOptions {
74
+ export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
75
+ /** Fallback when the browser cannot expose its top-level origin. Never an amount/payee authority. */
76
+ merchantOrigin?: string;
75
77
  vault: VaultClient;
76
78
  user: string;
77
79
  merchant: string;
78
- /** Display string for the approval screen. Optional when amountCents + currency are given. */
79
- amount?: string;
80
80
  /**
81
- * The amount as a number (smallest currency unit) with its ISO 4217 code.
82
- * See AuthorizeInput.amountCents: on a Stripe PaymentIntent confirm this
83
- * binds the approval to the intent's amount, checked at create and again
84
- * right before replay, so a larger charge is refused with nothing charged.
81
+ * Your hint at the amount, an integer in the currency's smallest unit or a
82
+ * decimal string in normal units, with its ISO 4217 code. See
83
+ * AuthorizeInput.amount: the processor's own amount is the higher authority
84
+ * and is read right before the card is sent; a hint lets a bad purchase be
85
+ * refused the moment it opens, and one that disagrees with the processor is
86
+ * refused with nothing charged.
85
87
  */
86
- amountCents?: number;
88
+ amount?: number | string;
87
89
  currency?: string;
90
+ /**
91
+ * The total the checkout page shows, the lowest authority: used only when
92
+ * neither the processor's request nor your `amount` names one. Pass a reader
93
+ * that returns an integer in the currency's smallest unit with its code
94
+ * (`{ amount: 4210, currency: 'usd' }`), read off the page however your
95
+ * page spells it; the adapters call it when a card request pauses, give it
96
+ * one second, and send nothing when it yields nothing. Absent: no page total.
97
+ */
98
+ pageAmount?: () => Promise<{
99
+ amount: number;
100
+ currency: string;
101
+ } | undefined>;
88
102
  cardId?: string;
89
103
  timeoutMs?: number;
90
104
  /** Browser interception setup deadline, separate from approval. Defaults to 30000 ms; maximum 300000 ms. */
package/dist/cdp.js CHANGED
@@ -274,6 +274,79 @@ export function withCorsHeaders(headers, cors) {
274
274
  function headerEntries(headers) {
275
275
  return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }));
276
276
  }
277
+ /**
278
+ * The origin of the top-level document the payment form is on, read when a
279
+ * card request pauses: the fact that names the merchant for the company's
280
+ * presets (the API's checkout_origin). An https origin, or http://localhost
281
+ * for local checkout; anything else, or a page that cannot be read, yields
282
+ * undefined and the authorization goes out without one (a merchant, category
283
+ * or place rule then refuses it as unknown, never the request itself).
284
+ */
285
+ async function pageOriginOf(readDocumentUrl, signal) {
286
+ let timer;
287
+ let abort;
288
+ try {
289
+ const documentUrl = await Promise.race([
290
+ Promise.resolve().then(readDocumentUrl).catch(() => undefined),
291
+ new Promise((resolve) => {
292
+ timer = setTimeout(resolve, 1_000);
293
+ abort = () => resolve(undefined);
294
+ signal.addEventListener('abort', abort, { once: true });
295
+ if (signal.aborted)
296
+ abort();
297
+ }),
298
+ ]);
299
+ if (typeof documentUrl !== 'string')
300
+ return undefined;
301
+ const url = new URL(documentUrl);
302
+ if (url.username || url.password)
303
+ return undefined;
304
+ if (url.protocol === 'https:' || (url.protocol === 'http:' && url.hostname === 'localhost'))
305
+ return url.origin;
306
+ return undefined;
307
+ }
308
+ catch {
309
+ return undefined;
310
+ }
311
+ finally {
312
+ if (timer)
313
+ clearTimeout(timer);
314
+ if (abort)
315
+ signal.removeEventListener('abort', abort);
316
+ }
317
+ }
318
+ /**
319
+ * The page total, the lowest amount authority, through the integrator's own
320
+ * reader (AttachOptions.pageAmount): an integer in the smallest unit with its
321
+ * currency, or nothing. A read is a courtesy, never a wait: it gets one
322
+ * second, and anything unreadable yields undefined so the authorization goes
323
+ * out without a page total.
324
+ */
325
+ const PAGE_AMOUNT_READ_MS = 1_000;
326
+ async function pageAmountOf(opts) {
327
+ if (!opts.pageAmount)
328
+ return undefined;
329
+ let timer;
330
+ try {
331
+ const raw = await Promise.race([
332
+ Promise.resolve().then(() => opts.pageAmount()).catch(() => undefined),
333
+ new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), PAGE_AMOUNT_READ_MS); }),
334
+ ]);
335
+ if (!raw || typeof raw !== 'object')
336
+ return undefined;
337
+ const { amount, currency } = raw;
338
+ if (!Number.isSafeInteger(amount) || amount < 0 || typeof currency !== 'string' || !/^[A-Za-z]{3}$/.test(currency))
339
+ return undefined;
340
+ return { amount: amount, currency: currency.toLowerCase() };
341
+ }
342
+ catch {
343
+ return undefined;
344
+ }
345
+ finally {
346
+ if (timer)
347
+ clearTimeout(timer);
348
+ }
349
+ }
277
350
  /**
278
351
  * Last-resort patterns: the built-in recognizers' hosts, derived the same way
279
352
  * as everything else. Used only when the vault hands back nothing at all (a
@@ -335,13 +408,14 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
335
408
  let setupFailureReported = false;
336
409
  const lifecycle = new CheckoutLifecycle(opts);
337
410
  let preparationFrameId;
338
- const preparationGate = new PreparationGate(opts, lifecycle, async () => {
411
+ const readDocumentUrl = async () => {
339
412
  const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
340
413
  if (typeof tree?.frameTree?.frame?.url !== 'string')
341
414
  throw new Error('merchant_document_unavailable');
342
415
  preparationFrameId = tree.frameTree.frame.id;
343
416
  return tree.frameTree.frame.url;
344
- });
417
+ };
418
+ const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
345
419
  const guards = paymentEndpointGuards(opts.paymentEndpoints);
346
420
  const armed = new Set();
347
421
  const arming = new Map();
@@ -533,14 +607,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
533
607
  activeRequest = { attempt, networkId, sessionId, frameId };
534
608
  if (preparation)
535
609
  await preparationGate.assertDocument();
610
+ const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
611
+ const pageAmount = await pageAmountOf(opts);
612
+ attempt.assertLive();
613
+ const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
614
+ ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
536
615
  attempt.assertLive();
537
616
  const replay = await opts.vault.authorize({
538
617
  user: opts.user,
539
618
  merchant: opts.merchant,
540
619
  amount: opts.amount,
541
- amountCents: opts.amountCents,
542
620
  currency: opts.currency,
543
621
  cardId: preparation?.cardId ?? opts.cardId,
622
+ executionMode: opts.executionMode,
623
+ grantId: opts.grantId,
624
+ merchantOrigin,
625
+ pageOrigin,
626
+ pageAmount,
544
627
  preparation,
545
628
  timeoutMs: opts.timeoutMs,
546
629
  signal: lifecycle.abort.signal,
@@ -675,11 +758,12 @@ export async function attachToPlaywright(page, opts) {
675
758
  throw new Error('Service workers are active; use a checkout context created with serviceWorkers: "block".');
676
759
  }
677
760
  const lifecycle = new CheckoutLifecycle(opts);
678
- const preparationGate = new PreparationGate(opts, lifecycle, async () => {
761
+ const readDocumentUrl = async () => {
679
762
  if (page.isClosed?.() || typeof page.url !== 'function')
680
763
  throw new Error('merchant_document_unavailable');
681
764
  return page.url();
682
- });
765
+ };
766
+ const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
683
767
  const guards = paymentEndpointGuards(opts.paymentEndpoints);
684
768
  // Playwright's own routing, NOT a hand-rolled CDP session.
685
769
  //
@@ -800,14 +884,23 @@ export async function attachToPlaywright(page, opts) {
800
884
  activeRequest = { request, frames, attempt };
801
885
  if (preparation)
802
886
  await preparationGate.assertDocument();
887
+ const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
888
+ const pageAmount = await pageAmountOf(opts);
889
+ assertRequestLive();
890
+ const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
891
+ ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
803
892
  assertRequestLive();
804
893
  const replay = await opts.vault.authorize({
805
894
  user: opts.user,
806
895
  merchant: opts.merchant,
807
896
  amount: opts.amount,
808
- amountCents: opts.amountCents,
809
897
  currency: opts.currency,
810
898
  cardId: preparation?.cardId ?? opts.cardId,
899
+ executionMode: opts.executionMode,
900
+ grantId: opts.grantId,
901
+ merchantOrigin,
902
+ pageOrigin,
903
+ pageAmount,
811
904
  preparation,
812
905
  timeoutMs: opts.timeoutMs,
813
906
  signal: lifecycle.abort.signal,