@agent-cards/sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/PREFLIGHT.md +34 -0
- package/README.md +1217 -15
- package/dist/aci.d.ts +173 -0
- package/dist/aci.generated.d.ts +87 -0
- package/dist/aci.generated.js +306 -0
- package/dist/aci.js +371 -0
- package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
- package/dist/adyen-merchant-hosted.generated.js +283 -49
- package/dist/agentcard.d.ts +9 -2
- package/dist/agentcard.js +4 -2
- package/dist/amadeus-checkout.generated.d.ts +57 -0
- package/dist/amadeus-checkout.generated.js +549 -0
- package/dist/amadeus.d.ts +41 -0
- package/dist/amadeus.js +81 -0
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/card-endpoint.d.ts +177 -0
- package/dist/card-endpoint.js +286 -0
- package/dist/card-fields.generated.d.ts +5 -3
- package/dist/card-fields.generated.js +284 -7
- package/dist/cdp.d.ts +6 -1
- package/dist/cdp.js +983 -147
- package/dist/client.d.ts +493 -13
- package/dist/client.js +1100 -82
- package/dist/creditguard.generated.d.ts +79 -0
- package/dist/creditguard.generated.js +662 -0
- package/dist/cse-body.js +11 -1
- package/dist/cybersource-flex.d.ts +183 -0
- package/dist/cybersource-flex.generated.d.ts +92 -0
- package/dist/cybersource-flex.generated.js +689 -0
- package/dist/cybersource-flex.js +275 -0
- package/dist/datatrans.d.ts +96 -0
- package/dist/datatrans.generated.d.ts +64 -0
- package/dist/datatrans.generated.js +426 -0
- package/dist/datatrans.js +178 -0
- package/dist/elavon-converge.d.ts +37 -0
- package/dist/elavon-converge.generated.d.ts +49 -0
- package/dist/elavon-converge.generated.js +439 -0
- package/dist/elavon-converge.js +84 -0
- package/dist/eprotect.d.ts +152 -0
- package/dist/eprotect.generated.d.ts +77 -0
- package/dist/eprotect.generated.js +498 -0
- package/dist/eprotect.js +222 -0
- package/dist/gmo.d.ts +63 -0
- package/dist/gmo.generated.d.ts +60 -0
- package/dist/gmo.generated.js +586 -0
- package/dist/gmo.js +119 -0
- package/dist/hosted-form.d.ts +3 -2
- package/dist/hosted-form.js +3 -2
- package/dist/index.d.ts +19 -3
- package/dist/index.js +6 -1
- package/dist/juspay-gate.d.ts +82 -0
- package/dist/juspay-gate.js +126 -0
- package/dist/juspay.generated.d.ts +89 -0
- package/dist/juspay.generated.js +705 -0
- package/dist/lifecycle.d.ts +62 -6
- package/dist/lifecycle.js +123 -8
- package/dist/merchant-card-endpoints.generated.d.ts +63 -0
- package/dist/merchant-card-endpoints.generated.js +1103 -0
- package/dist/merchant-hosted.d.ts +4 -1
- package/dist/merchant-hosted.js +5 -2
- package/dist/merchant-session-probe.d.ts +48 -0
- package/dist/merchant-session-probe.js +53 -0
- package/dist/merchant-session.generated.d.ts +69 -0
- package/dist/merchant-session.generated.js +919 -0
- package/dist/payrails.d.ts +133 -0
- package/dist/payrails.generated.d.ts +73 -0
- package/dist/payrails.generated.js +596 -0
- package/dist/payrails.js +305 -0
- package/dist/pelecard.d.ts +32 -0
- package/dist/pelecard.generated.d.ts +32 -0
- package/dist/pelecard.generated.js +283 -0
- package/dist/pelecard.js +42 -0
- package/dist/pre-claim.d.ts +84 -3
- package/dist/pre-claim.js +987 -9
- package/dist/preflight-catalog.json +2310 -263
- package/dist/preflight-schemas.json +187 -6
- package/dist/preflight.generated.d.ts +1 -1
- package/dist/preflight.generated.js +247 -4
- package/dist/preparation.d.ts +5 -2
- package/dist/preparation.js +36 -10
- package/dist/prepared-processor.d.ts +86 -5
- package/dist/prepared-processor.js +157 -6
- package/dist/primer.d.ts +53 -0
- package/dist/primer.generated.d.ts +38 -0
- package/dist/primer.generated.js +624 -0
- package/dist/primer.js +82 -0
- package/dist/registry.d.ts +114 -12
- package/dist/registry.js +85 -11
- package/dist/shiji-sts.d.ts +144 -0
- package/dist/shiji-sts.generated.d.ts +72 -0
- package/dist/shiji-sts.generated.js +913 -0
- package/dist/shiji-sts.js +222 -0
- package/dist/stripe-checkout.generated.d.ts +1 -1
- package/dist/stripe-checkout.generated.js +735 -10
- package/dist/stripe-setup.d.ts +16 -0
- package/dist/stripe-setup.js +61 -0
- package/dist/substitutions.generated.js +187 -29
- package/dist/verifone.d.ts +68 -0
- package/dist/verifone.generated.d.ts +51 -0
- package/dist/verifone.generated.js +1401 -0
- package/dist/verifone.js +119 -0
- package/dist/voice-types.d.ts +27 -0
- package/dist/voice.d.ts +6 -1
- package/dist/voice.js +6 -1
- package/dist/wspay.d.ts +41 -0
- package/dist/wspay.generated.d.ts +41 -0
- package/dist/wspay.generated.js +370 -0
- package/dist/wspay.js +65 -0
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: 0149082f3796b4b5bbe843a6ab8e4f34f551de49e3edfc69ccf66d3cba1ba272
|
|
4
|
+
// src/json-path.js
|
|
5
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
6
|
+
function isPlainObject(v) {
|
|
7
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
8
|
+
}
|
|
9
|
+
function jsonAt(root, path) {
|
|
10
|
+
let node = root;
|
|
11
|
+
for (const step of path) {
|
|
12
|
+
if (typeof step === "string") {
|
|
13
|
+
if (!isPlainObject(node) || FORBIDDEN_KEYS.has(step) || !Object.prototype.hasOwnProperty.call(node, step))
|
|
14
|
+
return { found: false };
|
|
15
|
+
}
|
|
16
|
+
else if (!Array.isArray(node) || !Number.isSafeInteger(step) || step < 0 || step >= node.length) {
|
|
17
|
+
return { found: false };
|
|
18
|
+
}
|
|
19
|
+
node = node[step];
|
|
20
|
+
}
|
|
21
|
+
return { found: true, value: node };
|
|
22
|
+
}
|
|
23
|
+
function roundTrips(parsed, body) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(parsed) === body;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sameKeyIgnoringCase(key, step) {
|
|
32
|
+
return !FORBIDDEN_KEYS.has(key) && (key.toLowerCase() === step.toLowerCase() || key.toUpperCase() === step.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
function valuesIgnoringCase(root, path) {
|
|
35
|
+
let nodes = [root];
|
|
36
|
+
for (const step of path) {
|
|
37
|
+
const next = [];
|
|
38
|
+
for (const node of nodes) {
|
|
39
|
+
if (typeof step === "number") {
|
|
40
|
+
if (Array.isArray(node) && Number.isSafeInteger(step) && step >= 0 && step < node.length)
|
|
41
|
+
next.push(node[step]);
|
|
42
|
+
}
|
|
43
|
+
else if (isPlainObject(node)) {
|
|
44
|
+
for (const key of Object.keys(node))
|
|
45
|
+
if (sameKeyIgnoringCase(key, step))
|
|
46
|
+
next.push(node[key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
nodes = next;
|
|
50
|
+
}
|
|
51
|
+
return nodes;
|
|
52
|
+
}
|
|
53
|
+
function describePath(path) {
|
|
54
|
+
return path.length ? path.map((step) => typeof step === "number" ? `[${step}]` : `.${step}`).join("").replace(/^\./, "") : "(root)";
|
|
55
|
+
}
|
|
56
|
+
// src/url-template.js
|
|
57
|
+
var PLACEHOLDER_VALUE = /^[A-Za-z0-9._~@+-]{1,256}$/;
|
|
58
|
+
var placeholderValue = (value) => PLACEHOLDER_VALUE.test(value) && value !== "." && !value.includes("..");
|
|
59
|
+
function pathPlaceholders(template, pathname) {
|
|
60
|
+
if (typeof template !== "string" || typeof pathname !== "string")
|
|
61
|
+
return null;
|
|
62
|
+
if (!template.startsWith("/") || !pathname.startsWith("/"))
|
|
63
|
+
return null;
|
|
64
|
+
const want = template.split("/");
|
|
65
|
+
const got = pathname.split("/");
|
|
66
|
+
if (want.length !== got.length)
|
|
67
|
+
return null;
|
|
68
|
+
const values = {};
|
|
69
|
+
for (const [i, segment] of want.entries()) {
|
|
70
|
+
if (segment.startsWith(":")) {
|
|
71
|
+
if (!placeholderValue(got[i]))
|
|
72
|
+
return null;
|
|
73
|
+
values[segment.slice(1)] = got[i];
|
|
74
|
+
}
|
|
75
|
+
else if (segment !== got[i]) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return values;
|
|
80
|
+
}
|
|
81
|
+
function pathMatchesTemplate(template, pathname) {
|
|
82
|
+
return pathPlaceholders(template, pathname) !== null;
|
|
83
|
+
}
|
|
84
|
+
function queryPlaceholders(query, text) {
|
|
85
|
+
if (!query || typeof query !== "object" || Array.isArray(query))
|
|
86
|
+
return null;
|
|
87
|
+
const pairs = Object.entries(query);
|
|
88
|
+
if (pairs.length === 0)
|
|
89
|
+
return text === null ? {} : null;
|
|
90
|
+
if (typeof text !== "string")
|
|
91
|
+
return null;
|
|
92
|
+
const got = text.split("&");
|
|
93
|
+
if (got.length !== pairs.length)
|
|
94
|
+
return null;
|
|
95
|
+
const values = {};
|
|
96
|
+
for (const [i, [key, value]] of pairs.entries()) {
|
|
97
|
+
const mark = got[i].indexOf("=");
|
|
98
|
+
if (mark === -1 || got[i].slice(0, mark) !== key)
|
|
99
|
+
return null;
|
|
100
|
+
const written = got[i].slice(mark + 1);
|
|
101
|
+
if (typeof value !== "string")
|
|
102
|
+
return null;
|
|
103
|
+
if (value.startsWith(":")) {
|
|
104
|
+
if (!placeholderValue(written))
|
|
105
|
+
return null;
|
|
106
|
+
values[value.slice(1)] = written;
|
|
107
|
+
}
|
|
108
|
+
else if (written !== value) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return values;
|
|
113
|
+
}
|
|
114
|
+
// src/substitution-primitives.js
|
|
115
|
+
var SubstitutionError = class extends Error {
|
|
116
|
+
constructor(message) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "SubstitutionError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// src/deep-freeze.js
|
|
122
|
+
function deepFreeze(value) {
|
|
123
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
124
|
+
for (const child of Object.values(value))
|
|
125
|
+
deepFreeze(child);
|
|
126
|
+
Object.freeze(value);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
// src/merchant-card-endpoint-profiles.js
|
|
131
|
+
var MERCHANT_CARD_ENDPOINT_PSP = "merchant_card_endpoint";
|
|
132
|
+
var MERCHANT_CARD_ENDPOINT_MODE = "card_endpoint";
|
|
133
|
+
var BROWSER_HEADERS = /* @__PURE__ */ Object.freeze(["user-agent", "accept", "accept-language", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform"]);
|
|
134
|
+
var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
135
|
+
{
|
|
136
|
+
id: "att_payment_info",
|
|
137
|
+
merchant: "AT&T",
|
|
138
|
+
status: "observe",
|
|
139
|
+
evidence: [
|
|
140
|
+
"att.com buy flow bundles (release/26.09.23.REL.COMMERCEUI), read Sep 25, 2026: with isUnifiedMfeEnabled on, Networker posts JSON with credentials: include to /msapi/checkoutexporch/v1/paymentInfo?cartId=<CHECKOUT_MFE_CART_ID>.",
|
|
141
|
+
"Headers: content-type application/json, appname and idpctx-appname onlinecart, X-CSRF-TOKEN from sessionStorage csrf-token.",
|
|
142
|
+
"Body: flat JSON with cardNumber, cvv, expDate (the month), expYear, name, zip, type, amount, cardSubType (the brand, derived from the PAN), and savedPaymentData, includeAutoPay, autoPayInfoList, leaseId, transactionId, statusCode on other flows.",
|
|
143
|
+
"The card-free order POST (/msapi/checkoutexporch/v1/post) carries the SMS PIN step-up."
|
|
144
|
+
],
|
|
145
|
+
har: null,
|
|
146
|
+
checkout: "https://www.att.com",
|
|
147
|
+
endpoint: "https://www.att.com",
|
|
148
|
+
method: "POST",
|
|
149
|
+
path: "/msapi/checkoutexporch/v1/paymentInfo",
|
|
150
|
+
query: { cartId: ":cartId" },
|
|
151
|
+
encoding: "json",
|
|
152
|
+
session: {
|
|
153
|
+
cookies: true,
|
|
154
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-csrf-token", "appname", "idpctx-appname"],
|
|
155
|
+
required: ["x-csrf-token", "appname"]
|
|
156
|
+
},
|
|
157
|
+
card: {
|
|
158
|
+
pan: { at: ["cardNumber"], format: "digits" },
|
|
159
|
+
cvv: { at: ["cvv"], format: "digits" },
|
|
160
|
+
exp_month: { at: ["expDate"], format: "as_typed" },
|
|
161
|
+
exp_year: { at: ["expYear"], format: "as_typed" },
|
|
162
|
+
brands: [{ at: ["cardSubType"], map: null }],
|
|
163
|
+
bins: null
|
|
164
|
+
},
|
|
165
|
+
requires: [{ at: ["type"], one_of: ["CREDIT", "DEBIT", "ANYCARD"] }],
|
|
166
|
+
refuse: [
|
|
167
|
+
{ at: ["savedPaymentData"], when: "present" },
|
|
168
|
+
{ at: ["autoPayInfoList"], when: "present" },
|
|
169
|
+
{ at: ["leaseId"], when: "present" },
|
|
170
|
+
{ at: ["transactionId"], when: "present" },
|
|
171
|
+
{ at: ["statusCode"], when: "present" },
|
|
172
|
+
{ at: ["includeAutoPay"], when: "true" }
|
|
173
|
+
],
|
|
174
|
+
amount: { at: ["amount"], kind: "decimal_string" },
|
|
175
|
+
currency: "USD",
|
|
176
|
+
response: { headers: ["content-type", "set-cookie", "x-csrf-token"] },
|
|
177
|
+
bootstrap: "/robots.txt",
|
|
178
|
+
timeout_ms: 3e4,
|
|
179
|
+
charge_timing: "deferred",
|
|
180
|
+
pending: [
|
|
181
|
+
"card.brands: the cardSubType value for each brand",
|
|
182
|
+
"card.exp_month and card.exp_year: how many digits expDate and expYear carry",
|
|
183
|
+
"whether cartId also rides in the body"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "italo_booking_credit_card",
|
|
188
|
+
merchant: "Italo",
|
|
189
|
+
status: "observe",
|
|
190
|
+
evidence: [
|
|
191
|
+
"biglietti.italotreno.com public bundles (API client chunk 901970b7, payment step chunk 6297), read Sep 25, 2026: axios posts JSON, without withCredentials, to https://api-biglietti.italotreno.com/api/v1/booking/{bookingId}/payments/credit-card.",
|
|
192
|
+
"Headers: Content-Type application/json, Authorization Bearer <BIGSessionToken>, X-BIG-working-session-id.",
|
|
193
|
+
"Body: accountName, accountNumber (PAN, spaces stripped), CVV, expirationMonth (two-digit string), expirationYear (four-digit number), termsAndConditions true, StoreCreditCard (saves the card), creditCardId (saved cards), paymentMethodCode (VI, MC, AX, DC, JC, OT from payment.fns.cardType).",
|
|
194
|
+
"The answer carries validationMetadata.PaReq, a Nexi 3DS HTML snippet the page injects; the 3DS return and the commit are card-free."
|
|
195
|
+
],
|
|
196
|
+
har: null,
|
|
197
|
+
checkout: "https://biglietti.italotreno.com",
|
|
198
|
+
endpoint: "https://api-biglietti.italotreno.com",
|
|
199
|
+
method: "POST",
|
|
200
|
+
path: "/api/v1/booking/:bookingId/payments/credit-card",
|
|
201
|
+
query: {},
|
|
202
|
+
encoding: "json",
|
|
203
|
+
session: {
|
|
204
|
+
cookies: false,
|
|
205
|
+
headers: [...BROWSER_HEADERS, "content-type", "authorization", "x-big-working-session-id"],
|
|
206
|
+
required: ["authorization", "x-big-working-session-id"]
|
|
207
|
+
},
|
|
208
|
+
card: {
|
|
209
|
+
pan: { at: ["accountNumber"], format: "digits" },
|
|
210
|
+
cvv: { at: ["CVV"], format: "digits" },
|
|
211
|
+
exp_month: { at: ["expirationMonth"], format: "mm" },
|
|
212
|
+
exp_year: { at: ["expirationYear"], format: "yyyy_int" },
|
|
213
|
+
brands: [{ at: ["paymentMethodCode"], map: { visa: "VI", mastercard: "MC" } }],
|
|
214
|
+
bins: null
|
|
215
|
+
},
|
|
216
|
+
requires: [{ at: ["termsAndConditions"], equals: true }],
|
|
217
|
+
refuse: [
|
|
218
|
+
{ at: ["StoreCreditCard"], when: "true" },
|
|
219
|
+
{ at: ["creditCardId"], when: "present" }
|
|
220
|
+
],
|
|
221
|
+
amount: null,
|
|
222
|
+
currency: "EUR",
|
|
223
|
+
response: { headers: ["content-type"] },
|
|
224
|
+
bootstrap: "/robots.txt",
|
|
225
|
+
timeout_ms: 3e4,
|
|
226
|
+
charge_timing: "at_request",
|
|
227
|
+
pending: [
|
|
228
|
+
"whether the validationMetadata.PaReq answer carries the card number",
|
|
229
|
+
"whether expire is sent absent or as null"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "universal_orlando_guest_checkout",
|
|
234
|
+
merchant: "Universal Orlando Resort",
|
|
235
|
+
status: "observe",
|
|
236
|
+
evidence: [
|
|
237
|
+
"www.universalorlando.com/web-store bundle (main-YEGFGVHU.js payCard and buildPaymentUrl), read Sep 25, 2026: Angular HttpClient posts JSON, without withCredentials, to https://secure-api.universalparks.com/instances/{instance-id}/commerce-checkout for a guest checkout.",
|
|
238
|
+
"Headers: Authorization Bearer, X-IBM-Client-ID, WCToken, WCTrustedToken, x-instance-id, x-resort-area-code, x-source-Id, X-UNIWebService-ApiKey, -AppVersion, -Device, -Platform, Akamai-BM-Telemetry.",
|
|
239
|
+
"Body: order_id, currency_code USD, amount (a string), number, cvv, expiration {month, year} as integers, save_to_wallet, cardinal-3d-secure {reference_id, jwt, window_size}, billing and contact fields.",
|
|
240
|
+
"A Cardinal challenge answers 400 CARDINAL_3DS_MORE_INFO_REQUIRED and re-posts the card with the Cardinal jwt, which the refuse rules stop."
|
|
241
|
+
],
|
|
242
|
+
har: null,
|
|
243
|
+
checkout: "https://www.universalorlando.com",
|
|
244
|
+
endpoint: "https://secure-api.universalparks.com",
|
|
245
|
+
method: "POST",
|
|
246
|
+
path: "/instances/:instanceId/commerce-checkout",
|
|
247
|
+
query: {},
|
|
248
|
+
encoding: "json",
|
|
249
|
+
session: {
|
|
250
|
+
cookies: false,
|
|
251
|
+
headers: [
|
|
252
|
+
...BROWSER_HEADERS,
|
|
253
|
+
"content-type",
|
|
254
|
+
"authorization",
|
|
255
|
+
"x-ibm-client-id",
|
|
256
|
+
"wctoken",
|
|
257
|
+
"wctrustedtoken",
|
|
258
|
+
"x-instance-id",
|
|
259
|
+
"x-resort-area-code",
|
|
260
|
+
"x-source-id",
|
|
261
|
+
"x-uniwebservice-apikey",
|
|
262
|
+
"x-uniwebservice-appversion",
|
|
263
|
+
"x-uniwebservice-device",
|
|
264
|
+
"x-uniwebservice-platform",
|
|
265
|
+
"akamai-bm-telemetry"
|
|
266
|
+
],
|
|
267
|
+
required: ["authorization", "x-ibm-client-id", "wctoken", "wctrustedtoken"]
|
|
268
|
+
},
|
|
269
|
+
card: {
|
|
270
|
+
pan: { at: ["number"], format: "digits" },
|
|
271
|
+
cvv: { at: ["cvv"], format: "digits" },
|
|
272
|
+
exp_month: { at: ["expiration", "month"], format: "m_int" },
|
|
273
|
+
exp_year: { at: ["expiration", "year"], format: "yyyy_int" },
|
|
274
|
+
brands: [],
|
|
275
|
+
bins: null
|
|
276
|
+
},
|
|
277
|
+
requires: [
|
|
278
|
+
{ at: ["currency_code"], equals: "USD" },
|
|
279
|
+
{ at: ["order_id"], present: true }
|
|
280
|
+
],
|
|
281
|
+
refuse: [
|
|
282
|
+
{ at: ["save_to_wallet"], when: "true" },
|
|
283
|
+
{ at: ["cardinal-3d-secure", "jwt"], when: "nonempty" },
|
|
284
|
+
{ at: ["cardinal-3d-secure", "reference_id"], when: "nonempty" }
|
|
285
|
+
],
|
|
286
|
+
amount: { at: ["amount"], kind: "decimal_string" },
|
|
287
|
+
currency: { at: ["currency_code"] },
|
|
288
|
+
response: { headers: ["content-type"] },
|
|
289
|
+
bootstrap: "/robots.txt",
|
|
290
|
+
timeout_ms: 3e4,
|
|
291
|
+
charge_timing: "at_request",
|
|
292
|
+
pending: ["how long a captured akamai-bm-telemetry header stays accepted"]
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "target_optical_pi_add",
|
|
296
|
+
merchant: "Target Optical",
|
|
297
|
+
status: "observe",
|
|
298
|
+
evidence: [
|
|
299
|
+
"www.targetoptical.com HCL Commerce storefront scripts (CheckoutPayments.populateProtocolData, wc.service.invoke over $.ajax), read Sep 25, 2026: a same-origin form POST to /AjaxOrderChangeServicePIAddTO with X-Requested-With XMLHttpRequest and the WCS and Akamai cookies.",
|
|
300
|
+
"Body: storeId, catalogId, langId, payMethodId (brand code), piAmount (order total), billing_address_id, account (the PAN), cc_cvc, expire_month, expire_year, cc_brand, cc_nameoncard, requesttype ajax, authToken (WCS anti-CSRF), forterToken; saveCreditCard=on or creditCardToUse for a saved card.",
|
|
301
|
+
"The card is checked or authorized at this step; the later PIUpdate, OrderPrepare and SubmitOrder calls are card-free."
|
|
302
|
+
],
|
|
303
|
+
har: null,
|
|
304
|
+
checkout: "https://www.targetoptical.com",
|
|
305
|
+
endpoint: "https://www.targetoptical.com",
|
|
306
|
+
method: "POST",
|
|
307
|
+
path: "/AjaxOrderChangeServicePIAddTO",
|
|
308
|
+
query: {},
|
|
309
|
+
encoding: "form",
|
|
310
|
+
session: {
|
|
311
|
+
cookies: true,
|
|
312
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-requested-with"],
|
|
313
|
+
required: ["x-requested-with"]
|
|
314
|
+
},
|
|
315
|
+
card: {
|
|
316
|
+
pan: { at: "account", format: "digits" },
|
|
317
|
+
cvv: { at: "cc_cvc", format: "digits" },
|
|
318
|
+
exp_month: { at: "expire_month", format: "as_typed" },
|
|
319
|
+
exp_year: { at: "expire_year", format: "as_typed" },
|
|
320
|
+
brands: [{ at: "cc_brand", map: null }, { at: "payMethodId", map: null }],
|
|
321
|
+
bins: null
|
|
322
|
+
},
|
|
323
|
+
requires: [
|
|
324
|
+
{ at: "requesttype", equals: "ajax" },
|
|
325
|
+
{ at: "authToken", present: true },
|
|
326
|
+
{ at: "piAmount", present: true }
|
|
327
|
+
],
|
|
328
|
+
refuse: [
|
|
329
|
+
{ at: "saveCreditCard", when: "present" },
|
|
330
|
+
{ at: "creditCardToUse", when: "present" }
|
|
331
|
+
],
|
|
332
|
+
amount: { at: "piAmount", kind: "decimal_string" },
|
|
333
|
+
currency: "USD",
|
|
334
|
+
response: { headers: ["content-type", "set-cookie"] },
|
|
335
|
+
bootstrap: "/robots.txt",
|
|
336
|
+
timeout_ms: 3e4,
|
|
337
|
+
charge_timing: "deferred",
|
|
338
|
+
pending: [
|
|
339
|
+
"card.brands: the cc_brand and payMethodId values for each brand",
|
|
340
|
+
"card.exp_month and card.exp_year: how many digits expire_month and expire_year carry",
|
|
341
|
+
"whether the visible account1_1 field is also sent"
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "opus_signup_payment",
|
|
346
|
+
merchant: "Opus Virtual Offices",
|
|
347
|
+
status: "observe",
|
|
348
|
+
evidence: [
|
|
349
|
+
"www.opusvirtualoffices.com signup payment step, read Sep 25, 2026: fetch posts new FormData(form#checkout-form) as multipart/form-data with the browser boundary and credentials: include to /api/signup/process/payment/; the only set header is Accept application/json.",
|
|
350
|
+
"Card fields: ccname, ccnumber (digits), expdatem (01 to 12), expdatey (four digits), ccv, CreditCardType (1 Visa, 2 Mastercard, 3 Discover, 4 Amex; the page takes Mastercard for 51 to 55 only).",
|
|
351
|
+
"Order fields: uid (the signup id from step 1), prepaymny (the term, from the page radio buttons in payment chunk 0wj2kykjptexg: 1 monthly, or 3, 6 or 11 months prepaid), payment_option CreditCard or Wire; no amount (the server prices the uid and the term).",
|
|
352
|
+
"The page also sets signedcompany, bcphone and bstate, and copies utm_* cookies and visitor_id (as opusrc) into the form.",
|
|
353
|
+
"The subscription renews to the card on file, which a single-use card declines."
|
|
354
|
+
],
|
|
355
|
+
har: null,
|
|
356
|
+
checkout: "https://www.opusvirtualoffices.com",
|
|
357
|
+
endpoint: "https://www.opusvirtualoffices.com",
|
|
358
|
+
method: "POST",
|
|
359
|
+
path: "/api/signup/process/payment/",
|
|
360
|
+
query: {},
|
|
361
|
+
encoding: "multipart",
|
|
362
|
+
session: {
|
|
363
|
+
cookies: true,
|
|
364
|
+
headers: [...BROWSER_HEADERS, "content-type"],
|
|
365
|
+
required: []
|
|
366
|
+
},
|
|
367
|
+
card: {
|
|
368
|
+
pan: { at: "ccnumber", format: "digits" },
|
|
369
|
+
cvv: { at: "ccv", format: "digits" },
|
|
370
|
+
exp_month: { at: "expdatem", format: "mm" },
|
|
371
|
+
exp_year: { at: "expdatey", format: "yyyy" },
|
|
372
|
+
brands: [{ at: "CreditCardType", map: { visa: "1", mastercard: "2" } }],
|
|
373
|
+
bins: { mastercard: ["51", "52", "53", "54", "55"] }
|
|
374
|
+
},
|
|
375
|
+
requires: [
|
|
376
|
+
{ at: "payment_option", equals: "CreditCard" },
|
|
377
|
+
{ at: "uid", present: true },
|
|
378
|
+
{ at: "prepaymny", one_of: ["1", "3", "6", "11"] }
|
|
379
|
+
],
|
|
380
|
+
refuse: [],
|
|
381
|
+
amount: null,
|
|
382
|
+
currency: "USD",
|
|
383
|
+
response: { headers: ["content-type", "set-cookie"] },
|
|
384
|
+
bootstrap: "/robots.txt",
|
|
385
|
+
timeout_ms: 3e4,
|
|
386
|
+
charge_timing: "recurring",
|
|
387
|
+
pending: [
|
|
388
|
+
"the amount each prepaymny term charges, so the approval can show what the chosen term costs",
|
|
389
|
+
"a renewal policy: each renewal charges the card on file with no approval of its own"
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
]);
|
|
393
|
+
function wireProjection(profiles) {
|
|
394
|
+
return deepFreeze(profiles.map(({ evidence, bootstrap, timeout_ms, ...profile }) => ({ ...profile })));
|
|
395
|
+
}
|
|
396
|
+
function clientProjection(profiles) {
|
|
397
|
+
return deepFreeze(profiles.map(({ evidence, ...profile }) => ({ ...profile })));
|
|
398
|
+
}
|
|
399
|
+
var MERCHANT_CARD_ENDPOINT_RECOGNIZERS = /* @__PURE__ */ wireProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
400
|
+
var MERCHANT_CARD_ENDPOINT_CLIENT_TABLE = /* @__PURE__ */ clientProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
401
|
+
function merchantCardEndpointById(id) {
|
|
402
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((entry) => entry.id === id) ?? null : null;
|
|
403
|
+
}
|
|
404
|
+
function trustedMerchantCardEndpoint(idOrEntry) {
|
|
405
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
406
|
+
if (!profile)
|
|
407
|
+
throw new Error("Refusing to pay: unknown merchant card endpoint.");
|
|
408
|
+
return profile;
|
|
409
|
+
}
|
|
410
|
+
function merchantCardEndpointRecognizerById(id) {
|
|
411
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_RECOGNIZERS.find((entry) => entry.id === id) ?? null : null;
|
|
412
|
+
}
|
|
413
|
+
function merchantCardEndpointLive(idOrEntry) {
|
|
414
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
415
|
+
return !!profile && profile.status === "enabled" && profile.pending.length === 0 && typeof profile.har === "string" && profile.har !== "" && profile.charge_timing !== "recurring";
|
|
416
|
+
}
|
|
417
|
+
// src/merchant-card-endpoints.js
|
|
418
|
+
var MERCHANT_CARD_ENDPOINT_LIMITS = /* @__PURE__ */ Object.freeze({
|
|
419
|
+
bodyBytes: 128 * 1024,
|
|
420
|
+
answerBytes: 256 * 1024,
|
|
421
|
+
urlLength: 8192,
|
|
422
|
+
headerValueBytes: 8 * 1024,
|
|
423
|
+
headerBytes: 32 * 1024,
|
|
424
|
+
cookies: 64
|
|
425
|
+
});
|
|
426
|
+
var NEVER_FROM_CAPTURE = /^(?:cookie|cookie2|set-cookie|origin|referer|host|content-length|transfer-encoding|connection|keep-alive|upgrade|te|trailer|expect|proxy-.*|:.*)$/;
|
|
427
|
+
var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
428
|
+
var COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,256}$/;
|
|
429
|
+
var COOKIE_VALUE = /^(?:[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*|"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*")$/;
|
|
430
|
+
var BOUNDARY = /^[A-Za-z0-9'()+_,\-./:=?]{1,70}$/;
|
|
431
|
+
var BUNDLE_MEMBERS = /* @__PURE__ */ new Set(["url", "page_origin", "headers", "cookies", "referer", "body"]);
|
|
432
|
+
function utf8Length(text) {
|
|
433
|
+
let bytes = 0;
|
|
434
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
435
|
+
const unit = text.charCodeAt(i);
|
|
436
|
+
if (unit < 128)
|
|
437
|
+
bytes += 1;
|
|
438
|
+
else if (unit < 2048)
|
|
439
|
+
bytes += 2;
|
|
440
|
+
else if (unit >= 55296 && unit <= 56319 && i + 1 < text.length) {
|
|
441
|
+
bytes += 4;
|
|
442
|
+
i += 1;
|
|
443
|
+
}
|
|
444
|
+
else
|
|
445
|
+
bytes += 3;
|
|
446
|
+
}
|
|
447
|
+
return bytes;
|
|
448
|
+
}
|
|
449
|
+
function cardBrand(number) {
|
|
450
|
+
if (number.startsWith("4"))
|
|
451
|
+
return "visa";
|
|
452
|
+
const two = Number(number.slice(0, 2));
|
|
453
|
+
const four = Number(number.slice(0, 4));
|
|
454
|
+
if (two >= 51 && two <= 55 || four >= 2221 && four <= 2720)
|
|
455
|
+
return "mastercard";
|
|
456
|
+
if (/^3[47]/.test(number))
|
|
457
|
+
return "amex";
|
|
458
|
+
if (/^(?:6011|65|64[4-9])/.test(number))
|
|
459
|
+
return "discover";
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
function cardFacts(card) {
|
|
463
|
+
if (!isPlainObject(card))
|
|
464
|
+
throw new SubstitutionError("the card is missing");
|
|
465
|
+
const number = typeof card.number === "string" ? card.number : "";
|
|
466
|
+
if (!/^\d{12,19}$/.test(number))
|
|
467
|
+
throw new SubstitutionError("the card number is not 12 to 19 digits");
|
|
468
|
+
const cvc = typeof card.cvc === "string" ? card.cvc : "";
|
|
469
|
+
if (!/^\d{3,4}$/.test(cvc))
|
|
470
|
+
throw new SubstitutionError("the card security code is not 3 or 4 digits");
|
|
471
|
+
const monthText = typeof card.month === "number" || typeof card.month === "string" ? String(card.month) : "";
|
|
472
|
+
if (!/^\d{1,2}$/.test(monthText) || Number(monthText) < 1 || Number(monthText) > 12)
|
|
473
|
+
throw new SubstitutionError("the card expiry month is not 1 to 12");
|
|
474
|
+
const yearText = typeof card.year === "number" || typeof card.year === "string" ? String(card.year) : "";
|
|
475
|
+
if (!/^(?:\d{2}|\d{4})$/.test(yearText))
|
|
476
|
+
throw new SubstitutionError("the card expiry year is not 2 or 4 digits");
|
|
477
|
+
const year = yearText.length === 2 ? 2e3 + Number(yearText) : Number(yearText);
|
|
478
|
+
return { number, cvc, month: Number(monthText), year, brand: cardBrand(number) };
|
|
479
|
+
}
|
|
480
|
+
var digitForms = (digit) => `(?:${digit}|\\\\u003${digit}|%3${digit}|�*${48 + Number(digit)};|�*3${digit};)`;
|
|
481
|
+
var SEPARATOR = String.raw `(?:\\(?:[/\\"bfnrt]|u(?:00(?:[01][0-9a-f]|2[0-9a-f]|3[a-f]|40|5[b-f]|60|7[b-f]|[89a-f][0-9a-f])|0[1-9a-f][0-9a-f]{2}|[1-9a-f][0-9a-f]{3}))` + String.raw `|%(?:[01][0-9a-f]|2[0-9a-f]|3[a-f]|40|5[b-f]|60|7[b-f]|[89a-f][0-9a-f])` + String.raw `|&(?:#0*(?:[0-9]|[1-3][0-9]|4[0-7]|5[89]|6[0-4]|9[1-6]|12[3-9]|1[3-9][0-9]|[2-9][0-9]{2}|[1-9][0-9]{3,6})|#x0*(?:[0-9a-f]|[01][0-9a-f]|2[0-9a-f]|3[a-f]|40|5[b-f]|60|7[b-f]|[89a-f][0-9a-f]|[1-9a-f][0-9a-f]{2,5})|[a-z][a-z0-9]{1,31});` + String.raw `|[^0-9A-Za-z\\%&]|%(?![0-9a-f]{2})|&(?!#?x?[0-9a-z]+;))`;
|
|
482
|
+
function digitRun(digits) {
|
|
483
|
+
return new RegExp([...digits].map(digitForms).join(`${SEPARATOR}*`), "gi");
|
|
484
|
+
}
|
|
485
|
+
function countRuns(text, digits) {
|
|
486
|
+
return (text.match(digitRun(digits)) ?? []).length;
|
|
487
|
+
}
|
|
488
|
+
var CODE_KEY = /(?:cvv2?|cvc2?|cvn|csc|cid|ccv|security[\s_-]?code|card[\s_-]?code|verification[\s_-]?(?:code|value))(?:["']|%22|\\")?\s*(?::|=|%3[aAdD])\s*(?:["']|%22|\\")?\s*(\d{3,4})(?!\d)/gi;
|
|
489
|
+
var CODE_NAME = /(?:cvv2?|cvc2?|cvn|csc|cid(?![a-z])|ccv|security[\s_-]?code|card[\s_-]?code|verification[\s_-]?(?:code|value))/gi;
|
|
490
|
+
var CODE_WINDOW = 96;
|
|
491
|
+
function codeRun(cvc) {
|
|
492
|
+
return new RegExp(`(?:^|[^0-9])${[...cvc].map(digitForms).join("")}(?![0-9])`, "i");
|
|
493
|
+
}
|
|
494
|
+
function codeNearKey(text, code) {
|
|
495
|
+
for (const key of text.matchAll(CODE_NAME)) {
|
|
496
|
+
const from = Math.max(0, key.index - CODE_WINDOW);
|
|
497
|
+
const to = Math.min(text.length, key.index + key[0].length + CODE_WINDOW);
|
|
498
|
+
let near = text.slice(from, to);
|
|
499
|
+
if (from > 0 && /[0-9]/.test(text[from - 1]))
|
|
500
|
+
near = near.replace(/^[0-9]+/, "");
|
|
501
|
+
if (to < text.length && /[0-9]/.test(text[to]))
|
|
502
|
+
near = near.replace(/[0-9]+$/, "");
|
|
503
|
+
if (code.test(near))
|
|
504
|
+
return true;
|
|
505
|
+
for (const [run] of near.replace(BASE64_ESCAPE, unescapeBase64).matchAll(CODE_BASE64_RUN)) {
|
|
506
|
+
for (let skip = 0; skip < 4; skip += 1)
|
|
507
|
+
if (code.test(base64Bytes(run, skip).replace(/\0/g, "")))
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
function onlyOnce(variants, name) {
|
|
514
|
+
if (variants === 0)
|
|
515
|
+
return `${name} is missing`;
|
|
516
|
+
if (variants > 1)
|
|
517
|
+
return `${name} appears more than once`;
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
function multipartBoundary(contentType) {
|
|
521
|
+
const match = typeof contentType === "string" ? /^multipart\/form-data;\s*boundary=(?:"([^"]*)"|([^\s;"]*))$/i.exec(contentType) : null;
|
|
522
|
+
const boundary = match ? match[1] ?? match[2] : null;
|
|
523
|
+
return boundary && BOUNDARY.test(boundary) ? boundary : null;
|
|
524
|
+
}
|
|
525
|
+
function contentTypeRefusal(p, contentType) {
|
|
526
|
+
if (typeof contentType !== "string")
|
|
527
|
+
return `the card request to ${p.merchant} names no content type`;
|
|
528
|
+
const ok = p.encoding === "json" ? /^application\/json(?:;\s*charset=utf-8)?$/i.test(contentType) : p.encoding === "form" ? /^application\/x-www-form-urlencoded(?:;\s*charset=utf-8)?$/i.test(contentType) : multipartBoundary(contentType) !== null;
|
|
529
|
+
return ok ? null : `the card request to ${p.merchant} is not sent as ${p.encoding}`;
|
|
530
|
+
}
|
|
531
|
+
function forbiddenKeyIn(value, depth = 0) {
|
|
532
|
+
if (depth > 64)
|
|
533
|
+
return true;
|
|
534
|
+
if (Array.isArray(value))
|
|
535
|
+
return value.some((item) => forbiddenKeyIn(item, depth + 1));
|
|
536
|
+
if (isPlainObject(value))
|
|
537
|
+
return Object.keys(value).some((key) => FORBIDDEN_KEYS.has(key) || forbiddenKeyIn(value[key], depth + 1));
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
function parseForm(body) {
|
|
541
|
+
const fields = [];
|
|
542
|
+
if (body === "")
|
|
543
|
+
return { fields };
|
|
544
|
+
let offset = 0;
|
|
545
|
+
for (const pair of body.split("&")) {
|
|
546
|
+
const mark = pair.indexOf("=");
|
|
547
|
+
if (mark <= 0)
|
|
548
|
+
return { refusal: "the paused form body has a field with no name or no value" };
|
|
549
|
+
try {
|
|
550
|
+
fields.push({
|
|
551
|
+
name: decodeURIComponent(pair.slice(0, mark).replace(/\+/g, " ")),
|
|
552
|
+
value: decodeURIComponent(pair.slice(mark + 1).replace(/\+/g, " ")),
|
|
553
|
+
start: offset + mark + 1,
|
|
554
|
+
end: offset + pair.length
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return { refusal: "the paused form body is not valid form encoding" };
|
|
559
|
+
}
|
|
560
|
+
offset += pair.length + 1;
|
|
561
|
+
}
|
|
562
|
+
return { fields };
|
|
563
|
+
}
|
|
564
|
+
function parseMultipart(body, boundary) {
|
|
565
|
+
const CRLF = "\r\n";
|
|
566
|
+
const dash = `--${boundary}`;
|
|
567
|
+
const delimiter = `${CRLF}${dash}`;
|
|
568
|
+
if (!body.startsWith(`${dash}${CRLF}`))
|
|
569
|
+
return { refusal: "the paused multipart body does not open with its declared boundary" };
|
|
570
|
+
const fields = [];
|
|
571
|
+
let position = dash.length + CRLF.length;
|
|
572
|
+
for (;;) {
|
|
573
|
+
const headerEnd = body.indexOf(`${CRLF}${CRLF}`, position);
|
|
574
|
+
if (headerEnd === -1)
|
|
575
|
+
return { refusal: "a part of the paused multipart body has no header end" };
|
|
576
|
+
const start = headerEnd + 4;
|
|
577
|
+
const end = body.indexOf(delimiter, start);
|
|
578
|
+
if (end === -1)
|
|
579
|
+
return { refusal: "a part of the paused multipart body is not closed by its boundary" };
|
|
580
|
+
let name = null;
|
|
581
|
+
for (const line of body.slice(position, headerEnd).split(CRLF)) {
|
|
582
|
+
const colon = line.indexOf(":");
|
|
583
|
+
const header = colon > 0 ? line.slice(0, colon).trim().toLowerCase() : "";
|
|
584
|
+
const value = colon > 0 ? line.slice(colon + 1).trim() : "";
|
|
585
|
+
if (header === "content-disposition" && name === null) {
|
|
586
|
+
const disposition = /^form-data; name="([^"\\\r\n]*)"$/.exec(value);
|
|
587
|
+
if (!disposition)
|
|
588
|
+
return { refusal: "a part of the paused multipart body is not a plain form field" };
|
|
589
|
+
name = disposition[1];
|
|
590
|
+
}
|
|
591
|
+
else if (header !== "content-type" || !/^text\/plain(?:;\s*charset=utf-8)?$/i.test(value)) {
|
|
592
|
+
return { refusal: "a part of the paused multipart body is not a text field" };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (name === null)
|
|
596
|
+
return { refusal: "a part of the paused multipart body names no field" };
|
|
597
|
+
fields.push({ name, value: body.slice(start, end), start, end });
|
|
598
|
+
position = end + delimiter.length;
|
|
599
|
+
if (body.startsWith("--", position)) {
|
|
600
|
+
const rest = body.slice(position + 2);
|
|
601
|
+
return rest === "" || rest === CRLF ? { fields } : { refusal: "the paused multipart body carries bytes after its closing boundary" };
|
|
602
|
+
}
|
|
603
|
+
if (!body.startsWith(CRLF, position))
|
|
604
|
+
return { refusal: "a boundary in the paused multipart body is not followed by a line break" };
|
|
605
|
+
position += CRLF.length;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function sameName(name, wanted) {
|
|
609
|
+
return !FORBIDDEN_KEYS.has(name) && (name.toLowerCase() === wanted.toLowerCase() || name.toUpperCase() === wanted.toUpperCase());
|
|
610
|
+
}
|
|
611
|
+
function jsonReader(parsed) {
|
|
612
|
+
return { parsed, exact: (at) => jsonAt(parsed, at), variants: (at) => valuesIgnoringCase(parsed, at), label: describePath };
|
|
613
|
+
}
|
|
614
|
+
function fieldReader(fields) {
|
|
615
|
+
return {
|
|
616
|
+
fields,
|
|
617
|
+
exact: (at) => {
|
|
618
|
+
const hits = fields.filter((field) => field.name === at);
|
|
619
|
+
return hits.length === 1 ? { found: true, value: hits[0].value, field: hits[0] } : { found: false };
|
|
620
|
+
},
|
|
621
|
+
variants: (at) => fields.filter((field) => sameName(field.name, at)).map((field) => field.value),
|
|
622
|
+
label: (at) => at
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function readBody(p, body, contentType) {
|
|
626
|
+
if (typeof body !== "string")
|
|
627
|
+
return { refusal: "the paused body is not text" };
|
|
628
|
+
if (utf8Length(body) > MERCHANT_CARD_ENDPOINT_LIMITS.bodyBytes)
|
|
629
|
+
return { refusal: "the paused body is larger than a card request may be" };
|
|
630
|
+
const typeRefusal = contentTypeRefusal(p, contentType);
|
|
631
|
+
if (typeRefusal)
|
|
632
|
+
return { refusal: typeRefusal };
|
|
633
|
+
if (p.encoding === "json") {
|
|
634
|
+
let parsed2;
|
|
635
|
+
try {
|
|
636
|
+
parsed2 = JSON.parse(body);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return { refusal: "the paused body is not JSON" };
|
|
640
|
+
}
|
|
641
|
+
if (!isPlainObject(parsed2))
|
|
642
|
+
return { refusal: "the paused body is not a JSON object" };
|
|
643
|
+
if (!roundTrips(parsed2, body))
|
|
644
|
+
return { refusal: "the paused body does not round-trip through JSON, so its rules cannot be read" };
|
|
645
|
+
if (forbiddenKeyIn(parsed2))
|
|
646
|
+
return { refusal: "the paused body names a prototype key" };
|
|
647
|
+
return { reader: jsonReader(parsed2) };
|
|
648
|
+
}
|
|
649
|
+
const parsed = p.encoding === "form" ? parseForm(body) : parseMultipart(body, multipartBoundary(contentType));
|
|
650
|
+
if (parsed.refusal)
|
|
651
|
+
return parsed;
|
|
652
|
+
if (parsed.fields.some((field) => FORBIDDEN_KEYS.has(field.name)))
|
|
653
|
+
return { refusal: "the paused body names a prototype key" };
|
|
654
|
+
return { reader: fieldReader(parsed.fields) };
|
|
655
|
+
}
|
|
656
|
+
var SLOT_NAMES = /* @__PURE__ */ Object.freeze({
|
|
657
|
+
pan: "the card number",
|
|
658
|
+
cvv: "the security code",
|
|
659
|
+
exp_month: "the expiry month",
|
|
660
|
+
exp_year: "the expiry year",
|
|
661
|
+
brand: "the card network code"
|
|
662
|
+
});
|
|
663
|
+
function cardSlots(p) {
|
|
664
|
+
return [
|
|
665
|
+
["pan", p.card.pan],
|
|
666
|
+
["cvv", p.card.cvv],
|
|
667
|
+
["exp_month", p.card.exp_month],
|
|
668
|
+
["exp_year", p.card.exp_year],
|
|
669
|
+
...p.card.brands.map((brand) => ["brand", brand])
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
function placeholderFits(slot, spec, value) {
|
|
673
|
+
const text = typeof value === "string";
|
|
674
|
+
const int = Number.isInteger(value);
|
|
675
|
+
switch (slot) {
|
|
676
|
+
case "pan":
|
|
677
|
+
return text && /^\d{12,19}$/.test(value);
|
|
678
|
+
case "cvv":
|
|
679
|
+
return text && /^\d{3,4}$/.test(value);
|
|
680
|
+
case "exp_month":
|
|
681
|
+
if (spec.format === "mm")
|
|
682
|
+
return text && /^(?:0[1-9]|1[0-2])$/.test(value);
|
|
683
|
+
if (spec.format === "m_int")
|
|
684
|
+
return int && value >= 1 && value <= 12;
|
|
685
|
+
return spec.format === "as_typed" && (text && /^(?:0?[1-9]|1[0-2])$/.test(value) || int && value >= 1 && value <= 12);
|
|
686
|
+
case "exp_year":
|
|
687
|
+
if (spec.format === "yyyy")
|
|
688
|
+
return text && /^\d{4}$/.test(value);
|
|
689
|
+
if (spec.format === "yyyy_int")
|
|
690
|
+
return int && value >= 1e3 && value <= 9999;
|
|
691
|
+
return spec.format === "as_typed" && (text && /^(?:\d{2}|\d{4})$/.test(value) || int && (value >= 0 && value <= 99 || value >= 1e3 && value <= 9999));
|
|
692
|
+
case "brand":
|
|
693
|
+
return spec.map ? Object.values(spec.map).includes(value) : text && value !== "";
|
|
694
|
+
default:
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function isNothing(value) {
|
|
699
|
+
return value === null || value === "" || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
|
|
700
|
+
}
|
|
701
|
+
function rulesRefusal(p, reader, body) {
|
|
702
|
+
const request = `the paused body is not ${p.merchant}'s card request`;
|
|
703
|
+
for (const [slot, spec] of cardSlots(p)) {
|
|
704
|
+
const label = reader.label(spec.at);
|
|
705
|
+
const once = onlyOnce(reader.variants(spec.at).length, label);
|
|
706
|
+
if (once)
|
|
707
|
+
return `${request}: ${once}`;
|
|
708
|
+
const at = reader.exact(spec.at);
|
|
709
|
+
if (!at.found)
|
|
710
|
+
return `${request}: ${label} is not spelled as reviewed`;
|
|
711
|
+
if (!placeholderFits(slot, spec, at.value))
|
|
712
|
+
return `${request}: ${label} is not ${SLOT_NAMES[slot]} in the reviewed form`;
|
|
713
|
+
}
|
|
714
|
+
if (countRuns(body, reader.exact(p.card.pan.at).value) !== 1) {
|
|
715
|
+
return `${request}: the card number appears outside ${reader.label(p.card.pan.at)}`;
|
|
716
|
+
}
|
|
717
|
+
for (const rule of p.requires) {
|
|
718
|
+
const label = reader.label(rule.at);
|
|
719
|
+
const variants = reader.variants(rule.at);
|
|
720
|
+
const at = reader.exact(rule.at);
|
|
721
|
+
const holds = (value) => "equals" in rule ? value === rule.equals : "one_of" in rule ? rule.one_of.includes(value) : !isNothing(value);
|
|
722
|
+
if (!at.found || variants.length !== 1 || !holds(at.value))
|
|
723
|
+
return `${request}: ${label} is not what the review requires`;
|
|
724
|
+
}
|
|
725
|
+
for (const rule of p.refuse) {
|
|
726
|
+
const values = reader.variants(rule.at);
|
|
727
|
+
const fires = rule.when === "present" ? values.length > 0 : rule.when === "nonempty" ? values.some((value) => !isNothing(value)) : p.encoding === "json" ? values.some((value) => value !== false && value !== null) : values.some((value) => value !== "" && value !== "false");
|
|
728
|
+
if (fires)
|
|
729
|
+
return `the paused body asks ${p.merchant} for more than this purchase: ${reader.label(rule.at)}`;
|
|
730
|
+
}
|
|
731
|
+
if (p.amount) {
|
|
732
|
+
const label = reader.label(p.amount.at);
|
|
733
|
+
const at = reader.exact(p.amount.at);
|
|
734
|
+
const fits = p.amount.kind === "decimal_string" ? typeof at.value === "string" && /^\d{1,9}(?:\.\d{1,2})?$/.test(at.value) : p.encoding === "json" ? Number.isSafeInteger(at.value) && at.value >= 0 : typeof at.value === "string" && /^\d{1,12}$/.test(at.value);
|
|
735
|
+
if (!at.found || reader.variants(p.amount.at).length !== 1 || !fits)
|
|
736
|
+
return `${request}: the amount at ${label} is not a ${p.amount.kind.replace("_", " ")}`;
|
|
737
|
+
}
|
|
738
|
+
if (isPlainObject(p.currency)) {
|
|
739
|
+
const at = reader.exact(p.currency.at);
|
|
740
|
+
if (!at.found || reader.variants(p.currency.at).length !== 1 || typeof at.value !== "string" || !/^[A-Z]{3}$/.test(at.value)) {
|
|
741
|
+
return `${request}: ${reader.label(p.currency.at)} is not a currency code`;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
function merchantCardEndpointBodyRefusal(profile, body, contentType) {
|
|
747
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
748
|
+
const read = readBody(p, body, contentType);
|
|
749
|
+
return read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
750
|
+
}
|
|
751
|
+
var TWO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["usd", "eur", "gbp", "cad", "aud"]);
|
|
752
|
+
function merchantCardEndpointAmount(profile, body, contentType) {
|
|
753
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
754
|
+
const read = readBody(p, body, contentType);
|
|
755
|
+
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
756
|
+
if (refusal)
|
|
757
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
758
|
+
const currency = (typeof p.currency === "string" ? p.currency : read.reader.exact(p.currency.at).value).toLowerCase();
|
|
759
|
+
if (!p.amount)
|
|
760
|
+
return { amountCents: null, currency };
|
|
761
|
+
const value = read.reader.exact(p.amount.at).value;
|
|
762
|
+
if (p.amount.kind === "minor_int")
|
|
763
|
+
return { amountCents: Number(value), currency };
|
|
764
|
+
if (!TWO_DECIMAL_CURRENCIES.has(currency))
|
|
765
|
+
throw new Error(`Refusing to pay: ${p.merchant}'s amount is in a currency this build does not read.`);
|
|
766
|
+
const [whole, fraction = ""] = value.split(".");
|
|
767
|
+
return { amountCents: Number(whole) * 100 + Number(fraction.padEnd(2, "0")), currency };
|
|
768
|
+
}
|
|
769
|
+
function endpointMatches(p, rawUrl, method, opts) {
|
|
770
|
+
if (typeof rawUrl !== "string" || rawUrl.length > MERCHANT_CARD_ENDPOINT_LIMITS.urlLength)
|
|
771
|
+
return false;
|
|
772
|
+
if (typeof method !== "string" || method.toUpperCase() !== p.method)
|
|
773
|
+
return false;
|
|
774
|
+
if (rawUrl.includes("#"))
|
|
775
|
+
return false;
|
|
776
|
+
let url;
|
|
777
|
+
try {
|
|
778
|
+
url = new URL(rawUrl);
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
if (url.href !== rawUrl || url.protocol !== "https:" || url.port || url.username || url.password)
|
|
784
|
+
return false;
|
|
785
|
+
if (url.origin !== p.endpoint)
|
|
786
|
+
return false;
|
|
787
|
+
if (opts?.checkoutOrigin !== void 0 && opts.checkoutOrigin !== p.checkout)
|
|
788
|
+
return false;
|
|
789
|
+
if (!pathMatchesTemplate(p.path, url.pathname))
|
|
790
|
+
return false;
|
|
791
|
+
const mark = rawUrl.indexOf("?");
|
|
792
|
+
return queryPlaceholders(p.query, mark === -1 ? null : rawUrl.slice(mark + 1)) !== null;
|
|
793
|
+
}
|
|
794
|
+
function merchantCardEndpointMatches(profile, rawUrl, method, opts) {
|
|
795
|
+
return endpointMatches(trustedMerchantCardEndpoint(profile), rawUrl, method, opts);
|
|
796
|
+
}
|
|
797
|
+
function matchMerchantCardEndpoint(rawUrl, method, opts) {
|
|
798
|
+
return MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((p) => endpointMatches(p, rawUrl, method, opts)) ?? null;
|
|
799
|
+
}
|
|
800
|
+
function assertMerchantCardEndpoint(authorization) {
|
|
801
|
+
if (!isPlainObject(authorization))
|
|
802
|
+
throw new Error("Refusing to pay: malformed payment endpoint.");
|
|
803
|
+
if (authorization.mode !== MERCHANT_CARD_ENDPOINT_MODE || authorization.psp !== MERCHANT_CARD_ENDPOINT_PSP) {
|
|
804
|
+
throw new Error("Refusing to pay: this payment is not a reviewed merchant's own card request.");
|
|
805
|
+
}
|
|
806
|
+
const named = authorization.merchant_card_endpoint;
|
|
807
|
+
if (!isPlainObject(named) || typeof named.profile !== "string")
|
|
808
|
+
throw new Error("Refusing to pay: this payment names no merchant Agentcard reviewed.");
|
|
809
|
+
const profile = trustedMerchantCardEndpoint(named.profile);
|
|
810
|
+
let url;
|
|
811
|
+
try {
|
|
812
|
+
url = new URL(authorization.url);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
throw new Error("Refusing to pay: malformed payment endpoint.");
|
|
816
|
+
}
|
|
817
|
+
const method = authorization.method === void 0 ? "POST" : authorization.method;
|
|
818
|
+
if (!endpointMatches(profile, authorization.url, method)) {
|
|
819
|
+
throw new Error(`Refusing to pay: ${url.hostname} is not ${profile.merchant}'s reviewed card endpoint.`);
|
|
820
|
+
}
|
|
821
|
+
if (named.endpoint_host !== void 0 && named.endpoint_host !== url.hostname) {
|
|
822
|
+
throw new Error(`Refusing to pay: this payment names a different host from ${url.hostname}, the one it pays.`);
|
|
823
|
+
}
|
|
824
|
+
if (named.page_origin !== profile.checkout) {
|
|
825
|
+
throw new Error(`Refusing to pay: this payment was not started on ${profile.merchant}'s checkout page.`);
|
|
826
|
+
}
|
|
827
|
+
if (!merchantCardEndpointLive(profile))
|
|
828
|
+
throw new Error(`Refusing to pay: Agentcard has not turned on payments to ${profile.merchant} yet.`);
|
|
829
|
+
return Object.assign(url, { profile });
|
|
830
|
+
}
|
|
831
|
+
function refererKept(p, referer) {
|
|
832
|
+
if (typeof referer !== "string" || referer.length > MERCHANT_CARD_ENDPOINT_LIMITS.headerValueBytes || !PRINTABLE_ASCII.test(referer) || referer.includes("#"))
|
|
833
|
+
return false;
|
|
834
|
+
let url;
|
|
835
|
+
try {
|
|
836
|
+
url = new URL(referer);
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
return url.href === referer && url.origin === p.checkout && !url.username && !url.password;
|
|
842
|
+
}
|
|
843
|
+
function lowerHeaders(headers) {
|
|
844
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
845
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
846
|
+
const lower = name.toLowerCase();
|
|
847
|
+
if (Object.prototype.hasOwnProperty.call(out, lower))
|
|
848
|
+
return null;
|
|
849
|
+
out[lower] = value;
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
854
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
855
|
+
const limits = MERCHANT_CARD_ENDPOINT_LIMITS;
|
|
856
|
+
if (!isPlainObject(bundle))
|
|
857
|
+
return "the session bundle is missing";
|
|
858
|
+
if (Object.keys(bundle).some((member) => !BUNDLE_MEMBERS.has(member)))
|
|
859
|
+
return "the session bundle carries a member it may not";
|
|
860
|
+
if (!endpointMatches(p, bundle.url, p.method))
|
|
861
|
+
return `the session bundle's URL is not ${p.merchant}'s reviewed card endpoint`;
|
|
862
|
+
if (bundle.page_origin !== p.checkout)
|
|
863
|
+
return `the card request was not paused on ${p.checkout}`;
|
|
864
|
+
const raw = bundle.headers === void 0 ? {} : bundle.headers;
|
|
865
|
+
const headers = isPlainObject(raw) ? lowerHeaders(raw) : null;
|
|
866
|
+
if (!headers)
|
|
867
|
+
return "the session headers are not one value per name";
|
|
868
|
+
let total = 0;
|
|
869
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
870
|
+
if (NEVER_FROM_CAPTURE.test(name) || !p.session.headers.includes(name))
|
|
871
|
+
return `the session bundle carries a header outside ${p.merchant}'s reviewed list`;
|
|
872
|
+
if (typeof value !== "string" || !PRINTABLE_ASCII.test(value) || value.length > limits.headerValueBytes)
|
|
873
|
+
return `the session header ${name} is not printable text within the limit`;
|
|
874
|
+
total += name.length + value.length;
|
|
875
|
+
}
|
|
876
|
+
if (total > limits.headerBytes)
|
|
877
|
+
return "the session headers are larger than the limit";
|
|
878
|
+
for (const name of p.session.required) {
|
|
879
|
+
if (typeof headers[name] !== "string" || headers[name] === "")
|
|
880
|
+
return `the session bundle is missing ${name}`;
|
|
881
|
+
}
|
|
882
|
+
const cookies = bundle.cookies === void 0 ? [] : bundle.cookies;
|
|
883
|
+
if (!Array.isArray(cookies))
|
|
884
|
+
return "the session cookies are not a list";
|
|
885
|
+
if (!p.session.cookies && cookies.length > 0)
|
|
886
|
+
return `${p.merchant}'s card request carries no cookies`;
|
|
887
|
+
if (cookies.length > limits.cookies)
|
|
888
|
+
return "the session bundle carries more cookies than the limit";
|
|
889
|
+
let cookieBytes = 0;
|
|
890
|
+
for (const cookie of cookies) {
|
|
891
|
+
if (!isPlainObject(cookie) || Object.keys(cookie).sort().join(",") !== "name,value" || typeof cookie.name !== "string" || !COOKIE_NAME.test(cookie.name) || typeof cookie.value !== "string" || !COOKIE_VALUE.test(cookie.value))
|
|
892
|
+
return "a session cookie is not an RFC 6265 name and value";
|
|
893
|
+
if (cookie.name.length + cookie.value.length + 1 > limits.headerValueBytes)
|
|
894
|
+
return "a session cookie is larger than the limit";
|
|
895
|
+
cookieBytes += cookie.name.length + cookie.value.length + 3;
|
|
896
|
+
}
|
|
897
|
+
if (cookieBytes > limits.headerBytes)
|
|
898
|
+
return "the session cookies are larger than the limit";
|
|
899
|
+
if (bundle.referer !== void 0 && !refererKept(p, bundle.referer))
|
|
900
|
+
return `the session Referer is not a page on ${p.checkout}`;
|
|
901
|
+
return merchantCardEndpointBodyRefusal(p.id, bundle.body, headers["content-type"]);
|
|
902
|
+
}
|
|
903
|
+
function slotValue(slot, spec, placeholder, facts) {
|
|
904
|
+
if (slot === "pan")
|
|
905
|
+
return facts.number;
|
|
906
|
+
if (slot === "cvv")
|
|
907
|
+
return facts.cvc;
|
|
908
|
+
if (slot === "exp_month") {
|
|
909
|
+
const padded = String(facts.month).padStart(2, "0");
|
|
910
|
+
if (spec.format === "mm")
|
|
911
|
+
return padded;
|
|
912
|
+
if (spec.format === "m_int" || typeof placeholder === "number")
|
|
913
|
+
return facts.month;
|
|
914
|
+
return placeholder.length === 2 ? padded : String(facts.month);
|
|
915
|
+
}
|
|
916
|
+
if (spec.format === "yyyy")
|
|
917
|
+
return String(facts.year);
|
|
918
|
+
if (spec.format === "yyyy_int")
|
|
919
|
+
return facts.year;
|
|
920
|
+
if (typeof placeholder === "number")
|
|
921
|
+
return placeholder >= 1e3 ? facts.year : facts.year % 100;
|
|
922
|
+
return placeholder.length === 4 ? String(facts.year) : String(facts.year % 100).padStart(2, "0");
|
|
923
|
+
}
|
|
924
|
+
function writeJson(parsed, at, value) {
|
|
925
|
+
const parent = jsonAt(parsed, at.slice(0, -1));
|
|
926
|
+
const key = at[at.length - 1];
|
|
927
|
+
const owned = parent.found && (typeof key === "number" ? Array.isArray(parent.value) && key < parent.value.length : isPlainObject(parent.value) && !FORBIDDEN_KEYS.has(key) && Object.prototype.hasOwnProperty.call(parent.value, key));
|
|
928
|
+
if (!owned)
|
|
929
|
+
throw new SubstitutionError(`the paused body has no field at ${describePath(at)}`);
|
|
930
|
+
parent.value[key] = value;
|
|
931
|
+
}
|
|
932
|
+
function substituteMerchantCardEndpoint(body, contentType, profile, card) {
|
|
933
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
934
|
+
const read = readBody(p, body, contentType);
|
|
935
|
+
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
936
|
+
if (refusal)
|
|
937
|
+
throw new SubstitutionError(refusal);
|
|
938
|
+
const facts = cardFacts(card);
|
|
939
|
+
const typed = read.reader.exact(p.card.pan.at).value;
|
|
940
|
+
if (typed === facts.number)
|
|
941
|
+
throw new SubstitutionError("the paused body already carries the real card number");
|
|
942
|
+
const bins = p.card.bins && facts.brand && Object.prototype.hasOwnProperty.call(p.card.bins, facts.brand) ? p.card.bins[facts.brand] : null;
|
|
943
|
+
if (bins && !bins.some((prefix) => facts.number.startsWith(prefix)))
|
|
944
|
+
throw new SubstitutionError(`${p.merchant} does not take this card's number range`);
|
|
945
|
+
const writes = [];
|
|
946
|
+
for (const [slot, spec] of cardSlots(p)) {
|
|
947
|
+
const at = read.reader.exact(spec.at);
|
|
948
|
+
if (slot === "brand" && !spec.map)
|
|
949
|
+
continue;
|
|
950
|
+
let value;
|
|
951
|
+
if (slot === "brand") {
|
|
952
|
+
value = facts.brand && Object.prototype.hasOwnProperty.call(spec.map, facts.brand) ? spec.map[facts.brand] : void 0;
|
|
953
|
+
if (value === void 0)
|
|
954
|
+
throw new SubstitutionError(`${p.merchant} does not take this card's network`);
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
value = slotValue(slot, spec, at.value, facts);
|
|
958
|
+
}
|
|
959
|
+
writes.push({ at: spec.at, found: at, value });
|
|
960
|
+
}
|
|
961
|
+
let out = body;
|
|
962
|
+
if (p.encoding === "json") {
|
|
963
|
+
for (const write of writes)
|
|
964
|
+
writeJson(read.reader.parsed, write.at, write.value);
|
|
965
|
+
out = JSON.stringify(read.reader.parsed);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
for (const write of [...writes].sort((a, b) => b.found.field.start - a.found.field.start)) {
|
|
969
|
+
const text = p.encoding === "form" ? encodeURIComponent(String(write.value)).replace(/%20/g, "+") : String(write.value);
|
|
970
|
+
out = out.slice(0, write.found.field.start) + text + out.slice(write.found.field.end);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (countRuns(out, typed) !== 0)
|
|
974
|
+
throw new SubstitutionError("the typed card number would still reach the merchant");
|
|
975
|
+
if (countRuns(out, facts.number) !== 1)
|
|
976
|
+
throw new SubstitutionError("the card number would reach the merchant outside its field");
|
|
977
|
+
if (!merchantCardEndpointLive(p))
|
|
978
|
+
throw new SubstitutionError(`Agentcard has not turned on payments to ${p.merchant} yet`);
|
|
979
|
+
return out;
|
|
980
|
+
}
|
|
981
|
+
function merchantEndpointRequest(profile, bundle, card) {
|
|
982
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
983
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
984
|
+
if (refusal)
|
|
985
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
986
|
+
const captured = lowerHeaders(bundle.headers ?? {});
|
|
987
|
+
const body = substituteMerchantCardEndpoint(bundle.body, captured["content-type"], p.id, card);
|
|
988
|
+
const headers = { "content-type": p.encoding === "form" ? "application/x-www-form-urlencoded; charset=UTF-8" : captured["content-type"] };
|
|
989
|
+
for (const [name, value] of Object.entries(captured))
|
|
990
|
+
if (name !== "content-type")
|
|
991
|
+
headers[name] = value;
|
|
992
|
+
const cookies = bundle.cookies ?? [];
|
|
993
|
+
if (p.session.cookies && cookies.length > 0)
|
|
994
|
+
headers.cookie = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
995
|
+
headers.origin = p.checkout;
|
|
996
|
+
if (bundle.referer !== void 0)
|
|
997
|
+
headers.referer = bundle.referer;
|
|
998
|
+
return { url: bundle.url, method: p.method, headers, body };
|
|
999
|
+
}
|
|
1000
|
+
function headerPairs(headers) {
|
|
1001
|
+
if (headers === void 0)
|
|
1002
|
+
return [];
|
|
1003
|
+
if (Array.isArray(headers)) {
|
|
1004
|
+
return headers.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string") ? headers : null;
|
|
1005
|
+
}
|
|
1006
|
+
if (!isPlainObject(headers))
|
|
1007
|
+
return null;
|
|
1008
|
+
const pairs = [];
|
|
1009
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1010
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
1011
|
+
if (typeof item !== "string")
|
|
1012
|
+
return null;
|
|
1013
|
+
pairs.push([name, item]);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return pairs;
|
|
1017
|
+
}
|
|
1018
|
+
function answerParts(response) {
|
|
1019
|
+
const pairs = isPlainObject(response) ? headerPairs(response.headers) : null;
|
|
1020
|
+
if (!pairs || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || typeof response.bodyText !== "string" || utf8Length(response.bodyText) > MERCHANT_CARD_ENDPOINT_LIMITS.answerBytes) {
|
|
1021
|
+
throw new Error("Refusing to read: the merchant's answer is malformed.");
|
|
1022
|
+
}
|
|
1023
|
+
return pairs;
|
|
1024
|
+
}
|
|
1025
|
+
var BASE64_RUN = /[A-Za-z0-9+/_-]{8,}/g;
|
|
1026
|
+
var STREAM_RUN_LENGTH = 16;
|
|
1027
|
+
var CODE_BASE64_RUN = /[A-Za-z0-9+/_-]{4,}/g;
|
|
1028
|
+
var BASE64_ESCAPE = /\\\/|\\u002[bBfF]|%2[bBfF]|&#(?:43|47|x2[bBfF]);/g;
|
|
1029
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1030
|
+
var unescapeBase64 = (escape) => /43|[bB]/.test(escape) ? "+" : "/";
|
|
1031
|
+
function base64Bytes(run, skip) {
|
|
1032
|
+
let bits = 0;
|
|
1033
|
+
let value = 0;
|
|
1034
|
+
let out = "";
|
|
1035
|
+
for (let i = skip; i < run.length; i += 1) {
|
|
1036
|
+
const char = run[i];
|
|
1037
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : BASE64_ALPHABET.indexOf(char));
|
|
1038
|
+
bits += 6;
|
|
1039
|
+
if (bits >= 8) {
|
|
1040
|
+
bits -= 8;
|
|
1041
|
+
out += String.fromCharCode(value >> bits);
|
|
1042
|
+
value &= (1 << bits) - 1;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
1047
|
+
function compressedStream(bytes) {
|
|
1048
|
+
const first = bytes.charCodeAt(0);
|
|
1049
|
+
const second = bytes.charCodeAt(1);
|
|
1050
|
+
return first === 31 && second === 139 || (first & 15) === 8 && first >> 4 <= 7 && (first << 8 | second) % 31 === 0;
|
|
1051
|
+
}
|
|
1052
|
+
function carriesCard(text, facts, pan, code, depth, seen) {
|
|
1053
|
+
if (text.search(pan) !== -1)
|
|
1054
|
+
return true;
|
|
1055
|
+
for (const match of text.matchAll(CODE_KEY))
|
|
1056
|
+
if (match[1] === facts.cvc)
|
|
1057
|
+
return true;
|
|
1058
|
+
if (codeNearKey(text, code))
|
|
1059
|
+
return true;
|
|
1060
|
+
if (depth === 0)
|
|
1061
|
+
return false;
|
|
1062
|
+
for (const [run] of text.replace(BASE64_ESCAPE, unescapeBase64).matchAll(BASE64_RUN)) {
|
|
1063
|
+
for (let skip = 0; skip < 4; skip += 1) {
|
|
1064
|
+
const bytes = base64Bytes(run, skip);
|
|
1065
|
+
if (run.length >= STREAM_RUN_LENGTH && compressedStream(bytes))
|
|
1066
|
+
seen.compressed = true;
|
|
1067
|
+
if (carriesCard(bytes.replace(/\0/g, ""), facts, pan, code, depth - 1, seen))
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
function responseCardLeak(card, response) {
|
|
1074
|
+
const facts = cardFacts(card);
|
|
1075
|
+
const pan = digitRun(facts.number);
|
|
1076
|
+
const code = codeRun(facts.cvc);
|
|
1077
|
+
const seen = { compressed: false };
|
|
1078
|
+
for (const text of [response.bodyText, ...answerParts(response).flat()]) {
|
|
1079
|
+
if (carriesCard(text, facts, pan, code, 2, seen))
|
|
1080
|
+
return "card_in_response";
|
|
1081
|
+
}
|
|
1082
|
+
return seen.compressed ? "unreadable_in_response" : null;
|
|
1083
|
+
}
|
|
1084
|
+
function merchantCardEndpointAnswer(profile, response, card) {
|
|
1085
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
1086
|
+
const pairs = answerParts(response);
|
|
1087
|
+
const withheld = responseCardLeak(card, response);
|
|
1088
|
+
if (withheld)
|
|
1089
|
+
return { status: response.status, withheld, headers: {}, set_cookies: [], bodyText: null };
|
|
1090
|
+
const headers = {};
|
|
1091
|
+
const setCookies = [];
|
|
1092
|
+
for (const [rawName, value] of pairs) {
|
|
1093
|
+
const name = rawName.toLowerCase();
|
|
1094
|
+
if (!p.response.headers.includes(name))
|
|
1095
|
+
continue;
|
|
1096
|
+
if (name === "set-cookie")
|
|
1097
|
+
setCookies.push(value);
|
|
1098
|
+
else
|
|
1099
|
+
headers[name] = Object.prototype.hasOwnProperty.call(headers, name) ? `${headers[name]}, ${value}` : value;
|
|
1100
|
+
}
|
|
1101
|
+
return { status: response.status, headers, set_cookies: setCookies, bodyText: response.bodyText };
|
|
1102
|
+
}
|
|
1103
|
+
export { MERCHANT_CARD_ENDPOINT_CLIENT_TABLE, MERCHANT_CARD_ENDPOINT_LIMITS, MERCHANT_CARD_ENDPOINT_MODE, MERCHANT_CARD_ENDPOINT_PROFILES, MERCHANT_CARD_ENDPOINT_PSP, MERCHANT_CARD_ENDPOINT_RECOGNIZERS, SubstitutionError, assertMerchantCardEndpoint, matchMerchantCardEndpoint, merchantCardEndpointAmount, merchantCardEndpointAnswer, merchantCardEndpointBodyRefusal, merchantCardEndpointById, merchantCardEndpointLive, merchantCardEndpointMatches, merchantCardEndpointRecognizerById, merchantCardEndpointSessionRefusal, merchantEndpointRequest, responseCardLeak, substituteMerchantCardEndpoint, trustedMerchantCardEndpoint };
|