@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/CHANGELOG.md +12 -0
- package/README.md +31 -24
- package/dist/braintree.d.ts +2 -10
- package/dist/braintree.generated.d.ts +10 -0
- package/dist/braintree.generated.js +302 -0
- package/dist/braintree.js +2 -302
- package/dist/builtin-registry.generated.d.ts +2 -0
- package/dist/builtin-registry.generated.js +1 -0
- package/dist/cdp.d.ts +23 -9
- package/dist/cdp.js +99 -6
- package/dist/client.d.ts +124 -27
- package/dist/client.js +211 -27
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +27 -1
- package/dist/owned-shop.generated.d.ts +24 -0
- package/dist/owned-shop.generated.js +108 -0
- package/dist/preparation.js +3 -2
- package/dist/registry.d.ts +2 -19
- package/dist/registry.js +3 -188
- package/dist/substitute.d.ts +5 -9
- package/dist/substitute.js +5 -69
- package/dist/substitutions.generated.d.ts +10 -0
- package/dist/substitutions.generated.js +66 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- Clear an earlier failure or authentication reason when the merchant confirms a completed order. The final checkout state retains the authorization, order and payment mode without carrying a stale error.
|
|
6
|
+
|
|
7
|
+
- Pass execution routing metadata through authorization and browser attachment with `executionMode`, `grantId` and `merchantOrigin`. The exported `ExecutionMetadata` type describes the result; the matching API and Vault determine whether the user approves the purchase or an existing grant applies. Autopilot execution remains limited to the configured controlled Stripe test flow, with production Autopilot disabled.
|
|
8
|
+
- Share generated processor metadata with the payment core while keeping the published SDK free of runtime package dependencies.
|
|
9
|
+
|
|
10
|
+
## 0.7.0
|
|
11
|
+
|
|
12
|
+
- A company can put rules on the Vault purchases it chooses (a merchant list, a currency, a spend cap, a time window) by attaching a named preset to a stored card. A purchase the rules refuse now surfaces as `PresetRefusedError`, a typed decline: at stage `create` no authorization exists and nobody was asked; at stage `pre_replay` the authorization is `declined` with the rule's reason. It carries `code`, `presetName`, `rule`, `preset`, `attachment` (the card with its last four digits) and the rule's own statement; when several presets refuse the same purchase, `refusals` names every one and the other fields are the first. The adapters quiet the page's retry as for any decline, and a create-time refusal is never treated as a permanent misconfiguration.
|
|
13
|
+
- Requires the matching API and Vault release.
|
|
14
|
+
|
|
3
15
|
## 0.6.0
|
|
4
16
|
|
|
5
17
|
- 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.
|
package/README.md
CHANGED
|
@@ -54,30 +54,28 @@ await attachToCdp(cdp, pageSessionId, {
|
|
|
54
54
|
vault,
|
|
55
55
|
user: 'usr_123', // whose card should pay
|
|
56
56
|
merchant: 'vanman.shop',
|
|
57
|
-
|
|
57
|
+
amount: 583, // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
|
|
58
58
|
currency: 'usd', // "$5.83" is derived for the approval screen
|
|
59
59
|
onApprovalUrl: (url) => sendToUser(url), // iMessage, SMS, push, your call
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`amount:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
read show one amount. Tokenization requests carry no amount, so there the
|
|
80
|
-
pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
|
|
63
|
+
`amount` is your hint: an integer in the currency's smallest unit (583 for
|
|
64
|
+
$5.83), or a decimal string in normal units ('5.83'), with `currency`. The
|
|
65
|
+
processor's own amount is the higher authority: Agentcard reads it from the
|
|
66
|
+
paused request where the processor puts it there, or from the Stripe intent
|
|
67
|
+
the request names, right before the cardholder's device replays, and the
|
|
68
|
+
company's caps are judged on it. A hint lets a bad purchase be refused the
|
|
69
|
+
moment it opens; a hint more than one smallest unit away from the processor's
|
|
70
|
+
amount is refused with nothing charged (`AmountMismatchError`), and after the
|
|
71
|
+
replay the charge is reconciled against the approval
|
|
72
|
+
(`ReplayResponse.amountVerified`, `chargedAmount`, and `chargedKind`:
|
|
73
|
+
`captured` for a succeeded intent's `amount_received`, `authorized` for a
|
|
74
|
+
manual-capture intent's `amount_capturable`, `none` when nothing is collected
|
|
75
|
+
yet; plus the `checkout_authorization.amount_mismatch` webhook to your server
|
|
76
|
+
when the charge disagreed). Every result carries `amountAuthority`:
|
|
77
|
+
`processor`, `agent`, `page`, or `none`. A display string is never sent;
|
|
78
|
+
Agentcard derives it.
|
|
81
79
|
|
|
82
80
|
Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
|
|
83
81
|
request for approval and resumes it only while the merchant request remains
|
|
@@ -129,7 +127,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
|
|
|
129
127
|
| Processor | Status |
|
|
130
128
|
|---|---|
|
|
131
129
|
| Shopify | supported, verified end to end |
|
|
132
|
-
| Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms
|
|
130
|
+
| Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as `amount` + `currency` must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
|
|
133
131
|
| 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
132
|
| Checkout.com | supported |
|
|
135
133
|
| 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 |
|
|
@@ -352,7 +350,7 @@ value continues to work. Choose `requireMerchantResult: true` for a pilot:
|
|
|
352
350
|
|
|
353
351
|
```ts
|
|
354
352
|
const checkout = await attachToPlaywright(page, {
|
|
355
|
-
vault, user, merchant,
|
|
353
|
+
vault, user, merchant, amount, currency,
|
|
356
354
|
requireMerchantResult: true,
|
|
357
355
|
onStateChange: state => recordState(state),
|
|
358
356
|
onUserAction: action => deliverPrivatelyToUser(action),
|
|
@@ -395,7 +393,7 @@ Unrecognized merchant-server endpoints remain outside this guard unless listed
|
|
|
395
393
|
in `paymentEndpoints`; this is not a guarantee against a merchant charging a
|
|
396
394
|
saved token on its own server.
|
|
397
395
|
A direct card-bearing PaymentIntent confirm remains supported with the backend's
|
|
398
|
-
existing amount verification when `
|
|
396
|
+
existing amount verification when `amount` and `currency` are supplied.
|
|
399
397
|
|
|
400
398
|
Hosted-form submissions also always stay blocked because their payment outcome
|
|
401
399
|
is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
|
|
@@ -417,7 +415,7 @@ Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before t
|
|
|
417
415
|
```ts
|
|
418
416
|
const checkout = await attachToPlaywright(page, {
|
|
419
417
|
vault, user: 'your-user-id', merchant: 'Example merchant',
|
|
420
|
-
|
|
418
|
+
amount: 100, currency: 'USD',
|
|
421
419
|
onApprovalUrl: deliverPrivatelyToCardholder,
|
|
422
420
|
});
|
|
423
421
|
const preparation = await checkout.prepare({
|
|
@@ -429,7 +427,7 @@ const preparation = await checkout.prepare({
|
|
|
429
427
|
await page.getByRole('button', { name: 'Pay', exact: true }).click();
|
|
430
428
|
```
|
|
431
429
|
|
|
432
|
-
`prepare()` is available on both Playwright and raw CDP controllers. It requires `
|
|
430
|
+
`prepare()` is available on both Playwright and raw CDP controllers. It requires `amount` 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's authority is `agent`; a card token does not enforce the merchant's eventual charge amount.
|
|
433
431
|
|
|
434
432
|
| Processor | `environment` | Fresh native request |
|
|
435
433
|
| --- | --- | --- |
|
|
@@ -519,3 +517,12 @@ coverage classifications, `recognized_traffic_share` is traffic-weighted, and
|
|
|
519
517
|
`purchase_success_rate` stays null without observed merchant outcomes. Do not
|
|
520
518
|
substitute the assessor for a browser/merchant validation run or use native
|
|
521
519
|
Kernel adapter coverage as evidence for this SDK's coverage.
|
|
520
|
+
### Autopilot execution metadata
|
|
521
|
+
|
|
522
|
+
When the cardholder has enabled an eligible spending rule in their vault, the same authorization can run through autopilot. The SDK keeps polling the existing authorization and returns optional `executionMode: 'autopilot' | 'user_approval'` and `grantId` metadata. Older API responses remain supported.
|
|
523
|
+
|
|
524
|
+
`authorize()` accepts optional `executionMode` and `grantId` routing hints, sent as `execution_mode` and `grant_id`. These never establish permission to spend; the protected payment service checks the cardholder's signed rule. Autopilot suppresses `onApprovalUrl` while it is executing. A definite fallback to user approval delivers the existing authorization's URL once. A lost outcome remains `PaymentOutcomeUnknownError`; it does not create or submit a second payment.
|
|
525
|
+
|
|
526
|
+
Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
|
|
527
|
+
|
|
528
|
+
The adapters also send the observed top-level HTTPS `merchantOrigin`, such as `https://shop.example`, without a path, query or trailing slash. Direct `authorize()` callers can supply that origin explicitly. It is a routing hint; the protected adapter independently verifies the processor account, payee and amount. If a browser cannot provide its top-level URL, an explicit `merchantOrigin` option can supply the hint; otherwise the regular approval path remains available.
|
package/dist/braintree.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export type BraintreeEnvironment
|
|
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;
|
|
1
|
+
export { braintreeEnvironment, classifyBraintreeRequest, isPreparedBraintreeRequest, readTokenizationJson } from './braintree.generated.js';
|
|
2
|
+
export type { BraintreeEnvironment, BraintreeRequestKind } from './braintree.generated.js';
|
|
@@ -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
|
+
}
|