@agent-cards/checkout 0.7.0 → 0.9.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 +15 -1
- package/PREFLIGHT.md +294 -0
- package/README.md +16 -3
- 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 +38 -6
- 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/playwright.d.ts +3 -0
- package/dist/playwright.js +3 -0
- package/dist/preflight-capabilities.generated.d.ts +1165 -0
- package/dist/preflight-capabilities.generated.js +1799 -0
- package/dist/preflight-catalog.json +3892 -0
- package/dist/preflight-playwright.d.ts +34 -0
- package/dist/preflight-playwright.js +262 -0
- package/dist/preflight-schemas.json +1063 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.generated.d.ts +1867 -0
- package/dist/preflight.generated.js +482 -0
- package/dist/preflight.js +2 -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/examples/preflight/classify-direct.mjs +21 -0
- package/examples/preflight/classify-kernel.mjs +30 -0
- package/examples/preflight/inspect-browser.mjs +44 -0
- package/examples/preflight/kernel-profile.empty.json +11 -0
- package/examples/preflight/mollie-hosted.observations.json +23 -0
- package/examples/preflight/mollie-hosted.result.json +103 -0
- package/examples/preflight/stripe-script.direct.result.json +92 -0
- package/examples/preflight/stripe-script.observations.json +16 -0
- package/examples/preflight/stripe-script.result.json +87 -0
- package/package.json +16 -7
|
@@ -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';
|
|
@@ -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,7 +71,9 @@ 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;
|