@agent-cards/sdk 0.21.0 → 0.23.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 (113) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/PREFLIGHT.md +32 -0
  3. package/README.md +1177 -23
  4. package/dist/aci.d.ts +173 -0
  5. package/dist/aci.generated.d.ts +87 -0
  6. package/dist/aci.generated.js +306 -0
  7. package/dist/aci.js +371 -0
  8. package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
  9. package/dist/adyen-merchant-hosted.generated.js +302 -56
  10. package/dist/agentcard.d.ts +9 -2
  11. package/dist/agentcard.js +4 -2
  12. package/dist/amadeus-checkout.generated.d.ts +57 -0
  13. package/dist/amadeus-checkout.generated.js +549 -0
  14. package/dist/amadeus.d.ts +41 -0
  15. package/dist/amadeus.js +81 -0
  16. package/dist/builtin-registry.generated.js +1 -1
  17. package/dist/card-endpoint-facts.d.ts +96 -0
  18. package/dist/card-endpoint-facts.js +275 -0
  19. package/dist/card-endpoint-page.d.ts +54 -0
  20. package/dist/card-endpoint-page.js +69 -0
  21. package/dist/card-endpoint.d.ts +298 -0
  22. package/dist/card-endpoint.js +438 -0
  23. package/dist/card-fields.generated.d.ts +5 -3
  24. package/dist/card-fields.generated.js +284 -7
  25. package/dist/cdp.d.ts +5 -1
  26. package/dist/cdp.js +1155 -141
  27. package/dist/client.d.ts +456 -20
  28. package/dist/client.js +1072 -104
  29. package/dist/creditguard.generated.d.ts +79 -0
  30. package/dist/creditguard.generated.js +662 -0
  31. package/dist/cse-body.js +11 -1
  32. package/dist/cybersource-flex.d.ts +30 -0
  33. package/dist/cybersource-flex.generated.d.ts +27 -1
  34. package/dist/cybersource-flex.generated.js +42 -2
  35. package/dist/cybersource-flex.js +46 -0
  36. package/dist/datatrans.d.ts +96 -0
  37. package/dist/datatrans.generated.d.ts +64 -0
  38. package/dist/datatrans.generated.js +426 -0
  39. package/dist/datatrans.js +178 -0
  40. package/dist/ekashu.d.ts +35 -0
  41. package/dist/ekashu.js +55 -0
  42. package/dist/elavon-converge.d.ts +37 -0
  43. package/dist/elavon-converge.generated.d.ts +49 -0
  44. package/dist/elavon-converge.generated.js +439 -0
  45. package/dist/elavon-converge.js +84 -0
  46. package/dist/eprotect.d.ts +152 -0
  47. package/dist/eprotect.generated.d.ts +77 -0
  48. package/dist/eprotect.generated.js +498 -0
  49. package/dist/eprotect.js +222 -0
  50. package/dist/hosted-form.d.ts +3 -2
  51. package/dist/hosted-form.js +3 -2
  52. package/dist/index.d.ts +17 -3
  53. package/dist/index.js +6 -1
  54. package/dist/juspay-gate.d.ts +82 -0
  55. package/dist/juspay-gate.js +126 -0
  56. package/dist/juspay.generated.d.ts +89 -0
  57. package/dist/juspay.generated.js +705 -0
  58. package/dist/lifecycle.d.ts +60 -6
  59. package/dist/lifecycle.js +122 -8
  60. package/dist/merchant-card-endpoints.generated.d.ts +122 -0
  61. package/dist/merchant-card-endpoints.generated.js +4261 -0
  62. package/dist/merchant-hosted.d.ts +4 -1
  63. package/dist/merchant-hosted.js +5 -2
  64. package/dist/merchant-session-probe.d.ts +48 -0
  65. package/dist/merchant-session-probe.js +53 -0
  66. package/dist/merchant-session.generated.d.ts +69 -0
  67. package/dist/merchant-session.generated.js +919 -0
  68. package/dist/payrails.d.ts +133 -0
  69. package/dist/payrails.generated.d.ts +73 -0
  70. package/dist/payrails.generated.js +596 -0
  71. package/dist/payrails.js +305 -0
  72. package/dist/pelecard.d.ts +32 -0
  73. package/dist/pelecard.generated.d.ts +32 -0
  74. package/dist/pelecard.generated.js +283 -0
  75. package/dist/pelecard.js +42 -0
  76. package/dist/pre-claim.d.ts +81 -2
  77. package/dist/pre-claim.js +1023 -5
  78. package/dist/preflight-catalog.json +2267 -288
  79. package/dist/preflight-schemas.json +183 -6
  80. package/dist/preflight.generated.d.ts +1 -1
  81. package/dist/preflight.generated.js +243 -4
  82. package/dist/preparation.d.ts +12 -3
  83. package/dist/preparation.js +63 -12
  84. package/dist/prepared-processor.d.ts +84 -5
  85. package/dist/prepared-processor.js +147 -6
  86. package/dist/primer.d.ts +53 -0
  87. package/dist/primer.generated.d.ts +38 -0
  88. package/dist/primer.generated.js +624 -0
  89. package/dist/primer.js +82 -0
  90. package/dist/registry.d.ts +100 -8
  91. package/dist/registry.js +85 -11
  92. package/dist/shiji-sts.d.ts +144 -0
  93. package/dist/shiji-sts.generated.d.ts +72 -0
  94. package/dist/shiji-sts.generated.js +913 -0
  95. package/dist/shiji-sts.js +222 -0
  96. package/dist/stripe-checkout.generated.d.ts +1 -1
  97. package/dist/stripe-checkout.generated.js +764 -9
  98. package/dist/stripe-setup.d.ts +16 -0
  99. package/dist/stripe-setup.js +61 -0
  100. package/dist/substitutions.generated.js +187 -29
  101. package/dist/verifone.d.ts +68 -0
  102. package/dist/verifone.generated.d.ts +51 -0
  103. package/dist/verifone.generated.js +1413 -0
  104. package/dist/verifone.js +119 -0
  105. package/dist/voice-types.d.ts +27 -0
  106. package/dist/voice.d.ts +6 -1
  107. package/dist/voice.js +6 -1
  108. package/dist/wspay.d.ts +41 -0
  109. package/dist/wspay.generated.d.ts +41 -0
  110. package/dist/wspay.generated.js +370 -0
  111. package/dist/wspay.js +65 -0
  112. package/examples/preflight/kernel-native/inventory.json +1 -1
  113. package/package.json +3 -3
@@ -0,0 +1,426 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: 0179250ba6f44c33967a08dc4ce3a891d8b73f0f7eb60e9cab34f801dc2a4c57
4
+ // src/datatrans.js
5
+ var DATATRANS_HOSTS = Object.freeze(["pay.datatrans.com", "pay.sandbox.datatrans.com"]);
6
+ var DATATRANS_TOKENIZE_PATH = "/upp/payment/SecureFields/paymentField";
7
+ var DATATRANS_REFUSAL_REASONS = Object.freeze(["unsupported", "card_on_file", "brand_mismatch", "unregistered_merchant"]);
8
+ var DATATRANS_INVALID_REQUEST = "invalid_datatrans_request";
9
+ var DATATRANS_CARD_ON_FILE_REFUSED = "datatrans_card_on_file_refused";
10
+ var DATATRANS_REGISTRATION_POLICIES = Object.freeze(["refuse", "disclose", "agentcard_single_use"]);
11
+ var DATATRANS_REGISTRATION_POLICY = "refuse";
12
+ var DATATRANS_REVIEWED_MERCHANT_IDS = Object.freeze({
13
+ "pay.datatrans.com": Object.freeze([]),
14
+ "pay.sandbox.datatrans.com": Object.freeze(["1110022968", "1100007006"])
15
+ });
16
+ var DATATRANS_PAYMENT_METHODS = Object.freeze(["VIS", "ECA", "AMX", "DIN", "DIS", "JCB", "CUP", "MAU", "UAP", "DNK"]);
17
+ var DATATRANS_TEST_PANS = Object.freeze([
18
+ "4242424242424242",
19
+ "4900000000000086",
20
+ "4900000000000003",
21
+ "5404000000000001",
22
+ "5200000000000007",
23
+ "5200000000000080",
24
+ "375811111111115",
25
+ "375000000000007",
26
+ "375811111111123",
27
+ "36168002586009",
28
+ "36167719110012",
29
+ "6011000000000004",
30
+ "3569990010030442",
31
+ "3569990010030400",
32
+ "6362970000457013",
33
+ "3841000000000007",
34
+ "6200104444444447",
35
+ "6210946888060008",
36
+ "6759000000000018",
37
+ "6759000000000026",
38
+ "4000001000000034",
39
+ "4000001000000018",
40
+ "4000001000000042",
41
+ "4000001000000026",
42
+ "5100001000000022",
43
+ "5100001000000048",
44
+ "5100001000000014",
45
+ "5100001000000030",
46
+ "340000100000024",
47
+ "340000100000040",
48
+ "340000100000016",
49
+ "340000100000032",
50
+ "36000010000022",
51
+ "3550998599961037"
52
+ ]);
53
+ var UNSUPPORTED_REFUSAL = "Refusing to pay: unsupported Datatrans card request.";
54
+ var CARD_ON_FILE_REFUSAL = "Refusing to pay: this Datatrans payment would let the merchant keep your card on file, which needs your consent first. Nothing was sent.";
55
+ var BRAND_MISMATCH_REFUSAL = "Refusing to pay: this Datatrans payment names a card brand that is not your card's. Nothing was sent.";
56
+ var UNREGISTERED_MERCHANT_REFUSAL = "Refusing to pay: this Datatrans payment goes to a merchant account Agentcard has not reviewed. Nothing was sent.";
57
+ var FILL_REFUSAL = "Could not fill the card fields for Datatrans. Nothing was sent.";
58
+ var BODY_LIMIT = 8192;
59
+ var RESPONSE_LIMIT = 4096;
60
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
61
+ var owns = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
62
+ var exact = (pattern, value) => typeof value === "string" && pattern.exec(value)?.[0] === value;
63
+ var text = (value, max) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
64
+ var digits = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max && !/[^0-9]/.test(value);
65
+ var panLike = (value) => (value.match(/\p{Nd}/gu) ?? []).length >= 12;
66
+ var plain = (value, max) => text(value, max) && !panLike(value);
67
+ function encodeDatatransFormValue(value) {
68
+ return encodeURIComponent(String(value)).replace(/%20/g, "+");
69
+ }
70
+ function decodeFormComponent(raw) {
71
+ try {
72
+ return decodeURIComponent(raw.replace(/\+/g, " "));
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ function parseDatatransForm(body) {
79
+ if (typeof body !== "string" || body === "" || body.length > BODY_LIMIT)
80
+ return null;
81
+ const pairs = [];
82
+ const seen = /* @__PURE__ */ new Set();
83
+ for (const chunk of body.split("&")) {
84
+ const eq = chunk.indexOf("=");
85
+ if (eq < 1)
86
+ return null;
87
+ const rawKey = chunk.slice(0, eq), raw = chunk.slice(eq + 1);
88
+ const key = decodeFormComponent(rawKey), value = decodeFormComponent(raw);
89
+ if (key === null || value === null || seen.has(key))
90
+ return null;
91
+ try {
92
+ if (encodeDatatransFormValue(key) !== rawKey || encodeDatatransFormValue(value) !== raw)
93
+ return null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ seen.add(key);
99
+ pairs.push({ key, value, rawKey, raw });
100
+ }
101
+ return pairs;
102
+ }
103
+ function serializeDatatransForm(pairs) {
104
+ return pairs.map(({ key, value }) => `${encodeDatatransFormValue(key)}=${encodeDatatransFormValue(value)}`).join("&");
105
+ }
106
+ function isDatatransTokenizeUrl(url, method = "POST") {
107
+ if (method !== "POST" || typeof url !== "string" || url.length > 256 || url.includes("?") || url.includes("#"))
108
+ return false;
109
+ let parsed;
110
+ try {
111
+ parsed = new URL(url);
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ return parsed.href === url && parsed.protocol === "https:" && !parsed.port && !parsed.username && !parsed.password && DATATRANS_HOSTS.includes(parsed.hostname) && parsed.pathname === DATATRANS_TOKENIZE_PATH;
117
+ }
118
+ function onPaymentFieldPath(url) {
119
+ if (typeof url !== "string" || url.length > 2048)
120
+ return false;
121
+ try {
122
+ const parsed = new URL(url);
123
+ return DATATRANS_HOSTS.includes(parsed.hostname.replace(/\.$/, "")) && parsed.pathname.toLowerCase() === DATATRANS_TOKENIZE_PATH.toLowerCase();
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ var KEYS = /* @__PURE__ */ new Set([
130
+ "mode",
131
+ "formId",
132
+ "cardNumber",
133
+ "cvv",
134
+ "iban",
135
+ "accountNumber",
136
+ "branchCode",
137
+ "paymentMethod",
138
+ "countryCode",
139
+ "merchantId",
140
+ "expy",
141
+ "expm",
142
+ "email",
143
+ "phoneNumber",
144
+ "usage",
145
+ "3D",
146
+ "browserUserAgent",
147
+ "browserJavaEnabled",
148
+ "browserLanguage",
149
+ "browserColorDepth",
150
+ "browserScreenHeight",
151
+ "browserScreenWidth",
152
+ "browserTZ"
153
+ ]);
154
+ var REQUIRED = ["mode", "formId", "cardNumber", "paymentMethod", "merchantId", "expy", "expm"];
155
+ var EMPTY_ONLY = /* @__PURE__ */ new Set(["iban", "accountNumber", "branchCode", "countryCode", "3D"]);
156
+ var RULES = {
157
+ mode: (value) => value === "TOKENIZE",
158
+ formId: (value) => exact(/^[0-9]{18}$/, value),
159
+ cardNumber: (value) => digits(value, 12, 19),
160
+ cvv: (value) => value === "" || digits(value, 3, 4),
161
+ paymentMethod: (value) => DATATRANS_PAYMENT_METHODS.includes(value),
162
+ merchantId: (value) => exact(/^[0-9]{10}$/, value),
163
+ expm: (value) => exact(/^(?:0?[1-9]|1[0-2])$/, value),
164
+ expy: (value) => exact(/^(?:[0-9]{2}|20[0-9]{2})$/, value),
165
+ usage: (value) => value === "" || value === "SIMPLE" || value === "REGISTRATION",
166
+ email: (value) => value === "" || text(value, 254) && exact(/^[^\s@]{1,64}@[^\s@]{1,189}$/, value),
167
+ phoneNumber: (value) => value === "" || exact(/^\+?[0-9 ()./-]{1,32}$/, value),
168
+ browserUserAgent: (value) => value !== "" && text(value, 1024),
169
+ browserJavaEnabled: (value) => value === "true" || value === "false",
170
+ browserLanguage: (value) => value === "" || exact(/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8}){0,4}$/, value),
171
+ browserColorDepth: (value) => exact(/^[0-9]{1,3}$/, value),
172
+ browserScreenHeight: (value) => exact(/^[0-9]{1,5}$/, value),
173
+ browserScreenWidth: (value) => exact(/^[0-9]{1,5}$/, value),
174
+ browserTZ: (value) => exact(/^-?[0-9]{1,4}$/, value)
175
+ };
176
+ function judgeBody(body) {
177
+ const pairs = parseDatatransForm(body);
178
+ if (!pairs)
179
+ return null;
180
+ const fields = new Map(pairs.map(({ key, value }) => [key, value]));
181
+ for (const [key, value] of fields) {
182
+ if (!KEYS.has(key))
183
+ return null;
184
+ if (EMPTY_ONLY.has(key) ? value !== "" : !RULES[key](value))
185
+ return null;
186
+ }
187
+ if (!REQUIRED.every((key) => fields.has(key)))
188
+ return null;
189
+ return { pairs, fields, usage: fields.get("usage") ?? "" };
190
+ }
191
+ function datatransRegistrationRefusal(usage, card, policy = DATATRANS_REGISTRATION_POLICY) {
192
+ if (usage !== "REGISTRATION")
193
+ return null;
194
+ if (policy !== "disclose" && policy !== "agentcard_single_use")
195
+ return "card_on_file";
196
+ if (card === void 0)
197
+ return null;
198
+ if (!record(card) || card.cardOnFileConsent !== true)
199
+ return "card_on_file";
200
+ return policy === "agentcard_single_use" && card.agentcardSingleUse !== true ? "card_on_file" : null;
201
+ }
202
+ function datatransCardOnFileDisclosure(body, policy = DATATRANS_REGISTRATION_POLICY) {
203
+ const judged = judgeBody(body);
204
+ return judged !== null && judged.usage === "REGISTRATION" && (policy === "disclose" || policy === "agentcard_single_use");
205
+ }
206
+ function datatransBodyRefusal(body, card, policy = DATATRANS_REGISTRATION_POLICY) {
207
+ const judged = judgeBody(body);
208
+ if (!judged)
209
+ return "unsupported";
210
+ return datatransRegistrationRefusal(judged.usage, card, policy);
211
+ }
212
+ function datatransMerchantId(body) {
213
+ const judged = judgeBody(body);
214
+ return judged ? judged.fields.get("merchantId") : null;
215
+ }
216
+ function carriesCard(body) {
217
+ if (typeof body !== "string" || body === "")
218
+ return false;
219
+ for (const chunk of body.split("&")) {
220
+ const eq = chunk.indexOf("=");
221
+ if (eq <= 0 || eq === chunk.length - 1)
222
+ continue;
223
+ if (decodeFormComponent(chunk.slice(0, eq)) === "cardNumber")
224
+ return true;
225
+ }
226
+ return false;
227
+ }
228
+ var CONTENT_TYPE = /^application\/x-www-form-urlencoded(?:;\s*charset=utf-8)?$/i;
229
+ function headersRefusal(headers) {
230
+ if (!record(headers))
231
+ return "unsupported";
232
+ const folded = /* @__PURE__ */ new Map();
233
+ for (const [name, value] of Object.entries(headers)) {
234
+ const lower = name.toLowerCase();
235
+ if (folded.has(lower) || typeof value !== "string")
236
+ return "unsupported";
237
+ folded.set(lower, value);
238
+ }
239
+ return exact(CONTENT_TYPE, folded.get("content-type")) ? null : "unsupported";
240
+ }
241
+ function classifyDatatransRequest(url, method, body, headers, policy = DATATRANS_REGISTRATION_POLICY) {
242
+ if (method !== "POST" || !onPaymentFieldPath(url) || !carriesCard(body))
243
+ return null;
244
+ if (!isDatatransTokenizeUrl(url))
245
+ return "unsupported";
246
+ return headersRefusal(headers) ?? datatransBodyRefusal(body, void 0, policy) ?? "card";
247
+ }
248
+ function datatransTokenizeRefusal(request, card, policy = DATATRANS_REGISTRATION_POLICY) {
249
+ if (!record(request) || request.method !== "POST" || !isDatatransTokenizeUrl(request.url))
250
+ return "unsupported";
251
+ return headersRefusal(request.headers) ?? datatransBodyRefusal(request.body, card, policy);
252
+ }
253
+ function isDatatransTokenizeRequest(request, card) {
254
+ return datatransTokenizeRefusal(request, card) === null;
255
+ }
256
+ function datatransMerchantRefusal(url, body, merchants = DATATRANS_REVIEWED_MERCHANT_IDS) {
257
+ const merchantId = datatransMerchantId(body);
258
+ let host = "";
259
+ try {
260
+ host = new URL(url).hostname;
261
+ }
262
+ catch {
263
+ return "unregistered_merchant";
264
+ }
265
+ const listed = record(merchants) && owns(merchants, host) && Array.isArray(merchants[host]) ? merchants[host] : [];
266
+ return merchantId !== null && listed.includes(merchantId) ? null : "unregistered_merchant";
267
+ }
268
+ function datatransDeviceRefusal(request, card, policy = DATATRANS_REGISTRATION_POLICY, merchants = DATATRANS_REVIEWED_MERCHANT_IDS) {
269
+ const refusal = datatransTokenizeRefusal(request, card, policy);
270
+ if (refusal)
271
+ return refusal;
272
+ if (record(card) && digits(card.number, 12, 19) && datatransBrandRefusal(request.body, card) !== null)
273
+ return "brand_mismatch";
274
+ return datatransMerchantRefusal(request.url, request.body, merchants);
275
+ }
276
+ function datatransRefusalMessage(reason) {
277
+ if (reason === "card_on_file")
278
+ return CARD_ON_FILE_REFUSAL;
279
+ if (reason === "brand_mismatch")
280
+ return BRAND_MISMATCH_REFUSAL;
281
+ if (reason === "unregistered_merchant")
282
+ return UNREGISTERED_MERCHANT_REFUSAL;
283
+ return UNSUPPORTED_REFUSAL;
284
+ }
285
+ function datatransCardBrand(number) {
286
+ if (!digits(number, 12, 19))
287
+ return null;
288
+ const two = Number(number.slice(0, 2));
289
+ const four = Number(number.slice(0, 4));
290
+ if (number[0] === "4")
291
+ return "VIS";
292
+ if (two >= 51 && two <= 55 || four >= 2221 && four <= 2720)
293
+ return "ECA";
294
+ if (two === 34 || two === 37)
295
+ return "AMX";
296
+ return null;
297
+ }
298
+ function datatransBrandRefusal(body, card) {
299
+ const judged = judgeBody(body);
300
+ if (!judged)
301
+ return "unsupported";
302
+ const brand = datatransCardBrand(card && card.number);
303
+ return brand !== null && brand === judged.fields.get("paymentMethod") ? null : "brand_mismatch";
304
+ }
305
+ function injectDatatransCard(body, card, policy = DATATRANS_REGISTRATION_POLICY) {
306
+ const judged = judgeBody(body);
307
+ if (!judged)
308
+ throw new Error(UNSUPPORTED_REFUSAL);
309
+ if (datatransRegistrationRefusal(judged.usage, card ?? null, policy) !== null)
310
+ throw new Error(CARD_ON_FILE_REFUSAL);
311
+ const month = card && (Number.isInteger(card.month) || digits(card.month, 1, 2)) ? Number(card.month) : NaN;
312
+ const year = card && (Number.isInteger(card.year) || digits(card.year, 2, 4)) ? Number(card.year) : NaN;
313
+ if (!card || !digits(card.number, 12, 19) || !(month >= 1 && month <= 12) || !(year >= 0 && year <= 99 || year >= 2e3 && year <= 2099)) {
314
+ throw new Error(FILL_REFUSAL);
315
+ }
316
+ if (datatransBrandRefusal(body, card) !== null)
317
+ throw new Error(BRAND_MISMATCH_REFUSAL);
318
+ const fields = judged.fields;
319
+ if (!fields.get("cvv") || !digits(card.cvc, 3, 4))
320
+ throw new Error(FILL_REFUSAL);
321
+ const yy = String(year % 100).padStart(2, "0");
322
+ const values = {
323
+ cardNumber: card.number,
324
+ cvv: card.cvc,
325
+ expm: fields.get("expm").length === 2 ? String(month).padStart(2, "0") : String(month),
326
+ expy: fields.get("expy").length === 4 ? `20${yy}` : yy
327
+ };
328
+ const written = judged.pairs.map((pair) => owns(values, pair.key) ? `${pair.rawKey}=${encodeDatatransFormValue(values[pair.key])}` : `${pair.rawKey}=${pair.raw}`);
329
+ return { body: written.join("&"), where: ["cardNumber", "cvv", "expm", "expy"] };
330
+ }
331
+ var TRANSACTION_ID = /^[0-9]{18}$/;
332
+ var CARD_WORD = /^[A-Za-z][A-Za-z _-]{0,31}$/;
333
+ var COUNTRY = /^[A-Z]{2}$/;
334
+ var CARD_INFO_KEYS = ["brand", "type", "usage", "country", "issuer", "coBrand"];
335
+ var UNFOLLOWED_KEYS = ["tds2EnrollmentResult", "redirect", "dcc", "mcp"];
336
+ var ERROR_CODES = [
337
+ [/^Invalid merchantId$/, "invalid_merchant_id"],
338
+ [/^Invalid merchant setup\b/, "invalid_merchant_setup"],
339
+ [/^Invalid field$/, "invalid_field"],
340
+ [/^Field .{1,64} not allowed for (?:alias version|mode) /, "field_not_allowed"],
341
+ [/^Invalid session$/, "invalid_session"],
342
+ [/^Too many requests$/, "too_many_requests"]
343
+ ];
344
+ function readAnswer(body) {
345
+ if (typeof body !== "string" || body === "" || body.length > RESPONSE_LIMIT)
346
+ return null;
347
+ try {
348
+ const value = JSON.parse(body);
349
+ return record(value) ? value : null;
350
+ }
351
+ catch {
352
+ return null;
353
+ }
354
+ }
355
+ var optionalText = (value, pattern) => value === null || exact(pattern, value);
356
+ function rebuildCardInfo(info) {
357
+ if (!record(info) || !Object.keys(info).every((key) => CARD_INFO_KEYS.includes(key)) || !exact(CARD_WORD, info.brand))
358
+ return null;
359
+ if (owns(info, "type") && !optionalText(info.type, CARD_WORD) || owns(info, "usage") && !optionalText(info.usage, CARD_WORD) || owns(info, "country") && !optionalText(info.country, COUNTRY) || owns(info, "issuer") && info.issuer !== null && !plain(info.issuer, 128) || owns(info, "coBrand") && info.coBrand !== null && !plain(info.coBrand, 64))
360
+ return null;
361
+ const safe = {};
362
+ for (const key of CARD_INFO_KEYS)
363
+ if (owns(info, key))
364
+ safe[key] = info[key];
365
+ return safe;
366
+ }
367
+ function rebuildSuccess(value) {
368
+ if (Object.keys(value).length !== 2 || !exact(TRANSACTION_ID, value.transactionId))
369
+ return null;
370
+ const cardInfo = rebuildCardInfo(value.cardInfo);
371
+ return cardInfo ? { transactionId: value.transactionId, cardInfo } : null;
372
+ }
373
+ function rebuildError(value) {
374
+ return Object.keys(value).length === 1 && plain(value.error, 256) && value.error.trim() !== "" ? { error: value.error } : null;
375
+ }
376
+ function datatransErrorCode(message) {
377
+ for (const [pattern, code] of ERROR_CODES)
378
+ if (typeof message === "string" && pattern.test(message))
379
+ return code;
380
+ return "datatrans_error";
381
+ }
382
+ function datatransResponseOutcome(status, body, card) {
383
+ if (status === 408 || status === 429)
384
+ return { outcome: "unknown", reason: "datatrans_status_unconfirmed" };
385
+ if (status >= 400 && status < 500)
386
+ return { outcome: "refused", code: `http_${status}` };
387
+ if (status !== 200)
388
+ return { outcome: "unknown", reason: "datatrans_status_unconfirmed" };
389
+ const value = readAnswer(body);
390
+ if (!value)
391
+ return { outcome: "unknown", reason: "datatrans_unreadable" };
392
+ if (owns(value, "error")) {
393
+ const error = rebuildError(value);
394
+ if (!error)
395
+ return { outcome: "unknown", reason: "datatrans_unexpected_response" };
396
+ const code = datatransErrorCode(error.error);
397
+ return code === "too_many_requests" ? { outcome: "unknown", reason: "datatrans_repeat_request" } : { outcome: "refused", code };
398
+ }
399
+ if (UNFOLLOWED_KEYS.some((key) => owns(value, key)))
400
+ return { outcome: "unknown", reason: "datatrans_unsupported_flow" };
401
+ const safe = rebuildSuccess(value);
402
+ if (!safe)
403
+ return { outcome: "unknown", reason: "datatrans_unexpected_response" };
404
+ if (card && digits(card.number, 12, 19) && safe.transactionId.includes(card.number)) {
405
+ return { outcome: "unknown", reason: "datatrans_token_mismatch" };
406
+ }
407
+ return { outcome: "approved" };
408
+ }
409
+ function sanitizeDatatransTokenResponse(body) {
410
+ const value = readAnswer(body);
411
+ const fail = () => {
412
+ throw new Error("Invalid Datatrans tokenization response.");
413
+ };
414
+ if (!value)
415
+ return fail();
416
+ if (owns(value, "error"))
417
+ return JSON.stringify(rebuildError(value) ?? fail());
418
+ if (UNFOLLOWED_KEYS.some((key) => owns(value, key)))
419
+ return fail();
420
+ return JSON.stringify(rebuildSuccess(value) ?? fail());
421
+ }
422
+ function datatransFacts(status, body) {
423
+ const verdict = datatransResponseOutcome(status, body);
424
+ return { outcome: verdict.outcome, refusalCode: verdict.outcome === "refused" ? verdict.code : null };
425
+ }
426
+ export { DATATRANS_CARD_ON_FILE_REFUSED, DATATRANS_HOSTS, DATATRANS_INVALID_REQUEST, DATATRANS_PAYMENT_METHODS, DATATRANS_REFUSAL_REASONS, DATATRANS_REGISTRATION_POLICIES, DATATRANS_REGISTRATION_POLICY, DATATRANS_REVIEWED_MERCHANT_IDS, DATATRANS_TEST_PANS, DATATRANS_TOKENIZE_PATH, classifyDatatransRequest, datatransBodyRefusal, datatransBrandRefusal, datatransCardBrand, datatransCardOnFileDisclosure, datatransDeviceRefusal, datatransErrorCode, datatransFacts, datatransMerchantId, datatransMerchantRefusal, datatransRefusalMessage, datatransRegistrationRefusal, datatransResponseOutcome, datatransTokenizeRefusal, encodeDatatransFormValue, injectDatatransCard, isDatatransTokenizeRequest, isDatatransTokenizeUrl, parseDatatransForm, sanitizeDatatransTokenResponse, serializeDatatransForm };
@@ -0,0 +1,178 @@
1
+ // Datatrans / PCI Proxy Secure Fields 2.0.0 in TOKENIZE mode (psp `datatrans`), on
2
+ // pay.datatrans.com (live) and pay.sandbox.datatrans.com (sandbox): the card form British Airways's
3
+ // Nexus checkout loads. Datatrans's card-number iframe reads the CVV from its same-origin CVV frame
4
+ // and sends the whole card in one same-origin XHR, POST /upp/payment/SecureFields/paymentField,
5
+ // form encoded. This module holds what the SDK decides about that iframe's requests. The rules
6
+ // themselves are payment-core's (datatrans.generated.ts): the API applies the same ones at create,
7
+ // and the cardholder's device applies them again before it writes a card. The one rule meant to
8
+ // change after a build ships, the card-on-file policy, is never read from that copy: the API serves
9
+ // the policy its create applies with the recognizer (Recognizer.cardOnFile), and this build follows
10
+ // it at each sync.
11
+ //
12
+ // The processor ships dark. The API serves the datatrans recognizer only to a build that declares
13
+ // the `datatrans_secure_fields_tokenize` capability (client.ts SUPPORTED_CAPABILITIES) and only to
14
+ // an org Agentcard enabled for it, so an SDK the API never served it leaves every Datatrans request
15
+ // alone.
16
+ import { DATATRANS_CARD_ON_FILE_REFUSED, DATATRANS_HOSTS, DATATRANS_INVALID_REQUEST, DATATRANS_REGISTRATION_POLICIES, classifyDatatransRequest, datatransResponseOutcome, isDatatransTokenizeUrl, parseDatatransForm, } from './datatrans.generated.js';
17
+ export const DATATRANS_PSP = 'datatrans';
18
+ /** The dark-launch capability the API serves the datatrans recognizer behind. */
19
+ export const DATATRANS_CAPABILITY = 'datatrans_secure_fields_tokenize';
20
+ /** True when the URL's host is one of Datatrans's two pay hosts. */
21
+ export function isDatatransHost(url) {
22
+ try {
23
+ return DATATRANS_HOSTS.includes(new URL(url).hostname);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * The card-on-file policy this build applies to a Datatrans card post: the one the API served with
31
+ * the datatrans recognizer (Recognizer.cardOnFile, VaultClient.cardOnFilePolicy) when payment-core
32
+ * defines it (`refuse`, `disclose`, `agentcard_single_use`), and `refuse` for anything else, an entry
33
+ * that names none included. The decision is payment-core's DATATRANS_REGISTRATION_POLICY, which the
34
+ * API reads each time it serves the catalog and again at create, so a change to that one line
35
+ * reaches every build already published at its next syncRegistry(), with no SDK release.
36
+ */
37
+ export function datatransCardOnFilePolicy(served) {
38
+ return typeof served === 'string' && DATATRANS_REGISTRATION_POLICIES.includes(served) ? served : 'refuse';
39
+ }
40
+ /**
41
+ * The pause decision for a POST the registry recognized on a Datatrans pay host, by payment-core's
42
+ * classifyDatatransRequest under the card-on-file policy the API served (datatransCardOnFilePolicy):
43
+ * 'card' the exact card URL with jQuery's form type and a sound TOKENIZE card body: it pauses. A
44
+ * usage=REGISTRATION post is one too once the served policy allows card on file, and the
45
+ * API and the cardholder's device then apply that policy;
46
+ * a DatatransRefusalReason: it is refused in the browser;
47
+ * null no card in the body (a wallet, bank-account or CVV-only post) or not the card path:
48
+ * the SDK's ordinary handling decides, and such a post continues untouched.
49
+ */
50
+ export function datatransPauseDecision(request, servedPolicy) {
51
+ const verdict = classifyDatatransRequest(request.url, 'POST', request.body ?? '', request.headers ?? {}, datatransCardOnFilePolicy(servedPolicy));
52
+ if (verdict === 'card')
53
+ return 'card';
54
+ if (verdict === 'card_on_file')
55
+ return DATATRANS_CARD_ON_FILE_REFUSED;
56
+ return verdict === null ? null : DATATRANS_INVALID_REQUEST;
57
+ }
58
+ // ── The repeat rule ─────────────────────────────────────────────────────────
59
+ // British Airways's Nexus page tokenizes on blur, not on Pay: it submits the Secure Fields form
60
+ // whenever a valid field loses focus, on autofill and whenever the expiry changes, and field.js
61
+ // itself answers only a byte-identical submit from its cache. Without this rule each of those posts
62
+ // would be a new approval on the cardholder's phone for a card the phone already tokenized into the
63
+ // same form. So once an approved answer is handed to a card post, a later card post from the same
64
+ // frame that differs from it only in the expiry (expm, expy) and the browser details (browser*) is
65
+ // answered here, with that stored answer: no create, no prompt, no second replay. Those are the
66
+ // fields the design lets a re-submit change: the device writes the Vault card's own expiry, and the
67
+ // browser details describe the window, not the card. Every other field must match: the formId, the
68
+ // merchantId, the placeholder card number and CVV, the brand, usage, and the traveller's email and
69
+ // phone number, which Nexus sends for Amex and which reach Datatrans with the real card. A new
70
+ // formId (the iframe reloaded), another placeholder, other contact data or another frame is a new
71
+ // question. The answer also belongs to one approval's handoff: the gate in pre-claim.ts hands it
72
+ // out only while that approval is the attachment's current one, so the order completing or failing,
73
+ // a latched attachment and a new authorization all end it. The post names no amount and no order, so
74
+ // once the answer reached the page the gate asks the application's merchant resolver before each
75
+ // repeat and answers only while the order is still pending; an attachment with no resolver answers no
76
+ // repeat after the handoff. The answer also ends once its form goes quiet (DATATRANS_REPEAT_IDLE_MS).
77
+ /**
78
+ * How long an approved Datatrans answer is replayed to its own frame: the token's own lifetime
79
+ * (PCI Proxy keeps a tokenization readable for 30 minutes). Past it the stored transactionId names a
80
+ * token the merchant can no longer read, so a later post is a new question.
81
+ */
82
+ export const DATATRANS_REPEAT_WINDOW_MS = 30 * 60_000;
83
+ /**
84
+ * How long a stored answer waits for its form's next post. British Airways re-submits while the agent
85
+ * finishes the card form, right after the approved answer. A form that posts nothing for this long has
86
+ * moved on: the order was placed or abandoned, which the SDK cannot tell apart without reconcile(). Its
87
+ * next post is a new approval. Each answered re-submit restarts the wait, and the token's lifetime
88
+ * still ends every answer.
89
+ */
90
+ export const DATATRANS_REPEAT_IDLE_MS = 5 * 60_000;
91
+ /** The most answers one attachment keeps; the oldest goes first. */
92
+ const MAX_REMEMBERED = 8;
93
+ /** The fields a re-submit may change and still be the same question: the expiry and the browser details. */
94
+ const REPEAT_FREE_FIELDS = new Set(['expm', 'expy', 'browserUserAgent', 'browserJavaEnabled',
95
+ 'browserLanguage', 'browserColorDepth', 'browserScreenHeight', 'browserScreenWidth', 'browserTZ']);
96
+ function repeatKey(url, body) {
97
+ if (!isDatatransTokenizeUrl(url) || typeof body !== 'string')
98
+ return null;
99
+ const pairs = parseDatatransForm(body);
100
+ if (!pairs)
101
+ return null;
102
+ const kept = pairs.filter(({ key }) => !REPEAT_FREE_FIELDS.has(key)).map(({ key, value }) => [key, value]);
103
+ const fields = new Map(kept);
104
+ if (!['formId', 'merchantId', 'cardNumber', 'paymentMethod'].every((name) => fields.get(name)))
105
+ return null;
106
+ return { url, fields: JSON.stringify(kept) };
107
+ }
108
+ function sameKey(a, b) {
109
+ return a.url === b.url && a.fields === b.fields;
110
+ }
111
+ /**
112
+ * The approved Datatrans answers one attachment handed to a card post, per frame. `frame` is the
113
+ * adapter's identity for the request's frame (the CDP frameId, the Playwright Frame); a request
114
+ * whose frame is unknown is never remembered and never answered. Only an answer payment-core reads
115
+ * as approved ({transactionId, cardInfo}) is kept: an error answer leaves the next post a new
116
+ * question, so the page can try again.
117
+ */
118
+ export class DatatransRepeats {
119
+ windowMs;
120
+ now;
121
+ idleMs;
122
+ // `at`: when the approved answer reached the page; `seen`: when its form last posted and was answered.
123
+ remembered = [];
124
+ constructor(windowMs = DATATRANS_REPEAT_WINDOW_MS, now = Date.now, idleMs = DATATRANS_REPEAT_IDLE_MS) {
125
+ this.windowMs = windowMs;
126
+ this.now = now;
127
+ this.idleMs = idleMs;
128
+ }
129
+ /**
130
+ * Keep the answer a token replay handed to this card post, when Datatrans approved it. Called
131
+ * after the answer reached the browser, so it never throws: a failure here only leaves the
132
+ * page's next post a new question.
133
+ */
134
+ remember(frame, url, body, replay) {
135
+ try {
136
+ if (frame === undefined || frame === null || frame === '')
137
+ return;
138
+ if (replay.mode === 'cse' || replay.mode === 'hosted_form')
139
+ return;
140
+ const key = repeatKey(url, body);
141
+ if (!key || datatransResponseOutcome(replay.status, replay.body, undefined).outcome !== 'approved')
142
+ return;
143
+ this.prune();
144
+ this.remembered = this.remembered.filter((entry) => !(entry.frame === frame && sameKey(entry.key, key)));
145
+ const at = this.now();
146
+ this.remembered.push({ frame, key, authorizationId: replay.authorizationId, status: replay.status,
147
+ headers: { ...replay.headers }, body: replay.body, at, seen: at });
148
+ if (this.remembered.length > MAX_REMEMBERED)
149
+ this.remembered.shift();
150
+ }
151
+ catch { /* see above: the next post is a new question */ }
152
+ }
153
+ /**
154
+ * The stored answer for a repeat of an answered card post from the same frame, or null. Only an
155
+ * answer from `authorizationId`, the approval the attachment is handing off now, is handed out,
156
+ * and every answer from another approval is dropped: a later authorization ends the window of
157
+ * every earlier one. An answer whose form stayed quiet past the idle bound is dropped too, and
158
+ * handing one out restarts that bound.
159
+ */
160
+ answerFor(frame, url, body, authorizationId) {
161
+ if (frame === undefined || frame === null || frame === '' || !authorizationId)
162
+ return null;
163
+ const key = repeatKey(url, body);
164
+ if (!key)
165
+ return null;
166
+ this.prune();
167
+ this.remembered = this.remembered.filter((entry) => entry.authorizationId === authorizationId);
168
+ const entry = this.remembered.find((candidate) => candidate.frame === frame && sameKey(candidate.key, key));
169
+ if (!entry)
170
+ return null;
171
+ entry.seen = this.now();
172
+ return { authorizationId: entry.authorizationId, response: { status: entry.status, headers: { ...entry.headers }, body: entry.body } };
173
+ }
174
+ prune() {
175
+ const now = this.now();
176
+ this.remembered = this.remembered.filter((entry) => now - entry.at < this.windowMs && now - entry.seen < this.idleMs);
177
+ }
178
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * NMI's Hosted Payment Page (formerly eKashu), as this SDK reads it.
3
+ *
4
+ * The merchant's page posts an entry form, top-level, to https://live.ekashu.com/ (test:
5
+ * test.ekashu.com), and NMI answers with its own card page. That entry carries no card, and it
6
+ * is the one request this SDK pauses: the cardholder's device repeats it over the end-to-end TLS
7
+ * relay and fills the card into the page NMI returns to it (payment-core processors/ekashu.js).
8
+ * Everything else the agent's browser posts to NMI carries a card the agent typed: the card page's
9
+ * own post (/index.php) and its 3-D Secure calls (ajax_3d_secure_v2_*.php). This SDK aborts every
10
+ * one of them, so a placeholder card never reaches NMI.
11
+ *
12
+ * The hosts are payment-core's EKASHU_HOSTS; payment-core.test.mjs pins the two lists together.
13
+ */
14
+ export declare const EKASHU_PSP = "ekashu";
15
+ export declare const EKASHU_HOSTS: readonly string[];
16
+ /** Whether this URL is on one of NMI's hosted payment page hosts (https, standard port, no credentials). */
17
+ export declare function isEkashuHost(url: string): boolean;
18
+ /**
19
+ * The only request on NMI's hosts that is the merchant's entry: a POST to the host's root, with no
20
+ * query. The entry the SDK pauses; every other POST there carries a card and is aborted.
21
+ */
22
+ export declare function isEkashuEntry(url: string, method: string): boolean;
23
+ /** The entry URL on the same NMI host as this URL, the one the registry recognizes when this build is armed for NMI. */
24
+ export declare function ekashuEntryUrl(url: string): string | null;
25
+ /**
26
+ * Whether an adapter must hand this request to the pre-claim gates: it is on one of NMI's hosts
27
+ * and this build's registry serves that host's entry. The card page's own post and its 3-D Secure
28
+ * calls are not the entry, so the registry's card-request test (anchored to the entry) does not
29
+ * name them; the CDP adapter still pauses them through the recognizer's host-wide URL patterns,
30
+ * and attachToPlaywright's route predicate asks this, so both adapters show the gates every
31
+ * request to an armed NMI host.
32
+ */
33
+ export declare function isArmedEkashuUrl(vault: {
34
+ isCardRequest(url: string, method?: string): boolean;
35
+ }, url: string): boolean;