@agent-cards/checkout 0.7.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 +8 -1
- package/README.md +9 -0
- 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 +4 -2
- package/dist/cdp.js +36 -4
- package/dist/client.d.ts +13 -4
- package/dist/client.js +80 -8
- package/dist/index.d.ts +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/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,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
4
11
|
|
|
5
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.
|
|
6
13
|
- Requires the matching API and Vault release.
|
package/README.md
CHANGED
|
@@ -517,3 +517,12 @@ coverage classifications, `recognized_traffic_share` is traffic-weighted, and
|
|
|
517
517
|
`purchase_success_rate` stays null without observed merchant outcomes. Do not
|
|
518
518
|
substitute the assessor for a browser/merchant validation run or use native
|
|
519
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
|
+
}
|
package/dist/braintree.js
CHANGED
|
@@ -1,302 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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';
|