@agent-cards/checkout 0.18.0 → 0.19.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +91 -7
  4. package/dist/adyen-merchant-hosted.generated.d.ts +277 -0
  5. package/dist/adyen-merchant-hosted.generated.js +1902 -0
  6. package/dist/builtin-registry.generated.js +1 -1
  7. package/dist/cdp.d.ts +4 -1
  8. package/dist/cdp.js +416 -217
  9. package/dist/client.d.ts +236 -5
  10. package/dist/client.js +514 -11
  11. package/dist/cse-body.d.ts +25 -0
  12. package/dist/cse-body.js +41 -0
  13. package/dist/fiserv.d.ts +65 -0
  14. package/dist/fiserv.generated.d.ts +73 -0
  15. package/dist/fiserv.generated.js +830 -0
  16. package/dist/fiserv.js +104 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +5 -1
  19. package/dist/lifecycle.d.ts +15 -1
  20. package/dist/lifecycle.js +28 -3
  21. package/dist/merchant-handoff.d.ts +54 -0
  22. package/dist/merchant-handoff.js +100 -0
  23. package/dist/merchant-hosted.d.ts +140 -0
  24. package/dist/merchant-hosted.js +170 -0
  25. package/dist/merchant-total-watch.d.ts +115 -0
  26. package/dist/merchant-total-watch.js +268 -0
  27. package/dist/merchant-total.d.ts +257 -0
  28. package/dist/merchant-total.js +383 -0
  29. package/dist/pre-claim.d.ts +123 -0
  30. package/dist/pre-claim.js +386 -0
  31. package/dist/preflight-catalog.json +132 -0
  32. package/dist/preflight-schemas.json +14 -2
  33. package/dist/preflight.generated.js +15 -1
  34. package/dist/preparation.d.ts +7 -0
  35. package/dist/preparation.js +33 -6
  36. package/dist/prepared-processor.d.ts +36 -3
  37. package/dist/prepared-processor.js +53 -3
  38. package/dist/registry.d.ts +45 -0
  39. package/dist/registry.js +14 -0
  40. package/dist/stripe-checkout.generated.js +96 -7
  41. package/dist/substitutions.generated.d.ts +2 -1
  42. package/dist/substitutions.generated.js +758 -6
  43. package/examples/preflight/kernel-native/inventory.json +1 -1
  44. package/package.json +3 -3
@@ -0,0 +1,1902 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: 582753ad6e0ff77d4f5c10416b259928d1e00ea236a2f9d55af5865ef9c08243
4
+ // src/deep-freeze.js
5
+ function deepFreeze(value) {
6
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
7
+ for (const child of Object.values(value))
8
+ deepFreeze(child);
9
+ Object.freeze(value);
10
+ }
11
+ return value;
12
+ }
13
+ // src/json-path.js
14
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
15
+ function isPlainObject(v) {
16
+ return !!v && typeof v === "object" && !Array.isArray(v);
17
+ }
18
+ var MAX_PATH_STEPS = 16;
19
+ function isJsonPath(path) {
20
+ return Array.isArray(path) && path.length <= MAX_PATH_STEPS && path.every((step) => typeof step === "string" ? step.length > 0 && !FORBIDDEN_KEYS.has(step) : Number.isSafeInteger(step) && step >= 0);
21
+ }
22
+ function jsonAt(root, path) {
23
+ let node = root;
24
+ for (const step of path) {
25
+ if (typeof step === "string") {
26
+ if (!isPlainObject(node) || FORBIDDEN_KEYS.has(step) || !Object.prototype.hasOwnProperty.call(node, step))
27
+ return { found: false };
28
+ }
29
+ else if (!Array.isArray(node) || !Number.isSafeInteger(step) || step < 0 || step >= node.length) {
30
+ return { found: false };
31
+ }
32
+ node = node[step];
33
+ }
34
+ return { found: true, value: node };
35
+ }
36
+ function roundTrips(parsed, body) {
37
+ try {
38
+ return JSON.stringify(parsed) === body;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function pathsOverlap(a, b) {
45
+ const n = Math.min(a.length, b.length);
46
+ for (let i = 0; i < n; i += 1)
47
+ if (a[i] !== b[i])
48
+ return false;
49
+ return true;
50
+ }
51
+ function describePath(path) {
52
+ return path.length ? path.map((step) => typeof step === "number" ? `[${step}]` : `.${step}`).join("").replace(/^\./, "") : "(root)";
53
+ }
54
+ // src/url-template.js
55
+ var PLACEHOLDER_VALUE = /^[A-Za-z0-9._~@+-]{1,256}$/;
56
+ var PLACEHOLDER_NAME = /^[A-Za-z][A-Za-z0-9_]{0,31}$/;
57
+ var LITERAL_SEGMENT = /^[A-Za-z0-9._~@+-]{1,256}$/;
58
+ var QUERY_KEY = /^[A-Za-z0-9._~-]{1,64}$/;
59
+ var QUERY_LITERAL = /^[A-Za-z0-9._~,-]{0,256}$/;
60
+ var MAX_QUERIES = 8;
61
+ var MAX_PAIRS = 16;
62
+ var placeholderValue = (value) => PLACEHOLDER_VALUE.test(value) && value !== "." && !value.includes("..");
63
+ function pathPlaceholders(template, pathname) {
64
+ if (typeof template !== "string" || typeof pathname !== "string")
65
+ return null;
66
+ if (!template.startsWith("/") || !pathname.startsWith("/"))
67
+ return null;
68
+ const want = template.split("/");
69
+ const got = pathname.split("/");
70
+ if (want.length !== got.length)
71
+ return null;
72
+ const values = {};
73
+ for (const [i, segment] of want.entries()) {
74
+ if (segment.startsWith(":")) {
75
+ if (!placeholderValue(got[i]))
76
+ return null;
77
+ values[segment.slice(1)] = got[i];
78
+ }
79
+ else if (segment !== got[i]) {
80
+ return null;
81
+ }
82
+ }
83
+ return values;
84
+ }
85
+ function pathMatchesTemplate(template, pathname) {
86
+ return pathPlaceholders(template, pathname) !== null;
87
+ }
88
+ function queryPlaceholders(query, text) {
89
+ if (!query || typeof query !== "object" || Array.isArray(query))
90
+ return null;
91
+ const pairs = Object.entries(query);
92
+ if (pairs.length === 0)
93
+ return text === null ? {} : null;
94
+ if (typeof text !== "string")
95
+ return null;
96
+ const got = text.split("&");
97
+ if (got.length !== pairs.length)
98
+ return null;
99
+ const values = {};
100
+ for (const [i, [key, value]] of pairs.entries()) {
101
+ const mark = got[i].indexOf("=");
102
+ if (mark === -1 || got[i].slice(0, mark) !== key)
103
+ return null;
104
+ const written = got[i].slice(mark + 1);
105
+ if (typeof value !== "string")
106
+ return null;
107
+ if (value.startsWith(":")) {
108
+ if (!placeholderValue(written))
109
+ return null;
110
+ values[value.slice(1)] = written;
111
+ }
112
+ else if (written !== value) {
113
+ return null;
114
+ }
115
+ }
116
+ return values;
117
+ }
118
+ function strictUrl(rawUrl) {
119
+ if (typeof rawUrl !== "string" || rawUrl.includes("#"))
120
+ return null;
121
+ let url;
122
+ try {
123
+ url = new URL(rawUrl);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ if (url.href !== rawUrl || url.protocol !== "https:" || url.port || url.username || url.password)
129
+ return null;
130
+ return url;
131
+ }
132
+ function urlTemplateMatch(template, rawUrl) {
133
+ const url = strictUrl(rawUrl);
134
+ if (!url || !template || typeof template !== "object")
135
+ return null;
136
+ const fromPath = pathPlaceholders(template.path, url.pathname);
137
+ if (!fromPath)
138
+ return null;
139
+ const mark = rawUrl.indexOf("?");
140
+ const text = mark === -1 ? null : rawUrl.slice(mark + 1);
141
+ const queries = Array.isArray(template.queries) ? template.queries : [template.query];
142
+ for (const query of queries) {
143
+ const fromQuery = queryPlaceholders(query, text);
144
+ if (fromQuery)
145
+ return { origin: url.origin, placeholders: { ...fromPath, ...fromQuery } };
146
+ }
147
+ return null;
148
+ }
149
+ function templatePlaceholders(template) {
150
+ const names = [];
151
+ if (typeof template?.path === "string") {
152
+ for (const segment of template.path.split("/"))
153
+ if (segment.startsWith(":"))
154
+ names.push(segment.slice(1));
155
+ }
156
+ const queries = Array.isArray(template?.queries) ? template.queries : [template?.query];
157
+ for (const query of queries) {
158
+ if (query && typeof query === "object") {
159
+ for (const value of Object.values(query))
160
+ if (typeof value === "string" && value.startsWith(":"))
161
+ names.push(value.slice(1));
162
+ }
163
+ }
164
+ return names;
165
+ }
166
+ function pathTemplateRefusal(template) {
167
+ if (typeof template !== "string" || !template.startsWith("/") || template.length > 1024)
168
+ return "the path is not a template starting with /";
169
+ const names = [];
170
+ for (const segment of template.split("/").slice(1)) {
171
+ if (segment.startsWith(":")) {
172
+ if (!PLACEHOLDER_NAME.test(segment.slice(1)))
173
+ return `the path placeholder ${segment} is not a name`;
174
+ names.push(segment.slice(1));
175
+ }
176
+ else if (!LITERAL_SEGMENT.test(segment) || segment === "." || segment === "..") {
177
+ return `the path segment "${segment}" is not one literal segment`;
178
+ }
179
+ }
180
+ if (new Set(names).size !== names.length)
181
+ return "a path placeholder is named twice";
182
+ const literal = template.split("/").slice(1).map((segment) => segment.startsWith(":") ? "x" : segment).join("/");
183
+ return new URL(`https://template.invalid/${literal}`).pathname === `/${literal}` ? null : "the path is not written the way a URL writes it";
184
+ }
185
+ function queryTemplateRefusal(query) {
186
+ if (!query || typeof query !== "object" || Array.isArray(query))
187
+ return "the query is not an object of key=value pairs";
188
+ const pairs = Object.entries(query);
189
+ if (pairs.length > MAX_PAIRS)
190
+ return `the query has more than ${MAX_PAIRS} pairs`;
191
+ for (const [key, value] of pairs) {
192
+ if (!QUERY_KEY.test(key))
193
+ return `the query key "${key}" is not unreserved characters`;
194
+ if (typeof value !== "string")
195
+ return `the query value of ${key} is not text`;
196
+ if (value.startsWith(":") ? !PLACEHOLDER_NAME.test(value.slice(1)) : !QUERY_LITERAL.test(value)) {
197
+ return `the query value of ${key} is neither a literal nor a :name placeholder`;
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+ function urlTemplateRefusal(template) {
203
+ if (!template || typeof template !== "object")
204
+ return "the template is not an object";
205
+ const pathRefusal2 = pathTemplateRefusal(template.path);
206
+ if (pathRefusal2)
207
+ return pathRefusal2;
208
+ const hasQuery = Object.prototype.hasOwnProperty.call(template, "query");
209
+ const hasQueries = Object.prototype.hasOwnProperty.call(template, "queries");
210
+ if (hasQuery === hasQueries)
211
+ return "the template names exactly one of query and queries";
212
+ const queries = hasQueries ? template.queries : [template.query];
213
+ if (!Array.isArray(queries) || queries.length === 0 || queries.length > MAX_QUERIES)
214
+ return `queries is not a list of 1 to ${MAX_QUERIES}`;
215
+ const pathNames = templatePlaceholders({ path: template.path, query: {} });
216
+ for (const query of queries) {
217
+ const refusal = queryTemplateRefusal(query);
218
+ if (refusal)
219
+ return refusal;
220
+ const names = [...pathNames, ...templatePlaceholders({ query })];
221
+ if (new Set(names).size !== names.length)
222
+ return "a placeholder is named twice in one request shape";
223
+ }
224
+ const texts = queries.map((query) => JSON.stringify(Object.entries(query)));
225
+ return new Set(texts).size === texts.length ? null : "a query is listed twice";
226
+ }
227
+ // src/merchant-amount.js
228
+ var MERCHANT_AMOUNT_REFUSE_KINDS = /* @__PURE__ */ deepFreeze(["present", "absent", "true", "unset", "false", "nonzero", "nonEmpty", "empty"]);
229
+ var MERCHANT_EXCHANGES_MAX = 16;
230
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
231
+ var FORMATS = ["decimal2", "decimal", "number"];
232
+ var SOURCE_MEMBERS = [
233
+ "id",
234
+ "origin",
235
+ "format",
236
+ "responses",
237
+ "total",
238
+ "unit",
239
+ "currency",
240
+ "currencyPath",
241
+ "bind",
242
+ "match",
243
+ "requires",
244
+ "refuse",
245
+ "voidedBy",
246
+ "maxAgeMs",
247
+ "afterPayment"
248
+ ];
249
+ var SOURCE_REQUIRED = ["id", "origin", "format", "responses", "total", "unit", "currency", "bind", "maxAgeMs"];
250
+ var AFTER_MEMBERS = ["origin", "format", "responses", "total", "unit", "currency", "currencyPath", "bind", "requires", "refuse"];
251
+ var AFTER_REQUIRED = ["origin", "format", "responses", "total", "unit", "currency", "bind"];
252
+ var MAX = { responses: 16, bind: 6, match: 4, requires: 8, refuse: 12, voidedBy: 16, request: 4 };
253
+ var MAX_AGE_MS = { min: 1e3, max: 36e5 };
254
+ var INPUT_ID = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
255
+ var SCRIPT_VAR = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
256
+ var PLACEHOLDER_NAME2 = /^[A-Za-z][A-Za-z0-9_]{0,31}$/;
257
+ var ZERO_DECIMAL = /* @__PURE__ */ new Set(["BIF", "CLP", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF"]);
258
+ var THREE_DECIMAL = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
259
+ var minorDigits = (currency) => ZERO_DECIMAL.has(currency) ? 0 : THREE_DECIMAL.has(currency) ? 3 : 2;
260
+ var primitive = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
261
+ var has = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
262
+ function membersRefusal(entry, allowed, required, label) {
263
+ if (!isPlainObject(entry))
264
+ return `${label} is not an object`;
265
+ const keys = Object.keys(entry);
266
+ const extra = keys.find((key) => !allowed.includes(key));
267
+ if (extra)
268
+ return `${label} carries ${extra}, which is not a member`;
269
+ const missing = required.find((key) => !keys.includes(key));
270
+ return missing ? `${label} names no ${missing}` : null;
271
+ }
272
+ function listRefusal(list, max, label, each, { optional = true, min = 0 } = {}) {
273
+ if (list === void 0 && optional)
274
+ return null;
275
+ if (!Array.isArray(list) || list.length < min || list.length > max)
276
+ return `${label} is not a list of ${min} to ${max}`;
277
+ for (const [i, entry] of list.entries()) {
278
+ const refusal = each(entry, `${label} ${i}`);
279
+ if (refusal)
280
+ return refusal;
281
+ }
282
+ return null;
283
+ }
284
+ var pathRefusal = (path, label) => isJsonPath(path) && path.length > 0 ? null : `${label} is not a JSON path of keys and indices`;
285
+ function equalsRuleRefusal(rule, label, { root }) {
286
+ const byPath = root && isPlainObject(rule) && has(rule, "equalsPath");
287
+ if (byPath && has(rule, "equals"))
288
+ return `${label} names both equals and equalsPath`;
289
+ const members = root ? ["path", byPath ? "equalsPath" : "equals", "root"] : ["path", "equals"];
290
+ return membersRefusal(rule, members, ["path", byPath ? "equalsPath" : "equals"], label) ?? pathRefusal(rule.path, `${label}'s path`) ?? (byPath ? pathRefusal(rule.equalsPath, `${label}'s equalsPath`) : primitive(rule.equals) ? null : `${label} compares with something other than text, a number or a boolean`) ?? (rule.root === void 0 || rule.root === true ? null : `${label}'s root is not true`);
291
+ }
292
+ function refuseRuleRefusal(rule, label) {
293
+ return membersRefusal(rule, ["path", "when", "root"], ["path", "when"], label) ?? pathRefusal(rule.path, `${label}'s path`) ?? (MERCHANT_AMOUNT_REFUSE_KINDS.includes(rule.when) ? null : `${label} fires on ${String(rule.when)}, which is not a kind`) ?? (rule.root === void 0 || rule.root === true ? null : `${label}'s root is not true`);
294
+ }
295
+ function shapeRefusal(entry, label, { format, response }) {
296
+ const allowed = response ? ["method", "path", "query", "queries", "status", "request", "at"] : ["method", "path", "query", "queries"];
297
+ const members = membersRefusal(entry, allowed, ["method", "path"], label);
298
+ if (members)
299
+ return members;
300
+ if (!METHODS.includes(entry.method))
301
+ return `${label}'s method is not one of ${METHODS.join(", ")}`;
302
+ const template = urlTemplateRefusal(entry);
303
+ if (template)
304
+ return `${label}: ${template}`;
305
+ if (entry.status !== void 0 && !["2xx", "4xx"].includes(entry.status))
306
+ return `${label}'s status is not 2xx or 4xx`;
307
+ if ((entry.request !== void 0 || entry.at !== void 0) && format !== "json")
308
+ return `${label} reads JSON from an HTML source`;
309
+ if (entry.at !== void 0 && !isJsonPath(entry.at))
310
+ return `${label}'s at is not a JSON path`;
311
+ return listRefusal(entry.request, MAX.request, `${label}'s request rule`, (rule, ruleLabel) => equalsRuleRefusal(rule, ruleLabel, { root: false }), { min: 1 });
312
+ }
313
+ function totalRefusal(total, format, unit, currency) {
314
+ if (format === "json") {
315
+ const members = membersRefusal(total, ["path", "format"], ["path", "format"], "the total");
316
+ if (members)
317
+ return members;
318
+ const path = pathRefusal(total.path, "the total's path");
319
+ if (path)
320
+ return path;
321
+ }
322
+ else {
323
+ const members = membersRefusal(total, ["input", "scriptVar", "format"], ["input", "format"], "the total");
324
+ if (members)
325
+ return members;
326
+ if (!INPUT_ID.test(total.input ?? ""))
327
+ return "the total's input is not an element id";
328
+ if (total.scriptVar !== void 0 && !SCRIPT_VAR.test(total.scriptVar))
329
+ return "the total's scriptVar is not a script name";
330
+ if (total.format !== "decimal")
331
+ return "an HTML total is decimal text";
332
+ }
333
+ if (!FORMATS.includes(total.format))
334
+ return `the total's format is not one of ${FORMATS.join(", ")}`;
335
+ if (!["major", "minor"].includes(unit))
336
+ return "the unit is not major or minor";
337
+ if (total.format !== "number" && unit !== "major")
338
+ return "a decimal total is in major units";
339
+ if (total.format === "decimal2" && minorDigits(currency) !== 2)
340
+ return `a decimal2 total needs a currency with two decimals, and ${currency} has ${minorDigits(currency)}`;
341
+ return null;
342
+ }
343
+ function bindRefusal(rule, label, { format, shapes, profile }) {
344
+ if (!isPlainObject(rule))
345
+ return `${label} is not an object`;
346
+ const keys = Object.keys(rule).sort().join(",");
347
+ const cardPath = (path) => pathRefusal(path, `${label}'s card path`) ?? (Array.isArray(profile.holder) && pathsOverlap(path, profile.holder) ? `${label} reads the card object` : null);
348
+ switch (keys) {
349
+ case "card,source":
350
+ if (format !== "json")
351
+ return `${label} reads JSON from an HTML source`;
352
+ return pathRefusal(rule.source, `${label}'s source path`) ?? cardPath(rule.card);
353
+ case "card,request":
354
+ if (format !== "json")
355
+ return `${label} reads JSON from an HTML source`;
356
+ return pathRefusal(rule.request, `${label}'s request path`) ?? cardPath(rule.card);
357
+ case "card,placeholder":
358
+ case "cardPlaceholder,placeholder": {
359
+ if (!PLACEHOLDER_NAME2.test(rule.placeholder ?? "") || !shapes.some((shape) => templatePlaceholders(shape).includes(rule.placeholder))) {
360
+ return `${label} names a placeholder no request shape has`;
361
+ }
362
+ if (keys === "card,placeholder")
363
+ return cardPath(rule.card);
364
+ return templatePlaceholders({ path: profile.path, query: profile.query }).includes(rule.cardPlaceholder) ? null : `${label} names a placeholder the card URL does not have`;
365
+ }
366
+ case "samePage":
367
+ return rule.samePage === true ? null : `${label}'s samePage is not true`;
368
+ default:
369
+ return `${label} is not one of the bind shapes`;
370
+ }
371
+ }
372
+ var bindApplies = (rule, shape) => !has(rule, "placeholder") || templatePlaceholders(shape).includes(rule.placeholder);
373
+ function matchRefusal(rule, label, profile) {
374
+ return membersRefusal(rule, ["source", "card", "whenCard", "sourceDefault"], ["source", "card"], label) ?? pathRefusal(rule.source, `${label}'s source path`) ?? pathRefusal(rule.card, `${label}'s card path`) ?? (Array.isArray(profile.holder) && pathsOverlap(rule.card, profile.holder) ? `${label} reads the card object` : null) ?? (rule.whenCard === void 0 ? null : pathRefusal(rule.whenCard, `${label}'s whenCard path`)) ?? (rule.sourceDefault === void 0 || primitive(rule.sourceDefault) ? null : `${label}'s sourceDefault is not text, a number or a boolean`);
375
+ }
376
+ function stageRefusal(stage, profile, { after }) {
377
+ const label = after ? "afterPayment" : "the amount source";
378
+ const members = membersRefusal(stage, after ? AFTER_MEMBERS : SOURCE_MEMBERS, after ? AFTER_REQUIRED : SOURCE_REQUIRED, label);
379
+ if (members)
380
+ return members;
381
+ if (!after && !/^[a-z][a-z0-9_]{2,63}$/.test(stage.id))
382
+ return "the amount source's id is not a lower-case name";
383
+ if (!["checkout", "endpoint"].includes(stage.origin))
384
+ return `${label}'s origin is not checkout or endpoint`;
385
+ const format = stage.format;
386
+ if (!["json", "html"].includes(format))
387
+ return `${label}'s format is not json or html`;
388
+ if (!/^[A-Z]{3}$/.test(stage.currency ?? ""))
389
+ return `${label}'s currency is not an ISO code`;
390
+ const shapes = stage.responses;
391
+ const listed = listRefusal(shapes, MAX.responses, `${label}'s response`, (entry, entryLabel) => shapeRefusal(entry, entryLabel, { format, response: true }), { optional: false, min: 1 }) ?? totalRefusal(stage.total, format, stage.unit, stage.currency);
392
+ if (listed)
393
+ return listed;
394
+ if (stage.currencyPath !== void 0 && (format !== "json" || pathRefusal(stage.currencyPath, "currencyPath")))
395
+ return `${label}'s currencyPath is not a JSON path`;
396
+ const bound = listRefusal(stage.bind, MAX.bind, `${label}'s bind rule`, (rule, ruleLabel) => bindRefusal(rule, ruleLabel, { format, shapes, profile }), { optional: false, min: 1 });
397
+ if (bound)
398
+ return bound;
399
+ const unbound = shapes.findIndex((shape) => !stage.bind.some((rule) => bindApplies(rule, shape)));
400
+ if (unbound !== -1)
401
+ return `${label}'s response ${unbound} is tied to the card request by no bind rule`;
402
+ if (format !== "json" && ["match", "requires", "refuse"].some((key) => stage[key] !== void 0))
403
+ return `${label} reads JSON rules from an HTML source`;
404
+ return listRefusal(stage.requires, MAX.requires, `${label}'s requires rule`, (rule, ruleLabel) => equalsRuleRefusal(rule, ruleLabel, { root: true })) ?? listRefusal(stage.refuse, MAX.refuse, `${label}'s refuse rule`, refuseRuleRefusal) ?? (after ? null : listRefusal(stage.match, MAX.match, "the amount source's match rule", (rule, ruleLabel) => matchRefusal(rule, ruleLabel, profile))) ?? (after ? null : listRefusal(stage.voidedBy, MAX.voidedBy, "the amount source's voidedBy shape", (entry, entryLabel) => shapeRefusal(entry, entryLabel, { format, response: false }))) ?? (after || Number.isSafeInteger(stage.maxAgeMs) && stage.maxAgeMs >= MAX_AGE_MS.min && stage.maxAgeMs <= MAX_AGE_MS.max ? null : `maxAgeMs is not a whole number of milliseconds from ${MAX_AGE_MS.min} to ${MAX_AGE_MS.max}`);
405
+ }
406
+ function merchantAmountSourceRefusal(profile) {
407
+ if (!isPlainObject(profile))
408
+ return "the profile is not an object";
409
+ const source = profile.amountSource;
410
+ if (source === void 0)
411
+ return null;
412
+ const refusal = stageRefusal(source, profile, { after: false });
413
+ if (refusal)
414
+ return refusal;
415
+ return source.afterPayment === void 0 ? null : stageRefusal(source.afterPayment, profile, { after: true });
416
+ }
417
+ var fail = (code, reason) => ({ ok: false, code, reason });
418
+ function minorFromDecimalText(text, digits, places) {
419
+ const m = /^(\d{1,13})(?:\.(\d+))?$/.exec(text);
420
+ if (!m)
421
+ return null;
422
+ const fraction = m[2] ?? "";
423
+ if (fraction.length > places || /[^0]/.test(fraction.slice(digits)))
424
+ return null;
425
+ const minor = Number(m[1]) * 10 ** digits + Number((fraction + "0".repeat(digits)).slice(0, digits) || "0");
426
+ return Number.isSafeInteger(minor) ? minor : null;
427
+ }
428
+ function totalMinor(value, format, unit, currency) {
429
+ const digits = minorDigits(currency);
430
+ if (format === "decimal2")
431
+ return typeof value === "string" && digits === 2 && /^\d{1,13}\.\d{2}$/.test(value) ? minorFromDecimalText(value, 2, 2) : null;
432
+ if (format === "decimal")
433
+ return typeof value === "string" ? minorFromDecimalText(value, digits, 4) : null;
434
+ if (format !== "number" || typeof value !== "number" || !Number.isFinite(value) || value < 0)
435
+ return null;
436
+ if (unit === "minor")
437
+ return Number.isSafeInteger(value) ? value : null;
438
+ const text = String(value);
439
+ return /e/i.test(text) ? null : minorFromDecimalText(text, digits, 17);
440
+ }
441
+ var INPUT_TAG = /<input\b([^<>]*)>/gi;
442
+ var ATTRIBUTE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
443
+ var withoutComments = (html) => html.replace(/<!--[\s\S]*?-->/g, "");
444
+ function inputValues(html, id) {
445
+ const values = [];
446
+ for (const tag of withoutComments(html).matchAll(INPUT_TAG)) {
447
+ const attributes = /* @__PURE__ */ new Map();
448
+ for (const attribute of tag[1].matchAll(ATTRIBUTE)) {
449
+ const name = attribute[1].toLowerCase();
450
+ if (!attributes.has(name))
451
+ attributes.set(name, attribute[2] ?? attribute[3] ?? attribute[4] ?? "");
452
+ }
453
+ if (attributes.get("id") === id)
454
+ values.push(attributes.has("value") ? attributes.get("value") : null);
455
+ }
456
+ return values;
457
+ }
458
+ function scriptVarValues(html, name) {
459
+ const pattern = new RegExp(`(?:^|[^A-Za-z0-9_$.])var\\s+${name.replace(/\$/g, "\\$")}\\s*=\\s*([^;<\\n]*);`, "g");
460
+ return [...withoutComments(html).matchAll(pattern)].map((m) => m[1].trim());
461
+ }
462
+ function htmlTotal(html, total) {
463
+ if (typeof html !== "string")
464
+ return { found: false };
465
+ const inputs = inputValues(html, total.input);
466
+ if (inputs.length > 1)
467
+ return { ambiguous: true };
468
+ if (inputs.length === 1)
469
+ return { found: true, value: inputs[0] };
470
+ const vars = total.scriptVar ? scriptVarValues(html, total.scriptVar) : [];
471
+ if (vars.length > 1)
472
+ return { ambiguous: true };
473
+ return vars.length === 1 ? { found: true, value: vars[0] } : { found: false };
474
+ }
475
+ var zeroish = (value) => value === 0 || typeof value === "string" && /^0+(?:\.0+)?$/.test(value);
476
+ var emptyish = (value) => value === null || value === false || value === "" || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
477
+ function fires(when, at) {
478
+ const value = at.found ? at.value : void 0;
479
+ switch (when) {
480
+ case "present":
481
+ return at.found;
482
+ case "absent":
483
+ return !at.found;
484
+ case "true":
485
+ return at.found && value !== false && value !== null;
486
+ case "unset":
487
+ return !at.found || value === false || value === null;
488
+ case "false":
489
+ return at.found && value === false;
490
+ case "nonzero":
491
+ return at.found && value !== null && value !== false && !zeroish(value);
492
+ case "nonEmpty":
493
+ return at.found && !emptyish(value);
494
+ case "empty":
495
+ return !at.found || emptyish(value);
496
+ default:
497
+ return true;
498
+ }
499
+ }
500
+ function readerFor(exchange, shape) {
501
+ const at = shape.at ?? [];
502
+ const parse = (text) => {
503
+ try {
504
+ return typeof text === "string" ? JSON.parse(text) : void 0;
505
+ }
506
+ catch {
507
+ return void 0;
508
+ }
509
+ };
510
+ let body;
511
+ let request;
512
+ let bodyRead = false;
513
+ let requestRead = false;
514
+ const parsedBody = () => {
515
+ if (!bodyRead) {
516
+ bodyRead = true;
517
+ body = parse(exchange.body);
518
+ }
519
+ return body;
520
+ };
521
+ const parsedRequest = () => {
522
+ if (!requestRead) {
523
+ requestRead = true;
524
+ request = parse(exchange.requestBody);
525
+ }
526
+ return request;
527
+ };
528
+ return {
529
+ parsedBody,
530
+ read: (path, root) => parsedBody() === void 0 ? { found: false } : jsonAt(parsedBody(), root ? path : [...at, ...path]),
531
+ readRequest: (path) => parsedRequest() === void 0 ? { found: false } : jsonAt(parsedRequest(), path)
532
+ };
533
+ }
534
+ function shapeFor(shapes, exchange, origin, { response }) {
535
+ if (typeof exchange.method !== "string" || typeof exchange.url !== "string")
536
+ return null;
537
+ for (const shape of shapes) {
538
+ if (exchange.method.toUpperCase() !== shape.method)
539
+ continue;
540
+ const match = urlTemplateMatch(shape, exchange.url);
541
+ if (!match || origin !== null && match.origin !== origin)
542
+ continue;
543
+ if (response) {
544
+ const status = shape.status ?? "2xx";
545
+ if (!(status === "2xx" ? exchange.status >= 200 && exchange.status < 300 : exchange.status >= 400 && exchange.status < 500))
546
+ continue;
547
+ const reader = readerFor(exchange, shape);
548
+ if (shape.request && !shape.request.every((rule) => {
549
+ const at = reader.readRequest(rule.path);
550
+ return at.found && at.value === rule.equals;
551
+ }))
552
+ continue;
553
+ return { shape, placeholders: match.placeholders, reader };
554
+ }
555
+ return { shape, placeholders: match.placeholders };
556
+ }
557
+ return null;
558
+ }
559
+ function isOrigin(value) {
560
+ if (typeof value !== "string")
561
+ return false;
562
+ let url;
563
+ try {
564
+ url = new URL(`${value}/`);
565
+ }
566
+ catch {
567
+ return false;
568
+ }
569
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && url.origin === value;
570
+ }
571
+ var bindKey = (value) => typeof value === "string" && value.length > 0 || typeof value === "number" && Number.isFinite(value) ? value : void 0;
572
+ function cardContext(profile, card) {
573
+ const merchant = profile.merchant;
574
+ if (!isPlainObject(card))
575
+ return fail("invalid", `the paused ${merchant} card request is missing`);
576
+ const { url, method, body, pausedAt, checkoutOrigin, page, declaredEndpoint } = card;
577
+ const methodOk = typeof method === "string" && method.toUpperCase() === profile.method;
578
+ let match;
579
+ let pairs;
580
+ if (declaredEndpoint !== void 0) {
581
+ const declared = methodOk && typeof declaredEndpoint === "string" && url === declaredEndpoint ? strictUrl(url)?.origin ?? null : null;
582
+ if (!declared || checkoutOrigin !== void 0 && !isOrigin(checkoutOrigin)) {
583
+ return fail("invalid", `the paused request is not the declared endpoint shaped like ${merchant}'s card request, from its declared checkout page`);
584
+ }
585
+ const shaped = urlTemplateMatch({ path: profile.path, query: profile.query }, url);
586
+ match = { origin: declared, placeholders: shaped ? shaped.placeholders : {} };
587
+ pairs = [{ endpoint: declared, checkout: checkoutOrigin }];
588
+ }
589
+ else {
590
+ match = methodOk ? urlTemplateMatch({ path: profile.path, query: profile.query }, url) : null;
591
+ pairs = match ? profile.origins.filter((pair) => pair.endpoint === match.origin && (checkoutOrigin === void 0 || pair.checkout === checkoutOrigin)) : [];
592
+ }
593
+ if (pairs.length === 0)
594
+ return fail("invalid", `the paused request is not ${merchant}'s card request from its checkout page`);
595
+ let parsed;
596
+ try {
597
+ parsed = JSON.parse(body);
598
+ }
599
+ catch {
600
+ parsed = void 0;
601
+ }
602
+ if (!isPlainObject(parsed))
603
+ return fail("invalid", `the paused ${merchant} card request is not a JSON object`);
604
+ if (typeof pausedAt !== "number" || !Number.isFinite(pausedAt) || pausedAt < 0)
605
+ return fail("invalid", "the time the card request paused is missing");
606
+ if (page !== void 0 && (typeof page !== "string" || page.length === 0 || page.length > 256))
607
+ return fail("invalid", "the card request's page is not an id");
608
+ return {
609
+ ok: true,
610
+ merchant,
611
+ origin: match.origin,
612
+ placeholders: match.placeholders,
613
+ body: parsed,
614
+ pausedAt,
615
+ page,
616
+ checkoutOrigins: [...new Set(pairs.map((pair) => pair.checkout).filter((origin) => origin !== void 0))]
617
+ };
618
+ }
619
+ var stageOrigin = (stage, ctx) => stage.origin === "endpoint" ? ctx.origin : ctx.checkoutOrigins.length === 1 ? ctx.checkoutOrigins[0] : null;
620
+ function cardBindValue(rule, ctx) {
621
+ if (has(rule, "samePage"))
622
+ return ctx.page;
623
+ if (has(rule, "cardPlaceholder"))
624
+ return ctx.placeholders[rule.cardPlaceholder];
625
+ const at = jsonAt(ctx.body, rule.card);
626
+ return at.found ? bindKey(at.value) : void 0;
627
+ }
628
+ function responseBindValue(rule, exchange, candidate) {
629
+ if (has(rule, "samePage"))
630
+ return typeof exchange.page === "string" && exchange.page ? exchange.page : void 0;
631
+ if (has(rule, "placeholder"))
632
+ return candidate.placeholders[rule.placeholder];
633
+ const at = has(rule, "request") ? candidate.reader.readRequest(rule.request) : candidate.reader.read(rule.source);
634
+ return at.found ? bindKey(at.value) : void 0;
635
+ }
636
+ var describeBind = (rule) => has(rule, "samePage") ? "page id" : has(rule, "cardPlaceholder") ? `:${rule.cardPlaceholder} in its URL` : describePath(rule.card);
637
+ function tie(stage, exchange, candidate, ctx) {
638
+ let state = "bound";
639
+ for (const rule of stage.bind) {
640
+ if (!bindApplies(rule, candidate.shape))
641
+ continue;
642
+ const got = responseBindValue(rule, exchange, candidate);
643
+ if (got === void 0)
644
+ state = "unknown";
645
+ else if (got !== cardBindValue(rule, ctx))
646
+ return "other";
647
+ }
648
+ return state;
649
+ }
650
+ function commonInput(profile, stage, input) {
651
+ const refusal = merchantAmountSourceRefusal(profile);
652
+ if (refusal)
653
+ return fail("unreviewed", `${profile.merchant}'s amount source is not a reviewed declaration: ${refusal}`);
654
+ const ctx = cardContext(profile, input?.card);
655
+ if (!ctx.ok)
656
+ return ctx;
657
+ const responses = input?.responses;
658
+ const valid = Array.isArray(responses) && responses.length <= MERCHANT_EXCHANGES_MAX && responses.every((r) => isPlainObject(r) && typeof r.method === "string" && typeof r.url === "string" && Number.isSafeInteger(r.status) && r.status >= 100 && r.status <= 599 && typeof r.completedAt === "number" && Number.isFinite(r.completedAt) && r.completedAt >= 0 && (r.sentAt === void 0 || typeof r.sentAt === "number" && Number.isFinite(r.sentAt) && r.sentAt >= 0) && (r.page === void 0 || typeof r.page === "string") && (r.requestBody === void 0 || typeof r.requestBody === "string") && typeof r.body === "string");
659
+ if (!valid)
660
+ return fail("invalid", `the ${profile.merchant} responses are not a list of at most ${MERCHANT_EXCHANGES_MAX} the rules can read`);
661
+ const origin = stageOrigin(stage, ctx);
662
+ if (!origin)
663
+ return fail("invalid", `the checkout page is not named, so ${profile.merchant}'s responses have no one origin`);
664
+ const unbound = stage.bind.find((rule) => cardBindValue(rule, ctx) === void 0);
665
+ if (unbound)
666
+ return fail("unbound", `the paused card request carries no ${describeBind(unbound)} to tie ${profile.merchant}'s total to`);
667
+ return { ok: true, ctx, responses, origin };
668
+ }
669
+ function readResponse(stage, candidate, ctx, { after }) {
670
+ const merchant = ctx.merchant;
671
+ const { reader } = candidate;
672
+ if (stage.format === "json" && reader.parsedBody() === void 0)
673
+ return fail("unreadable", `${merchant}'s response is not JSON`);
674
+ for (const rule of stage.requires ?? []) {
675
+ const at = reader.read(rule.path, rule.root);
676
+ if (has(rule, "equalsPath")) {
677
+ const other = reader.read(rule.equalsPath, rule.root);
678
+ if (!at.found || !other.found || !primitive(at.value) || at.value !== other.value) {
679
+ return fail("refused", `${merchant}'s response does not say ${describePath(rule.path)} equals ${describePath(rule.equalsPath)}`);
680
+ }
681
+ continue;
682
+ }
683
+ if (!at.found || at.value !== rule.equals)
684
+ return fail("refused", `${merchant}'s response does not say ${describePath(rule.path)} is ${JSON.stringify(rule.equals)}`);
685
+ }
686
+ for (const rule of stage.refuse ?? []) {
687
+ if (fires(rule.when, reader.read(rule.path, rule.root)))
688
+ return fail("refused", `${merchant}'s response refuses this payment: ${describePath(rule.path)} is ${rule.when}`);
689
+ }
690
+ const total = stage.format === "json" ? reader.read(stage.total.path) : htmlTotal(candidate.exchange.body, stage.total);
691
+ if (total.ambiguous)
692
+ return fail("unreadable", `${merchant}'s page names its total more than once`);
693
+ const minor = total.found ? totalMinor(total.value, stage.total.format, stage.unit, stage.currency) : null;
694
+ if (minor === null)
695
+ return fail("unreadable", `${merchant}'s total is not a ${stage.total.format} amount in ${stage.unit} units of ${stage.currency}`);
696
+ if (!after && minor <= 0)
697
+ return fail("refused", `${merchant}'s total is not above zero, so there is nothing for the card to pay`);
698
+ let currency = stage.currency;
699
+ if (stage.currencyPath) {
700
+ const named = reader.read(stage.currencyPath);
701
+ if (!named.found || typeof named.value !== "string" || !/^[A-Za-z]{3}$/.test(named.value))
702
+ return fail("unreadable", `${merchant}'s response names no currency`);
703
+ if (!after && named.value !== stage.currency)
704
+ return fail("mismatch", `${merchant}'s total is in ${named.value}, not the ${stage.currency} Agentcard reviewed`);
705
+ currency = named.value.toUpperCase();
706
+ }
707
+ return { ok: true, amountCents: minor, currency };
708
+ }
709
+ function adyenAmount(value) {
710
+ if (!isPlainObject(value))
711
+ return null;
712
+ const minor = typeof value.value === "number" ? Number.isSafeInteger(value.value) && value.value >= 0 ? value.value : null : typeof value.value === "string" && /^\d{1,15}$/.test(value.value) ? Number(value.value) : null;
713
+ const currency = typeof value.currency === "string" && /^[A-Za-z]{3}$/.test(value.currency) ? value.currency.toUpperCase() : null;
714
+ return minor === null || !currency ? null : { minor, currency };
715
+ }
716
+ function readMerchantAmount(profile, input) {
717
+ const stage = isPlainObject(profile) ? profile.amountSource : void 0;
718
+ if (!isPlainObject(stage))
719
+ return fail("no_source", `${profile?.merchant ?? "This merchant"}'s profile names no source for its own total`);
720
+ const common = commonInput(profile, stage, input);
721
+ if (!common.ok)
722
+ return common;
723
+ const { ctx, responses, origin } = common;
724
+ const merchant = profile.merchant;
725
+ const seen = [];
726
+ let inFlight = false;
727
+ for (const exchange of responses) {
728
+ const match = shapeFor(stage.responses, exchange, origin, { response: true });
729
+ if (!match)
730
+ continue;
731
+ const candidate = { exchange, ...match };
732
+ if (exchange.completedAt > ctx.pausedAt) {
733
+ if (exchange.sentAt !== void 0 && exchange.sentAt <= ctx.pausedAt && tie(stage, exchange, candidate, ctx) !== "other")
734
+ inFlight = true;
735
+ continue;
736
+ }
737
+ seen.push({ ...candidate, tie: tie(stage, exchange, candidate, ctx) });
738
+ }
739
+ const bound = seen.filter((candidate) => candidate.tie === "bound");
740
+ if (bound.length === 0)
741
+ return fail("no_source", `no ${merchant} response with the total of this order was seen before the card request`);
742
+ const latest = Math.max(...bound.map((candidate) => candidate.exchange.completedAt));
743
+ const tied = bound.filter((candidate) => candidate.exchange.completedAt === latest);
744
+ const chosen = tied[tied.length - 1];
745
+ if (seen.some((candidate) => candidate.tie === "unknown" && candidate.exchange.completedAt > latest)) {
746
+ return fail("stale", `a newer ${merchant} response could not be tied to this order, so its latest total is not known`);
747
+ }
748
+ if (inFlight)
749
+ return fail("stale", `a ${merchant} request that left before the card request answered only after it, so the total the card pays is not known`);
750
+ const ageMs = ctx.pausedAt - latest;
751
+ if (ageMs > stage.maxAgeMs) {
752
+ return fail("stale", `${merchant}'s latest total for this order was read ${Math.round(ageMs / 1e3)} s before the card request, past the ${stage.maxAgeMs / 1e3} s its profile allows`);
753
+ }
754
+ const voided = (stage.voidedBy ?? []).length > 0 && responses.some((exchange) => {
755
+ const at = exchange.sentAt ?? exchange.completedAt;
756
+ return at > latest && at <= ctx.pausedAt && shapeFor(stage.voidedBy, exchange, origin, { response: false }) !== null;
757
+ });
758
+ if (voided)
759
+ return fail("stale", `${merchant}'s order changed after its latest total was read`);
760
+ const reading = readResponse(stage, chosen, ctx, { after: false });
761
+ if (!reading.ok)
762
+ return reading;
763
+ for (const other of tied.slice(0, -1)) {
764
+ const again = readResponse(stage, other, ctx, { after: false });
765
+ if (!again.ok || again.amountCents !== reading.amountCents || again.currency !== reading.currency) {
766
+ return fail("stale", `two ${merchant} responses for this order at the same instant disagree`);
767
+ }
768
+ }
769
+ for (const rule of stage.match ?? []) {
770
+ if (rule.whenCard && !jsonAt(ctx.body, rule.whenCard).found)
771
+ continue;
772
+ const card = jsonAt(ctx.body, rule.card);
773
+ const source = chosen.reader.read(rule.source);
774
+ const want = source.found ? source.value : rule.sourceDefault;
775
+ if (!card.found || want === void 0 || card.value !== want) {
776
+ return fail("stale", `the card request was built from another ${merchant} response than the latest one (${describePath(rule.card)})`);
777
+ }
778
+ }
779
+ let bodyAmount = "none";
780
+ if (Array.isArray(profile.amount)) {
781
+ const at = jsonAt(ctx.body, profile.amount);
782
+ bodyAmount = at.found ? "equal" : "absent";
783
+ if (at.found) {
784
+ const named = adyenAmount(at.value);
785
+ if (!named)
786
+ return fail("unreadable", `the amount in ${merchant}'s card request is not the {value, currency} Agentcard reviewed`);
787
+ if (named.currency !== reading.currency || named.minor !== reading.amountCents) {
788
+ return fail("mismatch", `${merchant}'s card request asks for ${named.minor} ${named.currency}, but its own total is ${reading.amountCents} ${reading.currency}`);
789
+ }
790
+ }
791
+ }
792
+ return {
793
+ ok: true,
794
+ amount: { amountCents: reading.amountCents, currency: reading.currency.toLowerCase() },
795
+ source: { kind: "response", id: stage.id, method: chosen.shape.method, path: chosen.shape.path, completedAt: latest, ageMs, bodyAmount }
796
+ };
797
+ }
798
+ function readMerchantCharged(profile, input) {
799
+ const stage = isPlainObject(profile) && isPlainObject(profile.amountSource) ? profile.amountSource.afterPayment : void 0;
800
+ if (!isPlainObject(stage))
801
+ return fail("no_reader", `${profile?.merchant ?? "This merchant"}'s profile reads no total after payment`);
802
+ const common = commonInput(profile, stage, input);
803
+ if (!common.ok)
804
+ return common;
805
+ const { ctx, responses, origin } = common;
806
+ const readings = [];
807
+ for (const exchange of responses) {
808
+ if (exchange.completedAt < ctx.pausedAt)
809
+ continue;
810
+ const match = shapeFor(stage.responses, exchange, origin, { response: true });
811
+ if (!match)
812
+ continue;
813
+ const candidate = { exchange, ...match };
814
+ if (tie(stage, exchange, candidate, ctx) !== "bound")
815
+ continue;
816
+ const reading = readResponse(stage, candidate, ctx, { after: true });
817
+ if ("amountCents" in reading)
818
+ readings.push({ amountCents: reading.amountCents, currency: reading.currency, shape: match.shape, completedAt: exchange.completedAt });
819
+ }
820
+ if (readings.length === 0)
821
+ return fail("unread", `no ${profile.merchant} response after the card request says what it charged`);
822
+ const other = readings.filter((reading) => reading.currency !== stage.currency);
823
+ const worst = (other.length > 0 ? other : readings).reduce((top, reading) => reading.amountCents > top.amountCents ? reading : top);
824
+ return {
825
+ ok: true,
826
+ charged: { amountCents: worst.amountCents, currency: worst.currency.toLowerCase() },
827
+ source: { kind: "response", id: `${profile.amountSource.id}.after_payment`, method: worst.shape.method, path: worst.shape.path, completedAt: worst.completedAt },
828
+ readings: readings.length
829
+ };
830
+ }
831
+ function newestMerchantExchanges(exchanges) {
832
+ if (!Array.isArray(exchanges))
833
+ return [];
834
+ return exchanges.map((exchange, index) => ({ exchange, index })).filter(({ exchange }) => isPlainObject(exchange) && typeof exchange.completedAt === "number" && Number.isFinite(exchange.completedAt)).sort((a, b) => a.exchange.completedAt - b.exchange.completedAt || a.index - b.index).slice(-MERCHANT_EXCHANGES_MAX).map(({ exchange }) => exchange);
835
+ }
836
+ function standIn(value) {
837
+ if (Array.isArray(value))
838
+ return value.map(() => true);
839
+ if (isPlainObject(value))
840
+ return Object.fromEntries(Object.keys(value).filter((key) => !FORBIDDEN_KEYS.has(key)).map((key) => [key, true]));
841
+ return typeof value === "string" ? value === "" ? "" : "*" : value;
842
+ }
843
+ function assign(node, step, value) {
844
+ if (typeof step === "number") {
845
+ while (node.length < step)
846
+ node.push(null);
847
+ node[step] = value;
848
+ }
849
+ else {
850
+ Object.defineProperty(node, step, { value, enumerable: true, writable: true, configurable: true });
851
+ }
852
+ }
853
+ function setAt(root, path, value) {
854
+ if (path.length === 0)
855
+ return value;
856
+ const container = (typeof path[0] === "number" ? Array.isArray(root) : isPlainObject(root)) ? root : typeof path[0] === "number" ? [] : {};
857
+ let node = container;
858
+ for (const [i, step] of path.entries()) {
859
+ if (i === path.length - 1) {
860
+ assign(node, step, value);
861
+ break;
862
+ }
863
+ const wantArray = typeof path[i + 1] === "number";
864
+ let child = typeof step === "number" ? node[step] : has(node, step) ? node[step] : void 0;
865
+ if (wantArray ? !Array.isArray(child) : !isPlainObject(child)) {
866
+ child = wantArray ? [] : {};
867
+ assign(node, step, child);
868
+ }
869
+ node = child;
870
+ }
871
+ return container;
872
+ }
873
+ function projectJson(text, reads) {
874
+ let parsed;
875
+ try {
876
+ parsed = JSON.parse(text);
877
+ }
878
+ catch {
879
+ return "";
880
+ }
881
+ let out = {};
882
+ const ordered = [...reads].sort((a, b) => a.path.length - b.path.length || (a.keep === b.keep ? 0 : a.keep === "shape" ? -1 : 1));
883
+ for (const read of ordered) {
884
+ const at = jsonAt(parsed, read.path);
885
+ if (!at.found)
886
+ continue;
887
+ const primitiveValue = at.value === null || typeof at.value !== "object";
888
+ out = setAt(out, read.path, read.keep === "value" && primitiveValue ? at.value : standIn(at.value));
889
+ }
890
+ return JSON.stringify(out);
891
+ }
892
+ function projectHtml(html, total) {
893
+ if (typeof html !== "string")
894
+ return "";
895
+ const inputs = inputValues(html, total.input);
896
+ const safe = (value, pattern) => pattern.test(value) ? value : "!";
897
+ if (inputs.length) {
898
+ return inputs.map((value) => value === null ? `<input id="${total.input}">` : `<input id="${total.input}" value="${safe(value, /^[^"<>]*$/)}">`).join("");
899
+ }
900
+ return (total.scriptVar ? scriptVarValues(html, total.scriptVar) : []).map((value) => `<script>var ${total.scriptVar} = ${safe(value, /^[^;<\n]*$/)};</script>`).join("");
901
+ }
902
+ function projectMerchantResponse(profile, stageName, exchange) {
903
+ const source = isPlainObject(profile) ? profile.amountSource : void 0;
904
+ const stage = stageName === "source" ? source : stageName === "charged" && isPlainObject(source) ? source.afterPayment : void 0;
905
+ if (!isPlainObject(stage) || !isPlainObject(exchange) || merchantAmountSourceRefusal(profile) !== null)
906
+ return null;
907
+ const base = {
908
+ method: exchange.method,
909
+ url: exchange.url,
910
+ status: exchange.status,
911
+ completedAt: exchange.completedAt,
912
+ ...exchange.sentAt !== void 0 ? { sentAt: exchange.sentAt } : {},
913
+ ...exchange.page !== void 0 ? { page: exchange.page } : {}
914
+ };
915
+ const match = shapeFor(stage.responses, exchange, null, { response: true });
916
+ if (!match) {
917
+ const voids = stageName === "source" && Array.isArray(stage.voidedBy) && shapeFor(stage.voidedBy, exchange, null, { response: false });
918
+ return voids ? { ...base, body: "" } : null;
919
+ }
920
+ const at = match.shape.at ?? [];
921
+ const from = (path, root) => root ? path : [...at, ...path];
922
+ const value = (path, root) => ({ path: from(path, root), keep: "value" });
923
+ const reads = stage.format === "json" ? [
924
+ value(stage.total.path),
925
+ ...stage.currencyPath ? [value(stage.currencyPath)] : [],
926
+ ...stage.bind.filter((rule) => has(rule, "source")).map((rule) => value(rule.source)),
927
+ ...(stage.match ?? []).map((rule) => value(rule.source)),
928
+ ...(stage.requires ?? []).flatMap((rule) => [value(rule.path, rule.root), ...has(rule, "equalsPath") ? [value(rule.equalsPath, rule.root)] : []]),
929
+ ...(stage.refuse ?? []).map((rule) => ({ path: from(rule.path, rule.root), keep: rule.when === "nonzero" ? "value" : "shape" }))
930
+ ] : [];
931
+ const requestReads = [...(match.shape.request ?? []).map((rule) => rule.path), ...stage.bind.filter((rule) => has(rule, "request")).map((rule) => rule.request)];
932
+ return {
933
+ ...base,
934
+ ...requestReads.length ? { requestBody: projectJson(exchange.requestBody, requestReads.map((path) => ({ path, keep: "value" }))) } : {},
935
+ body: stage.format === "json" ? projectJson(exchange.body, reads) : projectHtml(exchange.body, stage.total)
936
+ };
937
+ }
938
+ function stageNamed(profile, stageName) {
939
+ const source = isPlainObject(profile) ? profile.amountSource : void 0;
940
+ if (stageName === "source")
941
+ return isPlainObject(source) ? source : void 0;
942
+ return stageName === "charged" && isPlainObject(source) && isPlainObject(source.afterPayment) ? source.afterPayment : void 0;
943
+ }
944
+ function requestShapeFor(shapes, exchange, origin) {
945
+ const match = shapeFor(shapes, exchange, origin, { response: false });
946
+ if (!match || !match.shape.request)
947
+ return match;
948
+ const reader = readerFor(exchange, match.shape);
949
+ return match.shape.request.every((rule) => {
950
+ const at = reader.readRequest(rule.path);
951
+ return at.found && at.value === rule.equals;
952
+ }) ? match : null;
953
+ }
954
+ function stageRequest(stage, stageName, exchange, origin) {
955
+ if (requestShapeFor(stage.responses, exchange, origin))
956
+ return true;
957
+ return stageName === "source" && Array.isArray(stage.voidedBy) && shapeFor(stage.voidedBy, exchange, origin, { response: false }) !== null;
958
+ }
959
+ function stageOriginsBefore(profile, stage, declaredEndpoint) {
960
+ if (declaredEndpoint !== void 0) {
961
+ if (stage.origin !== "endpoint")
962
+ return null;
963
+ const origin = typeof declaredEndpoint === "string" ? strictUrl(declaredEndpoint)?.origin : void 0;
964
+ return typeof origin === "string" ? [origin] : [];
965
+ }
966
+ const pairs = Array.isArray(profile.origins) ? profile.origins : [];
967
+ return [...new Set(pairs.map((pair) => isPlainObject(pair) ? stage.origin === "endpoint" ? pair.endpoint : pair.checkout : void 0).filter((origin) => typeof origin === "string"))];
968
+ }
969
+ function merchantAmountRequestStages(profile, request, scope) {
970
+ if (!isPlainObject(request) || merchantAmountSourceRefusal(profile) !== null)
971
+ return [];
972
+ const declaredEndpoint = isPlainObject(scope) ? scope.declaredEndpoint : void 0;
973
+ return ["source", "charged"].filter((name) => {
974
+ const stage = stageNamed(profile, name);
975
+ if (stage === void 0)
976
+ return false;
977
+ const origins = stageOriginsBefore(profile, stage, declaredEndpoint);
978
+ return origins === null ? stageRequest(stage, name, request, null) : origins.some((origin) => stageRequest(stage, name, request, origin));
979
+ });
980
+ }
981
+ function stageContext(profile, stageName, card) {
982
+ const stage = stageNamed(profile, stageName);
983
+ if (!stage)
984
+ return fail(stageName === "charged" ? "no_reader" : "no_source", `${profile?.merchant ?? "This merchant"}'s profile names no ${stageName === "charged" ? "total after payment" : "source for its own total"}`);
985
+ const refusal = merchantAmountSourceRefusal(profile);
986
+ if (refusal)
987
+ return fail("unreviewed", `${profile.merchant}'s amount source is not a reviewed declaration: ${refusal}`);
988
+ const ctx = cardContext(profile, card);
989
+ if (!ctx.ok)
990
+ return ctx;
991
+ const origin = stageOrigin(stage, ctx);
992
+ if (!origin)
993
+ return fail("invalid", `the checkout page is not named, so ${profile.merchant}'s responses have no one origin`);
994
+ return { ok: true, stage, origin };
995
+ }
996
+ function merchantExchangePending(profile, stageName, card, request) {
997
+ const context = stageContext(profile, stageName, card);
998
+ return context.ok && isPlainObject(request) && stageRequest(context.stage, stageName, request, context.origin);
999
+ }
1000
+ function selectMerchantExchanges(profile, stageName, input) {
1001
+ const context = stageContext(profile, stageName, input?.card);
1002
+ if (!context.ok)
1003
+ return context;
1004
+ const list = Array.isArray(input?.responses) ? input.responses : [];
1005
+ const kept = list.filter((exchange) => isPlainObject(exchange) && typeof exchange.completedAt === "number" && stageRequest(context.stage, stageName, exchange, context.origin)).map((exchange) => projectMerchantResponse(profile, stageName, exchange)).filter((exchange) => exchange !== null);
1006
+ return { ok: true, responses: newestMerchantExchanges(kept) };
1007
+ }
1008
+ // src/merchant-profiles.js
1009
+ var MERCHANT_HOSTED_PROFILES = /* @__PURE__ */ deepFreeze([
1010
+ {
1011
+ // Read Sep 23, 2026. assets.dunelm.com checkout-page bundle: submitPayment
1012
+ // sends {operationName: 'AdyenPayment', variables: {input: {paymentMethod,
1013
+ // browserInfo, idempotencyKey, amount, order, provisionalOrderId,
1014
+ // shopperInteraction, storePaymentMethod}}, query} through Apollo to the
1015
+ // page config's was.url, https://was.dunelm.com/graphql. The same config
1016
+ // names the client key and environment 'live' (adyen-web 5.31.3). Every other
1017
+ // GraphQL operation shares this endpoint, gift card redemption included, with
1018
+ // its own encrypted fields, so only operationName AdyenPayment is this
1019
+ // profile's card request. Public key read from checkoutshopper-live.adyen.com
1020
+ // on 2026-09-25 (UTC).
1021
+ id: "dunelm_was_graphql",
1022
+ merchant: "Dunelm",
1023
+ status: "observe",
1024
+ origins: [{ checkout: "https://www.dunelm.com", endpoint: "https://was.dunelm.com" }],
1025
+ method: "POST",
1026
+ path: "/graphql",
1027
+ query: {},
1028
+ requires: [{ path: ["operationName"], equals: "AdyenPayment" }],
1029
+ holder: ["variables", "input", "paymentMethod"],
1030
+ fields: {
1031
+ encryptedCardNumber: "encryptedCardNumber",
1032
+ encryptedExpiryMonth: "encryptedExpiryMonth",
1033
+ encryptedExpiryYear: "encryptedExpiryYear",
1034
+ encryptedSecurityCode: "encryptedSecurityCode"
1035
+ },
1036
+ // adyen-web stamps brand from the digits typed into its Card component, the
1037
+ // agent's dummy; Adyen refuses a brand that disagrees with the decrypted card
1038
+ // (422/109) and reads the brand off the card when none is sent.
1039
+ remove: ["brand"],
1040
+ refuse: [
1041
+ { path: ["variables", "input", "storePaymentMethod"], when: "true" },
1042
+ { path: ["variables", "input", "paymentMethod", "storedPaymentMethodId"], when: "present" },
1043
+ { path: ["variables", "input", "paymentMethod", "recurringDetailReference"], when: "present" }
1044
+ ],
1045
+ amount: ["variables", "input", "amount"],
1046
+ // Read Sep 25, 2026 (research amount_source/dunelm). The body names an amount only
1047
+ // when the gift_card flag is on and AdyenOrder answered, and then the card's share,
1048
+ // so the total comes from CheckoutView, the provisional order the page shows and
1049
+ // pays: a GraphQL query on the card request's own URL, told apart by operationName.
1050
+ // prices.total is a GraphQL Float in pounds; the response names no currency and
1051
+ // Dunelm sells only in GBP. After payment the card request's own answer carries
1052
+ // transaction.amount (pounds, unit inferred until a HAR).
1053
+ amountSource: {
1054
+ id: "dunelm_checkout_view",
1055
+ origin: "endpoint",
1056
+ format: "json",
1057
+ responses: [{
1058
+ method: "POST",
1059
+ path: "/graphql",
1060
+ query: {},
1061
+ request: [{ path: ["operationName"], equals: "CheckoutView" }],
1062
+ at: ["data", "viewer", "checkoutProvisionalOrder"]
1063
+ }],
1064
+ total: { path: ["prices", "total"], format: "number" },
1065
+ unit: "major",
1066
+ currency: "GBP",
1067
+ bind: [{ source: ["provisionalOrderId"], card: ["variables", "input", "provisionalOrderId"] }],
1068
+ // false means nothing is left for the card (a gift card paid it all).
1069
+ requires: [{ path: ["flags", "paymentRequired"], equals: true }],
1070
+ // A GraphQL error result is not a total, even beside partial data.
1071
+ refuse: [{ path: ["errors"], when: "present", root: true }],
1072
+ maxAgeMs: 9e5,
1073
+ afterPayment: {
1074
+ origin: "endpoint",
1075
+ format: "json",
1076
+ responses: [{
1077
+ method: "POST",
1078
+ path: "/graphql",
1079
+ query: {},
1080
+ request: [{ path: ["operationName"], equals: "AdyenPayment" }],
1081
+ at: ["data", "adyenPayment"]
1082
+ }],
1083
+ total: { path: ["transaction", "amount"], format: "number" },
1084
+ unit: "major",
1085
+ currency: "GBP",
1086
+ bind: [{ request: ["variables", "input", "provisionalOrderId"], card: ["variables", "input", "provisionalOrderId"] }]
1087
+ }
1088
+ },
1089
+ adyen: {
1090
+ environment: "live",
1091
+ clientKey: "live_3APNTGTFX5FENHWGWUJEKGYSKY7YKURV",
1092
+ publicKeySha256: "0fc63087b90dc0958fc2c024aafca1cd8b6feb34b6f53bb19b1b5649f3e6c316"
1093
+ }
1094
+ },
1095
+ {
1096
+ // Read Sep 23, 2026. www.cinemark.com/scripts/app/adyenIntegration.js posts
1097
+ // state.data plus root amount, store and additionalData, through
1098
+ // JSON.stringify, to /api/adyen/initiatePayment (stored cards go to its
1099
+ // sibling initiateStoredPayment), and maps the environment 'live' to
1100
+ // 'live-us'. A card-free observe run on the guest ticket checkout read the
1101
+ // client key from the three securedFields iframes on
1102
+ // checkoutshopper-live-us.adyen.com (adyen-web 6.x). The request body itself
1103
+ // was not captured: the guest email gate stopped the run before Pay. Public
1104
+ // key read from checkoutshopper-live-us.adyen.com on 2026-09-25 (UTC).
1105
+ id: "cinemark_initiate",
1106
+ merchant: "Cinemark",
1107
+ status: "observe",
1108
+ origins: [{ checkout: "https://www.cinemark.com", endpoint: "https://www.cinemark.com" }],
1109
+ method: "POST",
1110
+ path: "/api/adyen/initiatePayment",
1111
+ query: {},
1112
+ requires: [],
1113
+ holder: ["paymentMethod"],
1114
+ fields: {
1115
+ encryptedCardNumber: "encryptedCardNumber",
1116
+ encryptedExpiryMonth: "encryptedExpiryMonth",
1117
+ encryptedExpiryYear: "encryptedExpiryYear",
1118
+ encryptedSecurityCode: "encryptedSecurityCode"
1119
+ },
1120
+ // adyen-web 6.x stamps brand after its bin lookup answers, so a body carries
1121
+ // the dummy's brand on some Pay presses and not others: removed either way.
1122
+ remove: ["brand"],
1123
+ refuse: [
1124
+ { path: ["storePaymentMethod"], when: "true" },
1125
+ { path: ["paymentMethod", "storedPaymentMethodId"], when: "present" },
1126
+ { path: ["paymentMethod", "recurringDetailReference"], when: "present" },
1127
+ // adyenIntegration.js adds additionalData.existingPaymentId only when its
1128
+ // caller passes one to submitCheckout; the guest flow passes none. What it
1129
+ // points at (a stored card, or an earlier payment this one continues) is
1130
+ // unread, so a body that carries one is refused until a card-free capture
1131
+ // shows it.
1132
+ { path: ["additionalData", "existingPaymentId"], when: "present" },
1133
+ // Present only when Cinemark gift cards pay part of the order: the card then pays
1134
+ // only its share, which is not the order's total.
1135
+ { path: ["additionalData", "giftCardAmount"], when: "present" }
1136
+ ],
1137
+ amount: ["amount"],
1138
+ // Read Sep 25, 2026 (research amount_source/cinemark). The body's amount is computed
1139
+ // by the page's script in the agent's browser (total, plus any tip, minus gift
1140
+ // cards); the server renders the total into the checkout HTML, as the hidden input
1141
+ // CartSummary_TotalWithServiceFee (dollars), or the inline var cartTotal when that
1142
+ // input is not there. The page reloads /Checkout after every cart change. The cart
1143
+ // is the session's, so the response is tied to the card request by its page. A
1144
+ // tip makes the body differ from the total and is refused, as the rule for a body
1145
+ // amount beside a source says. After payment, initiatePayment's answer names the
1146
+ // amount (minor units) when Cinemark relays Adyen's response: a HAR confirms it.
1147
+ amountSource: {
1148
+ id: "cinemark_checkout_page_total",
1149
+ origin: "endpoint",
1150
+ format: "html",
1151
+ responses: [
1152
+ { method: "GET", path: "/checkout", query: {} },
1153
+ { method: "GET", path: "/Checkout", query: {} }
1154
+ ],
1155
+ total: { input: "CartSummary_TotalWithServiceFee", scriptVar: "cartTotal", format: "decimal" },
1156
+ unit: "major",
1157
+ currency: "USD",
1158
+ bind: [{ samePage: true }],
1159
+ maxAgeMs: 9e5,
1160
+ afterPayment: {
1161
+ origin: "endpoint",
1162
+ format: "json",
1163
+ responses: [
1164
+ { method: "POST", path: "/api/adyen/initiatePayment", query: {} },
1165
+ { method: "POST", path: "/api/adyen/submitAdditionalDetails", query: {} }
1166
+ ],
1167
+ total: { path: ["amount", "value"], format: "number" },
1168
+ unit: "minor",
1169
+ currency: "USD",
1170
+ currencyPath: ["amount", "currency"],
1171
+ bind: [{ samePage: true }]
1172
+ }
1173
+ },
1174
+ adyen: {
1175
+ environment: "live-us",
1176
+ clientKey: "live_AFKCBP443ZGQNIY4OCYW3YCKDY6VIKNN",
1177
+ publicKeySha256: "9dd5ef549c7454d60ca970ff76c8f4cac36d79f2961e59eca9116791e86cbcc5"
1178
+ }
1179
+ },
1180
+ {
1181
+ // Read Sep 23, 2026. payments.dickssportinggoods.com/library/dppjs/index-v2.js
1182
+ // (adyen-web 6.39.0): Dd() builds tenders.encryptedCardTenders[0] from the Card
1183
+ // component's four JWEs as cardNumber, expirationMonth, expirationYear and cvv,
1184
+ // beside cardType (adyen-web's first detected brand), saveForLater (the save-card
1185
+ // box) and vinBalue (the six digits onBinValue reports). The page posts it with
1186
+ // transactionReference, customer.device and any giftCardTenders to
1187
+ // <payments host>/prod/api/paygate/v2/authorization, the host picked from the
1188
+ // page's own (Dick's, Golf Galaxy, Public Lands, Going Going Gone). The same slot
1189
+ // carries Afterpay, CashApp and Affirm tenders with literal strings where the JWEs
1190
+ // go, and a saved card goes to tenders.savedCardTender instead. The body names no
1191
+ // amount: the paygate charges the cart it holds for transactionReference.
1192
+ // mountCCForm names the client key and environment 'live'. Public key read from
1193
+ // checkoutshopper-live.adyen.com on 2026-09-25 (UTC).
1194
+ id: "dsg_paygate_v2",
1195
+ merchant: "Dick's Sporting Goods",
1196
+ status: "observe",
1197
+ origins: [
1198
+ { checkout: "https://www.dickssportinggoods.com", endpoint: "https://payments.dickssportinggoods.com" },
1199
+ { checkout: "https://www.golfgalaxy.com", endpoint: "https://payments.golfgalaxy.com" },
1200
+ { checkout: "https://www.publiclands.com", endpoint: "https://payments.publiclands.com" },
1201
+ { checkout: "https://www.goinggoinggone.com", endpoint: "https://payments.goinggoinggone.com" }
1202
+ ],
1203
+ method: "POST",
1204
+ path: "/prod/api/paygate/v2/authorization",
1205
+ query: {},
1206
+ // The page sends true for every checkout its translator read, the only checkouts
1207
+ // the amount source below maps; a legacy checkout is not this card request.
1208
+ requires: [{ path: ["isModernOrder"], equals: true }],
1209
+ holder: ["tenders", "encryptedCardTenders", 0],
1210
+ fields: {
1211
+ encryptedCardNumber: "cardNumber",
1212
+ encryptedExpiryMonth: "expirationMonth",
1213
+ encryptedExpiryYear: "expirationYear",
1214
+ encryptedSecurityCode: "cvv"
1215
+ },
1216
+ // cardType and vinBalue describe the dummy the agent typed: the facts below
1217
+ // write the approved card's instead, so nothing is removed.
1218
+ remove: [],
1219
+ refuse: [
1220
+ { path: ["tenders", "encryptedCardTenders", 0, "saveForLater"], when: "true" },
1221
+ // A saved card, or a second card tender the Vault never encrypted.
1222
+ { path: ["tenders", "savedCardTender"], when: "present" },
1223
+ { path: ["tenders", "encryptedCardTenders", 1], when: "present" },
1224
+ // A gift card pays part of the order, so the card pays less than the total.
1225
+ { path: ["tenders", "giftCardTenders"], when: "present" }
1226
+ ],
1227
+ amount: null,
1228
+ // Read Sep 25, 2026 (research amount_source/dickssportinggoods). Every checkout call
1229
+ // on the checkout page answers the whole checkout, camelCase on the wire: the page
1230
+ // load's PATCH (or the create), then every address, shipping, promotion, gift card,
1231
+ // tip and ScoreCard change. pricing.totalRemaining is what the card pays, a string
1232
+ // with exactly two decimals ("123.45"; the page itself drops the dot for minor
1233
+ // units). transactionReference is the checkout key, in the body and the URL. After
1234
+ // payment the order submit and the confirmation read return the placed checkout.
1235
+ amountSource: {
1236
+ id: "dsg_checkout_pricing",
1237
+ origin: "checkout",
1238
+ format: "json",
1239
+ responses: [
1240
+ { method: "PATCH", path: "/api/v1/checkouts/:checkoutKey", query: {} },
1241
+ { method: "POST", path: "/api/v1/checkouts", query: {} },
1242
+ { method: "POST", path: "/api/v1/checkouts/:checkoutKey/addresses", queries: [{}, { type: "billing" }, { type: "shipping" }] },
1243
+ { method: "PUT", path: "/api/v1/checkouts/:checkoutKey/shipping/selected/groups/:groupNumber/mode", query: {} },
1244
+ { method: "PUT", path: "/api/v2/checkouts/:checkoutKey/promotion/cart", query: {} },
1245
+ { method: "DELETE", path: "/api/v2/checkouts/:checkoutKey/promotion/cart", query: {} },
1246
+ { method: "POST", path: "/api/v1/checkouts/:checkoutKey/gift-cards", query: {} },
1247
+ { method: "DELETE", path: "/api/v1/checkouts/:checkoutKey/gift-cards/:giftCardIndex", query: {} },
1248
+ { method: "PUT", path: "/api/v1/checkouts/:checkoutKey/tip", query: {} },
1249
+ { method: "PUT", path: "/api/v1/checkouts/:checkoutKey/scorecard", query: {} },
1250
+ { method: "DELETE", path: "/api/v1/checkouts/:checkoutKey/scorecard", query: {} },
1251
+ { method: "DELETE", path: "/api/v1/checkouts/:checkoutKey/v2/gifts", query: {} }
1252
+ ],
1253
+ total: { path: ["pricing", "totalRemaining"], format: "decimal2" },
1254
+ unit: "major",
1255
+ currency: "USD",
1256
+ currencyPath: ["pricing", "currency"],
1257
+ bind: [
1258
+ { source: ["checkoutKey"], card: ["transactionReference"] },
1259
+ { placeholder: "checkoutKey", card: ["transactionReference"] }
1260
+ ],
1261
+ // The order total and the card's share are the same text. Anything that lowers the
1262
+ // share below the total (a gift card, ScoreCard rewards) or lands in only one of them
1263
+ // (a tip, a donation) is refused until a HAR shows which one the charge follows, so
1264
+ // the approved amount and the placed checkout's total read after payment agree.
1265
+ requires: [{ path: ["pricing", "total"], equalsPath: ["pricing", "totalRemaining"] }],
1266
+ refuse: [
1267
+ // A placed checkout is not the one being paid.
1268
+ { path: ["orderPlacedTime"], when: "true" },
1269
+ // Gift cards and a checkout with no address yet (tax not estimated) wait for a HAR.
1270
+ { path: ["payment", "giftCards"], when: "nonEmpty" },
1271
+ { path: ["addresses"], when: "empty" }
1272
+ ],
1273
+ maxAgeMs: 9e5,
1274
+ afterPayment: {
1275
+ origin: "checkout",
1276
+ format: "json",
1277
+ responses: [
1278
+ { method: "POST", path: "/api/v1/checkouts/:checkoutKey", query: {} },
1279
+ { method: "GET", path: "/api/v1/checkouts/submitted/:checkoutKey", query: {} }
1280
+ ],
1281
+ // The order total, which the requires rule above held equal to the card's share
1282
+ // before payment. totalRemaining is not read here: after placement it can drop to
1283
+ // what is still unpaid, which would hide a higher charge.
1284
+ total: { path: ["pricing", "total"], format: "decimal2" },
1285
+ unit: "major",
1286
+ currency: "USD",
1287
+ currencyPath: ["pricing", "currency"],
1288
+ bind: [
1289
+ { source: ["checkoutKey"], card: ["transactionReference"] },
1290
+ { placeholder: "checkoutKey", card: ["transactionReference"] }
1291
+ ],
1292
+ refuse: [{ path: ["orderPlacedTime"], when: "unset" }]
1293
+ }
1294
+ },
1295
+ facts: [
1296
+ // adyen-web's brand codes, the ones the page's supported list names; Dick's own
1297
+ // private-label (plcc, comc) cards have no Agentcard brand and are refused.
1298
+ {
1299
+ path: ["tenders", "encryptedCardTenders", 0, "cardType"],
1300
+ fact: "brand",
1301
+ map: { visa: "visa", mastercard: "mc", amex: "amex", discover: "discover", diners: "diners", jcb: "jcb" }
1302
+ },
1303
+ { path: ["tenders", "encryptedCardTenders", 0, "vinBalue"], fact: "bin6" }
1304
+ ],
1305
+ adyen: {
1306
+ environment: "live",
1307
+ clientKey: "live_YJLFKT47JJG2RI7QIYX6HTKKDEDOEGOH",
1308
+ publicKeySha256: "da33611149dff3ab715be8671c0550bb8aec6112983b2473db9ee7afb829c110"
1309
+ }
1310
+ }
1311
+ ]);
1312
+ function adyenKeyHost(environment) {
1313
+ return typeof environment === "string" && /^(?:test|live(?:-[a-z]+)?)$/.test(environment) ? `checkoutshopper-${environment}.adyen.com` : null;
1314
+ }
1315
+ function wireProjection(profiles) {
1316
+ return deepFreeze(profiles.map(({ adyen, ...profile }) => ({
1317
+ ...profile,
1318
+ adyen: { environment: adyen.environment, clientKey: adyen.clientKey, keyHost: adyenKeyHost(adyen.environment) }
1319
+ })));
1320
+ }
1321
+ function clientProjection(profiles) {
1322
+ return deepFreeze(profiles.map((profile) => ({
1323
+ id: profile.id,
1324
+ merchant: profile.merchant,
1325
+ status: profile.status,
1326
+ origins: profile.origins,
1327
+ method: profile.method,
1328
+ path: profile.path,
1329
+ query: profile.query,
1330
+ ...profile.facts ? { facts: profile.facts } : {},
1331
+ adyen: { ...profile.adyen, keyHost: adyenKeyHost(profile.adyen.environment) }
1332
+ })));
1333
+ }
1334
+ var MERCHANT_PROFILE_RECOGNIZERS = /* @__PURE__ */ wireProjection(MERCHANT_HOSTED_PROFILES);
1335
+ var MERCHANT_PROFILE_CLIENT_TABLE = /* @__PURE__ */ clientProjection(MERCHANT_HOSTED_PROFILES);
1336
+ function merchantProfileById(id) {
1337
+ return typeof id === "string" ? MERCHANT_PROFILE_CLIENT_TABLE.find((entry) => entry.id === id) ?? null : null;
1338
+ }
1339
+ function trustedMerchantProfile(idOrEntry) {
1340
+ const id = typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id;
1341
+ const profile = merchantProfileById(id);
1342
+ if (!profile)
1343
+ throw new Error("Refusing to pay: unknown merchant profile.");
1344
+ return profile;
1345
+ }
1346
+ function merchantProfileRecognizerById(id) {
1347
+ return typeof id === "string" ? MERCHANT_PROFILE_RECOGNIZERS.find((entry) => entry.id === id) ?? null : null;
1348
+ }
1349
+ var ADYEN_DOMAINS = ["adyen.com", "adyenpayments.com"];
1350
+ var AGENTCARD_DOMAINS = ["agentcard.sh"];
1351
+ var IPV4_HOST = /^\d{1,3}(?:\.\d{1,3}){3}$/;
1352
+ function underDomain(hostname, domain) {
1353
+ return hostname === domain || hostname.endsWith(`.${domain}`);
1354
+ }
1355
+ function reviewedDomain(hostname) {
1356
+ const labels = hostname.split(".");
1357
+ return labels.length > 2 ? labels.slice(1).join(".") : hostname;
1358
+ }
1359
+ function declaredEndpointRefusal(endpoint) {
1360
+ let url;
1361
+ try {
1362
+ url = typeof endpoint === "string" && !endpoint.includes("#") ? new URL(endpoint) : null;
1363
+ }
1364
+ catch {
1365
+ url = null;
1366
+ }
1367
+ if (!url || url.href !== endpoint || url.protocol !== "https:" || url.port || url.username || url.password) {
1368
+ return "the declared endpoint is not an HTTPS URL in its serialized form, with no port, credentials or fragment";
1369
+ }
1370
+ const host = url.hostname.endsWith(".") ? url.hostname.slice(0, -1) : url.hostname;
1371
+ if (!host.includes(".") || host.startsWith("[") || IPV4_HOST.test(host) || underDomain(host, "localhost")) {
1372
+ return `${url.hostname} is not a public domain name, so it cannot be declared`;
1373
+ }
1374
+ if (ADYEN_DOMAINS.some((domain) => underDomain(host, domain)))
1375
+ return `${url.hostname} is a payment processor's host, not a merchant's own endpoint`;
1376
+ if (AGENTCARD_DOMAINS.some((domain) => underDomain(host, domain)))
1377
+ return `${url.hostname} is Agentcard's own host, not a merchant's endpoint`;
1378
+ const reviewed = MERCHANT_PROFILE_CLIENT_TABLE.flatMap((profile) => profile.origins.flatMap((pair) => [pair.checkout, pair.endpoint]));
1379
+ if (reviewed.some((origin) => underDomain(host, reviewedDomain(new URL(origin).hostname))))
1380
+ return `${url.hostname} belongs to a merchant Agentcard reviewed, so it cannot be declared`;
1381
+ return null;
1382
+ }
1383
+ function sameKeyIgnoringCase(key, step) {
1384
+ return !FORBIDDEN_KEYS.has(key) && (key.toLowerCase() === step.toLowerCase() || key.toUpperCase() === step.toUpperCase());
1385
+ }
1386
+ function valuesIgnoringCase(root, path) {
1387
+ let nodes = [root];
1388
+ for (const step of path) {
1389
+ const next = [];
1390
+ for (const node of nodes) {
1391
+ if (typeof step === "number") {
1392
+ if (Array.isArray(node) && Number.isSafeInteger(step) && step >= 0 && step < node.length)
1393
+ next.push(node[step]);
1394
+ }
1395
+ else if (isPlainObject(node)) {
1396
+ for (const key of Object.keys(node))
1397
+ if (sameKeyIgnoringCase(key, step))
1398
+ next.push(node[key]);
1399
+ }
1400
+ }
1401
+ nodes = next;
1402
+ }
1403
+ return nodes;
1404
+ }
1405
+ function merchantBodyRefusal(profile, body) {
1406
+ const p = merchantProfileRecognizerById(typeof profile === "string" ? profile : profile?.id);
1407
+ if (!p)
1408
+ throw new Error("Refusing to pay: unknown merchant profile.");
1409
+ let parsed;
1410
+ try {
1411
+ parsed = typeof body === "string" ? JSON.parse(body) : void 0;
1412
+ }
1413
+ catch {
1414
+ return "the paused body is not JSON";
1415
+ }
1416
+ if (!isPlainObject(parsed))
1417
+ return "the paused body is not a JSON object";
1418
+ if (!roundTrips(parsed, body))
1419
+ return "the paused body does not round-trip through JSON, so its rules cannot be read";
1420
+ for (const rule of p.requires) {
1421
+ const at = jsonAt(parsed, rule.path);
1422
+ if (!at.found || at.value !== rule.equals || valuesIgnoringCase(parsed, rule.path).some((value) => value !== rule.equals)) {
1423
+ return `the paused body is not ${p.merchant}'s card request: ${describePath(rule.path)} is not ${JSON.stringify(rule.equals)}`;
1424
+ }
1425
+ }
1426
+ for (const rule of p.refuse) {
1427
+ const values = valuesIgnoringCase(parsed, rule.path);
1428
+ const fires2 = rule.when === "true" ? values.some((value) => value !== false && value !== null) : values.length > 0;
1429
+ if (fires2)
1430
+ return `the paused body asks ${p.merchant} for more than this purchase: ${describePath(rule.path)}`;
1431
+ }
1432
+ return null;
1433
+ }
1434
+ // src/adyen-jwe.js
1435
+ var ADYEN_JWE_HEADER = { alg: "RSA-OAEP-256", enc: "A256GCM", version: "1" };
1436
+ var BASE64URL = /^[A-Za-z0-9_-]+$/;
1437
+ function isAdyenJweCompact(s) {
1438
+ if (typeof s !== "string" || s.length > 2048)
1439
+ return false;
1440
+ const parts = s.split(".");
1441
+ if (parts.length !== 5)
1442
+ return false;
1443
+ const [header, encryptedKey, iv, ciphertext, tag] = parts;
1444
+ if (!parts.every((p) => p.length > 0 && BASE64URL.test(p)))
1445
+ return false;
1446
+ if (encryptedKey.length !== 342 || iv.length !== 16 || tag.length !== 22)
1447
+ return false;
1448
+ if (ciphertext.length > 1024)
1449
+ return false;
1450
+ return isAdyenJweHeader(header);
1451
+ }
1452
+ function isAdyenJweHeader(segment) {
1453
+ if (!BASE64URL.test(segment) || segment.length > 128)
1454
+ return false;
1455
+ let parsed;
1456
+ try {
1457
+ parsed = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(segment.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
1458
+ }
1459
+ catch {
1460
+ return false;
1461
+ }
1462
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1463
+ return false;
1464
+ const obj = parsed;
1465
+ const keys = Object.keys(obj).sort();
1466
+ if (keys.join(",") !== "alg,enc,version")
1467
+ return false;
1468
+ return obj.alg === ADYEN_JWE_HEADER.alg && obj.enc === ADYEN_JWE_HEADER.enc && obj.version === ADYEN_JWE_HEADER.version;
1469
+ }
1470
+ // src/sha256.js
1471
+ var K = /* @__PURE__ */ new Uint32Array([
1472
+ 1116352408,
1473
+ 1899447441,
1474
+ 3049323471,
1475
+ 3921009573,
1476
+ 961987163,
1477
+ 1508970993,
1478
+ 2453635748,
1479
+ 2870763221,
1480
+ 3624381080,
1481
+ 310598401,
1482
+ 607225278,
1483
+ 1426881987,
1484
+ 1925078388,
1485
+ 2162078206,
1486
+ 2614888103,
1487
+ 3248222580,
1488
+ 3835390401,
1489
+ 4022224774,
1490
+ 264347078,
1491
+ 604807628,
1492
+ 770255983,
1493
+ 1249150122,
1494
+ 1555081692,
1495
+ 1996064986,
1496
+ 2554220882,
1497
+ 2821834349,
1498
+ 2952996808,
1499
+ 3210313671,
1500
+ 3336571891,
1501
+ 3584528711,
1502
+ 113926993,
1503
+ 338241895,
1504
+ 666307205,
1505
+ 773529912,
1506
+ 1294757372,
1507
+ 1396182291,
1508
+ 1695183700,
1509
+ 1986661051,
1510
+ 2177026350,
1511
+ 2456956037,
1512
+ 2730485921,
1513
+ 2820302411,
1514
+ 3259730800,
1515
+ 3345764771,
1516
+ 3516065817,
1517
+ 3600352804,
1518
+ 4094571909,
1519
+ 275423344,
1520
+ 430227734,
1521
+ 506948616,
1522
+ 659060556,
1523
+ 883997877,
1524
+ 958139571,
1525
+ 1322822218,
1526
+ 1537002063,
1527
+ 1747873779,
1528
+ 1955562222,
1529
+ 2024104815,
1530
+ 2227730452,
1531
+ 2361852424,
1532
+ 2428436474,
1533
+ 2756734187,
1534
+ 3204031479,
1535
+ 3329325298
1536
+ ]);
1537
+ var rotr = (x, n) => x >>> n | x << 32 - n;
1538
+ function utf8(text) {
1539
+ const out = [];
1540
+ for (const char of text) {
1541
+ let cp = char.codePointAt(0);
1542
+ if (cp >= 55296 && cp <= 57343)
1543
+ cp = 65533;
1544
+ if (cp < 128)
1545
+ out.push(cp);
1546
+ else if (cp < 2048)
1547
+ out.push(192 | cp >> 6, 128 | cp & 63);
1548
+ else if (cp < 65536)
1549
+ out.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
1550
+ else
1551
+ out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
1552
+ }
1553
+ return Uint8Array.from(out);
1554
+ }
1555
+ function sha256Hex(input) {
1556
+ const isBytes = Object.prototype.toString.call(input) === "[object Uint8Array]";
1557
+ if (typeof input !== "string" && !isBytes)
1558
+ throw new TypeError("sha256Hex takes a string or a Uint8Array.");
1559
+ const bytes = typeof input === "string" ? utf8(input) : input;
1560
+ const length = bytes.length;
1561
+ const padded = new Uint8Array((length + 8 >> 6) + 1 << 6);
1562
+ padded.set(bytes);
1563
+ padded[length] = 128;
1564
+ const view = new DataView(padded.buffer);
1565
+ view.setUint32(padded.length - 8, Math.floor(length / 536870912));
1566
+ view.setUint32(padded.length - 4, length << 3 >>> 0);
1567
+ const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
1568
+ const W = new Uint32Array(64);
1569
+ for (let offset = 0; offset < padded.length; offset += 64) {
1570
+ for (let t = 0; t < 16; t++)
1571
+ W[t] = view.getUint32(offset + t * 4);
1572
+ for (let t = 16; t < 64; t++) {
1573
+ const s0 = rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ W[t - 15] >>> 3;
1574
+ const s1 = rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ W[t - 2] >>> 10;
1575
+ W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
1576
+ }
1577
+ let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
1578
+ for (let t = 0; t < 64; t++) {
1579
+ const t1 = h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[t] + W[t] >>> 0;
1580
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
1581
+ h = g;
1582
+ g = f;
1583
+ f = e;
1584
+ e = d + t1 >>> 0;
1585
+ d = c;
1586
+ c = b;
1587
+ b = a;
1588
+ a = t1 + t2 >>> 0;
1589
+ }
1590
+ H[0] += a;
1591
+ H[1] += b;
1592
+ H[2] += c;
1593
+ H[3] += d;
1594
+ H[4] += e;
1595
+ H[5] += f;
1596
+ H[6] += g;
1597
+ H[7] += h;
1598
+ }
1599
+ return Array.from(H, (word) => word.toString(16).padStart(8, "0")).join("");
1600
+ }
1601
+ // src/adyen-merchant-hosted.js
1602
+ function matchProfileEndpoint(profile, rawUrl, method, opts) {
1603
+ const p = trustedMerchantProfile(profile);
1604
+ if (typeof rawUrl !== "string" || typeof method !== "string" || method.toUpperCase() !== p.method)
1605
+ return false;
1606
+ const match = urlTemplateMatch({ path: p.path, query: p.query }, rawUrl);
1607
+ if (!match)
1608
+ return false;
1609
+ const checkoutOrigin = opts?.checkoutOrigin;
1610
+ return p.origins.some((pair) => pair.endpoint === match.origin && (checkoutOrigin === void 0 || pair.checkout === checkoutOrigin));
1611
+ }
1612
+ function reviewedProfileFor(profile) {
1613
+ const id = trustedMerchantProfile(profile).id;
1614
+ return MERCHANT_PROFILE_RECOGNIZERS.find((entry) => entry.id === id);
1615
+ }
1616
+ function merchantAmountFromResponses(profile, input) {
1617
+ return readMerchantAmount(reviewedProfileFor(profile), input);
1618
+ }
1619
+ function merchantChargedFromResponses(profile, input) {
1620
+ return readMerchantCharged(reviewedProfileFor(profile), input);
1621
+ }
1622
+ function merchantAmountStagesOf(profile, request, scope) {
1623
+ return merchantAmountRequestStages(reviewedProfileFor(profile), request, scope);
1624
+ }
1625
+ function merchantAmountPending(profile, stage, card, request) {
1626
+ return merchantExchangePending(reviewedProfileFor(profile), stage, card, request);
1627
+ }
1628
+ function merchantAmountExchanges(profile, stage, input) {
1629
+ return selectMerchantExchanges(reviewedProfileFor(profile), stage, input);
1630
+ }
1631
+ // src/merchant-hosted-request.js
1632
+ function merchantTotalFromResponses(profileId, input) {
1633
+ const read = merchantAmountFromResponses(profileId, input);
1634
+ return read.ok ? { ...read, amount: { amount: read.amount.amountCents, currency: read.amount.currency } } : read;
1635
+ }
1636
+ function reviewedProfile(profileId) {
1637
+ const profile = merchantProfileRecognizerById(profileId);
1638
+ if (!profile)
1639
+ throw new Error("Refusing to pay: unknown merchant profile.");
1640
+ return profile;
1641
+ }
1642
+ function merchantProfileUrlRelation(profileId, rawUrl) {
1643
+ const profile = reviewedProfile(profileId);
1644
+ if (typeof rawUrl !== "string")
1645
+ return null;
1646
+ if (matchProfileEndpoint(profile.id, rawUrl, profile.method))
1647
+ return "endpoint";
1648
+ let url;
1649
+ try {
1650
+ url = new URL(rawUrl);
1651
+ }
1652
+ catch {
1653
+ return null;
1654
+ }
1655
+ if (url.protocol !== "https:" || url.port || url.username || url.password)
1656
+ return null;
1657
+ if (!profile.origins.some((pair) => pair.endpoint === url.origin))
1658
+ return null;
1659
+ return pathMatchesTemplate(profile.path, url.pathname) ? "sibling" : null;
1660
+ }
1661
+ var JWE_AT = /ey[A-Za-z0-9_-]{1,126}\.[A-Za-z0-9_-]{342}\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{1,1024}\.[A-Za-z0-9_-]{22}/y;
1662
+ var LEGACY_BLOB = /adyenjs_0_1_\d{1,4}(?:\$|%24)/i;
1663
+ function textCarriesCiphertext(text) {
1664
+ if (LEGACY_BLOB.test(text))
1665
+ return true;
1666
+ for (let at = text.indexOf("ey"); at !== -1; at = text.indexOf("ey", at + 1)) {
1667
+ JWE_AT.lastIndex = at;
1668
+ const match = JWE_AT.exec(text);
1669
+ if (match && isAdyenJweCompact(match[0]))
1670
+ return true;
1671
+ }
1672
+ return false;
1673
+ }
1674
+ function jsonStrings(root, out) {
1675
+ const stack = [root];
1676
+ while (stack.length > 0) {
1677
+ const value = stack.pop();
1678
+ if (typeof value === "string")
1679
+ out.push(value);
1680
+ else if (Array.isArray(value))
1681
+ for (const item of value)
1682
+ stack.push(item);
1683
+ else if (value !== null && typeof value === "object") {
1684
+ for (const [key, child] of Object.entries(value)) {
1685
+ out.push(key);
1686
+ stack.push(child);
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ function decodedTexts(text) {
1692
+ let parsed;
1693
+ if (/^\s*[[{"]/.test(text)) {
1694
+ try {
1695
+ parsed = JSON.parse(text);
1696
+ }
1697
+ catch {
1698
+ parsed = void 0;
1699
+ }
1700
+ }
1701
+ const out = [];
1702
+ if (parsed !== void 0)
1703
+ jsonStrings(parsed, out);
1704
+ else if (/[%+]/.test(text)) {
1705
+ for (const [name, value] of new URLSearchParams(text))
1706
+ out.push(name, value);
1707
+ }
1708
+ return out.filter((decoded) => decoded !== text);
1709
+ }
1710
+ var DECODE_DEPTH = 6;
1711
+ function carriesAt(text, depth) {
1712
+ if (textCarriesCiphertext(text))
1713
+ return true;
1714
+ const decoded = decodedTexts(text);
1715
+ if (decoded.length === 0)
1716
+ return false;
1717
+ if (depth >= DECODE_DEPTH)
1718
+ return true;
1719
+ return decoded.some((inner) => carriesAt(inner, depth + 1));
1720
+ }
1721
+ function carriesAdyenCardCiphertext(body) {
1722
+ if (typeof body !== "string" || body === "")
1723
+ return false;
1724
+ return carriesAt(body, 0);
1725
+ }
1726
+ var PASS = /* @__PURE__ */ Object.freeze({ verdict: "pass" });
1727
+ var PAUSE = /* @__PURE__ */ Object.freeze({ verdict: "pause" });
1728
+ var abort = (reason) => Object.freeze({ verdict: "abort", reason });
1729
+ function holderRefusal(profile, parsed) {
1730
+ const holder = jsonAt(parsed, profile.holder);
1731
+ if (!holder.found || !isPlainObject(holder.value)) {
1732
+ return `the paused body carries no card object where ${profile.merchant}'s card request holds it`;
1733
+ }
1734
+ const values = Object.values(profile.fields).map((name) => Object.prototype.hasOwnProperty.call(holder.value, name) ? holder.value[name] : void 0);
1735
+ if (!values.every((value) => typeof value === "string" && isAdyenJweCompact(value))) {
1736
+ return `the paused body does not carry the four encrypted card fields ${profile.merchant}'s card request holds a new card in`;
1737
+ }
1738
+ if (new Set(values).size !== values.length)
1739
+ return "two encrypted card fields in the paused body are the same";
1740
+ const type = holder.value.type;
1741
+ if (type !== void 0 && type !== "scheme")
1742
+ return "the paused body pays with something other than a card";
1743
+ return null;
1744
+ }
1745
+ function amountRefusal(profile, parsed) {
1746
+ const priced = isPlainObject(profile.amountSource);
1747
+ if (!Array.isArray(profile.amount)) {
1748
+ return priced ? null : `${profile.merchant}'s card request names no amount, so it cannot be checked against the amount the cardholder approves`;
1749
+ }
1750
+ const at = jsonAt(parsed, profile.amount);
1751
+ if (!at.found) {
1752
+ return priced ? null : `the paused body names no amount where ${profile.merchant}'s card request holds it, so it cannot be checked against the amount the cardholder approves`;
1753
+ }
1754
+ const amount = at.value;
1755
+ const value = isPlainObject(amount) ? amount.value : void 0;
1756
+ const currency = isPlainObject(amount) ? amount.currency : void 0;
1757
+ const valueOk = typeof value === "number" ? Number.isSafeInteger(value) && value >= 0 : typeof value === "string" && /^\d{1,15}$/.test(value.trim());
1758
+ const currencyOk = typeof currency === "string" && /^[A-Za-z]{3}$/.test(currency.trim());
1759
+ return valueOk && currencyOk ? null : `the amount where ${profile.merchant}'s card request holds it is not the {value, currency} Agentcard reviewed, so it cannot be checked`;
1760
+ }
1761
+ function classifyMerchantBody(profileId, body) {
1762
+ const profile = reviewedProfile(profileId);
1763
+ if (!carriesAdyenCardCiphertext(body))
1764
+ return PASS;
1765
+ const refusal = merchantBodyRefusal(profile.id, body);
1766
+ if (refusal)
1767
+ return abort(refusal);
1768
+ const parsed = JSON.parse(body);
1769
+ const card = holderRefusal(profile, parsed) ?? amountRefusal(profile, parsed);
1770
+ return card ? abort(card) : PAUSE;
1771
+ }
1772
+ function classifyMerchantRequest(profileId, rawUrl, method, body) {
1773
+ const profile = reviewedProfile(profileId);
1774
+ const relation = merchantProfileUrlRelation(profile.id, rawUrl);
1775
+ if (!relation || !carriesAdyenCardCiphertext(body))
1776
+ return PASS;
1777
+ if (relation === "sibling") {
1778
+ return abort(`the paused request sends a card to ${profile.merchant} somewhere other than the card request Agentcard reviewed`);
1779
+ }
1780
+ if (typeof method !== "string" || method.toUpperCase() !== profile.method) {
1781
+ return abort(`the paused request sends a card to ${profile.merchant} as ${String(method).toUpperCase()}, not as the ${profile.method} Agentcard reviewed`);
1782
+ }
1783
+ return classifyMerchantBody(profile.id, body);
1784
+ }
1785
+ function declaredUrlRelation(endpoint, rawUrl) {
1786
+ if (typeof endpoint !== "string" || typeof rawUrl !== "string")
1787
+ return null;
1788
+ if (rawUrl === endpoint)
1789
+ return "endpoint";
1790
+ let want;
1791
+ let got;
1792
+ try {
1793
+ want = new URL(endpoint);
1794
+ got = new URL(rawUrl);
1795
+ }
1796
+ catch {
1797
+ return null;
1798
+ }
1799
+ return got.origin === want.origin && got.pathname === want.pathname ? "sibling" : null;
1800
+ }
1801
+ function classifyDeclaredMerchantRequest(profileId, endpoint, rawUrl, method, body) {
1802
+ const profile = reviewedProfile(profileId);
1803
+ const relation = declaredUrlRelation(endpoint, rawUrl);
1804
+ if (!relation || !carriesAdyenCardCiphertext(body))
1805
+ return PASS;
1806
+ if (relation === "sibling")
1807
+ return abort("the paused request sends a card somewhere other than the endpoint this test checkout declared");
1808
+ if (typeof method !== "string" || method.toUpperCase() !== profile.method) {
1809
+ return abort(`the paused request sends a card as ${String(method).toUpperCase()}, not as the ${profile.method} the declared checkout takes`);
1810
+ }
1811
+ return classifyMerchantBody(profile.id, body);
1812
+ }
1813
+ function declaredTemplatedUrl(endpoint) {
1814
+ const url = new URL(endpoint);
1815
+ return `${url.origin}${url.pathname}`;
1816
+ }
1817
+ function merchantProfileTemplatedUrl(profileId, rawUrl) {
1818
+ const profile = reviewedProfile(profileId);
1819
+ let origin = null;
1820
+ try {
1821
+ origin = new URL(rawUrl).origin;
1822
+ }
1823
+ catch {
1824
+ origin = null;
1825
+ }
1826
+ const pair = profile.origins.find((candidate) => candidate.endpoint === origin) ?? profile.origins[0];
1827
+ const query = Object.entries(profile.query).map(([key, value]) => `${key}=${value}`).join("&");
1828
+ return `${pair.endpoint}${profile.path}${query ? `?${query}` : ""}`;
1829
+ }
1830
+ function keyPaths(root, out) {
1831
+ const stack = [[root, ""]];
1832
+ while (stack.length > 0) {
1833
+ const [value, prefix] = stack.pop();
1834
+ if (Array.isArray(value)) {
1835
+ for (const item of value)
1836
+ stack.push([item, `${prefix}[]`]);
1837
+ }
1838
+ else if (value === null || typeof value !== "object") {
1839
+ if (prefix)
1840
+ out.add(prefix);
1841
+ }
1842
+ else {
1843
+ for (const [key, child] of Object.entries(value))
1844
+ stack.push([child, prefix ? `${prefix}.${key}` : key]);
1845
+ }
1846
+ }
1847
+ }
1848
+ function merchantBodyKeyPathSha256(body) {
1849
+ let parsed;
1850
+ try {
1851
+ parsed = JSON.parse(body);
1852
+ }
1853
+ catch {
1854
+ return null;
1855
+ }
1856
+ const paths = /* @__PURE__ */ new Set();
1857
+ keyPaths(parsed, paths);
1858
+ return sha256Hex([...paths].sort().join("\n"));
1859
+ }
1860
+ var globEscape = (text) => text.replace(/[*?\\]/g, (char) => `\\${char}`);
1861
+ function merchantProfileUrlGlobs(profileId) {
1862
+ const profile = reviewedProfile(profileId);
1863
+ const path = profile.path.split("/").map((segment) => segment.startsWith(":") ? "*" : globEscape(segment)).join("/");
1864
+ return [...new Set(profile.origins.map((pair) => `${pair.endpoint}${path}*`))];
1865
+ }
1866
+ function declaredUrlGlobs(endpoint) {
1867
+ const url = new URL(endpoint);
1868
+ return [`${url.origin}${globEscape(url.pathname)}*`];
1869
+ }
1870
+ function canonical(value) {
1871
+ if (Array.isArray(value))
1872
+ return `[${value.map(canonical).join(",")}]`;
1873
+ if (isPlainObject(value)) {
1874
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
1875
+ }
1876
+ return JSON.stringify(value) ?? "null";
1877
+ }
1878
+ function merchantProfileRulesMatch(profileId, served) {
1879
+ const profile = reviewedProfile(profileId);
1880
+ if (!isPlainObject(served) || served.id !== profile.id)
1881
+ return false;
1882
+ const { status: _built, ...built } = profile;
1883
+ const { status: _served, ...copy } = served;
1884
+ try {
1885
+ return canonical(copy) === canonical(built);
1886
+ }
1887
+ catch {
1888
+ return false;
1889
+ }
1890
+ }
1891
+ function merchantProfileEnvironment(profileId) {
1892
+ return reviewedProfile(profileId).adyen.environment === "test" ? "sandbox" : "production";
1893
+ }
1894
+ function merchantProfileEndpointFor(profileId, checkoutOrigin) {
1895
+ const profile = reviewedProfile(profileId);
1896
+ const pair = profile.origins.find((candidate) => candidate.checkout === checkoutOrigin);
1897
+ if (!pair || profile.path.split("/").some((segment) => segment.startsWith(":")))
1898
+ return null;
1899
+ const query = Object.entries(profile.query).map(([key, value]) => `${key}=${value}`).join("&");
1900
+ return `${pair.endpoint}${profile.path}${query ? `?${query}` : ""}`;
1901
+ }
1902
+ export { MERCHANT_EXCHANGES_MAX, MERCHANT_PROFILE_RECOGNIZERS, carriesAdyenCardCiphertext, classifyDeclaredMerchantRequest, classifyMerchantBody, classifyMerchantRequest, declaredEndpointRefusal, declaredTemplatedUrl, declaredUrlGlobs, declaredUrlRelation, merchantAmountExchanges, merchantAmountFromResponses, merchantAmountPending, merchantAmountStagesOf, merchantBodyKeyPathSha256, merchantChargedFromResponses, merchantProfileEndpointFor, merchantProfileEnvironment, merchantProfileRulesMatch, merchantProfileTemplatedUrl, merchantProfileUrlGlobs, merchantProfileUrlRelation, merchantTotalFromResponses, sha256Hex };