@agent-cards/checkout 0.4.1 → 0.6.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - Recognize Paysafe Checkout 1.8's exact hosted tokenization endpoints and preserve its native credential and correlation headers. The matching API registry and Vault deployment are required.
6
+ - Recognize Checkout.com card tokenization at `card-acquisition-gateway.checkout.com/tokens` and its sandbox counterpart alongside the existing API hosts. The matching backend registry and Vault deployment are required; this entry does not establish native merchant payment completion.
7
+ - Prepare Worldpay, Bambora and Mercado Pago checkout before the merchant's first Pay action. Use `psp: 'worldpay'` with `environment: 'production' | 'sandbox'`, or `psp: 'bambora' | 'mercado_pago'` with `environment: 'shared'`. Shared endpoints do not establish processor test mode. The merchant's credentials and checkout configuration determine that mode.
8
+ - Keep the existing selected-card, merchant, document, amount, currency and one-use consent checks. The native request begins only after approval, and approval readiness lasts at most 30 seconds. The SDK does not extend processor deadlines or automatically retry after payment uncertainty. Matching API, Vault and preparation migration are required.
9
+ - Bound Playwright and CDP setup with `attachmentTimeoutMs`, defaulting to 30 seconds. A stalled setup throws a sanitized `CheckoutAttachmentError`; late acknowledgements cannot start approval or retry setup, and concurrent setup failures report once. Close the failed checkout page and start a fresh context before trying again.
10
+ - Preserve the original setup failure across attached frames: a transport failure reports `unavailable`, a deadline reports `timeout`, and an observed page closure reports `closed`.
11
+ - Reject processor URLs with credentials or nondefault ports consistently across SDK and API discovery. Registry synchronization updates endpoint recognition; upgrading the SDK is required for the new preparation and setup behavior.
12
+
13
+ The matching Vault/API release adds Mollie card-token and supported Airwallex intent-confirmation replay through browser-owned TLS, preserves Worldpay's native session media type and Bambora's `cvd` field, and reports Nuvei's unambiguous validation error as processor refusal. Local regression and Chromium fixture results do not establish native processor acceptance or a completed merchant purchase.
14
+
15
+ ## 0.5.0
16
+
17
+ - Prepare Braintree card checkout with `controller.prepare({ psp: 'braintree', environment: 'production' | 'sandbox' })` before the merchant's first Pay action. The cardholder approves and unlocks first; one fresh native request then uses that approval without another notification. The matching API and Vault release is required.
18
+ - Let native Braintree client-configuration queries pass without consuming consent. Prepared checkout accepts one card-tokenization mutation for the approved processor and environment. Other operations, ambiguous GraphQL payloads and legacy REST fallback cannot use the preparation.
19
+ - Keep the native request timeout and one-use, card, merchant, amount, currency and document bindings. Approval readiness lasts at most 30 seconds. Tokenization amounts remain display-only; a token does not establish a paid order.
20
+ - Add a real Chromium fixture with approval delayed 61 seconds before a fresh 60-second XHR, plus cancellation, request classification and binding regressions. The isolated fixture contacts no payment processor and is not live purchase proof.
21
+
3
22
  ## 0.4.1
4
23
 
5
24
  - Stop describing every processor request rejection as a card decline or proof that nothing was charged. `ProcessorRefusedError` keeps its existing class and code, with neutral wording and an optional `processorError` containing bounded Razorpay reason, source, step and payment/order identifiers from the matching API/Vault release. Older failure records cannot recover details that were not retained.
package/README.md CHANGED
@@ -82,7 +82,7 @@ pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
82
82
  Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
83
83
  request for approval and resumes it only while the merchant request remains
84
84
  live. Merchant timeouts still apply: Square's observed tokenization deadline
85
- is about 10 seconds for approval and token handoff. For human approval, use
85
+ is about 10 seconds, and Braintree's native request timeout is 60 seconds, including approval and token handoff. For human approval, use
86
86
  `controller.prepare()` before the first Pay action as shown below.
87
87
 
88
88
  Playwright:
@@ -130,7 +130,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
130
130
  |---|---|
131
131
  | Shopify | supported, verified end to end |
132
132
  | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms with `amountCents` + `currency` use backend amount verification. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
133
- | Braintree / PayPal | supported, verified end to end |
133
+ | Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK `0.5.0` confirmed merchant fulfillment and SDK `completed` using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. |
134
134
  | Checkout.com | supported |
135
135
  | VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
136
136
  | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it |
@@ -327,6 +327,16 @@ Playwright `CDPSession` is not the `CdpLike` interface. Raw CDP must preserve th
327
327
  Initial arming errors reject `attachToCdp`; a child that cannot be armed remains
328
328
  paused and reports `browser_interception_unavailable` for operator recovery.
329
329
 
330
+ Await attachment before clicking Pay. Both adapters stop waiting for browser
331
+ setup after 30 seconds and throw `CheckoutAttachmentError` with
332
+ `code: 'checkout_attachment_failed'` and `reason: 'timeout'`, `'closed'` or
333
+ `'unavailable'`. Use `attachmentTimeoutMs` to choose a setup deadline from 1 to
334
+ 300000 milliseconds, separately from the approval's `timeoutMs`.
335
+ Close the failed checkout page and create a fresh browser context before
336
+ trying again. A late setup response cannot reopen the failed attachment or
337
+ request approval; intercepted card requests remain blocked. A setup failure
338
+ does not establish the status of any earlier purchase.
339
+
330
340
  Use a checkout context created with `serviceWorkers: 'block'`. Playwright cannot
331
341
  route requests intercepted by a service worker. The SDK rejects already active
332
342
  service workers, but that check cannot prevent a site from registering one
@@ -400,7 +410,9 @@ do not reuse that token in a new attachment as a workaround. Configuration and
400
410
  unsupported-mode failures require fixing the integration. Bank flows requiring
401
411
  another confirmation and other stored-token chains remain unverified.
402
412
 
403
- Square's observed native tokenization request expires after about 10 seconds. SDK 0.4.0 adds approval before submission, requiring the matching preparation API and Vault deployment. Start human approval before the caller's first Pay action:
413
+ Worldpay, Bambora and Mercado Pago preparation requires SDK 0.6.0 or later and the matching API and Vault release.
414
+
415
+ Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before the first Pay action so the cardholder can approve before the native card request starts. The API and Vault deployments must support the selected processor:
404
416
 
405
417
  ```ts
406
418
  const checkout = await attachToPlaywright(page, {
@@ -409,19 +421,31 @@ const checkout = await attachToPlaywright(page, {
409
421
  onApprovalUrl: deliverPrivatelyToCardholder,
410
422
  });
411
423
  const preparation = await checkout.prepare({
412
- psp: 'square',
413
- environment: 'production', // explicit; use 'sandbox' for Square Sandbox
424
+ psp: 'braintree',
425
+ environment: 'production', // Square, Braintree and Worldpay: production or sandbox
414
426
  });
415
427
  // The cardholder has consented and unlocked the same approval document.
416
428
  // No processor request or payment has started.
417
429
  await page.getByRole('button', { name: 'Pay', exact: true }).click();
418
430
  ```
419
431
 
420
- `prepare()` is available on both Playwright and raw CDP controllers. It requires `amountCents` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency and Square environment bind one fresh request. The amount remains `display_only`; a Square token does not enforce the merchant's eventual charge amount.
432
+ `prepare()` is available on both Playwright and raw CDP controllers. It requires `amountCents` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount remains `display_only`; a card token does not enforce the merchant's eventual charge amount.
433
+
434
+ | Processor | `environment` | Fresh native request |
435
+ | --- | --- | --- |
436
+ | Square | `production` or `sandbox` | Matching Square `/v2/card-nonce` host |
437
+ | Braintree | `production` or `sandbox` | Matching Braintree GraphQL host and guest `TokenizeCreditCard` mutation |
438
+ | Worldpay | `production` or `sandbox` | Matching Access Worldpay host and `/sessions/card` |
439
+ | Bambora | `shared` | `/scripts/tokenization/tokens` on `api.bam.shift4api.net` or `api.na.bambora.com` |
440
+ | Mercado Pago | `shared` | `api.mercadopago.com/v1/card_tokens` with a fresh card body |
441
+
442
+ Use `environment: 'shared'` for Bambora and Mercado Pago because the same endpoint serves test and live requests. Agentcard cannot establish the processor's test mode from that URL or a credential prefix. Configure test mode through the merchant's processor account when testing. Agentcard's own `sandbox` flag remains separate. Prepared Worldpay, Bambora and Mercado Pago requests reject saved-card and recurring request bodies; a refused request retires the local preparation. Reconcile any existing merchant attempt before creating a new attachment.
443
+
444
+ Braintree's native `ClientConfiguration` GraphQL query can run before, during or after preparation without using the approval. Only a single `TokenizeCreditCard` mutation can consume the prepared Braintree checkout. Prepared requests require guest card tokenization with explicit `options.validate: false`; omitted validation options, saved-card fields and `validate: true` are refused. Other GraphQL operations, batches and compound mutations are blocked. Braintree's legacy REST fallback cannot consume a prepared authorization.
421
445
 
422
- Readiness lasts up to 30 seconds (`preparation.expiresAt`) and appears as `ready_to_submit`, with `paymentStatus: 'not_started'`. Trigger the caller-owned Pay action immediately after the promise resolves. Expiry, navigation, cancellation, an early request or a changed checkout fails closed. A preparation and its attachment are single use; reconcile any bound authorization before creating a new attachment. The SDK never clicks Pay, reuses a stale request, pauses Square timers, or automatically retries a failed prepared checkout.
446
+ Readiness lasts up to 30 seconds (`preparation.expiresAt`) and appears as `ready_to_submit`, with `paymentStatus: 'not_started'`. Trigger the caller-owned Pay action immediately after the promise resolves. Expiry, navigation, cancellation, an early request or a changed checkout fails closed. A preparation and its attachment are single use; reconcile any bound authorization before creating a new attachment. The SDK never clicks Pay, reuses a stale request, changes native request deadlines, or automatically retries a failed prepared checkout.
423
447
 
424
- After Pay, Square's native deadline still covers fresh authorization binding, device replay, relay and token handoff. A disconnected/backgrounded phone or slow transport can still miss it. A subsequent SCA challenge has its own lifetime after token handoff. If the merchant request aborts or its frame closes, the attachment blocks further requests and tries to retire the pre-replay authorization; a started replay or unconfirmed cancellation remains unknown. Without `prepare()`, approval loading and human interaction still share the native deadline, so delayed approval cannot finish that request.
448
+ After Pay, the processor's native deadline still covers fresh authorization binding, device replay and token handoff. A disconnected/backgrounded phone or slow transport can still miss it. A subsequent SCA challenge has its own lifetime after token handoff. If the merchant request aborts or its frame closes, the attachment blocks further requests and tries to retire the pre-replay authorization; a started replay or unconfirmed cancellation remains unknown. Without `prepare()`, approval loading and human interaction still share the native deadline, so delayed approval cannot finish that request.
425
449
 
426
450
  Lost authorization polling, local approval timeouts, or interrupted browser
427
451
  handoffs produce `outcome_unknown` and block automatic retry. The thrown
@@ -470,9 +494,7 @@ The browser fixtures never contact a payment service. The general suite uses
470
494
  allowlisted loopback proxy and a temporary self-signed TLS stub (requires the
471
495
  `openssl` CLI). All other proxy destinations are rejected. These suites use an
472
496
  in-process Agentcard API fixture and loopback merchant pages. The preparation
473
- fixture denies all external traffic, waits more than ten seconds before any
474
- card request, and then checks one fresh request with an unchanged ten-second
475
- fixture abort timer. It exercises SDK ordering, not the native Square SDK. It proves nested-frame pause/resume, agent control during approval,
497
+ fixture denies all external traffic and waits beyond each modeled request deadline before any card request: eleven seconds for Square, sixty-one seconds for Braintree, and six and a half seconds for Worldpay, Bambora and Mercado Pago. Each fixture then checks one fresh request with an unchanged abort timer. The five-second timer matches the inspected Worldpay and Bambora source; the Mercado Pago timer is a test boundary, not a measured native deadline. The fixture exercises SDK ordering with native-shaped request bodies and synthetic responses, not processor acceptance. It proves nested-frame pause/resume, agent control during approval,
476
498
  post-payment tasks in the same page, decline/expiry/cancel, unknown-outcome retry
477
499
  blocking, explicit unsupported endpoint behavior, and blocking an immediate real-browser
478
500
  Stripe token-to-intent fetch chain, including unrelated first intents, changed
@@ -0,0 +1,11 @@
1
+ /** An attachment failure has no payment or browser credentials in its message. */
2
+ export declare class CheckoutAttachmentError extends Error {
3
+ readonly reason: 'timeout' | 'closed' | 'unavailable';
4
+ readonly code = "checkout_attachment_failed";
5
+ constructor(reason: 'timeout' | 'closed' | 'unavailable');
6
+ }
7
+ /** Preserve a known setup cause without exposing raw transport errors. */
8
+ export declare function attachmentFailure(error: unknown): CheckoutAttachmentError;
9
+ export declare function attachmentDeadline(value: number | undefined): number;
10
+ /** Stop waiting, then reject every late continuation before its next command. */
11
+ export declare function withinAttachmentDeadline(arm: (assertActive: () => void) => Promise<void>, timeoutMs: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,50 @@
1
+ /** An attachment failure has no payment or browser credentials in its message. */
2
+ export class CheckoutAttachmentError extends Error {
3
+ reason;
4
+ code = 'checkout_attachment_failed';
5
+ constructor(reason) {
6
+ super('Could not attach checkout interception. Close this checkout page and retry in a fresh browser context.');
7
+ this.reason = reason;
8
+ this.name = 'CheckoutAttachmentError';
9
+ }
10
+ }
11
+ /** Preserve a known setup cause without exposing raw transport errors. */
12
+ export function attachmentFailure(error) {
13
+ return error instanceof CheckoutAttachmentError ? error : new CheckoutAttachmentError('unavailable');
14
+ }
15
+ export function attachmentDeadline(value) {
16
+ if (value === undefined)
17
+ return 30_000;
18
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 300_000) {
19
+ throw new Error('attachmentTimeoutMs must be an integer between 1 and 300000.');
20
+ }
21
+ return value;
22
+ }
23
+ /** Stop waiting, then reject every late continuation before its next command. */
24
+ export async function withinAttachmentDeadline(arm, timeoutMs, signal) {
25
+ let stopped;
26
+ let timer;
27
+ let onAbort = () => { };
28
+ const assertActive = () => { if (stopped)
29
+ throw stopped; };
30
+ const deadline = new Promise((_resolve, reject) => {
31
+ const stop = (failure) => { stopped ??= failure; reject(stopped); };
32
+ onAbort = () => stop(attachmentFailure(signal.reason));
33
+ signal.addEventListener('abort', onAbort, { once: true });
34
+ if (signal.aborted)
35
+ onAbort();
36
+ else
37
+ timer = setTimeout(() => stop(new CheckoutAttachmentError('timeout')), timeoutMs);
38
+ });
39
+ try {
40
+ await Promise.race([deadline, Promise.resolve().then(() => { assertActive(); return arm(assertActive); })]);
41
+ }
42
+ catch (error) {
43
+ stopped ??= attachmentFailure(error);
44
+ throw stopped;
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ signal.removeEventListener('abort', onAbort);
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ /** Classify native Braintree GraphQL requests without changing their bytes. */
2
+ export type BraintreeEnvironment = 'production' | 'sandbox';
3
+ export type BraintreeRequestKind = 'configuration' | 'tokenization' | 'invalid';
4
+ export declare function braintreeEnvironment(url: string): BraintreeEnvironment | undefined;
5
+ /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
6
+ export declare function readTokenizationJson(body: string | null): Record<string, unknown> | null;
7
+ /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
8
+ export declare function classifyBraintreeRequest(url: string, method: string, body: string | null): BraintreeRequestKind | undefined;
9
+ /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
10
+ export declare function isPreparedBraintreeRequest(body: string | null): boolean;
@@ -0,0 +1,302 @@
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
+ }
package/dist/cdp.d.ts CHANGED
@@ -87,6 +87,8 @@ export interface AttachOptions extends LifecycleOptions {
87
87
  currency?: string;
88
88
  cardId?: string;
89
89
  timeoutMs?: number;
90
+ /** Browser interception setup deadline, separate from approval. Defaults to 30000 ms; maximum 300000 ms. */
91
+ attachmentTimeoutMs?: number;
90
92
  /** Explicit payment endpoints to block if the registry cannot handle their method/format. Unlisted traffic is untouched. */
91
93
  paymentEndpoints?: readonly PaymentEndpointGuard[];
92
94
  onApprovalUrl?: (url: string) => void;