@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/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,20 +71,34 @@ export interface CdpLike {
|
|
|
71
71
|
send(method: string, params?: any, sessionId?: string): Promise<any>;
|
|
72
72
|
on(handler: (method: string, params: any, sessionId?: string) => void): void;
|
|
73
73
|
}
|
|
74
|
-
export interface AttachOptions extends LifecycleOptions {
|
|
74
|
+
export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
|
|
75
|
+
/** Fallback when the browser cannot expose its top-level origin. Never an amount/payee authority. */
|
|
76
|
+
merchantOrigin?: string;
|
|
75
77
|
vault: VaultClient;
|
|
76
78
|
user: string;
|
|
77
79
|
merchant: string;
|
|
78
|
-
/** Display string for the approval screen. Optional when amountCents + currency are given. */
|
|
79
|
-
amount?: string;
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* right before
|
|
81
|
+
* Your hint at the amount, an integer in the currency's smallest unit or a
|
|
82
|
+
* decimal string in normal units, with its ISO 4217 code. See
|
|
83
|
+
* AuthorizeInput.amount: the processor's own amount is the higher authority
|
|
84
|
+
* and is read right before the card is sent; a hint lets a bad purchase be
|
|
85
|
+
* refused the moment it opens, and one that disagrees with the processor is
|
|
86
|
+
* refused with nothing charged.
|
|
85
87
|
*/
|
|
86
|
-
|
|
88
|
+
amount?: number | string;
|
|
87
89
|
currency?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The total the checkout page shows, the lowest authority: used only when
|
|
92
|
+
* neither the processor's request nor your `amount` names one. Pass a reader
|
|
93
|
+
* that returns an integer in the currency's smallest unit with its code
|
|
94
|
+
* (`{ amount: 4210, currency: 'usd' }`), read off the page however your
|
|
95
|
+
* page spells it; the adapters call it when a card request pauses, give it
|
|
96
|
+
* one second, and send nothing when it yields nothing. Absent: no page total.
|
|
97
|
+
*/
|
|
98
|
+
pageAmount?: () => Promise<{
|
|
99
|
+
amount: number;
|
|
100
|
+
currency: string;
|
|
101
|
+
} | undefined>;
|
|
88
102
|
cardId?: string;
|
|
89
103
|
timeoutMs?: number;
|
|
90
104
|
/** Browser interception setup deadline, separate from approval. Defaults to 30000 ms; maximum 300000 ms. */
|
package/dist/cdp.js
CHANGED
|
@@ -274,6 +274,79 @@ export function withCorsHeaders(headers, cors) {
|
|
|
274
274
|
function headerEntries(headers) {
|
|
275
275
|
return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }));
|
|
276
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The origin of the top-level document the payment form is on, read when a
|
|
279
|
+
* card request pauses: the fact that names the merchant for the company's
|
|
280
|
+
* presets (the API's checkout_origin). An https origin, or http://localhost
|
|
281
|
+
* for local checkout; anything else, or a page that cannot be read, yields
|
|
282
|
+
* undefined and the authorization goes out without one (a merchant, category
|
|
283
|
+
* or place rule then refuses it as unknown, never the request itself).
|
|
284
|
+
*/
|
|
285
|
+
async function pageOriginOf(readDocumentUrl, signal) {
|
|
286
|
+
let timer;
|
|
287
|
+
let abort;
|
|
288
|
+
try {
|
|
289
|
+
const documentUrl = await Promise.race([
|
|
290
|
+
Promise.resolve().then(readDocumentUrl).catch(() => undefined),
|
|
291
|
+
new Promise((resolve) => {
|
|
292
|
+
timer = setTimeout(resolve, 1_000);
|
|
293
|
+
abort = () => resolve(undefined);
|
|
294
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
295
|
+
if (signal.aborted)
|
|
296
|
+
abort();
|
|
297
|
+
}),
|
|
298
|
+
]);
|
|
299
|
+
if (typeof documentUrl !== 'string')
|
|
300
|
+
return undefined;
|
|
301
|
+
const url = new URL(documentUrl);
|
|
302
|
+
if (url.username || url.password)
|
|
303
|
+
return undefined;
|
|
304
|
+
if (url.protocol === 'https:' || (url.protocol === 'http:' && url.hostname === 'localhost'))
|
|
305
|
+
return url.origin;
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
if (timer)
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
if (abort)
|
|
315
|
+
signal.removeEventListener('abort', abort);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The page total, the lowest amount authority, through the integrator's own
|
|
320
|
+
* reader (AttachOptions.pageAmount): an integer in the smallest unit with its
|
|
321
|
+
* currency, or nothing. A read is a courtesy, never a wait: it gets one
|
|
322
|
+
* second, and anything unreadable yields undefined so the authorization goes
|
|
323
|
+
* out without a page total.
|
|
324
|
+
*/
|
|
325
|
+
const PAGE_AMOUNT_READ_MS = 1_000;
|
|
326
|
+
async function pageAmountOf(opts) {
|
|
327
|
+
if (!opts.pageAmount)
|
|
328
|
+
return undefined;
|
|
329
|
+
let timer;
|
|
330
|
+
try {
|
|
331
|
+
const raw = await Promise.race([
|
|
332
|
+
Promise.resolve().then(() => opts.pageAmount()).catch(() => undefined),
|
|
333
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), PAGE_AMOUNT_READ_MS); }),
|
|
334
|
+
]);
|
|
335
|
+
if (!raw || typeof raw !== 'object')
|
|
336
|
+
return undefined;
|
|
337
|
+
const { amount, currency } = raw;
|
|
338
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || typeof currency !== 'string' || !/^[A-Za-z]{3}$/.test(currency))
|
|
339
|
+
return undefined;
|
|
340
|
+
return { amount: amount, currency: currency.toLowerCase() };
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
if (timer)
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
277
350
|
/**
|
|
278
351
|
* Last-resort patterns: the built-in recognizers' hosts, derived the same way
|
|
279
352
|
* as everything else. Used only when the vault hands back nothing at all (a
|
|
@@ -335,13 +408,14 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
335
408
|
let setupFailureReported = false;
|
|
336
409
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
337
410
|
let preparationFrameId;
|
|
338
|
-
const
|
|
411
|
+
const readDocumentUrl = async () => {
|
|
339
412
|
const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
|
|
340
413
|
if (typeof tree?.frameTree?.frame?.url !== 'string')
|
|
341
414
|
throw new Error('merchant_document_unavailable');
|
|
342
415
|
preparationFrameId = tree.frameTree.frame.id;
|
|
343
416
|
return tree.frameTree.frame.url;
|
|
344
|
-
}
|
|
417
|
+
};
|
|
418
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
|
|
345
419
|
const guards = paymentEndpointGuards(opts.paymentEndpoints);
|
|
346
420
|
const armed = new Set();
|
|
347
421
|
const arming = new Map();
|
|
@@ -533,14 +607,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
533
607
|
activeRequest = { attempt, networkId, sessionId, frameId };
|
|
534
608
|
if (preparation)
|
|
535
609
|
await preparationGate.assertDocument();
|
|
610
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
|
|
611
|
+
const pageAmount = await pageAmountOf(opts);
|
|
612
|
+
attempt.assertLive();
|
|
613
|
+
const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
|
|
614
|
+
? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
|
|
536
615
|
attempt.assertLive();
|
|
537
616
|
const replay = await opts.vault.authorize({
|
|
538
617
|
user: opts.user,
|
|
539
618
|
merchant: opts.merchant,
|
|
540
619
|
amount: opts.amount,
|
|
541
|
-
amountCents: opts.amountCents,
|
|
542
620
|
currency: opts.currency,
|
|
543
621
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
622
|
+
executionMode: opts.executionMode,
|
|
623
|
+
grantId: opts.grantId,
|
|
624
|
+
merchantOrigin,
|
|
625
|
+
pageOrigin,
|
|
626
|
+
pageAmount,
|
|
544
627
|
preparation,
|
|
545
628
|
timeoutMs: opts.timeoutMs,
|
|
546
629
|
signal: lifecycle.abort.signal,
|
|
@@ -675,11 +758,12 @@ export async function attachToPlaywright(page, opts) {
|
|
|
675
758
|
throw new Error('Service workers are active; use a checkout context created with serviceWorkers: "block".');
|
|
676
759
|
}
|
|
677
760
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
678
|
-
const
|
|
761
|
+
const readDocumentUrl = async () => {
|
|
679
762
|
if (page.isClosed?.() || typeof page.url !== 'function')
|
|
680
763
|
throw new Error('merchant_document_unavailable');
|
|
681
764
|
return page.url();
|
|
682
|
-
}
|
|
765
|
+
};
|
|
766
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
|
|
683
767
|
const guards = paymentEndpointGuards(opts.paymentEndpoints);
|
|
684
768
|
// Playwright's own routing, NOT a hand-rolled CDP session.
|
|
685
769
|
//
|
|
@@ -800,14 +884,23 @@ export async function attachToPlaywright(page, opts) {
|
|
|
800
884
|
activeRequest = { request, frames, attempt };
|
|
801
885
|
if (preparation)
|
|
802
886
|
await preparationGate.assertDocument();
|
|
887
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
|
|
888
|
+
const pageAmount = await pageAmountOf(opts);
|
|
889
|
+
assertRequestLive();
|
|
890
|
+
const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
|
|
891
|
+
? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
|
|
803
892
|
assertRequestLive();
|
|
804
893
|
const replay = await opts.vault.authorize({
|
|
805
894
|
user: opts.user,
|
|
806
895
|
merchant: opts.merchant,
|
|
807
896
|
amount: opts.amount,
|
|
808
|
-
amountCents: opts.amountCents,
|
|
809
897
|
currency: opts.currency,
|
|
810
898
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
899
|
+
executionMode: opts.executionMode,
|
|
900
|
+
grantId: opts.grantId,
|
|
901
|
+
merchantOrigin,
|
|
902
|
+
pageOrigin,
|
|
903
|
+
pageAmount,
|
|
811
904
|
preparation,
|
|
812
905
|
timeoutMs: opts.timeoutMs,
|
|
813
906
|
signal: lifecycle.abort.signal,
|