@agent-cards/sdk 0.22.0 → 0.23.1
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 +50 -8
- package/PREFLIGHT.md +3 -1
- package/README.md +120 -15
- package/dist/adyen-merchant-hosted.generated.d.ts +2 -1
- package/dist/adyen-merchant-hosted.generated.js +51 -39
- package/dist/card-endpoint-facts.d.ts +96 -0
- package/dist/card-endpoint-facts.js +275 -0
- package/dist/card-endpoint-page.d.ts +54 -0
- package/dist/card-endpoint-page.js +69 -0
- package/dist/card-endpoint.d.ts +132 -11
- package/dist/card-endpoint.js +169 -17
- package/dist/cdp.js +336 -28
- package/dist/client.d.ts +61 -9
- package/dist/client.js +133 -16
- package/dist/creditguard.generated.d.ts +1 -1
- package/dist/cybersource-flex.d.ts +30 -0
- package/dist/cybersource-flex.generated.d.ts +27 -1
- package/dist/cybersource-flex.generated.js +42 -2
- package/dist/cybersource-flex.js +46 -0
- package/dist/ekashu.d.ts +35 -0
- package/dist/ekashu.js +55 -0
- package/dist/lifecycle.d.ts +7 -4
- package/dist/lifecycle.js +6 -3
- package/dist/merchant-card-endpoints.generated.d.ts +60 -1
- package/dist/merchant-card-endpoints.generated.js +3264 -95
- package/dist/pre-claim.d.ts +9 -0
- package/dist/pre-claim.js +179 -13
- package/dist/preflight-catalog.json +48 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +11 -1
- package/dist/preparation.d.ts +7 -1
- package/dist/preparation.js +27 -2
- package/dist/stripe-checkout.generated.js +66 -1
- package/dist/verifone.generated.js +20 -8
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: ed096050a26a3c26d7fc8167d1bfbeb05e120b392a9aa7a34a924514c56fc870
|
|
4
4
|
// src/json-path.js
|
|
5
5
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
6
6
|
function isPlainObject(v) {
|
|
@@ -56,6 +56,10 @@ function describePath(path) {
|
|
|
56
56
|
// src/url-template.js
|
|
57
57
|
var PLACEHOLDER_VALUE = /^[A-Za-z0-9._~@+-]{1,256}$/;
|
|
58
58
|
var placeholderValue = (value) => PLACEHOLDER_VALUE.test(value) && value !== "." && !value.includes("..");
|
|
59
|
+
function queryPlaceholder(value) {
|
|
60
|
+
const mark = value.lastIndexOf(":");
|
|
61
|
+
return mark === -1 ? null : { prefix: value.slice(0, mark), name: value.slice(mark + 1) };
|
|
62
|
+
}
|
|
59
63
|
function pathPlaceholders(template, pathname) {
|
|
60
64
|
if (typeof template !== "string" || typeof pathname !== "string")
|
|
61
65
|
return null;
|
|
@@ -100,10 +104,14 @@ function queryPlaceholders(query, text) {
|
|
|
100
104
|
const written = got[i].slice(mark + 1);
|
|
101
105
|
if (typeof value !== "string")
|
|
102
106
|
return null;
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
const placeholder = queryPlaceholder(value);
|
|
108
|
+
if (placeholder) {
|
|
109
|
+
if (!written.startsWith(placeholder.prefix))
|
|
110
|
+
return null;
|
|
111
|
+
const rest = written.slice(placeholder.prefix.length);
|
|
112
|
+
if (!placeholderValue(rest))
|
|
105
113
|
return null;
|
|
106
|
-
values[
|
|
114
|
+
values[placeholder.name] = rest;
|
|
107
115
|
}
|
|
108
116
|
else if (written !== value) {
|
|
109
117
|
return null;
|
|
@@ -111,6 +119,37 @@ function queryPlaceholders(query, text) {
|
|
|
111
119
|
}
|
|
112
120
|
return values;
|
|
113
121
|
}
|
|
122
|
+
function strictUrl(rawUrl) {
|
|
123
|
+
if (typeof rawUrl !== "string" || rawUrl.includes("#"))
|
|
124
|
+
return null;
|
|
125
|
+
let url;
|
|
126
|
+
try {
|
|
127
|
+
url = new URL(rawUrl);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (url.href !== rawUrl || url.protocol !== "https:" || url.port || url.username || url.password)
|
|
133
|
+
return null;
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
136
|
+
function urlTemplateMatch(template, rawUrl) {
|
|
137
|
+
const url = strictUrl(rawUrl);
|
|
138
|
+
if (!url || !template || typeof template !== "object")
|
|
139
|
+
return null;
|
|
140
|
+
const fromPath = pathPlaceholders(template.path, url.pathname);
|
|
141
|
+
if (!fromPath)
|
|
142
|
+
return null;
|
|
143
|
+
const mark = rawUrl.indexOf("?");
|
|
144
|
+
const text = mark === -1 ? null : rawUrl.slice(mark + 1);
|
|
145
|
+
const queries = Array.isArray(template.queries) ? template.queries : [template.query];
|
|
146
|
+
for (const query of queries) {
|
|
147
|
+
const fromQuery = queryPlaceholders(query, text);
|
|
148
|
+
if (fromQuery)
|
|
149
|
+
return { origin: url.origin, placeholders: { ...fromPath, ...fromQuery } };
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
114
153
|
// src/substitution-primitives.js
|
|
115
154
|
var SubstitutionError = class extends Error {
|
|
116
155
|
constructor(message) {
|
|
@@ -127,10 +166,485 @@ function deepFreeze(value) {
|
|
|
127
166
|
}
|
|
128
167
|
return value;
|
|
129
168
|
}
|
|
169
|
+
// src/merchant-card-endpoint-documents.js
|
|
170
|
+
var HILTON_CREATE_RESERVATION_DOCUMENTS = /* @__PURE__ */ Object.freeze([
|
|
171
|
+
`mutation createReservation($language: String!, $input: ReservationInput!, $mfaInput: ReservationMFAInput, $scaInput: SCAInput, $enrollment: ReservationEnrollInput, $guestLocationCountry: String, $guestUpdate: ReservationGuestUpdateInput, $includePreArrivalOffers: Boolean! = false) {
|
|
172
|
+
createReservation(
|
|
173
|
+
language: $language
|
|
174
|
+
input: $input
|
|
175
|
+
mfaInput: $mfaInput
|
|
176
|
+
scaInput: $scaInput
|
|
177
|
+
enroll: $enrollment
|
|
178
|
+
guestUpdate: $guestUpdate
|
|
179
|
+
) {
|
|
180
|
+
data {
|
|
181
|
+
digitalPayment {
|
|
182
|
+
action {
|
|
183
|
+
paymentData {
|
|
184
|
+
md
|
|
185
|
+
paReq
|
|
186
|
+
}
|
|
187
|
+
redirectMethod
|
|
188
|
+
redirectUrl
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
enroll {
|
|
192
|
+
hhonorsNumber
|
|
193
|
+
resourceAlreadyExists
|
|
194
|
+
}
|
|
195
|
+
mfa {
|
|
196
|
+
transactionState
|
|
197
|
+
}
|
|
198
|
+
sca {
|
|
199
|
+
...RESERVATION_SCA_FRAGMENT
|
|
200
|
+
}
|
|
201
|
+
reservation {
|
|
202
|
+
...RESERVATION_FRAGMENT
|
|
203
|
+
}
|
|
204
|
+
guestUpdate {
|
|
205
|
+
preferredAddress {
|
|
206
|
+
statusCode
|
|
207
|
+
message
|
|
208
|
+
}
|
|
209
|
+
updatePaymentMethod {
|
|
210
|
+
statusCode
|
|
211
|
+
message
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
error {
|
|
216
|
+
code
|
|
217
|
+
message
|
|
218
|
+
context
|
|
219
|
+
notifications {
|
|
220
|
+
code
|
|
221
|
+
fields
|
|
222
|
+
message
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
fragment RESERVATION_SCA_FRAGMENT on SCA {
|
|
228
|
+
scaId
|
|
229
|
+
md
|
|
230
|
+
paReq
|
|
231
|
+
url
|
|
232
|
+
termUrl
|
|
233
|
+
}
|
|
234
|
+
fragment RESERVATION_FRAGMENT on Reservation {
|
|
235
|
+
additionalContext {
|
|
236
|
+
bookedFor
|
|
237
|
+
profile
|
|
238
|
+
program
|
|
239
|
+
userPermission
|
|
240
|
+
userRole
|
|
241
|
+
}
|
|
242
|
+
addOnsResModifyEligible
|
|
243
|
+
adjoiningRoomStay
|
|
244
|
+
adjoiningRoomsFailure
|
|
245
|
+
arrivalDate
|
|
246
|
+
autoUpgradedStay
|
|
247
|
+
cancelEligible
|
|
248
|
+
certificates {
|
|
249
|
+
totalPoints
|
|
250
|
+
totalPointsFmt
|
|
251
|
+
}
|
|
252
|
+
clientAccounts {
|
|
253
|
+
clientId
|
|
254
|
+
clientType
|
|
255
|
+
clientName
|
|
256
|
+
programAccountId
|
|
257
|
+
}
|
|
258
|
+
comments {
|
|
259
|
+
generalInfo
|
|
260
|
+
}
|
|
261
|
+
confNumber
|
|
262
|
+
cost {
|
|
263
|
+
currency {
|
|
264
|
+
currencyCode
|
|
265
|
+
currencySymbol
|
|
266
|
+
description
|
|
267
|
+
}
|
|
268
|
+
roomRevUSD: totalAmountBeforeTax(currencyCode: "USD")
|
|
269
|
+
totalAddOnsAmount
|
|
270
|
+
totalAddOnsAmountFmt
|
|
271
|
+
totalAddOnsAmountUSD: totalAddOnsAmount(currencyCode: "USD")
|
|
272
|
+
totalAmountBeforeTax
|
|
273
|
+
totalAmountAfterTaxFmt: guestTotalCostAfterTaxFmt
|
|
274
|
+
totalAmountAfterTax: guestTotalCostAfterTax
|
|
275
|
+
totalAmountBeforeTaxFmt
|
|
276
|
+
totalServiceCharges
|
|
277
|
+
totalServiceChargesFmt
|
|
278
|
+
totalServiceChargesUSD: totalServiceCharges(currencyCode: "USD")
|
|
279
|
+
totalTaxes
|
|
280
|
+
totalTaxesFmt
|
|
281
|
+
totalTaxesUSD: totalTaxes(currencyCode: "USD")
|
|
282
|
+
}
|
|
283
|
+
cxlNumber
|
|
284
|
+
departureDate
|
|
285
|
+
disclaimer {
|
|
286
|
+
confirmableUpgradeReward
|
|
287
|
+
confirmableUpgradeReward_noTx: confirmableUpgradeReward
|
|
288
|
+
diamond48
|
|
289
|
+
diamond48_noTx: diamond48
|
|
290
|
+
fullPrePayNonRefundable
|
|
291
|
+
fullPrePayNonRefundable_noTx: fullPrePayNonRefundable
|
|
292
|
+
hgfConfirmation
|
|
293
|
+
hgfConfirmation_noTx: hgfConfirmation
|
|
294
|
+
hgvMaxTermsAndConditions
|
|
295
|
+
hgvMaxTermsAndConditions_noTx: hgvMaxTermsAndConditions
|
|
296
|
+
hhonorsCancellationCharges
|
|
297
|
+
hhonorsCancellationCharges_noTx: hhonorsCancellationCharges
|
|
298
|
+
hhonorsPointsDeduction
|
|
299
|
+
hhonorsPointsDeduction_noTx: hhonorsPointsDeduction
|
|
300
|
+
hhonorsPrintedConfirmation
|
|
301
|
+
hhonorsPrintedConfirmation_noTx: hhonorsPrintedConfirmation
|
|
302
|
+
lengthOfStay
|
|
303
|
+
lengthOfStay_noTx: lengthOfStay
|
|
304
|
+
rightToCancel
|
|
305
|
+
rightToCancel_noTx: rightToCancel
|
|
306
|
+
totalRate
|
|
307
|
+
totalRate_noTx: totalRate
|
|
308
|
+
teamMemberEligibility
|
|
309
|
+
teamMemberEligibility_noTx: teamMemberEligibility
|
|
310
|
+
vatCharge
|
|
311
|
+
vatCharge_noTx: vatCharge
|
|
312
|
+
cancellationRefundPolicy
|
|
313
|
+
cancellationRefundPolicy_noTx: cancellationRefundPolicy
|
|
314
|
+
}
|
|
315
|
+
guarantee {
|
|
316
|
+
depositRequired
|
|
317
|
+
nonRefundable
|
|
318
|
+
cxlPolicyCode
|
|
319
|
+
cxlPolicyDesc
|
|
320
|
+
cxlPolicyDesc_noTx: cxlPolicyDesc
|
|
321
|
+
guarPolicyCode
|
|
322
|
+
guarPolicyDesc
|
|
323
|
+
guarMethodCode
|
|
324
|
+
taxDisclaimers {
|
|
325
|
+
text
|
|
326
|
+
title
|
|
327
|
+
}
|
|
328
|
+
disclaimer {
|
|
329
|
+
legal
|
|
330
|
+
legal_noTx: legal
|
|
331
|
+
}
|
|
332
|
+
paymentCard {
|
|
333
|
+
cardCode
|
|
334
|
+
cardName
|
|
335
|
+
cardNumberMasked(format: masked)
|
|
336
|
+
cardExpireDate
|
|
337
|
+
cardPaymentType
|
|
338
|
+
expireDate: cardExpireDateFmt(format: "MMM yyyy")
|
|
339
|
+
expireDateFull: cardExpireDateFmt(format: "MMMM yyyy")
|
|
340
|
+
expired
|
|
341
|
+
}
|
|
342
|
+
deposit {
|
|
343
|
+
amount
|
|
344
|
+
}
|
|
345
|
+
taxDisclaimers {
|
|
346
|
+
text
|
|
347
|
+
title
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
guest {
|
|
351
|
+
guestId
|
|
352
|
+
tier
|
|
353
|
+
name {
|
|
354
|
+
firstName
|
|
355
|
+
lastName
|
|
356
|
+
nameFmt
|
|
357
|
+
}
|
|
358
|
+
emails {
|
|
359
|
+
emailAddressMasked
|
|
360
|
+
emailType
|
|
361
|
+
emailAddressHashed
|
|
362
|
+
}
|
|
363
|
+
addresses {
|
|
364
|
+
addressLine1
|
|
365
|
+
addressLine2
|
|
366
|
+
city
|
|
367
|
+
country
|
|
368
|
+
state
|
|
369
|
+
postalCode
|
|
370
|
+
addressFmt
|
|
371
|
+
addressType
|
|
372
|
+
}
|
|
373
|
+
hhonorsNumber
|
|
374
|
+
phones {
|
|
375
|
+
phoneNumberMasked(format: masked)
|
|
376
|
+
phoneType
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
guestBenefits {
|
|
380
|
+
foodAndBeverageCreditBenefit {
|
|
381
|
+
description
|
|
382
|
+
heading
|
|
383
|
+
linkLabel
|
|
384
|
+
linkUrl
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
lastUpdatedBooker {
|
|
388
|
+
guestId
|
|
389
|
+
programAccountId
|
|
390
|
+
}
|
|
391
|
+
modifyEligible
|
|
392
|
+
nor1Upgrade(provider: "DOHWR") {
|
|
393
|
+
content {
|
|
394
|
+
button
|
|
395
|
+
description
|
|
396
|
+
firstName
|
|
397
|
+
title
|
|
398
|
+
}
|
|
399
|
+
offerLink
|
|
400
|
+
requested
|
|
401
|
+
success
|
|
402
|
+
}
|
|
403
|
+
notifications {
|
|
404
|
+
subType
|
|
405
|
+
text
|
|
406
|
+
type
|
|
407
|
+
}
|
|
408
|
+
originalBooker {
|
|
409
|
+
guestId
|
|
410
|
+
programAccountId
|
|
411
|
+
}
|
|
412
|
+
propCode
|
|
413
|
+
requests {
|
|
414
|
+
specialRequests {
|
|
415
|
+
pets
|
|
416
|
+
servicePets
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
restricted
|
|
420
|
+
rooms {
|
|
421
|
+
gnrNumber
|
|
422
|
+
resCreateDateFmt(format: "yyyy-MM-dd")
|
|
423
|
+
addOns {
|
|
424
|
+
addOnCost {
|
|
425
|
+
amountAfterTax
|
|
426
|
+
amountAfterTaxFmt
|
|
427
|
+
}
|
|
428
|
+
addOnDetails {
|
|
429
|
+
addOnAvailType
|
|
430
|
+
addOnDescription
|
|
431
|
+
addOnCode
|
|
432
|
+
addOnName
|
|
433
|
+
addOnPricing
|
|
434
|
+
addOnUnit
|
|
435
|
+
addOnUnitCategory
|
|
436
|
+
amountAfterTax
|
|
437
|
+
amountAfterTaxFmt
|
|
438
|
+
averageDailyRate
|
|
439
|
+
averageDailyRateFmt
|
|
440
|
+
categoryCode
|
|
441
|
+
counts {
|
|
442
|
+
numAddOns
|
|
443
|
+
fulfillmentDate
|
|
444
|
+
rate
|
|
445
|
+
rateFmt
|
|
446
|
+
}
|
|
447
|
+
numAddOnDays
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
additionalNames {
|
|
451
|
+
firstName
|
|
452
|
+
lastName
|
|
453
|
+
}
|
|
454
|
+
certificates {
|
|
455
|
+
certType
|
|
456
|
+
certNumber
|
|
457
|
+
totalPoints
|
|
458
|
+
totalPointsFmt
|
|
459
|
+
}
|
|
460
|
+
offers {
|
|
461
|
+
offerSource
|
|
462
|
+
originalRoomTypeName
|
|
463
|
+
}
|
|
464
|
+
preArrivalOffers(guestLocationCountry: $guestLocationCountry) @include(if: $includePreArrivalOffers) {
|
|
465
|
+
offeredRoomTypes(
|
|
466
|
+
filter: {isUpsell: true}
|
|
467
|
+
sort: [{by: displayCost, order: asc}]
|
|
468
|
+
) {
|
|
469
|
+
adaAccessibleRoom
|
|
470
|
+
cmaTotalPriceIndicator
|
|
471
|
+
displayCostFmtRnd: displayCostFmt(decimal: 0, strategy: ceiling)
|
|
472
|
+
displayCostHotelCurrency: displayCost
|
|
473
|
+
displayCostUsd: displayCost(currencyCode: "USD")
|
|
474
|
+
feeTransparencyIndicator
|
|
475
|
+
images(ratios: [threeByTwo], first: 1) {
|
|
476
|
+
altText
|
|
477
|
+
ratios {
|
|
478
|
+
url
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
offerDescription
|
|
482
|
+
roomRate {
|
|
483
|
+
amountAfterTax
|
|
484
|
+
amountAfterTaxFmt(decimal: 0, strategy: ceiling)
|
|
485
|
+
amountBeforeTaxFmt
|
|
486
|
+
averageRateFmt
|
|
487
|
+
fullAmountAfterTax: amountAfterTaxFmt
|
|
488
|
+
rateAmount
|
|
489
|
+
rateAmountFmt
|
|
490
|
+
rateChangeIndicator
|
|
491
|
+
rateDetails {
|
|
492
|
+
effectiveDateFmt(format: "medium")
|
|
493
|
+
effectiveDateFmtAda: effectiveDateFmt(format: "long")
|
|
494
|
+
rateAmount
|
|
495
|
+
rateAmountFmt
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
roomTypeCode
|
|
499
|
+
roomTypeName
|
|
500
|
+
}
|
|
501
|
+
offerId
|
|
502
|
+
offerType
|
|
503
|
+
originalRatePlanCode
|
|
504
|
+
originalRoomTypeCode
|
|
505
|
+
validUntil
|
|
506
|
+
}
|
|
507
|
+
numAdults
|
|
508
|
+
numChildren
|
|
509
|
+
childAges
|
|
510
|
+
autoUpgradedStay
|
|
511
|
+
isStaySmu
|
|
512
|
+
isStayUpsell
|
|
513
|
+
isStayUpsellOverAutoUpgrade
|
|
514
|
+
priorRoomType {
|
|
515
|
+
roomTypeName
|
|
516
|
+
}
|
|
517
|
+
cost {
|
|
518
|
+
currency {
|
|
519
|
+
currencyCode
|
|
520
|
+
currencySymbol
|
|
521
|
+
description
|
|
522
|
+
}
|
|
523
|
+
amountAfterTax: guestTotalCostAfterTax
|
|
524
|
+
amountAfterTaxFmt: guestTotalCostAfterTaxFmt
|
|
525
|
+
amountBeforeTax
|
|
526
|
+
amountBeforeTaxFmt
|
|
527
|
+
serviceChargeDetails
|
|
528
|
+
serviceChargeFeeType
|
|
529
|
+
serviceChargePeriods {
|
|
530
|
+
serviceCharges {
|
|
531
|
+
amount
|
|
532
|
+
amountFmt
|
|
533
|
+
description
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
rateAmountFmt
|
|
537
|
+
rateChanges
|
|
538
|
+
totalServiceCharges
|
|
539
|
+
totalServiceChargesFmt
|
|
540
|
+
totalTaxes
|
|
541
|
+
totalTaxesFmt
|
|
542
|
+
rateDetails(perNight: true) {
|
|
543
|
+
effectiveDateFmt(format: "medium")
|
|
544
|
+
effectiveDateFmtAda: effectiveDateFmt(format: "long")
|
|
545
|
+
rateAmount
|
|
546
|
+
rateAmountFmt
|
|
547
|
+
}
|
|
548
|
+
upgradedAmount
|
|
549
|
+
upgradedAmountFmt
|
|
550
|
+
}
|
|
551
|
+
guarantee {
|
|
552
|
+
cxlPolicyCode
|
|
553
|
+
cxlPolicyDesc
|
|
554
|
+
guarPolicyCode
|
|
555
|
+
guarPolicyDesc
|
|
556
|
+
}
|
|
557
|
+
numAdults
|
|
558
|
+
numChildren
|
|
559
|
+
ratePlan {
|
|
560
|
+
confidentialRates
|
|
561
|
+
hhonorsMembershipRequired
|
|
562
|
+
advancePurchase
|
|
563
|
+
promoCode
|
|
564
|
+
disclaimer {
|
|
565
|
+
diamond48
|
|
566
|
+
diamond48_noTx: diamond48
|
|
567
|
+
fullPrePayNonRefundable
|
|
568
|
+
fullPrePayNonRefundable_noTx: fullPrePayNonRefundable
|
|
569
|
+
hhonorsCancellationCharges
|
|
570
|
+
hhonorsCancellationCharges_noTx: hhonorsCancellationCharges
|
|
571
|
+
hhonorsPointsDeduction
|
|
572
|
+
hhonorsPointsDeduction_noTx: hhonorsPointsDeduction
|
|
573
|
+
hhonorsPrintedConfirmation
|
|
574
|
+
hhonorsPrintedConfirmation_noTx: hhonorsPrintedConfirmation
|
|
575
|
+
lengthOfStay
|
|
576
|
+
lengthOfStay_noTx: lengthOfStay
|
|
577
|
+
rightToCancel
|
|
578
|
+
rightToCancel_noTx: rightToCancel
|
|
579
|
+
totalRate
|
|
580
|
+
totalRate_noTx: totalRate
|
|
581
|
+
cancellationRefundPolicy
|
|
582
|
+
cancellationRefundPolicy_noTx: cancellationRefundPolicy
|
|
583
|
+
}
|
|
584
|
+
ratePlanCode
|
|
585
|
+
ratePlanName
|
|
586
|
+
ratePlanDesc
|
|
587
|
+
ratePlanDesc_noTx: ratePlanDesc
|
|
588
|
+
specialRateType
|
|
589
|
+
serviceChargesAndTaxesIncluded
|
|
590
|
+
}
|
|
591
|
+
roomType {
|
|
592
|
+
adaAccessibleRoom
|
|
593
|
+
carousel(first: 1) {
|
|
594
|
+
altText
|
|
595
|
+
variants(filter: {size: md}) {
|
|
596
|
+
url
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
roomTypeCode
|
|
600
|
+
roomTypeName
|
|
601
|
+
roomTypeDesc
|
|
602
|
+
roomOccupancy
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
roomUpgradesAvailable @include(if: $includePreArrivalOffers)
|
|
606
|
+
scaRequired
|
|
607
|
+
scaType
|
|
608
|
+
showAutoUpgradeIndicator
|
|
609
|
+
specialRateOptions {
|
|
610
|
+
corporateId
|
|
611
|
+
groupCode
|
|
612
|
+
hhonors
|
|
613
|
+
lta
|
|
614
|
+
pnd
|
|
615
|
+
promoCode
|
|
616
|
+
travelAgent
|
|
617
|
+
familyAndFriends
|
|
618
|
+
teamMember
|
|
619
|
+
owner
|
|
620
|
+
ownerHGV
|
|
621
|
+
smb
|
|
622
|
+
}
|
|
623
|
+
taxPeriods {
|
|
624
|
+
taxes {
|
|
625
|
+
description
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
totalNumAdults
|
|
629
|
+
totalNumChildren
|
|
630
|
+
totalNumRooms
|
|
631
|
+
unlimitedRewardsNumber
|
|
632
|
+
}`
|
|
633
|
+
]);
|
|
130
634
|
// src/merchant-card-endpoint-profiles.js
|
|
131
635
|
var MERCHANT_CARD_ENDPOINT_PSP = "merchant_card_endpoint";
|
|
132
636
|
var MERCHANT_CARD_ENDPOINT_MODE = "card_endpoint";
|
|
133
637
|
var BROWSER_HEADERS = /* @__PURE__ */ Object.freeze(["user-agent", "accept", "accept-language", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform"]);
|
|
638
|
+
var NAVIGATION_HEADERS = /* @__PURE__ */ Object.freeze([
|
|
639
|
+
"content-type",
|
|
640
|
+
"cache-control",
|
|
641
|
+
"upgrade-insecure-requests",
|
|
642
|
+
"sec-fetch-dest",
|
|
643
|
+
"sec-fetch-mode",
|
|
644
|
+
"sec-fetch-site",
|
|
645
|
+
"sec-fetch-user"
|
|
646
|
+
]);
|
|
647
|
+
var GUARANTEE_RATES = "a policy for guarantee rates: the hotel charges the card at the stay, by when a single-use card may have closed";
|
|
134
648
|
var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
135
649
|
{
|
|
136
650
|
id: "att_payment_info",
|
|
@@ -144,13 +658,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
144
658
|
],
|
|
145
659
|
har: null,
|
|
146
660
|
checkout: "https://www.att.com",
|
|
661
|
+
checkout_frame: false,
|
|
662
|
+
frame: null,
|
|
147
663
|
endpoint: "https://www.att.com",
|
|
148
664
|
method: "POST",
|
|
149
665
|
path: "/msapi/checkoutexporch/v1/paymentInfo",
|
|
150
666
|
query: { cartId: ":cartId" },
|
|
667
|
+
navigation: null,
|
|
151
668
|
encoding: "json",
|
|
152
669
|
session: {
|
|
153
670
|
cookies: true,
|
|
671
|
+
cookie_names: null,
|
|
154
672
|
headers: [...BROWSER_HEADERS, "content-type", "x-csrf-token", "appname", "idpctx-appname"],
|
|
155
673
|
required: ["x-csrf-token", "appname"]
|
|
156
674
|
},
|
|
@@ -159,9 +677,13 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
159
677
|
cvv: { at: ["cvv"], format: "digits" },
|
|
160
678
|
exp_month: { at: ["expDate"], format: "as_typed" },
|
|
161
679
|
exp_year: { at: ["expYear"], format: "as_typed" },
|
|
680
|
+
expiry: null,
|
|
681
|
+
last4: null,
|
|
682
|
+
leading: null,
|
|
162
683
|
brands: [{ at: ["cardSubType"], map: null }],
|
|
163
684
|
bins: null
|
|
164
685
|
},
|
|
686
|
+
merchant_key: null,
|
|
165
687
|
requires: [{ at: ["type"], one_of: ["CREDIT", "DEBIT", "ANYCARD"] }],
|
|
166
688
|
refuse: [
|
|
167
689
|
{ at: ["savedPaymentData"], when: "present" },
|
|
@@ -171,12 +693,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
171
693
|
{ at: ["statusCode"], when: "present" },
|
|
172
694
|
{ at: ["includeAutoPay"], when: "true" }
|
|
173
695
|
],
|
|
696
|
+
payment_method: null,
|
|
174
697
|
amount: { at: ["amount"], kind: "decimal_string" },
|
|
175
698
|
currency: "USD",
|
|
176
|
-
response: { headers: ["content-type", "set-cookie", "x-csrf-token"] },
|
|
699
|
+
response: { headers: ["content-type", "set-cookie", "x-csrf-token"], withhold: [] },
|
|
700
|
+
resends: [],
|
|
701
|
+
challenge: null,
|
|
702
|
+
facts: null,
|
|
177
703
|
bootstrap: "/robots.txt",
|
|
178
704
|
timeout_ms: 3e4,
|
|
179
705
|
charge_timing: "deferred",
|
|
706
|
+
once_per: null,
|
|
180
707
|
pending: [
|
|
181
708
|
"card.brands: the cardSubType value for each brand",
|
|
182
709
|
"card.exp_month and card.exp_year: how many digits expDate and expYear carry",
|
|
@@ -195,13 +722,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
195
722
|
],
|
|
196
723
|
har: null,
|
|
197
724
|
checkout: "https://biglietti.italotreno.com",
|
|
725
|
+
checkout_frame: false,
|
|
726
|
+
frame: null,
|
|
198
727
|
endpoint: "https://api-biglietti.italotreno.com",
|
|
199
728
|
method: "POST",
|
|
200
729
|
path: "/api/v1/booking/:bookingId/payments/credit-card",
|
|
201
730
|
query: {},
|
|
731
|
+
navigation: null,
|
|
202
732
|
encoding: "json",
|
|
203
733
|
session: {
|
|
204
734
|
cookies: false,
|
|
735
|
+
cookie_names: null,
|
|
205
736
|
headers: [...BROWSER_HEADERS, "content-type", "authorization", "x-big-working-session-id"],
|
|
206
737
|
required: ["authorization", "x-big-working-session-id"]
|
|
207
738
|
},
|
|
@@ -210,20 +741,29 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
210
741
|
cvv: { at: ["CVV"], format: "digits" },
|
|
211
742
|
exp_month: { at: ["expirationMonth"], format: "mm" },
|
|
212
743
|
exp_year: { at: ["expirationYear"], format: "yyyy_int" },
|
|
744
|
+
expiry: null,
|
|
745
|
+
last4: null,
|
|
746
|
+
leading: null,
|
|
213
747
|
brands: [{ at: ["paymentMethodCode"], map: { visa: "VI", mastercard: "MC" } }],
|
|
214
748
|
bins: null
|
|
215
749
|
},
|
|
750
|
+
merchant_key: null,
|
|
216
751
|
requires: [{ at: ["termsAndConditions"], equals: true }],
|
|
217
752
|
refuse: [
|
|
218
753
|
{ at: ["StoreCreditCard"], when: "true" },
|
|
219
754
|
{ at: ["creditCardId"], when: "present" }
|
|
220
755
|
],
|
|
756
|
+
payment_method: null,
|
|
221
757
|
amount: null,
|
|
222
758
|
currency: "EUR",
|
|
223
|
-
response: { headers: ["content-type"] },
|
|
759
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
760
|
+
resends: [],
|
|
761
|
+
challenge: null,
|
|
762
|
+
facts: null,
|
|
224
763
|
bootstrap: "/robots.txt",
|
|
225
764
|
timeout_ms: 3e4,
|
|
226
765
|
charge_timing: "at_request",
|
|
766
|
+
once_per: null,
|
|
227
767
|
pending: [
|
|
228
768
|
"whether the validationMetadata.PaReq answer carries the card number",
|
|
229
769
|
"whether expire is sent absent or as null"
|
|
@@ -241,13 +781,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
241
781
|
],
|
|
242
782
|
har: null,
|
|
243
783
|
checkout: "https://www.universalorlando.com",
|
|
784
|
+
checkout_frame: false,
|
|
785
|
+
frame: null,
|
|
244
786
|
endpoint: "https://secure-api.universalparks.com",
|
|
245
787
|
method: "POST",
|
|
246
788
|
path: "/instances/:instanceId/commerce-checkout",
|
|
247
789
|
query: {},
|
|
790
|
+
navigation: null,
|
|
248
791
|
encoding: "json",
|
|
249
792
|
session: {
|
|
250
793
|
cookies: false,
|
|
794
|
+
cookie_names: null,
|
|
251
795
|
headers: [
|
|
252
796
|
...BROWSER_HEADERS,
|
|
253
797
|
"content-type",
|
|
@@ -271,9 +815,13 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
271
815
|
cvv: { at: ["cvv"], format: "digits" },
|
|
272
816
|
exp_month: { at: ["expiration", "month"], format: "m_int" },
|
|
273
817
|
exp_year: { at: ["expiration", "year"], format: "yyyy_int" },
|
|
818
|
+
expiry: null,
|
|
819
|
+
last4: null,
|
|
820
|
+
leading: null,
|
|
274
821
|
brands: [],
|
|
275
822
|
bins: null
|
|
276
823
|
},
|
|
824
|
+
merchant_key: null,
|
|
277
825
|
requires: [
|
|
278
826
|
{ at: ["currency_code"], equals: "USD" },
|
|
279
827
|
{ at: ["order_id"], present: true }
|
|
@@ -283,12 +831,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
283
831
|
{ at: ["cardinal-3d-secure", "jwt"], when: "nonempty" },
|
|
284
832
|
{ at: ["cardinal-3d-secure", "reference_id"], when: "nonempty" }
|
|
285
833
|
],
|
|
834
|
+
payment_method: null,
|
|
286
835
|
amount: { at: ["amount"], kind: "decimal_string" },
|
|
287
836
|
currency: { at: ["currency_code"] },
|
|
288
|
-
response: { headers: ["content-type"] },
|
|
837
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
838
|
+
resends: [],
|
|
839
|
+
challenge: null,
|
|
840
|
+
facts: null,
|
|
289
841
|
bootstrap: "/robots.txt",
|
|
290
842
|
timeout_ms: 3e4,
|
|
291
843
|
charge_timing: "at_request",
|
|
844
|
+
once_per: null,
|
|
292
845
|
pending: ["how long a captured akamai-bm-telemetry header stays accepted"]
|
|
293
846
|
},
|
|
294
847
|
{
|
|
@@ -302,13 +855,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
302
855
|
],
|
|
303
856
|
har: null,
|
|
304
857
|
checkout: "https://www.targetoptical.com",
|
|
858
|
+
checkout_frame: false,
|
|
859
|
+
frame: null,
|
|
305
860
|
endpoint: "https://www.targetoptical.com",
|
|
306
861
|
method: "POST",
|
|
307
862
|
path: "/AjaxOrderChangeServicePIAddTO",
|
|
308
863
|
query: {},
|
|
864
|
+
navigation: null,
|
|
309
865
|
encoding: "form",
|
|
310
866
|
session: {
|
|
311
867
|
cookies: true,
|
|
868
|
+
cookie_names: null,
|
|
312
869
|
headers: [...BROWSER_HEADERS, "content-type", "x-requested-with"],
|
|
313
870
|
required: ["x-requested-with"]
|
|
314
871
|
},
|
|
@@ -317,9 +874,13 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
317
874
|
cvv: { at: "cc_cvc", format: "digits" },
|
|
318
875
|
exp_month: { at: "expire_month", format: "as_typed" },
|
|
319
876
|
exp_year: { at: "expire_year", format: "as_typed" },
|
|
877
|
+
expiry: null,
|
|
878
|
+
last4: null,
|
|
879
|
+
leading: null,
|
|
320
880
|
brands: [{ at: "cc_brand", map: null }, { at: "payMethodId", map: null }],
|
|
321
881
|
bins: null
|
|
322
882
|
},
|
|
883
|
+
merchant_key: null,
|
|
323
884
|
requires: [
|
|
324
885
|
{ at: "requesttype", equals: "ajax" },
|
|
325
886
|
{ at: "authToken", present: true },
|
|
@@ -329,12 +890,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
329
890
|
{ at: "saveCreditCard", when: "present" },
|
|
330
891
|
{ at: "creditCardToUse", when: "present" }
|
|
331
892
|
],
|
|
893
|
+
payment_method: null,
|
|
332
894
|
amount: { at: "piAmount", kind: "decimal_string" },
|
|
333
895
|
currency: "USD",
|
|
334
|
-
response: { headers: ["content-type", "set-cookie"] },
|
|
896
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
897
|
+
resends: [],
|
|
898
|
+
challenge: null,
|
|
899
|
+
facts: null,
|
|
335
900
|
bootstrap: "/robots.txt",
|
|
336
901
|
timeout_ms: 3e4,
|
|
337
902
|
charge_timing: "deferred",
|
|
903
|
+
once_per: null,
|
|
338
904
|
pending: [
|
|
339
905
|
"card.brands: the cc_brand and payMethodId values for each brand",
|
|
340
906
|
"card.exp_month and card.exp_year: how many digits expire_month and expire_year carry",
|
|
@@ -354,13 +920,17 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
354
920
|
],
|
|
355
921
|
har: null,
|
|
356
922
|
checkout: "https://www.opusvirtualoffices.com",
|
|
923
|
+
checkout_frame: false,
|
|
924
|
+
frame: null,
|
|
357
925
|
endpoint: "https://www.opusvirtualoffices.com",
|
|
358
926
|
method: "POST",
|
|
359
927
|
path: "/api/signup/process/payment/",
|
|
360
928
|
query: {},
|
|
929
|
+
navigation: null,
|
|
361
930
|
encoding: "multipart",
|
|
362
931
|
session: {
|
|
363
932
|
cookies: true,
|
|
933
|
+
cookie_names: null,
|
|
364
934
|
headers: [...BROWSER_HEADERS, "content-type"],
|
|
365
935
|
required: []
|
|
366
936
|
},
|
|
@@ -369,50 +939,2107 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
|
|
|
369
939
|
cvv: { at: "ccv", format: "digits" },
|
|
370
940
|
exp_month: { at: "expdatem", format: "mm" },
|
|
371
941
|
exp_year: { at: "expdatey", format: "yyyy" },
|
|
942
|
+
expiry: null,
|
|
943
|
+
last4: null,
|
|
944
|
+
leading: null,
|
|
372
945
|
brands: [{ at: "CreditCardType", map: { visa: "1", mastercard: "2" } }],
|
|
373
946
|
bins: { mastercard: ["51", "52", "53", "54", "55"] }
|
|
374
947
|
},
|
|
948
|
+
merchant_key: null,
|
|
375
949
|
requires: [
|
|
376
950
|
{ at: "payment_option", equals: "CreditCard" },
|
|
377
951
|
{ at: "uid", present: true },
|
|
378
952
|
{ at: "prepaymny", one_of: ["1", "3", "6", "11"] }
|
|
379
953
|
],
|
|
380
954
|
refuse: [],
|
|
955
|
+
payment_method: null,
|
|
381
956
|
amount: null,
|
|
382
957
|
currency: "USD",
|
|
383
|
-
response: { headers: ["content-type", "set-cookie"] },
|
|
958
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
959
|
+
resends: [],
|
|
960
|
+
challenge: null,
|
|
961
|
+
facts: null,
|
|
384
962
|
bootstrap: "/robots.txt",
|
|
385
963
|
timeout_ms: 3e4,
|
|
386
964
|
charge_timing: "recurring",
|
|
965
|
+
once_per: null,
|
|
387
966
|
pending: [
|
|
388
967
|
"the amount each prepaymny term charges, so the approval can show what the chosen term costs",
|
|
389
968
|
"a renewal policy: each renewal charges the card on file with no approval of its own"
|
|
390
969
|
]
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
id: "delta_payment_details",
|
|
973
|
+
merchant: "Delta Air Lines",
|
|
974
|
+
status: "observe",
|
|
975
|
+
evidence: [
|
|
976
|
+
"www.delta.com completepurchase 26.9.8 (main.js) and booking-config-2670.json, read Sep 25, 2026: the review-pay page posts JSON with withCredentials to APP_PAYMENT_SUBMIT, https://enterprisepayments-api.delta.com/efop/paymentDetails, after a Luhn check.",
|
|
977
|
+
"Headers (addBFFHeaders): X-Adapter web, Content-Type and Accept application/json, cartId, appId, channelId, purchasePath, and TransactionId, currencyCode, paymentReferenceId and skymilesNbr when set.",
|
|
978
|
+
'Body (pipe paymentFormValuesToSaveCreditCardRequestJSON): {cardDetails: {billingAdr, cardHolderName, cardType, expirationMonthNum, expirationYearNum, lastFourDigits, paymentCardNum, paymentCardSecurityCode, paymentType "Credit Card", savedFopId, ...}, paymentType "Credit Card"}; the expiry pipe pads the month to two digits and writes the year as 20YY.',
|
|
979
|
+
"cardType is the form's creditCardCode; Delta's own eligible-fop-current.json lists VI, CA (Mastercard), AX, DS, JC, DC and TP.",
|
|
980
|
+
"The answer returns a paymentReferenceId that 3DS (/3ds/initialize, /3ds/authenticate) and the purchase use, with no card. Saving the card to the wallet is a separate request (/wallet/paymentDetailsForWallet) the device never sends."
|
|
981
|
+
],
|
|
982
|
+
har: null,
|
|
983
|
+
checkout: "https://www.delta.com",
|
|
984
|
+
checkout_frame: false,
|
|
985
|
+
frame: null,
|
|
986
|
+
endpoint: "https://enterprisepayments-api.delta.com",
|
|
987
|
+
method: "POST",
|
|
988
|
+
path: "/efop/paymentDetails",
|
|
989
|
+
query: {},
|
|
990
|
+
navigation: null,
|
|
991
|
+
encoding: "json",
|
|
992
|
+
session: {
|
|
993
|
+
cookies: true,
|
|
994
|
+
cookie_names: null,
|
|
995
|
+
headers: [
|
|
996
|
+
...BROWSER_HEADERS,
|
|
997
|
+
"content-type",
|
|
998
|
+
"x-adapter",
|
|
999
|
+
"cartid",
|
|
1000
|
+
"appid",
|
|
1001
|
+
"channelid",
|
|
1002
|
+
"purchasepath",
|
|
1003
|
+
"transactionid",
|
|
1004
|
+
"currencycode",
|
|
1005
|
+
"paymentreferenceid",
|
|
1006
|
+
"skymilesnbr"
|
|
1007
|
+
],
|
|
1008
|
+
required: ["x-adapter", "cartid"]
|
|
1009
|
+
},
|
|
1010
|
+
card: {
|
|
1011
|
+
pan: { at: ["cardDetails", "paymentCardNum"], format: "digits" },
|
|
1012
|
+
cvv: { at: ["cardDetails", "paymentCardSecurityCode"], format: "digits" },
|
|
1013
|
+
exp_month: { at: ["cardDetails", "expirationMonthNum"], format: "mm" },
|
|
1014
|
+
exp_year: { at: ["cardDetails", "expirationYearNum"], format: "yyyy" },
|
|
1015
|
+
expiry: null,
|
|
1016
|
+
last4: { at: ["cardDetails", "lastFourDigits"], format: "digits" },
|
|
1017
|
+
leading: null,
|
|
1018
|
+
brands: [{ at: ["cardDetails", "cardType"], map: { visa: "VI", mastercard: "CA" } }],
|
|
1019
|
+
bins: null
|
|
1020
|
+
},
|
|
1021
|
+
merchant_key: null,
|
|
1022
|
+
requires: [
|
|
1023
|
+
{ at: ["paymentType"], equals: "Credit Card" },
|
|
1024
|
+
{ at: ["cardDetails", "paymentType"], equals: "Credit Card" }
|
|
1025
|
+
],
|
|
1026
|
+
refuse: [{ at: ["cardDetails", "savedFopId"], when: "nonempty" }],
|
|
1027
|
+
payment_method: null,
|
|
1028
|
+
amount: null,
|
|
1029
|
+
currency: "USD",
|
|
1030
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1031
|
+
resends: [],
|
|
1032
|
+
challenge: null,
|
|
1033
|
+
facts: null,
|
|
1034
|
+
bootstrap: "/robots.txt",
|
|
1035
|
+
timeout_ms: 3e4,
|
|
1036
|
+
charge_timing: "deferred",
|
|
1037
|
+
once_per: null,
|
|
1038
|
+
pending: [
|
|
1039
|
+
"the currency: the body names none, and a point of sale outside the US prices in its own"
|
|
1040
|
+
]
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
id: "hilton_create_reservation",
|
|
1044
|
+
merchant: "Hilton",
|
|
1045
|
+
status: "observe",
|
|
1046
|
+
evidence: [
|
|
1047
|
+
"Hilton's reservation app dx-res-ui (www.hilton.com/dx_reservation_app): payment page chunks 70b8777e (Aug 24) and 8a158c4c (Sep 8), _app 6b38675c and c2e248fe (Sep 15), read from archive.org copies Sep 25, 2026. Every Hilton brand property books through it, Hilton San Francisco Union Square (SFOFHHH) included.",
|
|
1048
|
+
"The GraphQL transport fetches POST ${DX_GRAPHQL_CLIENT_URI}?${new URLSearchParams({appName, appVersion, operationName, originalOpName, bl})} with body JSON.stringify({query, operationName, variables}): https://www.hilton.com/graphql/customer, appVersion dx-res-ui:<build>, which URLSearchParams writes as dx-res-ui%3A<build>.",
|
|
1049
|
+
"Headers: Content-Type application/json, dx-platform (web when the page sets none), visitorid, hltclientmessageid, and Authorization Bearer only when there is no webGuestMetadata cookie; a same-origin fetch, so the first-party cookies go with it.",
|
|
1050
|
+
"createReservation variables: {language, input: {totalAmountAfterTax (a number), currencyCode, guarantee: {guarMethodCode, paymentCard: {cardCode (from the typed digits), cardExpireDate, cardNumber, cvvString (only when the rate sets cvvRequired), cardholder fields, paymentId (a saved Honors card)}, deposit}, ...}, scaInput, mfaInput, enrollment, guestUpdate: {preferredAddress, updatePaymentMethod (saves the card to the account)}}.",
|
|
1051
|
+
"SCA (PSD2 or Adyen Protect) sends scaInput {returnUrl, browserInfo} on the first call, answers error 897 and sends createReservation again with {scaId, md, paRes} and the card; MFA (errors 995, 996) sends it again with mfaInput. Both are refused here, so only stays that need neither are paid.",
|
|
1052
|
+
"The body's query is the createReservation operationString with its two fragments, which the transport sends unchanged (z = {query, operationName, variables}). requires pins it to the text the archived Sep 15 _app chunk c2e248fe builds (merchant-card-endpoint-documents.js), so an argument written inline in the document or a second createReservation under an alias is refused."
|
|
1053
|
+
],
|
|
1054
|
+
har: null,
|
|
1055
|
+
checkout: "https://www.hilton.com",
|
|
1056
|
+
checkout_frame: false,
|
|
1057
|
+
frame: null,
|
|
1058
|
+
endpoint: "https://www.hilton.com",
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
path: "/graphql/customer",
|
|
1061
|
+
query: { appName: "dx-res-ui", appVersion: "dx-res-ui%3A:build", operationName: "createReservation", originalOpName: "createReservation", bl: ":language" },
|
|
1062
|
+
navigation: null,
|
|
1063
|
+
encoding: "json",
|
|
1064
|
+
session: {
|
|
1065
|
+
cookies: true,
|
|
1066
|
+
cookie_names: null,
|
|
1067
|
+
headers: [...BROWSER_HEADERS, "content-type", "authorization", "dx-platform", "visitorid", "hltclientmessageid"],
|
|
1068
|
+
required: ["dx-platform"]
|
|
1069
|
+
},
|
|
1070
|
+
card: {
|
|
1071
|
+
pan: { at: ["variables", "input", "guarantee", "paymentCard", "cardNumber"], format: "digits" },
|
|
1072
|
+
cvv: { at: ["variables", "input", "guarantee", "paymentCard", "cvvString"], format: "digits_if_asked" },
|
|
1073
|
+
exp_month: null,
|
|
1074
|
+
exp_year: null,
|
|
1075
|
+
expiry: { at: ["variables", "input", "guarantee", "paymentCard", "cardExpireDate"], format: "as_typed" },
|
|
1076
|
+
last4: null,
|
|
1077
|
+
leading: null,
|
|
1078
|
+
brands: [{ at: ["variables", "input", "guarantee", "paymentCard", "cardCode"], map: null }],
|
|
1079
|
+
bins: null
|
|
1080
|
+
},
|
|
1081
|
+
merchant_key: null,
|
|
1082
|
+
requires: [
|
|
1083
|
+
{ at: ["operationName"], equals: "createReservation" },
|
|
1084
|
+
{ at: ["query"], one_of: [...HILTON_CREATE_RESERVATION_DOCUMENTS] }
|
|
1085
|
+
],
|
|
1086
|
+
refuse: [
|
|
1087
|
+
{ at: ["variables", "input", "guarantee", "paymentCard", "paymentId"], when: "present" },
|
|
1088
|
+
{ at: ["variables", "scaInput"], when: "nonempty" },
|
|
1089
|
+
{ at: ["variables", "mfaInput"], when: "nonempty" },
|
|
1090
|
+
{ at: ["variables", "guestUpdate", "updatePaymentMethod"], when: "true" }
|
|
1091
|
+
],
|
|
1092
|
+
payment_method: null,
|
|
1093
|
+
amount: { at: ["variables", "input", "totalAmountAfterTax"], kind: "decimal_number" },
|
|
1094
|
+
currency: { at: ["variables", "input", "currencyCode"] },
|
|
1095
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1096
|
+
resends: [],
|
|
1097
|
+
challenge: null,
|
|
1098
|
+
facts: null,
|
|
1099
|
+
bootstrap: "/robots.txt",
|
|
1100
|
+
timeout_ms: 3e4,
|
|
1101
|
+
charge_timing: "deferred",
|
|
1102
|
+
once_per: null,
|
|
1103
|
+
pending: [
|
|
1104
|
+
"card.expiry: the form cardExpireDate is sent in",
|
|
1105
|
+
"card.brands: the cardCode value for each brand",
|
|
1106
|
+
"the query: whether createReservation adds language or other variables to it",
|
|
1107
|
+
"the createReservation GraphQL document the live build sends, which requires pins (it holds the archived Sep 15 build's): a document that is not pinned can inline guestUpdate.updatePaymentMethod, scaInput or mfaInput, or alias createReservation twice",
|
|
1108
|
+
GUARANTEE_RATES
|
|
1109
|
+
]
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
id: "ihg_hotel_reservation",
|
|
1113
|
+
merchant: "IHG Hotels & Resorts",
|
|
1114
|
+
status: "observe",
|
|
1115
|
+
evidence: [
|
|
1116
|
+
"IHG pay SPA pay-ng v14.144.1 (chunks 5PVRTMTV, 7U5FHKAZ and AO2OILHC, archive.org copies of Jul 24 and 27, 2026), read Sep 25, 2026; the Sep 5 Common Crawl copy of find-hotels v14.147.0 still sends checkout there.",
|
|
1117
|
+
'ReservationService.createReservation posts the first reservation, JSON with withCredentials, to https://apis.ihg.com/reservations/v3/hotels: {id, imageStatus "COMMIT", segments: [{payment: {paymentByPoints, paymentInformation: {paymentCard}, guaranteeType, billingAddress}, ...}], userProfiles, hotels}.',
|
|
1118
|
+
"paymentCard (getPaymentInformation): {cardCode, expireDate `${expirationMonth}${expirationYear}` (MMYY), cardHolderName, cvvNumber: {token: <CVV>}, cardNumber: {token: <PAN, spaces stripped>}}, plus 3dsData after a PSD2 check.",
|
|
1119
|
+
"Headers: x-ihg-api-key (public client key), content-type application/json; charset=UTF-8, ihg-language, X-IHG-SSO-TOKEN, x-accept-currency (USD), IHG-SellSource, Forter-Token; interceptors add IHG-SessionId and IHG-TransactionId.",
|
|
1120
|
+
"Hotels that set isPaymentServiceInformationRequired first send the PAN to /payments/threedsecure/v1/token ({cardToken: <PAN>, cardType}) and /paymentSetup (card.token: <PAN>) and put the result in 3dsData, which is refused here; China UnionPay and PayPal set cupTokenId and paymentSource, also refused. Both 3-D Secure posts are in resends below.",
|
|
1121
|
+
'pay-ng v14.149.0, read live Oct 6, 2026 (live-checks-oct6/ihg): the pay page posts the same reservation body, card in the clear, to /channels/direct/quickEnroll/v8/hotelReservations while its pre-ticked "Enroll for free as you book" box stays ticked (/quickEnroll/v9/hotelReservations behind the shouldUseNewQuickEnrolV9 flag, /quickEnroll/v8/multiRoomReservation for a cart), and to /reservations/v3/hotels/rooms/fromCart for more than one room, /reservations/v3/hotels/rooms/agent/reservations for a travel agent and /reservations/v3/businessRewardReservations for Business Rewards. Each is in resends below, so the SDK aborts it in every status.'
|
|
1122
|
+
],
|
|
1123
|
+
har: null,
|
|
1124
|
+
checkout: "https://www.ihg.com",
|
|
1125
|
+
checkout_frame: false,
|
|
1126
|
+
frame: null,
|
|
1127
|
+
endpoint: "https://apis.ihg.com",
|
|
1128
|
+
method: "POST",
|
|
1129
|
+
path: "/reservations/v3/hotels",
|
|
1130
|
+
query: {},
|
|
1131
|
+
navigation: null,
|
|
1132
|
+
encoding: "json",
|
|
1133
|
+
session: {
|
|
1134
|
+
cookies: true,
|
|
1135
|
+
cookie_names: null,
|
|
1136
|
+
headers: [
|
|
1137
|
+
...BROWSER_HEADERS,
|
|
1138
|
+
"content-type",
|
|
1139
|
+
"x-ihg-api-key",
|
|
1140
|
+
"ihg-language",
|
|
1141
|
+
"x-ihg-sso-token",
|
|
1142
|
+
"x-accept-currency",
|
|
1143
|
+
"ihg-sellsource",
|
|
1144
|
+
"forter-token",
|
|
1145
|
+
"ihg-sessionid",
|
|
1146
|
+
"ihg-transactionid"
|
|
1147
|
+
],
|
|
1148
|
+
required: ["x-ihg-api-key"]
|
|
1149
|
+
},
|
|
1150
|
+
card: {
|
|
1151
|
+
pan: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cardNumber", "token"], format: "digits" },
|
|
1152
|
+
cvv: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cvvNumber", "token"], format: "digits" },
|
|
1153
|
+
exp_month: null,
|
|
1154
|
+
exp_year: null,
|
|
1155
|
+
expiry: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "expireDate"], format: "mmyy" },
|
|
1156
|
+
last4: null,
|
|
1157
|
+
leading: null,
|
|
1158
|
+
brands: [{ at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cardCode"], map: null }],
|
|
1159
|
+
bins: null
|
|
1160
|
+
},
|
|
1161
|
+
merchant_key: null,
|
|
1162
|
+
requires: [
|
|
1163
|
+
{ at: ["imageStatus"], equals: "COMMIT" },
|
|
1164
|
+
{ at: ["segments", 0, "payment", "guaranteeType"], equals: "STANDARD_GUARANTEE" }
|
|
1165
|
+
],
|
|
1166
|
+
refuse: [
|
|
1167
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "3dsData"], when: "present" },
|
|
1168
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "cupTokenId"], when: "present" },
|
|
1169
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "paymentSource"], when: "present" }
|
|
1170
|
+
],
|
|
1171
|
+
payment_method: null,
|
|
1172
|
+
amount: null,
|
|
1173
|
+
currency: "USD",
|
|
1174
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1175
|
+
// The page's other posts of the same card (evidence above): an approval never covers them.
|
|
1176
|
+
resends: [
|
|
1177
|
+
{ method: "POST", path: "/channels/direct/quickEnroll/v8/hotelReservations", query: {} },
|
|
1178
|
+
{ method: "POST", path: "/channels/direct/quickEnroll/v9/hotelReservations", query: {} },
|
|
1179
|
+
{ method: "POST", path: "/channels/direct/quickEnroll/v8/multiRoomReservation", query: {} },
|
|
1180
|
+
{ method: "POST", path: "/reservations/v3/hotels/rooms/fromCart", query: {} },
|
|
1181
|
+
{ method: "POST", path: "/reservations/v3/hotels/rooms/agent/reservations", query: {} },
|
|
1182
|
+
{ method: "POST", path: "/reservations/v3/businessRewardReservations", query: {} },
|
|
1183
|
+
{ method: "POST", path: "/payments/threedsecure/v1/token", query: {} },
|
|
1184
|
+
{ method: "POST", path: "/payments/threedsecure/v1/paymentSetup", query: {} }
|
|
1185
|
+
],
|
|
1186
|
+
challenge: null,
|
|
1187
|
+
facts: null,
|
|
1188
|
+
bootstrap: "/robots.txt",
|
|
1189
|
+
timeout_ms: 3e4,
|
|
1190
|
+
charge_timing: "deferred",
|
|
1191
|
+
once_per: null,
|
|
1192
|
+
pending: [
|
|
1193
|
+
"card.brands: the cardCode value for each brand",
|
|
1194
|
+
"card.expiry: that expirationMonth is always two digits, so expireDate is MMYY",
|
|
1195
|
+
"the currency: a hotel outside the US charges in its own, which the body names only as hotels[0].bookingCurrency when it differs from the shopper's",
|
|
1196
|
+
GUARANTEE_RATES
|
|
1197
|
+
]
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
id: "aa_process_payment",
|
|
1201
|
+
merchant: "American Airlines",
|
|
1202
|
+
status: "observe",
|
|
1203
|
+
evidence: [
|
|
1204
|
+
"www.aa.com checkout SPA, builds 26.11.12-radio.0 (Sep 2 to 12) and 26.12.0 (Sep 15 to 20), read from archive.org copies Sep 25, 2026: CREDIT_CARD skips checkout/initiate and executePayment posts the request with httpClient.post(bffServiceUrl + 'processpayment'), same origin, bffServiceUrl /ecommerce/checkout-services/.",
|
|
1205
|
+
"Body (Oi, Ja, wi, then finalizePaymentRequest): {formOfPayments: [{creditCard: {creditCardType (derived from the typed digits), cardNumber, expirationMonth, expirationYear, cvvCode, nameOnCard}, billingAddress, saveCardAttributes, formOfPaymentType 'CREDIT_CARD'}], email, tripInsurance, isRecurringPurchaseSelected, responseVersion 'v2', cartId, userInteractionType, browserDetails}.",
|
|
1206
|
+
"Headers: Angular's X-XSRF-TOKEN from the XSRF-TOKEN cookie and an Authorization header from the interceptors; the aa.com cookies go with the same-origin request.",
|
|
1207
|
+
"THREE_DS_IDENTIFY_SHOPPER or THREE_DS_CHALLENGE_SHOPPER runs adyen-web createFromAction and posts the whole request again, card included, with threeDSStep and threeDSecureInfo; both are refused here, so only payments with no 3DS step are paid."
|
|
1208
|
+
],
|
|
1209
|
+
har: null,
|
|
1210
|
+
checkout: "https://www.aa.com",
|
|
1211
|
+
checkout_frame: false,
|
|
1212
|
+
frame: null,
|
|
1213
|
+
endpoint: "https://www.aa.com",
|
|
1214
|
+
method: "POST",
|
|
1215
|
+
path: "/ecommerce/checkout-services/processpayment",
|
|
1216
|
+
query: {},
|
|
1217
|
+
navigation: null,
|
|
1218
|
+
encoding: "json",
|
|
1219
|
+
session: {
|
|
1220
|
+
cookies: true,
|
|
1221
|
+
cookie_names: null,
|
|
1222
|
+
headers: [...BROWSER_HEADERS, "content-type", "authorization", "x-xsrf-token"],
|
|
1223
|
+
required: []
|
|
1224
|
+
},
|
|
1225
|
+
card: {
|
|
1226
|
+
pan: { at: ["formOfPayments", 0, "creditCard", "cardNumber"], format: "digits" },
|
|
1227
|
+
cvv: { at: ["formOfPayments", 0, "creditCard", "cvvCode"], format: "digits" },
|
|
1228
|
+
exp_month: { at: ["formOfPayments", 0, "creditCard", "expirationMonth"], format: "as_typed" },
|
|
1229
|
+
exp_year: { at: ["formOfPayments", 0, "creditCard", "expirationYear"], format: "as_typed" },
|
|
1230
|
+
expiry: null,
|
|
1231
|
+
last4: null,
|
|
1232
|
+
leading: null,
|
|
1233
|
+
brands: [{ at: ["formOfPayments", 0, "creditCard", "creditCardType"], map: null }],
|
|
1234
|
+
bins: null
|
|
1235
|
+
},
|
|
1236
|
+
merchant_key: null,
|
|
1237
|
+
requires: [
|
|
1238
|
+
{ at: ["formOfPayments", 0, "formOfPaymentType"], equals: "CREDIT_CARD" },
|
|
1239
|
+
{ at: ["responseVersion"], equals: "v2" },
|
|
1240
|
+
{ at: ["cartId"], present: true }
|
|
1241
|
+
],
|
|
1242
|
+
refuse: [
|
|
1243
|
+
{ at: ["formOfPayments", 0, "threeDSStep"], when: "present" },
|
|
1244
|
+
{ at: ["formOfPayments", 0, "threeDSecureInfo"], when: "present" },
|
|
1245
|
+
{ at: ["formOfPayments", 0, "saveCardAttributes", "saveCardToAccount"], when: "true" },
|
|
1246
|
+
{ at: ["isRecurringPurchaseSelected"], when: "true" }
|
|
1247
|
+
],
|
|
1248
|
+
payment_method: null,
|
|
1249
|
+
amount: null,
|
|
1250
|
+
currency: "USD",
|
|
1251
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1252
|
+
resends: [],
|
|
1253
|
+
challenge: null,
|
|
1254
|
+
facts: null,
|
|
1255
|
+
bootstrap: "/robots.txt",
|
|
1256
|
+
timeout_ms: 3e4,
|
|
1257
|
+
charge_timing: "at_request",
|
|
1258
|
+
once_per: null,
|
|
1259
|
+
pending: [
|
|
1260
|
+
"card.brands: the creditCardType value for each brand",
|
|
1261
|
+
"card.exp_month and card.exp_year: the form the expiry parser writes them in",
|
|
1262
|
+
"which of authorization and x-xsrf-token the call requires",
|
|
1263
|
+
"whether some shoppers check out on checkout.aa.com, which this profile does not cover"
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
id: "economybookings_vault",
|
|
1268
|
+
merchant: "EconomyBookings",
|
|
1269
|
+
status: "observe",
|
|
1270
|
+
evidence: [
|
|
1271
|
+
"www.economybookings.com checkout (_app-5ad4b35169e00887, chunks 3450 and 1658, flights payment-ef0a155c) and the vault's own vault.js v1.2 and app.610e8fa01edad0052179.js, read Sep 25, 2026: the page mounts VaultFields, one iframe per card field on https://vault.economybookings.com, and submit() hands the card number frame the booking ids.",
|
|
1272
|
+
"That frame posts the card with jQuery $.ajax, contentType application/json, no cookies and no auth header, to https://vault-api.economybookings.com/api/v1/vault: {cardNumber (digits), cvv, ccExpMonth (parseInt), ccExpYear (parseInt of the two-digit year), cardType ('visa', 'mastercard' or 'amex', from the typed digits), cardHolder, affiliateId, reservationId, requestId, carId, extras, transactionId when resubmitting, ...}.",
|
|
1273
|
+
"The answer is {status, transactionId, redirectUrl}; AwaitingAddressData sends saveAdditionalData with no card and then the card again with the transactionId, which is refused here. The vault charges the processor the booking is routed to from its own servers."
|
|
1274
|
+
],
|
|
1275
|
+
har: null,
|
|
1276
|
+
checkout: "https://www.economybookings.com",
|
|
1277
|
+
checkout_frame: false,
|
|
1278
|
+
frame: "https://vault.economybookings.com",
|
|
1279
|
+
endpoint: "https://vault-api.economybookings.com",
|
|
1280
|
+
method: "POST",
|
|
1281
|
+
path: "/api/v1/vault",
|
|
1282
|
+
query: {},
|
|
1283
|
+
navigation: null,
|
|
1284
|
+
encoding: "json",
|
|
1285
|
+
session: {
|
|
1286
|
+
cookies: false,
|
|
1287
|
+
cookie_names: null,
|
|
1288
|
+
headers: [...BROWSER_HEADERS, "content-type"],
|
|
1289
|
+
required: []
|
|
1290
|
+
},
|
|
1291
|
+
card: {
|
|
1292
|
+
pan: { at: ["cardNumber"], format: "digits" },
|
|
1293
|
+
cvv: { at: ["cvv"], format: "digits" },
|
|
1294
|
+
exp_month: { at: ["ccExpMonth"], format: "m_int" },
|
|
1295
|
+
exp_year: { at: ["ccExpYear"], format: "yy_int" },
|
|
1296
|
+
expiry: null,
|
|
1297
|
+
last4: null,
|
|
1298
|
+
leading: null,
|
|
1299
|
+
brands: [{ at: ["cardType"], map: { visa: "visa", mastercard: "mastercard" } }],
|
|
1300
|
+
bins: null
|
|
1301
|
+
},
|
|
1302
|
+
merchant_key: null,
|
|
1303
|
+
requires: [
|
|
1304
|
+
{ at: ["affiliateId"], equals: "economybookings" },
|
|
1305
|
+
{ at: ["reservationId"], present: true }
|
|
1306
|
+
],
|
|
1307
|
+
refuse: [{ at: ["transactionId"], when: "nonempty" }],
|
|
1308
|
+
payment_method: null,
|
|
1309
|
+
amount: null,
|
|
1310
|
+
currency: "USD",
|
|
1311
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1312
|
+
resends: [],
|
|
1313
|
+
challenge: null,
|
|
1314
|
+
facts: null,
|
|
1315
|
+
bootstrap: "/robots.txt",
|
|
1316
|
+
timeout_ms: 3e4,
|
|
1317
|
+
charge_timing: "at_request",
|
|
1318
|
+
once_per: null,
|
|
1319
|
+
pending: [
|
|
1320
|
+
"whether an AwaitingAddressData answer already holds an authorization, since the resubmission it asks for is refused here",
|
|
1321
|
+
"the currency: the booking is priced in the shopper's chosen currency, which the body does not name"
|
|
1322
|
+
]
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
id: "synxis_the_verb_hotel",
|
|
1326
|
+
merchant: "The Verb Hotel",
|
|
1327
|
+
status: "observe",
|
|
1328
|
+
evidence: [
|
|
1329
|
+
"www.theverbhotel.com books on the SynXis Booking Engine (gc.synxis.com/rez.aspx?Hotel=61148 answers 302 to be.synxis.com), chain 16362, hotel 61148. SynXis build ad0d448a02da67422922 (client bundle and checkout chunk 6, deployed Sep 25, 2026), read Sep 25, 2026.",
|
|
1330
|
+
"The confirm loop sends each room through route updateReservation: POST https://be.synxis.com/gw/itinerary/v1/updateReservation, Content-Type application/json; charset=utf-8, Accept application/json,application/x-javascript, Context BE, Accept-Language, ActivityID (sbeSessionID), Authorization only for a signed-in member, credentials include (apisession, sbeSessionID, Imperva and AWSALB cookies).",
|
|
1331
|
+
'Body: {version, Chain: {id}, Reservation: {action "Confirmed", Hotel: {id}, GuestList: [{Role "Primary", PaymentList: [{role "Primary", type "CreditCard", PaymentCard: {cardCode (VI, MC, AX, DS...), cardNumber, cardSecurityCode, expireDate "MM/YY", cardHolder}}], ...}], ...}, Source: {BrowserDetails}}; the expiry input is masked 99/99.',
|
|
1332
|
+
"The hotel's configuration shows no cvvCode field, so cardSecurityCode is sent empty; a later room sends the PaymentToken SynXis returned, and a PSD2 hotel sends consentToStoreCardData, both refused here.",
|
|
1333
|
+
"A RedirectHtml answer with VendorStatus Redirected sends the page through a vendor or 3DS step and then itinerary/v2/ReservationPaymentCallback, with no card."
|
|
1334
|
+
],
|
|
1335
|
+
har: null,
|
|
1336
|
+
checkout: "https://be.synxis.com",
|
|
1337
|
+
checkout_frame: false,
|
|
1338
|
+
frame: null,
|
|
1339
|
+
endpoint: "https://be.synxis.com",
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
path: "/gw/itinerary/v1/updateReservation",
|
|
1342
|
+
query: {},
|
|
1343
|
+
navigation: null,
|
|
1344
|
+
encoding: "json",
|
|
1345
|
+
session: {
|
|
1346
|
+
cookies: true,
|
|
1347
|
+
cookie_names: null,
|
|
1348
|
+
headers: [...BROWSER_HEADERS, "content-type", "context", "activityid", "authorization"],
|
|
1349
|
+
required: ["context", "activityid"]
|
|
1350
|
+
},
|
|
1351
|
+
card: {
|
|
1352
|
+
pan: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardNumber"], format: "digits" },
|
|
1353
|
+
cvv: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardSecurityCode"], format: "digits_if_asked" },
|
|
1354
|
+
exp_month: null,
|
|
1355
|
+
exp_year: null,
|
|
1356
|
+
expiry: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "expireDate"], format: "mm/yy" },
|
|
1357
|
+
last4: null,
|
|
1358
|
+
leading: null,
|
|
1359
|
+
brands: [{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardCode"], map: { visa: "VI", mastercard: "MC" } }],
|
|
1360
|
+
bins: null
|
|
1361
|
+
},
|
|
1362
|
+
merchant_key: null,
|
|
1363
|
+
requires: [
|
|
1364
|
+
{ at: ["Chain", "id"], one_of: [16362, "16362"] },
|
|
1365
|
+
{ at: ["Reservation", "Hotel", "id"], one_of: [61148, "61148"] },
|
|
1366
|
+
{ at: ["Reservation", "action"], equals: "Confirmed" },
|
|
1367
|
+
{ at: ["Reservation", "GuestList", 0, "Role"], equals: "Primary" },
|
|
1368
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "type"], equals: "CreditCard" }
|
|
1369
|
+
],
|
|
1370
|
+
refuse: [
|
|
1371
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "PaymentToken"], when: "nonempty" },
|
|
1372
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "consentToStoreCardData"], when: "true" },
|
|
1373
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "FintechPaymentProvider"], when: "nonempty" },
|
|
1374
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "InstallmentOptionId"], when: "nonempty" }
|
|
1375
|
+
],
|
|
1376
|
+
payment_method: null,
|
|
1377
|
+
amount: null,
|
|
1378
|
+
currency: "USD",
|
|
1379
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1380
|
+
resends: [],
|
|
1381
|
+
challenge: null,
|
|
1382
|
+
facts: null,
|
|
1383
|
+
bootstrap: "/robots.txt",
|
|
1384
|
+
timeout_ms: 3e4,
|
|
1385
|
+
charge_timing: "deferred",
|
|
1386
|
+
once_per: null,
|
|
1387
|
+
pending: [
|
|
1388
|
+
"that the primary guest is GuestList[0] and its card PaymentList[0]",
|
|
1389
|
+
"whether the hotel's rates take a deposit at booking or only hold the card",
|
|
1390
|
+
GUARANTEE_RATES
|
|
1391
|
+
]
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
id: "runsignup_register_pay",
|
|
1395
|
+
merchant: "RunSignUp",
|
|
1396
|
+
status: "observe",
|
|
1397
|
+
evidence: [
|
|
1398
|
+
"RunSignUp charges cards through Adyen from its own servers (its posts of Mar 2021, Aug 2021, Nov 2022 and Apr 2025); the browser never calls a processor. Read Sep 25, 2026.",
|
|
1399
|
+
"The pay step is /Race/Register/?regToken=<session>&raceId=<id>&track=pay (46 archived captures of that URL; no capture of the page itself survives). RunSignUp's archived card form posts form.serialize() with jQuery $.ajax to its own URL: payment[cardNumber], payment[cardExpiresMonth] and payment[cardExpiresYear] (menus), payment[cvv] and billing fields.",
|
|
1400
|
+
"The Aug 2023 help screenshot shows the same form: card number, Month and Year menus, CVV, billing fields, Save my credit card, and Confirm Payment."
|
|
1401
|
+
],
|
|
1402
|
+
har: null,
|
|
1403
|
+
checkout: "https://runsignup.com",
|
|
1404
|
+
checkout_frame: false,
|
|
1405
|
+
frame: null,
|
|
1406
|
+
endpoint: "https://runsignup.com",
|
|
1407
|
+
method: "POST",
|
|
1408
|
+
path: "/Race/Register/",
|
|
1409
|
+
query: { regToken: ":regToken", raceId: ":raceId", track: "pay" },
|
|
1410
|
+
navigation: null,
|
|
1411
|
+
encoding: "form",
|
|
1412
|
+
session: {
|
|
1413
|
+
cookies: true,
|
|
1414
|
+
cookie_names: null,
|
|
1415
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-requested-with"],
|
|
1416
|
+
required: []
|
|
1417
|
+
},
|
|
1418
|
+
card: {
|
|
1419
|
+
pan: { at: "payment[cardNumber]", format: "digits" },
|
|
1420
|
+
cvv: { at: "payment[cvv]", format: "digits" },
|
|
1421
|
+
exp_month: { at: "payment[cardExpiresMonth]", format: "as_typed" },
|
|
1422
|
+
exp_year: { at: "payment[cardExpiresYear]", format: "as_typed" },
|
|
1423
|
+
expiry: null,
|
|
1424
|
+
last4: null,
|
|
1425
|
+
leading: null,
|
|
1426
|
+
brands: [],
|
|
1427
|
+
bins: null
|
|
1428
|
+
},
|
|
1429
|
+
merchant_key: null,
|
|
1430
|
+
requires: [],
|
|
1431
|
+
refuse: [],
|
|
1432
|
+
payment_method: null,
|
|
1433
|
+
amount: null,
|
|
1434
|
+
currency: "USD",
|
|
1435
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1436
|
+
resends: [],
|
|
1437
|
+
challenge: null,
|
|
1438
|
+
facts: null,
|
|
1439
|
+
bootstrap: "/robots.txt",
|
|
1440
|
+
timeout_ms: 3e4,
|
|
1441
|
+
charge_timing: "at_request",
|
|
1442
|
+
once_per: null,
|
|
1443
|
+
pending: [
|
|
1444
|
+
"that Confirm Payment posts the form as an XHR to its own pay step URL, not as a document",
|
|
1445
|
+
"the card field names today: the four above come from an archived form",
|
|
1446
|
+
"the name of the Save my credit card field, for a refuse rule",
|
|
1447
|
+
"card.exp_month and card.exp_year: the values the Month and Year menus send"
|
|
1448
|
+
]
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
id: "mindbody_explore_billing",
|
|
1452
|
+
merchant: "Mindbody",
|
|
1453
|
+
status: "observe",
|
|
1454
|
+
evidence: [
|
|
1455
|
+
"Mindbody Explore (www.mindbodyonline.com/explore, main.e3d3d61c built Sep 18, 2026, chunk 5.54c48f67), read Sep 25, 2026: the studios' own sites (Yoga Flow SF, Black Swan Yoga) sell through Mindbody, and on Explore a new card goes through addBillingInfo.",
|
|
1456
|
+
"addBillingInfo posts {data: {type 'userBilling', attributes}} (the form values and primaryCard false, keys in snake case) to https://prod-mkt-gateway.mindbody.io/v1/user/billing with Content-Type and Accept application/vnd.api+json, X-MB-APP-BUILD, X-MB-APP-NAME, X-MB-APP-VERSION, X-MB-USER-SESSION-ID, X-MB-ON-BEHALF-OF for a dependent, and Authorization Bearer for the signed-in person; a cross-site fetch, so no cookies.",
|
|
1457
|
+
"The card form (AddEditCardForm) takes card_number, cvv, expiration_month, expiration_year and a billing address. The order then pays with the stored card by id (/payments, /compute_total, /process with an SCA redirect), with no card.",
|
|
1458
|
+
"The studios' Branded Web widgets check out in CartV2 on go.mindbodyonline.com, whose add-card call is a Next.js server action no public capture shows; it is not this profile."
|
|
1459
|
+
],
|
|
1460
|
+
har: null,
|
|
1461
|
+
checkout: "https://www.mindbodyonline.com",
|
|
1462
|
+
checkout_frame: false,
|
|
1463
|
+
frame: null,
|
|
1464
|
+
endpoint: "https://prod-mkt-gateway.mindbody.io",
|
|
1465
|
+
method: "POST",
|
|
1466
|
+
path: "/v1/user/billing",
|
|
1467
|
+
query: {},
|
|
1468
|
+
navigation: null,
|
|
1469
|
+
encoding: "json_api",
|
|
1470
|
+
session: {
|
|
1471
|
+
cookies: false,
|
|
1472
|
+
cookie_names: null,
|
|
1473
|
+
headers: [
|
|
1474
|
+
...BROWSER_HEADERS,
|
|
1475
|
+
"content-type",
|
|
1476
|
+
"authorization",
|
|
1477
|
+
"x-mb-app-build",
|
|
1478
|
+
"x-mb-app-name",
|
|
1479
|
+
"x-mb-app-version",
|
|
1480
|
+
"x-mb-user-session-id",
|
|
1481
|
+
"x-mb-on-behalf-of"
|
|
1482
|
+
],
|
|
1483
|
+
required: ["authorization", "x-mb-app-name", "x-mb-user-session-id"]
|
|
1484
|
+
},
|
|
1485
|
+
card: {
|
|
1486
|
+
pan: { at: ["data", "attributes", "card_number"], format: "digits" },
|
|
1487
|
+
cvv: { at: ["data", "attributes", "cvv"], format: "digits" },
|
|
1488
|
+
exp_month: { at: ["data", "attributes", "expiration_month"], format: "as_typed" },
|
|
1489
|
+
exp_year: { at: ["data", "attributes", "expiration_year"], format: "as_typed" },
|
|
1490
|
+
expiry: null,
|
|
1491
|
+
last4: null,
|
|
1492
|
+
leading: null,
|
|
1493
|
+
brands: [],
|
|
1494
|
+
bins: null
|
|
1495
|
+
},
|
|
1496
|
+
merchant_key: null,
|
|
1497
|
+
requires: [
|
|
1498
|
+
{ at: ["data", "type"], equals: "userBilling" },
|
|
1499
|
+
{ at: ["data", "attributes", "primary_card"], equals: false }
|
|
1500
|
+
],
|
|
1501
|
+
refuse: [],
|
|
1502
|
+
payment_method: null,
|
|
1503
|
+
amount: null,
|
|
1504
|
+
currency: "USD",
|
|
1505
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1506
|
+
resends: [],
|
|
1507
|
+
challenge: null,
|
|
1508
|
+
facts: null,
|
|
1509
|
+
bootstrap: "/robots.txt",
|
|
1510
|
+
timeout_ms: 3e4,
|
|
1511
|
+
charge_timing: "deferred",
|
|
1512
|
+
once_per: null,
|
|
1513
|
+
pending: [
|
|
1514
|
+
"the attribute names addBillingInfo writes, and whether card_number is sent without the spaces the input shows",
|
|
1515
|
+
"card.exp_month and card.exp_year: the values the expiry menus send",
|
|
1516
|
+
"card on file: this request saves the card to the person's Mindbody account and the order charges it by id, which the card-on-file disclosure must cover"
|
|
1517
|
+
]
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
id: "lookfantastic_place_order",
|
|
1521
|
+
merchant: "LookFantastic",
|
|
1522
|
+
status: "observe",
|
|
1523
|
+
evidence: [
|
|
1524
|
+
"THG Ingenuity's shared checkout (checkout.lookfantastic.com, build main.212010dd79e9cb4669a1 of May 2026 and its 673 chunks; the Sep 2025 build has the same shape), read Sep 25, 2026.",
|
|
1525
|
+
"placeOrderViaApiPoller fetches POST /checkout-api/v2/order/place-order once with credentials same-origin and headers Content-Type application/json, sessionId and x-thg-client checkout-web-js, then polls /checkout-api/v2/order/status with no card.",
|
|
1526
|
+
"Body (getPlaceOrderBody): {fullName, payerName, contactNumber, deliveryAddress, emailAddress, optedIntoMarketing, payment: {creditCardNumber, nameOnCard, expirationMonth, expirationYear, cv2, method 'new-credit-card', billingAddress, consentToReuse}, residentId, hasDigitalConsent, deliverySelection, consentGiven, reCaptchaToken, giftCards}.",
|
|
1527
|
+
"Two other card paths reach the same endpoint with no card number: BR-DGE hosted fields (cardDataProvider BRIDGE and a token) and Adyen V2 (method adyen_v2); neither passes this profile's rules. Checkout.com runs only 3DS in the page."
|
|
1528
|
+
],
|
|
1529
|
+
har: null,
|
|
1530
|
+
checkout: "https://checkout.lookfantastic.com",
|
|
1531
|
+
checkout_frame: false,
|
|
1532
|
+
frame: null,
|
|
1533
|
+
endpoint: "https://checkout.lookfantastic.com",
|
|
1534
|
+
method: "POST",
|
|
1535
|
+
path: "/checkout-api/v2/order/place-order",
|
|
1536
|
+
query: {},
|
|
1537
|
+
navigation: null,
|
|
1538
|
+
encoding: "json",
|
|
1539
|
+
session: {
|
|
1540
|
+
cookies: true,
|
|
1541
|
+
cookie_names: null,
|
|
1542
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-thg-client", "sessionid"],
|
|
1543
|
+
required: ["x-thg-client", "sessionid"]
|
|
1544
|
+
},
|
|
1545
|
+
card: {
|
|
1546
|
+
pan: { at: ["payment", "creditCardNumber"], format: "digits" },
|
|
1547
|
+
cvv: { at: ["payment", "cv2"], format: "digits" },
|
|
1548
|
+
exp_month: { at: ["payment", "expirationMonth"], format: "as_typed" },
|
|
1549
|
+
exp_year: { at: ["payment", "expirationYear"], format: "as_typed" },
|
|
1550
|
+
expiry: null,
|
|
1551
|
+
last4: null,
|
|
1552
|
+
leading: null,
|
|
1553
|
+
brands: [],
|
|
1554
|
+
bins: null
|
|
1555
|
+
},
|
|
1556
|
+
merchant_key: null,
|
|
1557
|
+
requires: [{ at: ["payment", "method"], equals: "new-credit-card" }],
|
|
1558
|
+
refuse: [
|
|
1559
|
+
{ at: ["payment", "consentToReuse"], when: "true" },
|
|
1560
|
+
{ at: ["payment", "cardDataProvider"], when: "present" },
|
|
1561
|
+
{ at: ["reCaptchaToken"], when: "nonempty" }
|
|
1562
|
+
],
|
|
1563
|
+
payment_method: null,
|
|
1564
|
+
amount: null,
|
|
1565
|
+
currency: "GBP",
|
|
1566
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1567
|
+
resends: [],
|
|
1568
|
+
challenge: null,
|
|
1569
|
+
facts: null,
|
|
1570
|
+
bootstrap: "/robots.txt",
|
|
1571
|
+
timeout_ms: 3e4,
|
|
1572
|
+
charge_timing: "at_request",
|
|
1573
|
+
once_per: null,
|
|
1574
|
+
pending: [
|
|
1575
|
+
"which card path lookfantastic.com has on: its own card fields (this profile), BR-DGE hosted fields or Adyen V2",
|
|
1576
|
+
"card.exp_month and card.exp_year: the form expirationMonth and expirationYear are sent in",
|
|
1577
|
+
"the currency: the order is priced in the shopper's currency_V6 cookie, which the body does not name",
|
|
1578
|
+
"whether the site fills reCaptchaToken, and a policy for a captcha token in the card request"
|
|
1579
|
+
]
|
|
1580
|
+
},
|
|
1581
|
+
// ── Merchants whose own page encrypts the card under the merchant's key ──────
|
|
1582
|
+
// Each ships in observe with pending items: until a HAR pins a key the device has
|
|
1583
|
+
// no key to encrypt under, and the requests around the card request (a second
|
|
1584
|
+
// token call, a page navigation, a re-sent ciphertext) are not in this mode yet.
|
|
1585
|
+
{
|
|
1586
|
+
id: "hyatt_card_token",
|
|
1587
|
+
merchant: "Hyatt",
|
|
1588
|
+
status: "observe",
|
|
1589
|
+
evidence: [
|
|
1590
|
+
"www.hyatt.com payment app bundles main.c587efa6.js (its SHA-384 is the 2026-03-03 payment page SRI hash) and main.8992684e.js (2026-08-27), read Sep 25, 2026 from web.archive.org: on mount the scapegoat client GETs /v1/cc/secure/key for {publicKey: PEM}; Book My Stay posts same-origin JSON with Content-Type and Accept application/json to /v1/cc/secure/tokens and, in parallel, /v1/cvv/secure/tokens.",
|
|
1591
|
+
"Card body: {pan, type, expYear, expMonth}. pan is node-forge encode64 of RSA-OAEP with md SHA-256 and mgf1 md SHA-1 (module wGyh) over the card number field, type is VS, MC, AX, DS, JC, DC or UP (map {AX: amex, DC: diners, DS: discover, JC: jcb, MC: mastercard, UP: unionpay, VS: visa}), expYear is 20YY and expMonth MM from the MM / YY field. The answer is {Token}.",
|
|
1592
|
+
"Then form.submit() navigates to /{locale}/book/makeReservation with token, cvv (the security code token), encrypted-card-number, credit-card-type, expiry-month and expiry-year. No third-party payment code and no frame load on the page (CSP default-src none, connect-src self)."
|
|
1593
|
+
],
|
|
1594
|
+
har: null,
|
|
1595
|
+
checkout: "https://www.hyatt.com",
|
|
1596
|
+
checkout_frame: false,
|
|
1597
|
+
frame: null,
|
|
1598
|
+
endpoint: "https://www.hyatt.com",
|
|
1599
|
+
method: "POST",
|
|
1600
|
+
path: "/v1/cc/secure/tokens",
|
|
1601
|
+
query: {},
|
|
1602
|
+
navigation: null,
|
|
1603
|
+
encoding: "json",
|
|
1604
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type"], required: [] },
|
|
1605
|
+
card: {
|
|
1606
|
+
pan: { at: ["pan"], format: "encrypted", plaintext: "digits" },
|
|
1607
|
+
cvv: null,
|
|
1608
|
+
exp_month: { at: ["expMonth"], format: "mm" },
|
|
1609
|
+
exp_year: { at: ["expYear"], format: "yyyy" },
|
|
1610
|
+
expiry: null,
|
|
1611
|
+
last4: null,
|
|
1612
|
+
leading: null,
|
|
1613
|
+
brands: [{ at: ["type"], map: { visa: "VS", mastercard: "MC", amex: "AX", discover: "DS" } }],
|
|
1614
|
+
bins: null
|
|
1615
|
+
},
|
|
1616
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-256", mgf1: "SHA-1", kid_at: null, keys: [] },
|
|
1617
|
+
requires: [],
|
|
1618
|
+
refuse: [],
|
|
1619
|
+
payment_method: null,
|
|
1620
|
+
amount: null,
|
|
1621
|
+
currency: "USD",
|
|
1622
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1623
|
+
resends: [],
|
|
1624
|
+
challenge: null,
|
|
1625
|
+
facts: null,
|
|
1626
|
+
bootstrap: "/robots.txt",
|
|
1627
|
+
timeout_ms: 3e4,
|
|
1628
|
+
charge_timing: "deferred",
|
|
1629
|
+
once_per: null,
|
|
1630
|
+
pending: [
|
|
1631
|
+
"merchant_key.keys: the key /v1/cc/secure/key serves, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1632
|
+
"the security code goes to /v1/cvv/secure/tokens in a request of its own (hyatt_cvv_token), sent beside this one, and one approval does not cover both yet",
|
|
1633
|
+
"makeReservation is a page navigation that carries encrypted-card-number, credit-card-type, expiry-month and expiry-year of the typed card, which this mode does not rewrite: whether Hyatt books from the token alone",
|
|
1634
|
+
"whether the card number is encrypted as typed, with spaces",
|
|
1635
|
+
"the booking currency: the request names none, so the profile takes stays priced in US dollars only"
|
|
1636
|
+
]
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
id: "hyatt_cvv_token",
|
|
1640
|
+
merchant: "Hyatt",
|
|
1641
|
+
status: "observe",
|
|
1642
|
+
evidence: [
|
|
1643
|
+
"The same Hyatt bundles: Book My Stay posts {cvv} to /v1/cvv/secure/tokens beside the card token call, cvv being the same RSA-OAEP (SHA-256, MGF1-SHA-1) base64 over the security code made on the field blur. The answer is {EncryptedCVV}, which the page puts in the book form as cvv.",
|
|
1644
|
+
"A saved-card booking posts the same body to the same path, for a card Hyatt already holds."
|
|
1645
|
+
],
|
|
1646
|
+
har: null,
|
|
1647
|
+
checkout: "https://www.hyatt.com",
|
|
1648
|
+
checkout_frame: false,
|
|
1649
|
+
frame: null,
|
|
1650
|
+
endpoint: "https://www.hyatt.com",
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
path: "/v1/cvv/secure/tokens",
|
|
1653
|
+
query: {},
|
|
1654
|
+
navigation: null,
|
|
1655
|
+
encoding: "json",
|
|
1656
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type"], required: [] },
|
|
1657
|
+
card: {
|
|
1658
|
+
pan: null,
|
|
1659
|
+
cvv: { at: ["cvv"], format: "encrypted", plaintext: "digits" },
|
|
1660
|
+
exp_month: null,
|
|
1661
|
+
exp_year: null,
|
|
1662
|
+
expiry: null,
|
|
1663
|
+
last4: null,
|
|
1664
|
+
leading: null,
|
|
1665
|
+
brands: [],
|
|
1666
|
+
bins: null
|
|
1667
|
+
},
|
|
1668
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-256", mgf1: "SHA-1", kid_at: null, keys: [] },
|
|
1669
|
+
requires: [],
|
|
1670
|
+
refuse: [],
|
|
1671
|
+
payment_method: null,
|
|
1672
|
+
amount: null,
|
|
1673
|
+
currency: "USD",
|
|
1674
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1675
|
+
resends: [],
|
|
1676
|
+
challenge: null,
|
|
1677
|
+
facts: null,
|
|
1678
|
+
bootstrap: "/robots.txt",
|
|
1679
|
+
timeout_ms: 3e4,
|
|
1680
|
+
charge_timing: "deferred",
|
|
1681
|
+
once_per: null,
|
|
1682
|
+
pending: [
|
|
1683
|
+
"merchant_key.keys: the key /v1/cc/secure/key serves, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1684
|
+
"one approval for both token calls: this one carries the security code of the card hyatt_card_token carries",
|
|
1685
|
+
"a saved-card booking posts the same body here for a card Hyatt holds, so the request alone does not say whose code it is",
|
|
1686
|
+
"makeReservation is a page navigation this mode does not rewrite (see hyatt_card_token)"
|
|
1687
|
+
]
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
id: "lifemiles_process_payment",
|
|
1691
|
+
merchant: "LifeMiles",
|
|
1692
|
+
status: "observe",
|
|
1693
|
+
evidence: [
|
|
1694
|
+
"website-generic-checkout-ui build d31b93cfe651e5f66a0c on the public CloudFront CDN, read Sep 25, 2026: the card page www.lifemiles.com/checkout-lifemiles posts JSON through axios, without withCredentials, to https://api.lifemiles.com/svc/checkout-new-flow-process-payment (env-config confirmationGeneric), with Accept and Content-Type application/json, realm lifemiles and Authorization Bearer <session token>.",
|
|
1695
|
+
'Body: {partner, language, processType, checkoutId, channel, country, stationId, ipAddress, currency, membershipNumber, deviceFingerPrintId, transactionId, selectedFlexCurve, saveCard, fop: {type: CC, amount: "0", unit: D, parameter: [{code, value}]}}. CCNUMCARD, CCCODCVV and CCFECEXP (the expiry as MMYYYY) are node-rsa pkcs1_oaep SHA-512 base64 under the cardKey in env-config.js; CCBIN is the first 7 digits of the number in the clear; CCFRANCHISE is VI, CA, AX, DC or DS (chunk 481 module 90156); CCTOKEN marks a saved card.',
|
|
1696
|
+
"cardKey is one RSA-2048 key (e = 65537) in the generic checkout, Club LifeMiles hub, member direct and air redemption env-config.js (last modified 2026-02-26). The 3DS calls after this request (payment/processor, payflow/3ds/enrollment and validate) carry only paymentId and checkoutId.",
|
|
1697
|
+
"Multiply Miles (partner mymlm) posts the typed number in the clear to /svc/credit-card-on-file-bin-not-saved before Pay, and Club LifeMiles (clubl) renews monthly, so neither partner is reviewed here."
|
|
1698
|
+
],
|
|
1699
|
+
har: null,
|
|
1700
|
+
checkout: "https://www.lifemiles.com",
|
|
1701
|
+
checkout_frame: false,
|
|
1702
|
+
frame: null,
|
|
1703
|
+
endpoint: "https://api.lifemiles.com",
|
|
1704
|
+
method: "POST",
|
|
1705
|
+
path: "/svc/checkout-new-flow-process-payment",
|
|
1706
|
+
query: {},
|
|
1707
|
+
navigation: null,
|
|
1708
|
+
encoding: "json",
|
|
1709
|
+
session: { cookies: false, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type", "authorization", "realm"], required: ["authorization", "realm"] },
|
|
1710
|
+
card: {
|
|
1711
|
+
pan: { at: ["fop", "parameter", { code: "CCNUMCARD" }, "value"], format: "encrypted", plaintext: "digits" },
|
|
1712
|
+
cvv: { at: ["fop", "parameter", { code: "CCCODCVV" }, "value"], format: "encrypted", plaintext: "digits" },
|
|
1713
|
+
exp_month: null,
|
|
1714
|
+
exp_year: null,
|
|
1715
|
+
expiry: { at: ["fop", "parameter", { code: "CCFECEXP" }, "value"], format: "encrypted", plaintext: "MMYYYY" },
|
|
1716
|
+
last4: null,
|
|
1717
|
+
leading: { at: ["fop", "parameter", { code: "CCBIN" }, "value"], digits: 7 },
|
|
1718
|
+
brands: [{ at: ["fop", "parameter", { code: "CCFRANCHISE" }, "value"], map: { visa: "VI", mastercard: "CA", amex: "AX", discover: "DS" } }],
|
|
1719
|
+
bins: null
|
|
1720
|
+
},
|
|
1721
|
+
merchant_key: {
|
|
1722
|
+
scheme: "rsa_oaep",
|
|
1723
|
+
hash: "SHA-512",
|
|
1724
|
+
mgf1: "SHA-512",
|
|
1725
|
+
kid_at: null,
|
|
1726
|
+
keys: [{
|
|
1727
|
+
kid: null,
|
|
1728
|
+
spki: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdiBpp7T3C7Ac6R+FTYimtCNzmQLRrCPVLrUZ2wjhjrGMsfeLt4EPbqw168mtJahzysvB+bjB5XZPiAci//nn+pDQK+x9iXS2Lb0RrTDwS8ey39THybEGw0295g4kQO8ZAw5uc9p+hkVcKwgOIkG5fe8GyQlFMhoySQhj/L75BGsbIbojybsW9AJ9K1KKy0CGCiCre6L5x9kuKfkFR0Q7khWTncmKenbkPOvgQXCg5r4+6Nk15imaiR4O1HKHInkPRSdu19kBYBqVXxXm3lYtY3NCOtPS0irRrAdi4UfT46/81ugvEm7y2xuFv32MSJIAzpAC4jHbB+HUD4sJQpVsQIDAQAB",
|
|
1729
|
+
sha256: "dc2a6ee45bf21c652dc3286244fa80e455200181e71a6b34d9d9a525a4ecfe79"
|
|
1730
|
+
}]
|
|
1731
|
+
},
|
|
1732
|
+
requires: [
|
|
1733
|
+
{ at: ["fop", "type"], equals: "CC" },
|
|
1734
|
+
{ at: ["partner"], one_of: ["lmbex", "avanc", "buylm", "tsflm"] }
|
|
1735
|
+
],
|
|
1736
|
+
refuse: [
|
|
1737
|
+
{ at: ["saveCard"], when: "true" },
|
|
1738
|
+
{ at: ["fop", "parameter", { code: "CCTOKEN" }], when: "present" }
|
|
1739
|
+
],
|
|
1740
|
+
payment_method: null,
|
|
1741
|
+
amount: null,
|
|
1742
|
+
currency: { at: ["currency"] },
|
|
1743
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1744
|
+
resends: [],
|
|
1745
|
+
challenge: null,
|
|
1746
|
+
facts: null,
|
|
1747
|
+
bootstrap: "/robots.txt",
|
|
1748
|
+
timeout_ms: 3e4,
|
|
1749
|
+
charge_timing: "at_request",
|
|
1750
|
+
once_per: null,
|
|
1751
|
+
pending: [
|
|
1752
|
+
"merchant_key.keys: that api.lifemiles.com decrypts with the cardKey its env-config.js carries (the key pinned here), which only a HAR or a person-run purchase shows",
|
|
1753
|
+
"whether the Akamai bot defense in front of api.lifemiles.com takes the relay's request (a plain GET to www.lifemiles.com answered 403 from Akamai)"
|
|
1754
|
+
]
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
id: "southwest_booking_confirmation",
|
|
1758
|
+
merchant: "Southwest Airlines",
|
|
1759
|
+
status: "observe",
|
|
1760
|
+
evidence: [
|
|
1761
|
+
"www.southwest.com air-booking-v2 14.0.3 bundles and bootstrap data.js, read Sep 25, 2026: processBookingPurchase posts same-origin JSON through use-http to /api/air-booking/v1/air-booking/page/air/booking/confirmation (points, companion and certificate bookings use /confirmation-secure), with x-api-key, x-app-id, x-app-version, x-channel-id and x-user-experience-id.",
|
|
1762
|
+
"The body carries creditCard {cityTown, country, countryCode, encryptedCardNumber, expiration, firstNameOnCard, lastNameOnCard, phoneNumber, provinceRegion, state, streetAddress, streetAddressSecond, zipCode, zipCodeRequired, creditCardType, description, descriptionEnabled, primary, securityCode} and paymentMethodSelected creditcard. expiration is the form value with / made -, securityCode is in the clear.",
|
|
1763
|
+
"encryptedCardNumber is jose EncryptJWT({creditCardNumbers: {cardNumber1: number}}) with protected header {alg, enc: A256CBC-HS512, kid, typ: JWT}, under the key with use enc from the cyberJwks JWKS (https://jwksapi.prod0.tok.jwks-prod.prod.southwest.com/jwks), alg taken from the JWK. creditCardType is VISA, RRVISA, MASTERCARD, AMEX, DISCOVER, DINERS or UATP."
|
|
1764
|
+
],
|
|
1765
|
+
har: null,
|
|
1766
|
+
checkout: "https://www.southwest.com",
|
|
1767
|
+
checkout_frame: false,
|
|
1768
|
+
frame: null,
|
|
1769
|
+
endpoint: "https://www.southwest.com",
|
|
1770
|
+
method: "POST",
|
|
1771
|
+
path: "/api/air-booking/v1/air-booking/page/air/booking/confirmation",
|
|
1772
|
+
query: {},
|
|
1773
|
+
navigation: null,
|
|
1774
|
+
encoding: "json",
|
|
1775
|
+
session: {
|
|
1776
|
+
cookies: true,
|
|
1777
|
+
cookie_names: null,
|
|
1778
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-api-key", "x-app-id", "x-app-version", "x-channel-id", "x-user-experience-id"],
|
|
1779
|
+
required: ["x-api-key", "x-channel-id"]
|
|
1780
|
+
},
|
|
1781
|
+
card: {
|
|
1782
|
+
pan: { at: ["creditCard", "encryptedCardNumber"], format: "encrypted", plaintext: { claims: ["creditCardNumbers", "cardNumber1"] } },
|
|
1783
|
+
cvv: { at: ["creditCard", "securityCode"], format: "digits" },
|
|
1784
|
+
exp_month: null,
|
|
1785
|
+
exp_year: null,
|
|
1786
|
+
expiry: { at: ["creditCard", "expiration"], format: "MM-YY" },
|
|
1787
|
+
last4: null,
|
|
1788
|
+
leading: null,
|
|
1789
|
+
brands: [{ at: ["creditCard", "creditCardType"], map: { visa: "VISA", mastercard: "MASTERCARD", amex: "AMEX", discover: "DISCOVER" } }],
|
|
1790
|
+
bins: null
|
|
1791
|
+
},
|
|
1792
|
+
merchant_key: { scheme: "jwe", alg: null, enc: "A256CBC-HS512", header: { typ: "JWT" }, keys: [] },
|
|
1793
|
+
requires: [{ at: ["paymentMethodSelected"], equals: "creditcard" }],
|
|
1794
|
+
refuse: [
|
|
1795
|
+
{ at: ["creditCard", "descriptionEnabled"], when: "true" },
|
|
1796
|
+
{ at: ["creditCard", "primary"], when: "true" },
|
|
1797
|
+
{ at: ["savedCreditCardSelected"], when: "true" },
|
|
1798
|
+
{ at: ["savedCreditCardToken"], when: "nonempty" },
|
|
1799
|
+
{ at: ["selectedCardInfo"], when: "nonempty" },
|
|
1800
|
+
{ at: ["fundsCoverTotalFlight"], when: "true" }
|
|
1801
|
+
],
|
|
1802
|
+
payment_method: null,
|
|
1803
|
+
amount: null,
|
|
1804
|
+
currency: "USD",
|
|
1805
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1806
|
+
resends: [],
|
|
1807
|
+
challenge: null,
|
|
1808
|
+
facts: null,
|
|
1809
|
+
bootstrap: "/robots.txt",
|
|
1810
|
+
timeout_ms: 3e4,
|
|
1811
|
+
charge_timing: "at_request",
|
|
1812
|
+
once_per: null,
|
|
1813
|
+
pending: [
|
|
1814
|
+
"merchant_key.keys: the key the cyberJwks JWKS marks use enc, pinned with its kid by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1815
|
+
"merchant_key.alg: the alg the JWK names, which the page writes into the protected header",
|
|
1816
|
+
"card.expiry: whether expiration carries MM-YY or MM-YYYY",
|
|
1817
|
+
"card.brands: whether the form sends RRVISA for a Rapid Rewards Visa, where the map writes VISA",
|
|
1818
|
+
"a trip insurance purchase sends the same encrypted number to /api/payments/v2/fund-tokens, a second card request this profile does not take",
|
|
1819
|
+
"the page re-encrypts and posts again when the answer names an expired key, which asks for a second approval"
|
|
1820
|
+
]
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
id: "united_card_token",
|
|
1824
|
+
merchant: "United Airlines",
|
|
1825
|
+
status: "observe",
|
|
1826
|
+
evidence: [
|
|
1827
|
+
"www.united.com main.4c89ef80de310ae7ad9d.js (the build a public urlscan.io scan of the live home page loaded on 2026-09-17) and its chunks 1402 and 91359, read Sep 25, 2026 from web.archive.org: for a guest the checkout page GETs /api/User/PublicKeyNonPCI and keeps the Keys[] entry with a PEM PublicKey and its Kid.",
|
|
1828
|
+
"On Buy now getCardTokenSaga posts JSON to /api/user/accountNumberTokenOAEP with X-Authorization-api bearer and Accept-Language: {ProfileCreditCard: {AccountNumberEncrypted, Name, Code, ExpirationDate: MM/YY, SecurityCode}, Kid}. AccountNumberEncrypted is base64 of RSA-OAEP with md SHA-1 (vendor module 53965) over the card number without spaces; SecurityCode is in the clear; Code is the card type GetCreditCardType answered. The answer carries AccountNumberToken and PersistentToken.",
|
|
1829
|
+
"The purchase saga waits for that token, then posts CompletePurchase with AccountNumberEncrypted again, AccountNumberLastFourDigits and the tokens; Lookup3DS sends the ciphertext again. GetCreditCardType and GetSecureCardToken receive the first 10 digits of the typed number."
|
|
1830
|
+
],
|
|
1831
|
+
har: null,
|
|
1832
|
+
checkout: "https://www.united.com",
|
|
1833
|
+
checkout_frame: false,
|
|
1834
|
+
frame: null,
|
|
1835
|
+
endpoint: "https://www.united.com",
|
|
1836
|
+
method: "POST",
|
|
1837
|
+
path: "/api/user/accountNumberTokenOAEP",
|
|
1838
|
+
query: {},
|
|
1839
|
+
navigation: null,
|
|
1840
|
+
encoding: "json",
|
|
1841
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type", "x-authorization-api"], required: ["x-authorization-api"] },
|
|
1842
|
+
card: {
|
|
1843
|
+
pan: { at: ["ProfileCreditCard", "AccountNumberEncrypted"], format: "encrypted", plaintext: "digits" },
|
|
1844
|
+
cvv: { at: ["ProfileCreditCard", "SecurityCode"], format: "digits" },
|
|
1845
|
+
exp_month: null,
|
|
1846
|
+
exp_year: null,
|
|
1847
|
+
expiry: { at: ["ProfileCreditCard", "ExpirationDate"], format: "MM/YY" },
|
|
1848
|
+
last4: null,
|
|
1849
|
+
leading: null,
|
|
1850
|
+
brands: [{ at: ["ProfileCreditCard", "Code"], map: null }],
|
|
1851
|
+
bins: null
|
|
1852
|
+
},
|
|
1853
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-1", mgf1: "SHA-1", kid_at: ["Kid"], keys: [] },
|
|
1854
|
+
requires: [],
|
|
1855
|
+
refuse: [],
|
|
1856
|
+
payment_method: null,
|
|
1857
|
+
amount: null,
|
|
1858
|
+
currency: "USD",
|
|
1859
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1860
|
+
resends: [],
|
|
1861
|
+
challenge: null,
|
|
1862
|
+
facts: null,
|
|
1863
|
+
bootstrap: "/robots.txt",
|
|
1864
|
+
timeout_ms: 3e4,
|
|
1865
|
+
charge_timing: "deferred",
|
|
1866
|
+
once_per: null,
|
|
1867
|
+
pending: [
|
|
1868
|
+
"merchant_key.keys: the key /api/User/PublicKeyNonPCI serves and its Kid, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1869
|
+
"card.brands: the Code GetCreditCardType answers for each network",
|
|
1870
|
+
"card.expiry: whether ExpirationDate carries MM/YY or MM/YYYY",
|
|
1871
|
+
"CompletePurchase and Lookup3DS send AccountNumberEncrypted again with the typed card, and its last four digits, which this mode does not rewrite: whether United charges from AccountNumberToken alone",
|
|
1872
|
+
"GetCreditCardType and GetSecureCardToken receive the first 10 digits of the typed card, so the card type and the Cardinal token describe it and not the real card"
|
|
1873
|
+
]
|
|
1874
|
+
},
|
|
1875
|
+
// ── Document form navigations: the page submits a form that navigates ──────
|
|
1876
|
+
{
|
|
1877
|
+
id: "myrec_athens_clarke_payment",
|
|
1878
|
+
merchant: "Athens-Clarke County Leisure Services",
|
|
1879
|
+
status: "observe",
|
|
1880
|
+
evidence: [
|
|
1881
|
+
`secure.myrecdept.com/auth/default.aspx?did=54048 (MyRec), archive.org captures of 2026-03-20 (200, "Online Payment for Athens-Clarke County Leisure Services") and 2026-05-29, read Sep 25, 2026: an ASP.NET WebForms page whose form aspnetForm posts back to ./default.aspx?did=54048&authkey=<a GUID per checkout> as a full document navigation; the card fields and the submit button sit outside the page's only UpdatePanel, and no async trigger is registered.`,
|
|
1882
|
+
"Card fields: ctl00$Content$txtAccountNumber (maxlength 16), ctl00$Content$txtCvvNumber (maxlength 4), ctl00$Content$lstExpMonth (01 to 12) and ctl00$Content$lstExpYear (four digits), beside the billing name, address, email and phone, __EVENTTARGET, __VIEWSTATE, __VIEWSTATEGENERATOR, __EVENTVALIDATION, and the button that posted: ctl00$Content$btnSubmitPayment, or ctl00$Content$btnCancel.",
|
|
1883
|
+
"A visible reCAPTCHA v2 checkbox sits above the submit button on every capture since 2025-09, so a solved g-recaptcha-response rides in the same body. The page expires after 15 minutes.",
|
|
1884
|
+
"MyRec charges the department's gateway (Authorize.Net here, by the page path) from its own servers; the browser never contacts a processor. The same page serves every MyRec department, each under its own did."
|
|
1885
|
+
],
|
|
1886
|
+
har: null,
|
|
1887
|
+
checkout: "https://secure.myrecdept.com",
|
|
1888
|
+
checkout_frame: false,
|
|
1889
|
+
frame: null,
|
|
1890
|
+
endpoint: "https://secure.myrecdept.com",
|
|
1891
|
+
method: "POST",
|
|
1892
|
+
path: "/auth/default.aspx",
|
|
1893
|
+
query: { did: "54048", authkey: ":authkey" },
|
|
1894
|
+
navigation: "top",
|
|
1895
|
+
encoding: "form",
|
|
1896
|
+
session: {
|
|
1897
|
+
cookies: true,
|
|
1898
|
+
cookie_names: ["ASP.NET_SessionId"],
|
|
1899
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
1900
|
+
required: []
|
|
1901
|
+
},
|
|
1902
|
+
card: {
|
|
1903
|
+
pan: { at: "ctl00$Content$txtAccountNumber", format: "digits" },
|
|
1904
|
+
cvv: { at: "ctl00$Content$txtCvvNumber", format: "digits" },
|
|
1905
|
+
exp_month: { at: "ctl00$Content$lstExpMonth", format: "mm" },
|
|
1906
|
+
exp_year: { at: "ctl00$Content$lstExpYear", format: "yyyy" },
|
|
1907
|
+
expiry: null,
|
|
1908
|
+
last4: null,
|
|
1909
|
+
leading: null,
|
|
1910
|
+
brands: [],
|
|
1911
|
+
bins: null
|
|
1912
|
+
},
|
|
1913
|
+
merchant_key: null,
|
|
1914
|
+
requires: [
|
|
1915
|
+
{ at: "ctl00$Content$btnSubmitPayment", present: true },
|
|
1916
|
+
{ at: "__EVENTTARGET", equals: "" },
|
|
1917
|
+
{ at: "__VIEWSTATE", present: true },
|
|
1918
|
+
{ at: "__EVENTVALIDATION", present: true }
|
|
1919
|
+
],
|
|
1920
|
+
refuse: [
|
|
1921
|
+
{ at: "ctl00$Content$btnCancel", when: "present" },
|
|
1922
|
+
{ at: "g-recaptcha-response", when: "nonempty" }
|
|
1923
|
+
],
|
|
1924
|
+
payment_method: null,
|
|
1925
|
+
amount: null,
|
|
1926
|
+
currency: "USD",
|
|
1927
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
1928
|
+
resends: [],
|
|
1929
|
+
challenge: null,
|
|
1930
|
+
facts: null,
|
|
1931
|
+
bootstrap: "/robots.txt",
|
|
1932
|
+
timeout_ms: 6e4,
|
|
1933
|
+
charge_timing: "at_request",
|
|
1934
|
+
once_per: { query: "authkey" },
|
|
1935
|
+
pending: [
|
|
1936
|
+
"a policy for the solved reCAPTCHA token the page sends in the same body (refused until then), and whether MyRec checks it against the address that posts",
|
|
1937
|
+
"which cookies the postback needs: ASP.NET_SessionId, another, or none",
|
|
1938
|
+
"the answer to the card postback (a redirect, a processing page or the form again) and how long it takes: the page says processing can take 1 to 2 minutes, longer than the 60 second session",
|
|
1939
|
+
"that the bootstrap path answers without a server error"
|
|
1940
|
+
]
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
id: "ladot_etims_guest_permit",
|
|
1944
|
+
merchant: "City of Los Angeles guest parking permits",
|
|
1945
|
+
status: "observe",
|
|
1946
|
+
evidence: [
|
|
1947
|
+
"wmq.etimspayments.com/pbw/include/la/rppguestinput.jsp (Conduent eTIMS Pay By Web), fetched Sep 25, 2026: the guest permit account check posts to /pbw/rppGuestInputAction.doh with hidden clientcode=17 and clientAccount=9, the Los Angeles client account that citations and permit renewals share. No payment SDK or processor frame on any Los Angeles page.",
|
|
1948
|
+
"shared/js/common.js (live, and the same function in the 2005 to 2012 archived copies) clears document.submitForm.cardNumber: eTIMS collects the card in its own form, submitForm, field cardNumber.",
|
|
1949
|
+
"Archived Los Angeles visits (2024-12-04, 2025-09-15) run input.jsp, inputAction.doh, virtualMerchantPBWSubmitPage.doh and confirmAction.doh; Arlington County's renewal journey on the same template runs rppVirtualMerchantSubmitAction.doh, the permit action this profile pins by analogy. eTIMS then charges Elavon Virtual Merchant from its own server.",
|
|
1950
|
+
"Every eTIMS form posts as a document navigation, bound to the HttpOnly JSESSIONID cookie each page sets."
|
|
1951
|
+
],
|
|
1952
|
+
har: null,
|
|
1953
|
+
checkout: "https://wmq.etimspayments.com",
|
|
1954
|
+
checkout_frame: false,
|
|
1955
|
+
frame: null,
|
|
1956
|
+
endpoint: "https://wmq.etimspayments.com",
|
|
1957
|
+
method: "POST",
|
|
1958
|
+
path: "/pbw/rppVirtualMerchantSubmitAction.doh",
|
|
1959
|
+
query: {},
|
|
1960
|
+
navigation: "top",
|
|
1961
|
+
encoding: "form",
|
|
1962
|
+
session: {
|
|
1963
|
+
cookies: true,
|
|
1964
|
+
cookie_names: ["JSESSIONID"],
|
|
1965
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
1966
|
+
required: []
|
|
1967
|
+
},
|
|
1968
|
+
card: {
|
|
1969
|
+
pan: { at: "cardNumber", format: "digits" },
|
|
1970
|
+
cvv: { at: "cvv", format: "digits" },
|
|
1971
|
+
exp_month: { at: "expMonth", format: "as_typed" },
|
|
1972
|
+
exp_year: { at: "expYear", format: "as_typed" },
|
|
1973
|
+
expiry: null,
|
|
1974
|
+
last4: null,
|
|
1975
|
+
leading: null,
|
|
1976
|
+
brands: [],
|
|
1977
|
+
bins: null
|
|
1978
|
+
},
|
|
1979
|
+
merchant_key: null,
|
|
1980
|
+
requires: [{ at: "clientcode", equals: "17" }],
|
|
1981
|
+
refuse: [],
|
|
1982
|
+
payment_method: null,
|
|
1983
|
+
amount: null,
|
|
1984
|
+
currency: "USD",
|
|
1985
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
1986
|
+
resends: [],
|
|
1987
|
+
challenge: null,
|
|
1988
|
+
facts: null,
|
|
1989
|
+
bootstrap: "/robots.txt",
|
|
1990
|
+
timeout_ms: 6e4,
|
|
1991
|
+
charge_timing: "at_request",
|
|
1992
|
+
once_per: { cookie: "JSESSIONID" },
|
|
1993
|
+
pending: [
|
|
1994
|
+
"the guest permit card action: rppVirtualMerchantSubmitAction.doh is the renewal action on the same template, and the guest flow's own is not archived",
|
|
1995
|
+
"card.cvv, card.exp_month and card.exp_year: only cardNumber has been seen, so these names stand in until a HAR shows the real ones and the expiry's digit counts",
|
|
1996
|
+
"whether the card form carries clientcode 17, and which of its buttons post to the same action",
|
|
1997
|
+
"which request carries the card: the submit action or the confirmAction.doh step after it",
|
|
1998
|
+
"that the Virtual Merchant step does not hand the browser to Converge's hosted page, which would make this Converge processor work instead",
|
|
1999
|
+
"the answer to the card post, and whether it echoes the card",
|
|
2000
|
+
"whether JSESSIONID changes after each payment, which once_per relies on: a session that outlives one payment would refuse the next permit paid in it as the same checkout",
|
|
2001
|
+
"that the bootstrap path answers without a server error"
|
|
2002
|
+
]
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
id: "sfmta_etims_citation",
|
|
2006
|
+
merchant: "San Francisco parking citations",
|
|
2007
|
+
status: "observe",
|
|
2008
|
+
evidence: [
|
|
2009
|
+
"sfmta.com/onlinecitation answers 301 to wmq.etimspayments.com/pbw/include/sanfrancisco/input.jsp (fetched Sep 25, 2026): Conduent eTIMS Pay By Web, clientcode 19. The lookup form posts to /pbw/inputAction.doh behind an image CAPTCHA, and the results form posts to /pbw/selectAction.doh with paymentMethod=card.",
|
|
2010
|
+
"shared/js/common.js clears document.submitForm.cardNumber: eTIMS collects the card in its own form, submitForm, field cardNumber, beside the name on the card, billing address, expiry month (05 - May), expiry year and security code, as a public San Francisco automation example lists them.",
|
|
2011
|
+
"Archived San Francisco visits (2023-10 to 2026-04) run input.jsp, inputAction.doh, selectAction.doh, then transFirstPBWMultiStepSubmitPage.doh (the card, after about 40 seconds of typing) and confirmAction.doh, the review step that completes the charge. eTIMS charges TransFirst Transaction Express from its own server.",
|
|
2012
|
+
"Every eTIMS form posts as a document navigation, bound to the HttpOnly JSESSIONID cookie each page sets."
|
|
2013
|
+
],
|
|
2014
|
+
har: null,
|
|
2015
|
+
checkout: "https://wmq.etimspayments.com",
|
|
2016
|
+
checkout_frame: false,
|
|
2017
|
+
frame: null,
|
|
2018
|
+
endpoint: "https://wmq.etimspayments.com",
|
|
2019
|
+
method: "POST",
|
|
2020
|
+
path: "/pbw/transFirstPBWMultiStepSubmitPage.doh",
|
|
2021
|
+
query: {},
|
|
2022
|
+
navigation: "top",
|
|
2023
|
+
encoding: "form",
|
|
2024
|
+
session: {
|
|
2025
|
+
cookies: true,
|
|
2026
|
+
cookie_names: ["JSESSIONID"],
|
|
2027
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2028
|
+
required: []
|
|
2029
|
+
},
|
|
2030
|
+
card: {
|
|
2031
|
+
pan: { at: "cardNumber", format: "digits" },
|
|
2032
|
+
cvv: { at: "cvv", format: "digits" },
|
|
2033
|
+
exp_month: { at: "expMonth", format: "as_typed" },
|
|
2034
|
+
exp_year: { at: "expYear", format: "as_typed" },
|
|
2035
|
+
expiry: null,
|
|
2036
|
+
last4: null,
|
|
2037
|
+
leading: null,
|
|
2038
|
+
brands: [],
|
|
2039
|
+
bins: null
|
|
2040
|
+
},
|
|
2041
|
+
merchant_key: null,
|
|
2042
|
+
requires: [{ at: "clientcode", equals: "19" }],
|
|
2043
|
+
refuse: [],
|
|
2044
|
+
payment_method: null,
|
|
2045
|
+
amount: null,
|
|
2046
|
+
currency: "USD",
|
|
2047
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2048
|
+
resends: [],
|
|
2049
|
+
challenge: null,
|
|
2050
|
+
facts: null,
|
|
2051
|
+
bootstrap: "/robots.txt",
|
|
2052
|
+
timeout_ms: 6e4,
|
|
2053
|
+
charge_timing: "deferred",
|
|
2054
|
+
once_per: { cookie: "JSESSIONID" },
|
|
2055
|
+
pending: [
|
|
2056
|
+
"card.cvv, card.exp_month and card.exp_year: only cardNumber has been seen, so these names stand in until a HAR shows the real ones and the expiry's digit counts",
|
|
2057
|
+
"whether the card form carries clientcode 19, and which of its buttons post to the same action",
|
|
2058
|
+
"whether the review page or confirmAction.doh, which the agent submits after the card, carries or echoes the card",
|
|
2059
|
+
"the answer to the card post, and whether it echoes the card",
|
|
2060
|
+
"whether JSESSIONID changes after each payment, which once_per relies on: a session that outlives one payment would refuse the next citation paid in it as the same checkout",
|
|
2061
|
+
"that the bootstrap path answers without a server error"
|
|
2062
|
+
]
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
id: "dedge_hotel_card_form",
|
|
2066
|
+
merchant: "D-EDGE hotel booking",
|
|
2067
|
+
status: "observe",
|
|
2068
|
+
evidence: [
|
|
2069
|
+
"HUMA Kotor Bay Hotel & Villas books through D-EDGE (secure-hotel-booking.com/d-edge/HUMA-Kotor-Bay-Hotel-Villas/J6UZ); D-EDGE's payment hub on pay.secure-hotel-booking.com (main.bundle.js, Last-Modified 2026-09-10) renders a CreditCard provider as an iframe at window.cardFrameUrl, card.secure-hotel-booking.com/<the session uuid>.",
|
|
2070
|
+
"The card frame, PciCardForm (archive.org 2026-07-06, 2026-07-23 and 2026-09-04), posts form action /validate/<session uuid>/Default natively, as a navigation of the iframe: hidden Id (the same uuid), CardNumber (maxlength 22), Month (1 to 12, no leading zero), Year (four digits), Cvv (maxlength 4) and CardHolderName. No processor SDK in the frame; D-EDGE charges from its own servers (WSPay for this hotel, by its terms).",
|
|
2071
|
+
"The frame also posts the card's first 8 digits to /cardCss/<session uuid>/Default to pick the card logo, which carries no full card and is not this request.",
|
|
2072
|
+
"secure-hotel-booking.com sits behind DataDome. The same card frame serves every D-EDGE hotel, so the profile names D-EDGE rather than one hotel; its currency is HUMA's."
|
|
2073
|
+
],
|
|
2074
|
+
har: null,
|
|
2075
|
+
checkout: "https://card.secure-hotel-booking.com",
|
|
2076
|
+
checkout_frame: false,
|
|
2077
|
+
frame: null,
|
|
2078
|
+
endpoint: "https://card.secure-hotel-booking.com",
|
|
2079
|
+
method: "POST",
|
|
2080
|
+
path: "/validate/:session/Default",
|
|
2081
|
+
query: {},
|
|
2082
|
+
navigation: "frame",
|
|
2083
|
+
encoding: "form",
|
|
2084
|
+
session: {
|
|
2085
|
+
cookies: true,
|
|
2086
|
+
cookie_names: ["datadome"],
|
|
2087
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2088
|
+
required: []
|
|
2089
|
+
},
|
|
2090
|
+
card: {
|
|
2091
|
+
pan: { at: "CardNumber", format: "digits" },
|
|
2092
|
+
cvv: { at: "Cvv", format: "digits" },
|
|
2093
|
+
exp_month: { at: "Month", format: "m" },
|
|
2094
|
+
exp_year: { at: "Year", format: "yyyy" },
|
|
2095
|
+
expiry: null,
|
|
2096
|
+
last4: null,
|
|
2097
|
+
leading: null,
|
|
2098
|
+
brands: [],
|
|
2099
|
+
bins: null
|
|
2100
|
+
},
|
|
2101
|
+
merchant_key: null,
|
|
2102
|
+
requires: [{ at: "Id", equals_path: "session" }],
|
|
2103
|
+
refuse: [],
|
|
2104
|
+
payment_method: null,
|
|
2105
|
+
amount: null,
|
|
2106
|
+
currency: "EUR",
|
|
2107
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2108
|
+
resends: [],
|
|
2109
|
+
challenge: null,
|
|
2110
|
+
facts: null,
|
|
2111
|
+
bootstrap: "/robots.txt",
|
|
2112
|
+
timeout_ms: 6e4,
|
|
2113
|
+
charge_timing: "at_request",
|
|
2114
|
+
once_per: { path: "session" },
|
|
2115
|
+
pending: [
|
|
2116
|
+
"HUMA's own payment step: whether its pay hub offers this card frame or a WSPay redirect (a HAR of one real booking's payment step)",
|
|
2117
|
+
"which cookies the card post needs, with DataDome in front of the host",
|
|
2118
|
+
"whether CardNumber is posted as digits or with the spaces the field allows",
|
|
2119
|
+
"the answer to the card post (a redirect, a 3DS step or a page), and whether a 3DS step sends the card again",
|
|
2120
|
+
"whether D-EDGE charges the card at this step or holds it for the hotel to charge later",
|
|
2121
|
+
"that the bootstrap path answers without a server error"
|
|
2122
|
+
]
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
id: "nic_common_checkout_payment",
|
|
2126
|
+
merchant: "NIC government checkout (Tyler Technologies)",
|
|
2127
|
+
status: "observe",
|
|
2128
|
+
evidence: [
|
|
2129
|
+
"NJMCdirect (NJ Judiciary municipal court payments) sends the browser to NIC Common Checkout, securecheckout.cdc.nicusa.com: the court portal opens CommonCheckPage/Default.aspx?token=<checkout GUID> top-level, a cookie check (?chkCap=Y) answers 302 to /Checkout/Payment?token=<GUID>, and that page sets the HttpOnly token, lang and ASP.NET_SessionId cookies and the F5 cookie TS011b1edc. The archive pairs about 40 checkout tokens with the court's return URL for the same GUID (status success, failure, duplicate or cancel).",
|
|
2130
|
+
"The archived NJ payment page (2026-07-03, structure only): form#paymentForm posts to /Checkout/Payment/ProcessPaymentEntry with Token, PayType (CC only for NJ), CustomerInfo.*, Phone, Email, the card in plain fields CCCardNumber (maxlength 16), CCCardType (VISA, MAST, AMEX, DISC and others, from NIC's BIN lookup), CCExpirationMonth (01 to 12), CCExpirationYear (four digits), CCCardCVV and CCNameOnCard, and the empty ACH, subscriber, PayPal, Gov2Go and Apple Pay fields. No anti-forgery token: the session cookies are the only binding.",
|
|
2131
|
+
"NIC's paymentjs bundle (sha256 f5733609dfe35a6b6ccd24478aacc123e1b7d8817e971275b94e6b80332530d0, byte-identical live, in the archive and on the Florida host): on #submitPayment it appends a hidden submitPayment and submits the form, a top-level navigation. The same URL takes cancelPayment and the PayPal and Gov2Go launch buttons. The Braintree and Tyler Orion drop-ins in the bundle are off for NJ. The only other card-adjacent calls, BinLookup and GetFee, send the first 11 digits and are not this request.",
|
|
2132
|
+
"NIC charges the card from its own servers and sends the browser to the agency's return URL with the checkout token; the agency records the payment inside that return. The amount is set by the agency when it creates the token (NJ folds a non-refundable 3% processing fee into it), so the body names none. The same page serves about 18 state agencies; the ones with NIC's reCAPTCHA put g-recaptcha-response in the body, which this profile refuses."
|
|
2133
|
+
],
|
|
2134
|
+
har: null,
|
|
2135
|
+
checkout: "https://securecheckout.cdc.nicusa.com",
|
|
2136
|
+
checkout_frame: false,
|
|
2137
|
+
frame: null,
|
|
2138
|
+
endpoint: "https://securecheckout.cdc.nicusa.com",
|
|
2139
|
+
method: "POST",
|
|
2140
|
+
path: "/Checkout/Payment/ProcessPaymentEntry",
|
|
2141
|
+
query: {},
|
|
2142
|
+
navigation: "top",
|
|
2143
|
+
encoding: "form",
|
|
2144
|
+
session: {
|
|
2145
|
+
cookies: true,
|
|
2146
|
+
cookie_names: ["token", "lang", "ASP.NET_SessionId", "TS01*"],
|
|
2147
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2148
|
+
required: []
|
|
2149
|
+
},
|
|
2150
|
+
card: {
|
|
2151
|
+
pan: { at: "CCCardNumber", format: "digits" },
|
|
2152
|
+
cvv: { at: "CCCardCVV", format: "digits" },
|
|
2153
|
+
exp_month: { at: "CCExpirationMonth", format: "mm" },
|
|
2154
|
+
exp_year: { at: "CCExpirationYear", format: "yyyy" },
|
|
2155
|
+
expiry: null,
|
|
2156
|
+
last4: null,
|
|
2157
|
+
leading: null,
|
|
2158
|
+
brands: [{ at: "CCCardType", map: { visa: "VISA", mastercard: "MAST", amex: "AMEX", discover: "DISC" } }],
|
|
2159
|
+
bins: null
|
|
2160
|
+
},
|
|
2161
|
+
merchant_key: null,
|
|
2162
|
+
requires: [
|
|
2163
|
+
{ at: "PayType", equals: "CC" },
|
|
2164
|
+
{ at: "Token", matches: "guid" },
|
|
2165
|
+
{ at: "Token", equals_cookie: "token" },
|
|
2166
|
+
{ at: "submitPayment", equals: "" }
|
|
2167
|
+
],
|
|
2168
|
+
refuse: [
|
|
2169
|
+
{ at: "cancelPayment", when: "present" },
|
|
2170
|
+
{ at: "launchPayPal", when: "present" },
|
|
2171
|
+
{ at: "launchGov2Go", when: "present" },
|
|
2172
|
+
{ at: "launchGov2GoPay", when: "present" },
|
|
2173
|
+
{ at: "ait", when: "present" },
|
|
2174
|
+
{ at: "g-recaptcha-response", when: "present" },
|
|
2175
|
+
{ at: "CCCardNumberBIN", when: "present" },
|
|
2176
|
+
{ at: "SubUserName", when: "nonempty" },
|
|
2177
|
+
{ at: "SubPassword", when: "nonempty" },
|
|
2178
|
+
{ at: "ACHRoutingNumber", when: "nonempty" },
|
|
2179
|
+
{ at: "ACHAccountNumber", when: "nonempty" },
|
|
2180
|
+
{ at: "ACHAccountNumberVerify", when: "nonempty" },
|
|
2181
|
+
{ at: "ACHNameOnAccount", when: "nonempty" },
|
|
2182
|
+
{ at: "ACHBankType", when: "present" },
|
|
2183
|
+
{ at: "AuthorizeAch", when: "true" },
|
|
2184
|
+
{ at: "PayPalToken", when: "nonempty" },
|
|
2185
|
+
{ at: "PaymentApiToken", when: "nonempty" },
|
|
2186
|
+
{ at: "Gov2GoPaymentApiPaymentType", when: "nonempty" },
|
|
2187
|
+
{ prefix: "ApplePay.PaymentAccount.", when: "nonempty" },
|
|
2188
|
+
{ prefix: "EmbeddedCheckoutPayment", when: "present" },
|
|
2189
|
+
{ prefix: "braintree", when: "present" }
|
|
2190
|
+
],
|
|
2191
|
+
payment_method: null,
|
|
2192
|
+
amount: null,
|
|
2193
|
+
currency: "USD",
|
|
2194
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2195
|
+
resends: [],
|
|
2196
|
+
challenge: null,
|
|
2197
|
+
facts: null,
|
|
2198
|
+
bootstrap: "/Checkout/bundles/html5SiteLayout",
|
|
2199
|
+
timeout_ms: 6e4,
|
|
2200
|
+
charge_timing: "at_request",
|
|
2201
|
+
once_per: { at: "Token" },
|
|
2202
|
+
pending: [
|
|
2203
|
+
"the answer to a paid post (a 302 to NIC's receipt or straight to the agency's return URL) and to a decline (a redirect back or the form again, and whether it echoes the card, a masked card or the expiry)",
|
|
2204
|
+
"every cookie the checkout sets after its cookie check (each TS01 name, whether lang matters)",
|
|
2205
|
+
"the CCCardType codes NIC's BIN lookup answers for Visa, Mastercard, Amex and Discover cards, credit and debit",
|
|
2206
|
+
"whether NIC's F5 front door and fraud rules accept the post from the relay's address and TLS with cookies minted in the agent's browser, and whether they read Origin, Referer or the sec-fetch headers",
|
|
2207
|
+
"the session's idle timeout, which bounds the approval",
|
|
2208
|
+
"Tyler Technologies' PCI attestation of compliance for this checkout host",
|
|
2209
|
+
"the form action's letter case: the server answers any case, and this profile pauses only the archived spelling, so a page that posts another spelling is not paused"
|
|
2210
|
+
]
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
id: "fastspring_popup_rekordcloud",
|
|
2214
|
+
merchant: "Lexicon",
|
|
2215
|
+
status: "observe",
|
|
2216
|
+
evidence: [
|
|
2217
|
+
"www.lexicondj.com/scripts/checkout.js (sha256 9802ab20), read Sep 26, 2026: loads FastSpring SBL 1.0.5 with data-storefront rekordcloud.onfastspring.com/popup-lexicon; FastSpring is the merchant of record and opens its popup checkout in a cross-origin iframe at https://rekordcloud.onfastspring.com/popup-lexicon/session/{sessionId}.",
|
|
2218
|
+
"FastSpring shared checkout app on cdn.onfastspring.com (foundation.checkout.js sha256 5334d89b doPayment, common.js sha256 21a031fd interceptor), read Sep 25 and 26, 2026: AngularJS $http posts JSON to <session URL>/payment with X-Session-Token from the page's session JSON, never a cookie.",
|
|
2219
|
+
"Body: contact, card {year, month, number (digits in groups of four, one space between), security}, sepa, ach, pad, bacs, upi, cpfNumber, dniNumber, paymentType, subscribe, captcha {token, captchaType}, recipientSelected, autoRenew (when the renewal checkbox shows), fingerprint; PayPal, wallets and bank debits post the same URL with another paymentType.",
|
|
2220
|
+
"Answers: next error, redirect (success), 3ds (Adyen form in the popup), ebanx-3ds (BRL: the page reads the typed card again and posts it to <session URL>/3ds/return-ebanx), antom-3ds and APM redirects.",
|
|
2221
|
+
"The popup document (script#viewdata) and each JSON answer carry order.totalWithTaxValue, order.currency, production, session.captchaRequired, hasRecurring and the selected items with their renewal (future).",
|
|
2222
|
+
"foundation.checkout.js (sha256 5334d89b), read Sep 30, 2026: the page also reads its order from PUT <session URL>/items/<path> (changeQuantity, onAutoRenewChange), POST <session URL>/items/<path> (changeQuantity for an unselected item, acceptOffer) and POST <session URL>/spi/sheerid; the contact fields post /customer/recognize/background when they lose focus (the interceptor drops /background), and the page reads nothing from that answer.",
|
|
2223
|
+
"Turnstile: renderCaptchaWidget runs when the card option is chosen (choosePaymentOptionWithFields), and turnstileCallback stores the token then, so a body holds a token minted before Pay.",
|
|
2224
|
+
"Research: processors-wave2/items/fastspring/DESIGN.md; the popup HTML itself needs a live session and was not read."
|
|
2225
|
+
],
|
|
2226
|
+
har: null,
|
|
2227
|
+
checkout: "https://rekordcloud.onfastspring.com",
|
|
2228
|
+
checkout_frame: true,
|
|
2229
|
+
frame: null,
|
|
2230
|
+
endpoint: "https://rekordcloud.onfastspring.com",
|
|
2231
|
+
method: "POST",
|
|
2232
|
+
path: "/popup-lexicon/session/:sessionId/payment",
|
|
2233
|
+
query: {},
|
|
2234
|
+
navigation: null,
|
|
2235
|
+
encoding: "json",
|
|
2236
|
+
session: {
|
|
2237
|
+
cookies: false,
|
|
2238
|
+
cookie_names: null,
|
|
2239
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-session-token"],
|
|
2240
|
+
required: ["x-session-token"]
|
|
2241
|
+
},
|
|
2242
|
+
card: {
|
|
2243
|
+
pan: { at: ["card", "number"], format: "grouped4" },
|
|
2244
|
+
cvv: { at: ["card", "security"], format: "digits" },
|
|
2245
|
+
exp_month: { at: ["card", "month"], format: "as_typed" },
|
|
2246
|
+
exp_year: { at: ["card", "year"], format: "as_typed" },
|
|
2247
|
+
expiry: null,
|
|
2248
|
+
last4: null,
|
|
2249
|
+
leading: null,
|
|
2250
|
+
brands: [],
|
|
2251
|
+
bins: null
|
|
2252
|
+
},
|
|
2253
|
+
merchant_key: null,
|
|
2254
|
+
requires: [{ at: ["paymentType"], equals: "card" }],
|
|
2255
|
+
refuse: [
|
|
2256
|
+
// Brazil's EBANX route sends the typed card a second time.
|
|
2257
|
+
{ at: ["cpfNumber"], when: "nonempty" },
|
|
2258
|
+
{ at: ["dniNumber"], when: "nonempty" },
|
|
2259
|
+
// A gift licenses someone else.
|
|
2260
|
+
{ at: ["recipientSelected"], when: "true" },
|
|
2261
|
+
{ at: ["recipient"], when: "present" },
|
|
2262
|
+
// FastSpring keeps the card for renewals, which waits on the card-on-file decision.
|
|
2263
|
+
{ at: ["autoRenew"], when: "true" },
|
|
2264
|
+
// An invoiced buyer's purchase order, another checkout than this one.
|
|
2265
|
+
{ at: ["customerReference"], when: "present" },
|
|
2266
|
+
// Another payment method's details beside the card.
|
|
2267
|
+
{ at: ["sepa", "iban"], when: "nonempty" },
|
|
2268
|
+
{ at: ["ach", "accountNum"], when: "nonempty" },
|
|
2269
|
+
{ at: ["pad", "accountNum"], when: "nonempty" },
|
|
2270
|
+
{ at: ["bacs", "accountNumber"], when: "nonempty" },
|
|
2271
|
+
{ at: ["googlepay"], when: "nonempty" },
|
|
2272
|
+
{ at: ["paymentKlarna"], when: "nonempty" },
|
|
2273
|
+
{ at: ["mercadopago"], when: "nonempty" }
|
|
2274
|
+
],
|
|
2275
|
+
payment_method: { at: ["paymentType"], card: ["card"] },
|
|
2276
|
+
amount: null,
|
|
2277
|
+
currency: "USD",
|
|
2278
|
+
response: { headers: ["content-type"], withhold: [{ at: ["next"], equals: "ebanx-3ds" }] },
|
|
2279
|
+
resends: [{ method: "POST", path: "/popup-lexicon/session/:sessionId/3ds/return-ebanx", query: {} }],
|
|
2280
|
+
// The device may send a body holding a Turnstile token for 240 seconds after the create. The
|
|
2281
|
+
// page minted the token when the card option rendered, before the pause, so this cannot keep
|
|
2282
|
+
// a slow approval from sending an expired one (pending below).
|
|
2283
|
+
challenge: { at: ["captcha", "token"], ttl_ms: 24e4 },
|
|
2284
|
+
facts: {
|
|
2285
|
+
reader: "fastspring_session",
|
|
2286
|
+
// Every answer FastSpring's page reads its order from (refreshPageResponse and
|
|
2287
|
+
// refreshOrderResponse): the popup document, the one-segment calls (/tax, /coupons,
|
|
2288
|
+
// /locale, /exempt), an item's quantity, renewal or add-on change, and a verified discount.
|
|
2289
|
+
responses: [
|
|
2290
|
+
{ method: "GET", path: "/popup-lexicon/session/:sessionId", query: {}, format: "html" },
|
|
2291
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/:step", query: {}, format: "json" },
|
|
2292
|
+
{ method: "GET", path: "/popup-lexicon/session/:sessionId/:step", query: {}, format: "json" },
|
|
2293
|
+
{ method: "PUT", path: "/popup-lexicon/session/:sessionId/items/:item", query: {}, format: "json" },
|
|
2294
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/items/:item", query: {}, format: "json" },
|
|
2295
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/spi/sheerid", query: {}, format: "json" }
|
|
2296
|
+
],
|
|
2297
|
+
// Customer recognition, posted when a contact field loses focus: the page reads nothing from its answer.
|
|
2298
|
+
inert: [{ method: "POST", path: "/popup-lexicon/session/:sessionId/customer/recognize", query: {} }],
|
|
2299
|
+
bind: "sessionId"
|
|
2300
|
+
},
|
|
2301
|
+
bootstrap: "/robots.txt",
|
|
2302
|
+
timeout_ms: 3e4,
|
|
2303
|
+
// Most of Lexicon's catalog renews monthly. The facts read refuses each renewing
|
|
2304
|
+
// order, and the worst case stays here until a purchase proves that read.
|
|
2305
|
+
charge_timing: "recurring",
|
|
2306
|
+
once_per: null,
|
|
2307
|
+
pending: [
|
|
2308
|
+
"which controller the popup runs (foundation or pinhole) and the exact /payment body it sends",
|
|
2309
|
+
"the popup document URL and the paths of the JSON answers that carry its session state",
|
|
2310
|
+
"whether /payment needs the AWSALB and AWSALBCORS cookies, Origin or Referer",
|
|
2311
|
+
"whether an X-XSRF-TOKEN header rides along",
|
|
2312
|
+
"whether rekordcloud sets session.captchaRequired",
|
|
2313
|
+
"how old a Turnstile token is when the card request pauses: the page mints it when the card option renders, so a body can still reach FastSpring expired inside the 240 seconds (a captcha error, nothing charged, a spent approval) until a preparation flow approves before Pay",
|
|
2314
|
+
"card.number sent grouped or plain; month and year widths",
|
|
2315
|
+
"the session currency for US buyers",
|
|
2316
|
+
"a card-on-file policy for the monthly plans and the add-on, before any order that renews"
|
|
2317
|
+
]
|
|
2318
|
+
}
|
|
2319
|
+
]);
|
|
2320
|
+
function wireProjection(profiles) {
|
|
2321
|
+
return deepFreeze(profiles.map(({ evidence, bootstrap, timeout_ms, ...profile }) => ({ ...profile })));
|
|
2322
|
+
}
|
|
2323
|
+
function clientProjection(profiles) {
|
|
2324
|
+
return deepFreeze(profiles.map(({ evidence, ...profile }) => ({ ...profile })));
|
|
2325
|
+
}
|
|
2326
|
+
var MERCHANT_CARD_ENDPOINT_RECOGNIZERS = /* @__PURE__ */ wireProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
2327
|
+
var MERCHANT_CARD_ENDPOINT_CLIENT_TABLE = /* @__PURE__ */ clientProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
2328
|
+
function merchantCardEndpointById(id) {
|
|
2329
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((entry) => entry.id === id) ?? null : null;
|
|
2330
|
+
}
|
|
2331
|
+
function trustedMerchantCardEndpoint(idOrEntry) {
|
|
2332
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
2333
|
+
if (!profile)
|
|
2334
|
+
throw new Error("Refusing to pay: unknown merchant card endpoint.");
|
|
2335
|
+
return profile;
|
|
2336
|
+
}
|
|
2337
|
+
function merchantCardEndpointRecognizerById(id) {
|
|
2338
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_RECOGNIZERS.find((entry) => entry.id === id) ?? null : null;
|
|
2339
|
+
}
|
|
2340
|
+
function merchantCardEndpointLive(idOrEntry) {
|
|
2341
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
2342
|
+
return !!profile && profile.status === "enabled" && profile.pending.length === 0 && typeof profile.har === "string" && profile.har !== "" && profile.charge_timing !== "recurring" && (profile.merchant_key == null || Array.isArray(profile.merchant_key.keys) && profile.merchant_key.keys.length > 0);
|
|
2343
|
+
}
|
|
2344
|
+
// src/sha256.js
|
|
2345
|
+
var K = /* @__PURE__ */ new Uint32Array([
|
|
2346
|
+
1116352408,
|
|
2347
|
+
1899447441,
|
|
2348
|
+
3049323471,
|
|
2349
|
+
3921009573,
|
|
2350
|
+
961987163,
|
|
2351
|
+
1508970993,
|
|
2352
|
+
2453635748,
|
|
2353
|
+
2870763221,
|
|
2354
|
+
3624381080,
|
|
2355
|
+
310598401,
|
|
2356
|
+
607225278,
|
|
2357
|
+
1426881987,
|
|
2358
|
+
1925078388,
|
|
2359
|
+
2162078206,
|
|
2360
|
+
2614888103,
|
|
2361
|
+
3248222580,
|
|
2362
|
+
3835390401,
|
|
2363
|
+
4022224774,
|
|
2364
|
+
264347078,
|
|
2365
|
+
604807628,
|
|
2366
|
+
770255983,
|
|
2367
|
+
1249150122,
|
|
2368
|
+
1555081692,
|
|
2369
|
+
1996064986,
|
|
2370
|
+
2554220882,
|
|
2371
|
+
2821834349,
|
|
2372
|
+
2952996808,
|
|
2373
|
+
3210313671,
|
|
2374
|
+
3336571891,
|
|
2375
|
+
3584528711,
|
|
2376
|
+
113926993,
|
|
2377
|
+
338241895,
|
|
2378
|
+
666307205,
|
|
2379
|
+
773529912,
|
|
2380
|
+
1294757372,
|
|
2381
|
+
1396182291,
|
|
2382
|
+
1695183700,
|
|
2383
|
+
1986661051,
|
|
2384
|
+
2177026350,
|
|
2385
|
+
2456956037,
|
|
2386
|
+
2730485921,
|
|
2387
|
+
2820302411,
|
|
2388
|
+
3259730800,
|
|
2389
|
+
3345764771,
|
|
2390
|
+
3516065817,
|
|
2391
|
+
3600352804,
|
|
2392
|
+
4094571909,
|
|
2393
|
+
275423344,
|
|
2394
|
+
430227734,
|
|
2395
|
+
506948616,
|
|
2396
|
+
659060556,
|
|
2397
|
+
883997877,
|
|
2398
|
+
958139571,
|
|
2399
|
+
1322822218,
|
|
2400
|
+
1537002063,
|
|
2401
|
+
1747873779,
|
|
2402
|
+
1955562222,
|
|
2403
|
+
2024104815,
|
|
2404
|
+
2227730452,
|
|
2405
|
+
2361852424,
|
|
2406
|
+
2428436474,
|
|
2407
|
+
2756734187,
|
|
2408
|
+
3204031479,
|
|
2409
|
+
3329325298
|
|
2410
|
+
]);
|
|
2411
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
2412
|
+
function utf8(text) {
|
|
2413
|
+
const out = [];
|
|
2414
|
+
for (const char of text) {
|
|
2415
|
+
let cp = char.codePointAt(0);
|
|
2416
|
+
if (cp >= 55296 && cp <= 57343)
|
|
2417
|
+
cp = 65533;
|
|
2418
|
+
if (cp < 128)
|
|
2419
|
+
out.push(cp);
|
|
2420
|
+
else if (cp < 2048)
|
|
2421
|
+
out.push(192 | cp >> 6, 128 | cp & 63);
|
|
2422
|
+
else if (cp < 65536)
|
|
2423
|
+
out.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
|
|
2424
|
+
else
|
|
2425
|
+
out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
|
|
2426
|
+
}
|
|
2427
|
+
return Uint8Array.from(out);
|
|
2428
|
+
}
|
|
2429
|
+
function sha256Hex(input) {
|
|
2430
|
+
const isBytes2 = Object.prototype.toString.call(input) === "[object Uint8Array]";
|
|
2431
|
+
if (typeof input !== "string" && !isBytes2)
|
|
2432
|
+
throw new TypeError("sha256Hex takes a string or a Uint8Array.");
|
|
2433
|
+
const bytes = typeof input === "string" ? utf8(input) : input;
|
|
2434
|
+
const length = bytes.length;
|
|
2435
|
+
const padded = new Uint8Array((length + 8 >> 6) + 1 << 6);
|
|
2436
|
+
padded.set(bytes);
|
|
2437
|
+
padded[length] = 128;
|
|
2438
|
+
const view = new DataView(padded.buffer);
|
|
2439
|
+
view.setUint32(padded.length - 8, Math.floor(length / 536870912));
|
|
2440
|
+
view.setUint32(padded.length - 4, length << 3 >>> 0);
|
|
2441
|
+
const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
2442
|
+
const W = new Uint32Array(64);
|
|
2443
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
2444
|
+
for (let t = 0; t < 16; t++)
|
|
2445
|
+
W[t] = view.getUint32(offset + t * 4);
|
|
2446
|
+
for (let t = 16; t < 64; t++) {
|
|
2447
|
+
const s0 = rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ W[t - 15] >>> 3;
|
|
2448
|
+
const s1 = rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ W[t - 2] >>> 10;
|
|
2449
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
2450
|
+
}
|
|
2451
|
+
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
2452
|
+
for (let t = 0; t < 64; t++) {
|
|
2453
|
+
const t1 = h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[t] + W[t] >>> 0;
|
|
2454
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
2455
|
+
h = g;
|
|
2456
|
+
g = f;
|
|
2457
|
+
f = e;
|
|
2458
|
+
e = d + t1 >>> 0;
|
|
2459
|
+
d = c;
|
|
2460
|
+
c = b;
|
|
2461
|
+
b = a;
|
|
2462
|
+
a = t1 + t2 >>> 0;
|
|
2463
|
+
}
|
|
2464
|
+
H[0] += a;
|
|
2465
|
+
H[1] += b;
|
|
2466
|
+
H[2] += c;
|
|
2467
|
+
H[3] += d;
|
|
2468
|
+
H[4] += e;
|
|
2469
|
+
H[5] += f;
|
|
2470
|
+
H[6] += g;
|
|
2471
|
+
H[7] += h;
|
|
2472
|
+
}
|
|
2473
|
+
return Array.from(H, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
2474
|
+
}
|
|
2475
|
+
// src/merchant-card-endpoint-keys.js
|
|
2476
|
+
var MerchantKeyError = class extends Error {
|
|
2477
|
+
constructor(reason) {
|
|
2478
|
+
super("Refusing to encrypt the card under the merchant's key.");
|
|
2479
|
+
this.name = "MerchantKeyError";
|
|
2480
|
+
this.code = "merchant_key_refused";
|
|
2481
|
+
this.reason = reason;
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
var refuse = (reason) => new MerchantKeyError(reason);
|
|
2485
|
+
var MERCHANT_KEY_HASHES = /* @__PURE__ */ Object.freeze({ "SHA-1": 20, "SHA-256": 32, "SHA-512": 64 });
|
|
2486
|
+
var MERCHANT_KEY_JWE_ALGS = /* @__PURE__ */ Object.freeze({ "RSA-OAEP": "SHA-1", "RSA-OAEP-256": "SHA-256" });
|
|
2487
|
+
var MERCHANT_KEY_JWE_ENCS = /* @__PURE__ */ Object.freeze({
|
|
2488
|
+
"A256CBC-HS512": Object.freeze({ key: 64, iv: 16, tag: 32 }),
|
|
2489
|
+
A256GCM: Object.freeze({ key: 32, iv: 12, tag: 16 })
|
|
2490
|
+
});
|
|
2491
|
+
var MERCHANT_KEY_SIZES = /* @__PURE__ */ Object.freeze([256, 384, 512]);
|
|
2492
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2493
|
+
var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
2494
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
2495
|
+
var RSA_ALGORITHM = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
|
|
2496
|
+
var MAX_ENCRYPTED_LENGTH = 4096;
|
|
2497
|
+
var isBytes = (value) => Object.prototype.toString.call(value) === "[object Uint8Array]";
|
|
2498
|
+
function encodeBase64(bytes, url = false) {
|
|
2499
|
+
const alphabet = url ? BASE64URL_ALPHABET : BASE64_ALPHABET;
|
|
2500
|
+
let out = "";
|
|
2501
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
2502
|
+
const a = bytes[i];
|
|
2503
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
2504
|
+
const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
2505
|
+
const triple = a << 16 | b << 8 | c;
|
|
2506
|
+
out += alphabet[triple >> 18 & 63] + alphabet[triple >> 12 & 63];
|
|
2507
|
+
if (i + 1 < bytes.length)
|
|
2508
|
+
out += alphabet[triple >> 6 & 63];
|
|
2509
|
+
else if (!url)
|
|
2510
|
+
out += "=";
|
|
2511
|
+
if (i + 2 < bytes.length)
|
|
2512
|
+
out += alphabet[triple & 63];
|
|
2513
|
+
else if (!url)
|
|
2514
|
+
out += "=";
|
|
2515
|
+
}
|
|
2516
|
+
return out;
|
|
2517
|
+
}
|
|
2518
|
+
function decodeBase64(text, url = false) {
|
|
2519
|
+
if (typeof text !== "string" || text.length === 0 || text.length > MAX_ENCRYPTED_LENGTH * 2)
|
|
2520
|
+
return null;
|
|
2521
|
+
const alphabet = url ? BASE64URL_ALPHABET : BASE64_ALPHABET;
|
|
2522
|
+
const body = url ? text : text.replace(/={1,2}$/, "");
|
|
2523
|
+
if (!url && text.length % 4 !== 0)
|
|
2524
|
+
return null;
|
|
2525
|
+
if (body.length % 4 === 1)
|
|
2526
|
+
return null;
|
|
2527
|
+
const out = new Uint8Array(Math.floor(body.length * 3 / 4));
|
|
2528
|
+
let bits = 0;
|
|
2529
|
+
let value = 0;
|
|
2530
|
+
let at = 0;
|
|
2531
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
2532
|
+
const digit = alphabet.indexOf(body[i]);
|
|
2533
|
+
if (digit === -1)
|
|
2534
|
+
return null;
|
|
2535
|
+
value = value << 6 | digit;
|
|
2536
|
+
bits += 6;
|
|
2537
|
+
if (bits >= 8) {
|
|
2538
|
+
bits -= 8;
|
|
2539
|
+
out[at++] = value >> bits & 255;
|
|
2540
|
+
value &= (1 << bits) - 1;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return encodeBase64(out, url) === text ? out : null;
|
|
2544
|
+
}
|
|
2545
|
+
var utf8Bytes = (text) => new TextEncoder().encode(text);
|
|
2546
|
+
function readElement(der, at, tag) {
|
|
2547
|
+
if (at + 2 > der.length || der[at] !== tag)
|
|
2548
|
+
throw refuse("key");
|
|
2549
|
+
let length = der[at + 1];
|
|
2550
|
+
let start = at + 2;
|
|
2551
|
+
if (length & 128) {
|
|
2552
|
+
const count = length & 127;
|
|
2553
|
+
if (count === 0 || count > 2 || start + count > der.length || der[start] === 0)
|
|
2554
|
+
throw refuse("key");
|
|
2555
|
+
length = count === 1 ? der[start] : der[start] * 256 + der[start + 1];
|
|
2556
|
+
if (length < 128)
|
|
2557
|
+
throw refuse("key");
|
|
2558
|
+
start += count;
|
|
2559
|
+
}
|
|
2560
|
+
if (start + length > der.length)
|
|
2561
|
+
throw refuse("key");
|
|
2562
|
+
return { start, end: start + length };
|
|
2563
|
+
}
|
|
2564
|
+
function readPositiveInteger(der, at) {
|
|
2565
|
+
const { start, end } = readElement(der, at, 2);
|
|
2566
|
+
if (end === start || der[start] & 128)
|
|
2567
|
+
throw refuse("key");
|
|
2568
|
+
if (der[start] === 0 && (end - start === 1 || !(der[start + 1] & 128)))
|
|
2569
|
+
throw refuse("key");
|
|
2570
|
+
return { bytes: der.subarray(der[start] === 0 ? start + 1 : start, end), end };
|
|
2571
|
+
}
|
|
2572
|
+
function toBigInt(bytes) {
|
|
2573
|
+
let hex = "";
|
|
2574
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
2575
|
+
hex += (bytes[i] < 16 ? "0" : "") + bytes[i].toString(16);
|
|
2576
|
+
return BigInt(`0x${hex || "0"}`);
|
|
2577
|
+
}
|
|
2578
|
+
function toBytes(value, size) {
|
|
2579
|
+
const hex = value.toString(16).padStart(size * 2, "0");
|
|
2580
|
+
if (hex.length !== size * 2)
|
|
2581
|
+
throw refuse("text");
|
|
2582
|
+
const out = new Uint8Array(size);
|
|
2583
|
+
for (let i = 0; i < size; i += 1)
|
|
2584
|
+
out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
|
|
2585
|
+
return out;
|
|
2586
|
+
}
|
|
2587
|
+
function modPow(base, exponent, modulus) {
|
|
2588
|
+
let result = 1n;
|
|
2589
|
+
let square = base % modulus;
|
|
2590
|
+
for (let rest = exponent; rest > 0n; rest >>= 1n) {
|
|
2591
|
+
if (rest & 1n)
|
|
2592
|
+
result = result * square % modulus;
|
|
2593
|
+
square = square * square % modulus;
|
|
2594
|
+
}
|
|
2595
|
+
return result;
|
|
2596
|
+
}
|
|
2597
|
+
function parseMerchantKey(key) {
|
|
2598
|
+
const spki = key && typeof key === "object" ? key.spki : void 0;
|
|
2599
|
+
const pin = key && typeof key === "object" ? key.sha256 : void 0;
|
|
2600
|
+
if (typeof spki !== "string" || spki.length > 1024 || typeof pin !== "string" || !SHA256_HEX.test(pin))
|
|
2601
|
+
throw refuse("key");
|
|
2602
|
+
const der = decodeBase64(spki);
|
|
2603
|
+
if (!der || sha256Hex(der) !== pin)
|
|
2604
|
+
throw refuse("key");
|
|
2605
|
+
const outer = readElement(der, 0, 48);
|
|
2606
|
+
if (outer.end !== der.length || outer.start + RSA_ALGORITHM.length > der.length)
|
|
2607
|
+
throw refuse("key");
|
|
2608
|
+
if (!RSA_ALGORITHM.every((byte, i) => der[outer.start + i] === byte))
|
|
2609
|
+
throw refuse("key");
|
|
2610
|
+
const bits = readElement(der, outer.start + RSA_ALGORITHM.length, 3);
|
|
2611
|
+
if (bits.end !== outer.end || bits.start === bits.end || der[bits.start] !== 0)
|
|
2612
|
+
throw refuse("key");
|
|
2613
|
+
const rsa = readElement(der, bits.start + 1, 48);
|
|
2614
|
+
if (rsa.end !== bits.end)
|
|
2615
|
+
throw refuse("key");
|
|
2616
|
+
const modulus = readPositiveInteger(der, rsa.start);
|
|
2617
|
+
const exponent = readPositiveInteger(der, modulus.end);
|
|
2618
|
+
if (exponent.end !== rsa.end)
|
|
2619
|
+
throw refuse("key");
|
|
2620
|
+
const n = modulus.bytes;
|
|
2621
|
+
if (!MERCHANT_KEY_SIZES.includes(n.length) || !(n[0] & 128) || !(n[n.length - 1] & 1))
|
|
2622
|
+
throw refuse("key");
|
|
2623
|
+
const e = exponent.bytes;
|
|
2624
|
+
if (e.length !== 3 || e[0] !== 1 || e[1] !== 0 || e[2] !== 1)
|
|
2625
|
+
throw refuse("key");
|
|
2626
|
+
return Object.freeze({ n: toBigInt(n), size: n.length });
|
|
2627
|
+
}
|
|
2628
|
+
function subtleOf(crypto) {
|
|
2629
|
+
if (!crypto || !crypto.subtle || typeof crypto.getRandomValues !== "function")
|
|
2630
|
+
throw refuse("crypto");
|
|
2631
|
+
return crypto.subtle;
|
|
2632
|
+
}
|
|
2633
|
+
async function digest(subtle, hash, bytes) {
|
|
2634
|
+
try {
|
|
2635
|
+
return new Uint8Array(await subtle.digest(hash, bytes));
|
|
2636
|
+
}
|
|
2637
|
+
catch {
|
|
2638
|
+
throw refuse("crypto");
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
async function mgf1(subtle, hash, seed, length) {
|
|
2642
|
+
const out = new Uint8Array(length);
|
|
2643
|
+
const input = new Uint8Array(seed.length + 4);
|
|
2644
|
+
input.set(seed);
|
|
2645
|
+
try {
|
|
2646
|
+
for (let counter = 0, filled = 0; filled < length; counter += 1) {
|
|
2647
|
+
input[seed.length] = counter >>> 24 & 255;
|
|
2648
|
+
input[seed.length + 1] = counter >>> 16 & 255;
|
|
2649
|
+
input[seed.length + 2] = counter >>> 8 & 255;
|
|
2650
|
+
input[seed.length + 3] = counter & 255;
|
|
2651
|
+
const block = await digest(subtle, hash, input);
|
|
2652
|
+
out.set(block.subarray(0, Math.min(block.length, length - filled)), filled);
|
|
2653
|
+
filled += block.length;
|
|
2654
|
+
block.fill(0);
|
|
2655
|
+
}
|
|
2656
|
+
return out;
|
|
2657
|
+
}
|
|
2658
|
+
finally {
|
|
2659
|
+
input.fill(0);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function randomBytes(crypto, length) {
|
|
2663
|
+
const out = new Uint8Array(length);
|
|
2664
|
+
try {
|
|
2665
|
+
crypto.getRandomValues(out);
|
|
2666
|
+
}
|
|
2667
|
+
catch {
|
|
2668
|
+
throw refuse("random");
|
|
2669
|
+
}
|
|
2670
|
+
return out;
|
|
2671
|
+
}
|
|
2672
|
+
async function rsaOaepEncrypt(key, message, { hash, mgf1: maskHash }, crypto, seed) {
|
|
2673
|
+
const subtle = subtleOf(crypto);
|
|
2674
|
+
const { n, size } = parseMerchantKey(key);
|
|
2675
|
+
const hLen = MERCHANT_KEY_HASHES[hash];
|
|
2676
|
+
if (!hLen || !MERCHANT_KEY_HASHES[maskHash])
|
|
2677
|
+
throw refuse("key");
|
|
2678
|
+
if (!isBytes(message) || message.length > size - 2 * hLen - 2)
|
|
2679
|
+
throw refuse("text");
|
|
2680
|
+
const db = new Uint8Array(size - hLen - 1);
|
|
2681
|
+
const em = new Uint8Array(size);
|
|
2682
|
+
let seedBytes = null;
|
|
2683
|
+
let dbMask = null;
|
|
2684
|
+
let seedMask = null;
|
|
2685
|
+
try {
|
|
2686
|
+
db.set(await digest(subtle, hash, new Uint8Array(0)));
|
|
2687
|
+
db[db.length - message.length - 1] = 1;
|
|
2688
|
+
db.set(message, db.length - message.length);
|
|
2689
|
+
if (seed !== void 0) {
|
|
2690
|
+
if (!isBytes(seed) || seed.length !== hLen)
|
|
2691
|
+
throw refuse("random");
|
|
2692
|
+
seedBytes = Uint8Array.from(seed);
|
|
2693
|
+
}
|
|
2694
|
+
else {
|
|
2695
|
+
seedBytes = randomBytes(crypto, hLen);
|
|
2696
|
+
}
|
|
2697
|
+
dbMask = await mgf1(subtle, maskHash, seedBytes, db.length);
|
|
2698
|
+
for (let i = 0; i < db.length; i += 1)
|
|
2699
|
+
db[i] ^= dbMask[i];
|
|
2700
|
+
seedMask = await mgf1(subtle, maskHash, db, hLen);
|
|
2701
|
+
for (let i = 0; i < hLen; i += 1)
|
|
2702
|
+
seedBytes[i] ^= seedMask[i];
|
|
2703
|
+
em.set(seedBytes, 1);
|
|
2704
|
+
em.set(db, 1 + hLen);
|
|
2705
|
+
return toBytes(modPow(toBigInt(em), 65537n, n), size);
|
|
2706
|
+
}
|
|
2707
|
+
finally {
|
|
2708
|
+
db.fill(0);
|
|
2709
|
+
em.fill(0);
|
|
2710
|
+
if (seedBytes)
|
|
2711
|
+
seedBytes.fill(0);
|
|
2712
|
+
if (dbMask)
|
|
2713
|
+
dbMask.fill(0);
|
|
2714
|
+
if (seedMask)
|
|
2715
|
+
seedMask.fill(0);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function merchantJweHeader(spec, kid) {
|
|
2719
|
+
return JSON.stringify({ alg: spec.alg, enc: spec.enc, kid, ...spec.header });
|
|
2720
|
+
}
|
|
2721
|
+
async function aesCbcHmacSeal(subtle, contentKey, iv, text, aad) {
|
|
2722
|
+
let macKey;
|
|
2723
|
+
let encKey;
|
|
2724
|
+
try {
|
|
2725
|
+
macKey = await subtle.importKey("raw", contentKey.subarray(0, 32), { name: "HMAC", hash: "SHA-512" }, false, ["sign"]);
|
|
2726
|
+
encKey = await subtle.importKey("raw", contentKey.subarray(32, 64), { name: "AES-CBC" }, false, ["encrypt"]);
|
|
2727
|
+
}
|
|
2728
|
+
catch {
|
|
2729
|
+
throw refuse("crypto");
|
|
2730
|
+
}
|
|
2731
|
+
let ciphertext;
|
|
2732
|
+
try {
|
|
2733
|
+
ciphertext = new Uint8Array(await subtle.encrypt({ name: "AES-CBC", iv }, encKey, text));
|
|
2734
|
+
}
|
|
2735
|
+
catch {
|
|
2736
|
+
throw refuse("crypto");
|
|
2737
|
+
}
|
|
2738
|
+
const al = new Uint8Array(8);
|
|
2739
|
+
const bits = aad.length * 8;
|
|
2740
|
+
new DataView(al.buffer).setUint32(0, Math.floor(bits / 4294967296));
|
|
2741
|
+
new DataView(al.buffer).setUint32(4, bits >>> 0);
|
|
2742
|
+
const macInput = new Uint8Array(aad.length + iv.length + ciphertext.length + 8);
|
|
2743
|
+
macInput.set(aad, 0);
|
|
2744
|
+
macInput.set(iv, aad.length);
|
|
2745
|
+
macInput.set(ciphertext, aad.length + iv.length);
|
|
2746
|
+
macInput.set(al, aad.length + iv.length + ciphertext.length);
|
|
2747
|
+
let mac;
|
|
2748
|
+
try {
|
|
2749
|
+
mac = new Uint8Array(await subtle.sign("HMAC", macKey, macInput));
|
|
2750
|
+
}
|
|
2751
|
+
catch {
|
|
2752
|
+
throw refuse("crypto");
|
|
2753
|
+
}
|
|
2754
|
+
const tag = mac.slice(0, 32);
|
|
2755
|
+
mac.fill(0);
|
|
2756
|
+
return { ciphertext, tag };
|
|
2757
|
+
}
|
|
2758
|
+
async function aesGcmSeal(subtle, contentKey, iv, text, aad) {
|
|
2759
|
+
try {
|
|
2760
|
+
const key = await subtle.importKey("raw", contentKey, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
2761
|
+
const sealed = new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 128 }, key, text));
|
|
2762
|
+
return { ciphertext: sealed.slice(0, sealed.length - 16), tag: sealed.slice(sealed.length - 16) };
|
|
2763
|
+
}
|
|
2764
|
+
catch {
|
|
2765
|
+
throw refuse("crypto");
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
async function jweCompactEncrypt(key, text, spec, kid, crypto) {
|
|
2769
|
+
const subtle = subtleOf(crypto);
|
|
2770
|
+
const hash = MERCHANT_KEY_JWE_ALGS[spec && spec.alg];
|
|
2771
|
+
const enc = MERCHANT_KEY_JWE_ENCS[spec && spec.enc];
|
|
2772
|
+
if (!hash || !enc || typeof kid !== "string" || kid === "")
|
|
2773
|
+
throw refuse("key");
|
|
2774
|
+
if (!isBytes(text))
|
|
2775
|
+
throw refuse("text");
|
|
2776
|
+
const header = encodeBase64(utf8Bytes(merchantJweHeader(spec, kid)), true);
|
|
2777
|
+
const aad = utf8Bytes(header);
|
|
2778
|
+
const contentKey = randomBytes(crypto, enc.key);
|
|
2779
|
+
try {
|
|
2780
|
+
const wrapped = await rsaOaepEncrypt(key, contentKey, { hash, mgf1: hash }, crypto);
|
|
2781
|
+
const iv = randomBytes(crypto, enc.iv);
|
|
2782
|
+
const { ciphertext, tag } = spec.enc === "A256GCM" ? await aesGcmSeal(subtle, contentKey, iv, text, aad) : await aesCbcHmacSeal(subtle, contentKey, iv, text, aad);
|
|
2783
|
+
return [header, encodeBase64(wrapped, true), encodeBase64(iv, true), encodeBase64(ciphertext, true), encodeBase64(tag, true)].join(".");
|
|
2784
|
+
}
|
|
2785
|
+
finally {
|
|
2786
|
+
contentKey.fill(0);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function readCompactJwe(value) {
|
|
2790
|
+
if (typeof value !== "string" || value.length > MAX_ENCRYPTED_LENGTH)
|
|
2791
|
+
return null;
|
|
2792
|
+
const parts = value.split(".");
|
|
2793
|
+
if (parts.length !== 5)
|
|
2794
|
+
return null;
|
|
2795
|
+
const bytes = parts.map((part) => decodeBase64(part, true));
|
|
2796
|
+
if (bytes.some((part) => part === null))
|
|
2797
|
+
return null;
|
|
2798
|
+
let headerText;
|
|
2799
|
+
let header;
|
|
2800
|
+
try {
|
|
2801
|
+
headerText = new TextDecoder("utf-8", { fatal: true }).decode(bytes[0]);
|
|
2802
|
+
header = JSON.parse(headerText);
|
|
2803
|
+
}
|
|
2804
|
+
catch {
|
|
2805
|
+
return null;
|
|
2806
|
+
}
|
|
2807
|
+
if (!header || typeof header !== "object" || Array.isArray(header) || JSON.stringify(header) !== headerText)
|
|
2808
|
+
return null;
|
|
2809
|
+
return { header, headerText, encryptedKey: bytes[1], iv: bytes[2], ciphertext: bytes[3], tag: bytes[4] };
|
|
2810
|
+
}
|
|
2811
|
+
function allowedSizes(merchantKey) {
|
|
2812
|
+
const sizes = [];
|
|
2813
|
+
for (const key of merchantKey.keys) {
|
|
2814
|
+
try {
|
|
2815
|
+
sizes.push(parseMerchantKey(key).size);
|
|
2816
|
+
}
|
|
2817
|
+
catch {
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
return merchantKey.keys.length ? sizes : MERCHANT_KEY_SIZES;
|
|
2821
|
+
}
|
|
2822
|
+
function encryptedValueFits(merchantKey, value) {
|
|
2823
|
+
if (!merchantKey || typeof value !== "string" || value.length > MAX_ENCRYPTED_LENGTH)
|
|
2824
|
+
return false;
|
|
2825
|
+
const sizes = allowedSizes(merchantKey);
|
|
2826
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
2827
|
+
const bytes = decodeBase64(value);
|
|
2828
|
+
return !!bytes && sizes.includes(bytes.length);
|
|
2829
|
+
}
|
|
2830
|
+
if (merchantKey.scheme !== "jwe")
|
|
2831
|
+
return false;
|
|
2832
|
+
const jwe = readCompactJwe(value);
|
|
2833
|
+
const enc = MERCHANT_KEY_JWE_ENCS[merchantKey.enc];
|
|
2834
|
+
if (!jwe || !enc)
|
|
2835
|
+
return false;
|
|
2836
|
+
const { header } = jwe;
|
|
2837
|
+
const kids = merchantKey.keys.map((key) => key.kid);
|
|
2838
|
+
const alg = merchantKey.alg === null ? header.alg : merchantKey.alg;
|
|
2839
|
+
if (!Object.prototype.hasOwnProperty.call(MERCHANT_KEY_JWE_ALGS, alg) || typeof header.kid !== "string" || header.kid === "" || kids.length && !kids.includes(header.kid))
|
|
2840
|
+
return false;
|
|
2841
|
+
if (jwe.headerText !== merchantJweHeader({ ...merchantKey, alg }, header.kid))
|
|
2842
|
+
return false;
|
|
2843
|
+
if (!sizes.includes(jwe.encryptedKey.length) || jwe.iv.length !== enc.iv || jwe.tag.length !== enc.tag || jwe.ciphertext.length === 0)
|
|
2844
|
+
return false;
|
|
2845
|
+
return merchantKey.enc === "A256GCM" || jwe.ciphertext.length % 16 === 0;
|
|
2846
|
+
}
|
|
2847
|
+
// src/merchant-card-endpoint-facts.js
|
|
2848
|
+
var TWO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["usd", "eur", "gbp", "cad", "aud"]);
|
|
2849
|
+
var MERCHANT_CARD_ENDPOINT_FACTS_TEXT_MAX = 1024 * 1024;
|
|
2850
|
+
var MAX_DEPTH = 8;
|
|
2851
|
+
var MAX_ITEMS = 256;
|
|
2852
|
+
var ROUNDING_MARGIN = 1;
|
|
2853
|
+
var fail = (refusal) => ({ ok: false, refusal });
|
|
2854
|
+
var VIEWDATA = /<script\b[^>]*\bid\s*=\s*["']viewdata["'][^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
2855
|
+
var ANGULAR_PREFIX = /^\)\]\}',?\n/;
|
|
2856
|
+
function parseJson(text) {
|
|
2857
|
+
try {
|
|
2858
|
+
return JSON.parse(text);
|
|
2859
|
+
}
|
|
2860
|
+
catch {
|
|
2861
|
+
return void 0;
|
|
391
2862
|
}
|
|
392
|
-
]);
|
|
393
|
-
function wireProjection(profiles) {
|
|
394
|
-
return deepFreeze(profiles.map(({ evidence, bootstrap, timeout_ms, ...profile }) => ({ ...profile })));
|
|
395
2863
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
2864
|
+
function fastspringState(format, text) {
|
|
2865
|
+
if (format === "html") {
|
|
2866
|
+
const scripts = [...text.matchAll(VIEWDATA)];
|
|
2867
|
+
if (scripts.length !== 1)
|
|
2868
|
+
return { refusal: "the checkout page does not hold exactly one order record" };
|
|
2869
|
+
const state2 = parseJson(scripts[0][1]);
|
|
2870
|
+
return isPlainObject(state2) ? { state: state2 } : { refusal: "the checkout page's order record is not a JSON object" };
|
|
2871
|
+
}
|
|
2872
|
+
const state = parseJson(text.replace(ANGULAR_PREFIX, ""));
|
|
2873
|
+
if (!isPlainObject(state))
|
|
2874
|
+
return { none: true };
|
|
2875
|
+
return isPlainObject(state.order) && isPlainObject(state.session) ? { state } : { none: true };
|
|
398
2876
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
2877
|
+
function selectedRenewals(groups, depth, counter) {
|
|
2878
|
+
if (!Array.isArray(groups) || depth > MAX_DEPTH)
|
|
2879
|
+
return null;
|
|
2880
|
+
const out = [];
|
|
2881
|
+
for (const group of groups) {
|
|
2882
|
+
if (!isPlainObject(group) || group.items !== void 0 && !Array.isArray(group.items))
|
|
2883
|
+
return null;
|
|
2884
|
+
for (const item of group.items ?? []) {
|
|
2885
|
+
counter.items += 1;
|
|
2886
|
+
if (!isPlainObject(item) || counter.items > MAX_ITEMS)
|
|
2887
|
+
return null;
|
|
2888
|
+
const nested = selectedRenewals(item.groups ?? [], depth + 1, counter);
|
|
2889
|
+
if (nested === null)
|
|
2890
|
+
return null;
|
|
2891
|
+
out.push({ selected: item.selected === true, future: item.future ? true : null, groups: nested });
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
return [{ items: out }];
|
|
403
2895
|
}
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
|
|
408
|
-
|
|
2896
|
+
function fastspringProjection(state) {
|
|
2897
|
+
const order = state.order;
|
|
2898
|
+
if (!isPlainObject(order))
|
|
2899
|
+
return null;
|
|
2900
|
+
const groups = selectedRenewals(order.groups ?? [], 0, { items: 0 });
|
|
2901
|
+
if (groups === null)
|
|
2902
|
+
return null;
|
|
2903
|
+
return {
|
|
2904
|
+
order: { totalWithTaxValue: order.totalWithTaxValue, currency: order.currency, groups },
|
|
2905
|
+
production: state.production,
|
|
2906
|
+
hasRecurring: state.hasRecurring === void 0 ? false : state.hasRecurring,
|
|
2907
|
+
forceFuture: state.forceFuture === void 0 ? false : state.forceFuture
|
|
2908
|
+
};
|
|
409
2909
|
}
|
|
410
|
-
function
|
|
411
|
-
return
|
|
2910
|
+
function anySelectedRenewal(groups) {
|
|
2911
|
+
return groups.some((group) => group.items.some((item) => item.selected && item.future === true || anySelectedRenewal(item.groups)));
|
|
412
2912
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
2913
|
+
function minorUnits(value) {
|
|
2914
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
2915
|
+
return null;
|
|
2916
|
+
const match = /^(\d{1,12})(?:\.(\d{1,2}))?$/.exec(String(value));
|
|
2917
|
+
return match ? Number(match[1]) * 100 + Number((match[2] ?? "").padEnd(2, "0")) : null;
|
|
2918
|
+
}
|
|
2919
|
+
function fastspringFacts(state, merchant) {
|
|
2920
|
+
const unreadable = `${merchant}'s order record is not in the layout Agentcard reviewed`;
|
|
2921
|
+
const projection = isPlainObject(state) ? fastspringProjection(state) : null;
|
|
2922
|
+
if (!projection || JSON.stringify(projection) !== JSON.stringify(state))
|
|
2923
|
+
return fail(unreadable);
|
|
2924
|
+
const { totalWithTaxValue, currency, groups } = projection.order;
|
|
2925
|
+
if (typeof currency !== "string" || !/^[A-Z]{3}$/.test(currency))
|
|
2926
|
+
return fail(unreadable);
|
|
2927
|
+
if (!TWO_DECIMAL_CURRENCIES.has(currency.toLowerCase()))
|
|
2928
|
+
return fail(`${merchant}'s order is in a currency this build does not read`);
|
|
2929
|
+
const amountCents = minorUnits(totalWithTaxValue);
|
|
2930
|
+
if (amountCents === null)
|
|
2931
|
+
return fail(`${merchant}'s order total is not a whole number of cents`);
|
|
2932
|
+
if (typeof projection.production !== "boolean" || typeof projection.hasRecurring !== "boolean" || typeof projection.forceFuture !== "boolean") {
|
|
2933
|
+
return fail(unreadable);
|
|
2934
|
+
}
|
|
2935
|
+
const recurring = projection.hasRecurring || projection.forceFuture || anySelectedRenewal(groups);
|
|
2936
|
+
return { ok: true, facts: { amount: { amountCents, currency: currency.toLowerCase() }, live: projection.production, recurring } };
|
|
2937
|
+
}
|
|
2938
|
+
var READERS = /* @__PURE__ */ Object.freeze({
|
|
2939
|
+
fastspring_session: Object.freeze({ state: fastspringState, project: fastspringProjection, facts: fastspringFacts })
|
|
2940
|
+
});
|
|
2941
|
+
var MERCHANT_CARD_ENDPOINT_FACT_READERS = /* @__PURE__ */ Object.freeze(Object.keys(READERS));
|
|
2942
|
+
function cardPlaceholders(p, cardUrl) {
|
|
2943
|
+
const match = urlTemplateMatch({ path: p.path, query: p.query }, cardUrl);
|
|
2944
|
+
return match && match.origin === p.endpoint ? match.placeholders : null;
|
|
2945
|
+
}
|
|
2946
|
+
function boundShape(p, card, shapes, exchange) {
|
|
2947
|
+
const bind = p.facts.bind;
|
|
2948
|
+
for (const shape of shapes) {
|
|
2949
|
+
if (shape.method !== exchange.method.toUpperCase())
|
|
2950
|
+
continue;
|
|
2951
|
+
const match = urlTemplateMatch(shape, exchange.url);
|
|
2952
|
+
if (match && typeof card[bind] === "string" && match.placeholders[bind] === card[bind])
|
|
2953
|
+
return shape;
|
|
2954
|
+
}
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
function shapeFor(p, cardUrl, exchange) {
|
|
2958
|
+
if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
|
|
2959
|
+
return null;
|
|
2960
|
+
const card = cardPlaceholders(p, cardUrl);
|
|
2961
|
+
const url = strictUrl(exchange.url);
|
|
2962
|
+
if (!card || !url || url.origin !== p.endpoint)
|
|
2963
|
+
return null;
|
|
2964
|
+
return boundShape(p, card, p.facts.responses, exchange);
|
|
2965
|
+
}
|
|
2966
|
+
function sessionPath(p, card) {
|
|
2967
|
+
const segments = p.path.split("/");
|
|
2968
|
+
const at = segments.indexOf(`:${p.facts.bind}`);
|
|
2969
|
+
if (at < 1)
|
|
2970
|
+
return null;
|
|
2971
|
+
const written = segments.slice(0, at + 1).map((segment) => segment.startsWith(":") ? card[segment.slice(1)] : segment);
|
|
2972
|
+
return written.every((segment) => typeof segment === "string") ? written.join("/") : null;
|
|
2973
|
+
}
|
|
2974
|
+
function merchantCardEndpointFactsRole(profile, cardUrl, exchange) {
|
|
2975
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
2976
|
+
if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
|
|
2977
|
+
return null;
|
|
2978
|
+
const card = cardPlaceholders(p, cardUrl);
|
|
2979
|
+
const session = card ? sessionPath(p, card) : null;
|
|
2980
|
+
let url;
|
|
2981
|
+
try {
|
|
2982
|
+
url = new URL(exchange.url);
|
|
2983
|
+
}
|
|
2984
|
+
catch {
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2987
|
+
if (!session || url.origin !== p.endpoint || url.pathname !== session && !url.pathname.startsWith(`${session}/`))
|
|
2988
|
+
return null;
|
|
2989
|
+
if (exchange.method.toUpperCase() === p.method && exchange.url === cardUrl)
|
|
2990
|
+
return "card";
|
|
2991
|
+
if (shapeFor(p, cardUrl, exchange))
|
|
2992
|
+
return "record";
|
|
2993
|
+
if (boundShape(p, card, p.facts.inert, exchange))
|
|
2994
|
+
return "inert";
|
|
2995
|
+
return "unread";
|
|
2996
|
+
}
|
|
2997
|
+
function projectMerchantCardEndpointFacts(profile, cardUrl, exchange) {
|
|
2998
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
2999
|
+
const shape = shapeFor(p, cardUrl, exchange);
|
|
3000
|
+
if (!shape)
|
|
3001
|
+
return { refusal: `the response is not ${p.merchant}'s record of this checkout` };
|
|
3002
|
+
if (!Number.isInteger(exchange.status) || exchange.status < 200 || exchange.status > 299)
|
|
3003
|
+
return { refusal: `${p.merchant}'s record of this checkout did not load` };
|
|
3004
|
+
if (typeof exchange.body !== "string" || exchange.body.length > MERCHANT_CARD_ENDPOINT_FACTS_TEXT_MAX)
|
|
3005
|
+
return { refusal: `${p.merchant}'s record of this checkout could not be read` };
|
|
3006
|
+
const reader = READERS[p.facts.reader];
|
|
3007
|
+
const read = reader.state(shape.format, exchange.body);
|
|
3008
|
+
if (read.none)
|
|
3009
|
+
return { none: true };
|
|
3010
|
+
if (read.refusal)
|
|
3011
|
+
return { refusal: read.refusal };
|
|
3012
|
+
const state = reader.project(read.state);
|
|
3013
|
+
if (!state)
|
|
3014
|
+
return { refusal: `${p.merchant}'s order record is not in the layout Agentcard reviewed` };
|
|
3015
|
+
return { record: { method: shape.method, url: exchange.url, state } };
|
|
3016
|
+
}
|
|
3017
|
+
function readMerchantCardEndpointFacts(profile, cardUrl, record) {
|
|
3018
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3019
|
+
if (!p.facts)
|
|
3020
|
+
return fail(`Agentcard reads no order record for ${p.merchant}`);
|
|
3021
|
+
if (!isPlainObject(record) || Object.keys(record).sort().join(",") !== "method,state,url")
|
|
3022
|
+
return fail(`the order record for ${p.merchant} is malformed`);
|
|
3023
|
+
if (!shapeFor(p, cardUrl, record))
|
|
3024
|
+
return fail(`the order record is not ${p.merchant}'s record of this checkout`);
|
|
3025
|
+
return READERS[p.facts.reader].facts(record.state, p.merchant);
|
|
3026
|
+
}
|
|
3027
|
+
function merchantCardEndpointFactsRefusal(profile, facts, agent) {
|
|
3028
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3029
|
+
const refuse2 = (code, reason) => ({ code, reason });
|
|
3030
|
+
if (!isPlainObject(facts) || !isPlainObject(facts.amount) || !isPlainObject(agent))
|
|
3031
|
+
return refuse2("malformed", `the order record for ${p.merchant} is malformed`);
|
|
3032
|
+
if (facts.recurring !== false)
|
|
3033
|
+
return refuse2("recurring", `${p.merchant}'s order renews on a schedule, and each renewal would charge the card with no approval of its own`);
|
|
3034
|
+
if (facts.live !== agent.live)
|
|
3035
|
+
return agent.live ? refuse2("test_checkout", `${p.merchant}'s checkout is in test mode`) : refuse2("live_checkout", `${p.merchant}'s checkout is live`);
|
|
3036
|
+
if (typeof agent.currency !== "string" || facts.amount.currency !== agent.currency.toLowerCase()) {
|
|
3037
|
+
return refuse2("currency", `${p.merchant}'s order is in another currency than the one approved`);
|
|
3038
|
+
}
|
|
3039
|
+
if (!Number.isSafeInteger(agent.amountCents) || Math.abs(facts.amount.amountCents - agent.amountCents) > ROUNDING_MARGIN) {
|
|
3040
|
+
return refuse2("amount", `${p.merchant}'s own total is not the amount the cardholder would be asked to approve`);
|
|
3041
|
+
}
|
|
3042
|
+
return null;
|
|
416
3043
|
}
|
|
417
3044
|
// src/merchant-card-endpoints.js
|
|
418
3045
|
var MERCHANT_CARD_ENDPOINT_LIMITS = /* @__PURE__ */ Object.freeze({
|
|
@@ -428,7 +3055,9 @@ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
|
428
3055
|
var COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,256}$/;
|
|
429
3056
|
var COOKIE_VALUE = /^(?:[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*|"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*")$/;
|
|
430
3057
|
var BOUNDARY = /^[A-Za-z0-9'()+_,\-./:=?]{1,70}$/;
|
|
431
|
-
var BUNDLE_MEMBERS = /* @__PURE__ */ new Set(["url", "page_origin", "headers", "cookies", "referer", "body"]);
|
|
3058
|
+
var BUNDLE_MEMBERS = /* @__PURE__ */ new Set(["url", "page_origin", "navigation", "headers", "cookies", "referer", "body"]);
|
|
3059
|
+
var SHAPES = /* @__PURE__ */ Object.freeze({ guid: /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/ });
|
|
3060
|
+
var NAVIGATION_FORM_TYPE = "application/x-www-form-urlencoded";
|
|
432
3061
|
function utf8Length(text) {
|
|
433
3062
|
let bytes = 0;
|
|
434
3063
|
for (let i = 0; i < text.length; i += 1) {
|
|
@@ -522,12 +3151,19 @@ function multipartBoundary(contentType) {
|
|
|
522
3151
|
const boundary = match ? match[1] ?? match[2] : null;
|
|
523
3152
|
return boundary && BOUNDARY.test(boundary) ? boundary : null;
|
|
524
3153
|
}
|
|
3154
|
+
function jsonBody(p) {
|
|
3155
|
+
return p.encoding === "json" || p.encoding === "json_api";
|
|
3156
|
+
}
|
|
525
3157
|
function contentTypeRefusal(p, contentType) {
|
|
526
3158
|
if (typeof contentType !== "string")
|
|
527
3159
|
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;
|
|
3160
|
+
const ok = p.encoding === "json" ? /^application\/json(?:;\s*charset=utf-8)?$/i.test(contentType) : p.encoding === "json_api" ? /^application\/vnd\.api\+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
3161
|
return ok ? null : `the card request to ${p.merchant} is not sent as ${p.encoding}`;
|
|
530
3162
|
}
|
|
3163
|
+
function merchantCardEndpointRequestOrigin(profile) {
|
|
3164
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3165
|
+
return p.frame ?? p.checkout;
|
|
3166
|
+
}
|
|
531
3167
|
function forbiddenKeyIn(value, depth = 0) {
|
|
532
3168
|
if (depth > 64)
|
|
533
3169
|
return true;
|
|
@@ -608,8 +3244,76 @@ function parseMultipart(body, boundary) {
|
|
|
608
3244
|
function sameName(name, wanted) {
|
|
609
3245
|
return !FORBIDDEN_KEYS.has(name) && (name.toLowerCase() === wanted.toLowerCase() || name.toUpperCase() === wanted.toUpperCase());
|
|
610
3246
|
}
|
|
3247
|
+
function isSelector(step) {
|
|
3248
|
+
if (!isPlainObject(step))
|
|
3249
|
+
return false;
|
|
3250
|
+
const keys = Object.keys(step);
|
|
3251
|
+
return keys.length === 1 && !FORBIDDEN_KEYS.has(keys[0]) && typeof step[keys[0]] === "string" && step[keys[0]] !== "";
|
|
3252
|
+
}
|
|
3253
|
+
var hasSelector = (at) => Array.isArray(at) && at.some(isSelector);
|
|
3254
|
+
function selectAt(root, at) {
|
|
3255
|
+
let node = root;
|
|
3256
|
+
const path = [];
|
|
3257
|
+
for (const step of at) {
|
|
3258
|
+
if (isSelector(step)) {
|
|
3259
|
+
const [[name, wanted]] = Object.entries(step);
|
|
3260
|
+
if (!Array.isArray(node))
|
|
3261
|
+
return { found: false };
|
|
3262
|
+
const hits = [];
|
|
3263
|
+
node.forEach((item, index) => {
|
|
3264
|
+
if (isPlainObject(item) && Object.prototype.hasOwnProperty.call(item, name) && item[name] === wanted)
|
|
3265
|
+
hits.push(index);
|
|
3266
|
+
});
|
|
3267
|
+
if (hits.length !== 1)
|
|
3268
|
+
return { found: false };
|
|
3269
|
+
path.push(hits[0]);
|
|
3270
|
+
node = node[hits[0]];
|
|
3271
|
+
}
|
|
3272
|
+
else {
|
|
3273
|
+
const next = jsonAt(node, [step]);
|
|
3274
|
+
if (!next.found)
|
|
3275
|
+
return { found: false };
|
|
3276
|
+
path.push(step);
|
|
3277
|
+
node = next.value;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return { found: true, value: node, path };
|
|
3281
|
+
}
|
|
3282
|
+
function selectVariants(root, at) {
|
|
3283
|
+
let nodes = [root];
|
|
3284
|
+
for (const step of at) {
|
|
3285
|
+
if (!isSelector(step)) {
|
|
3286
|
+
nodes = nodes.flatMap((node) => valuesIgnoringCase(node, [step]));
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
const [[name, wanted]] = Object.entries(step);
|
|
3290
|
+
const next = [];
|
|
3291
|
+
for (const node of nodes) {
|
|
3292
|
+
if (!Array.isArray(node))
|
|
3293
|
+
continue;
|
|
3294
|
+
for (const item of node) {
|
|
3295
|
+
if (isPlainObject(item) && Object.keys(item).some((key) => sameName(key, name) && typeof item[key] === "string" && (item[key].toLowerCase() === wanted.toLowerCase() || item[key].toUpperCase() === wanted.toUpperCase())))
|
|
3296
|
+
next.push(item);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
nodes = next;
|
|
3300
|
+
}
|
|
3301
|
+
return nodes;
|
|
3302
|
+
}
|
|
3303
|
+
function describeAt(at) {
|
|
3304
|
+
if (!hasSelector(at))
|
|
3305
|
+
return describePath(at);
|
|
3306
|
+
return at.map((step) => isSelector(step) ? `[${Object.keys(step)[0]}=${Object.values(step)[0]}]` : typeof step === "number" ? `[${step}]` : `.${step}`).join("").replace(/^\./, "");
|
|
3307
|
+
}
|
|
611
3308
|
function jsonReader(parsed) {
|
|
612
|
-
return {
|
|
3309
|
+
return {
|
|
3310
|
+
parsed,
|
|
3311
|
+
exact: (at) => hasSelector(at) ? selectAt(parsed, at) : jsonAt(parsed, at),
|
|
3312
|
+
variants: (at) => hasSelector(at) ? selectVariants(parsed, at) : valuesIgnoringCase(parsed, at),
|
|
3313
|
+
label: describeAt,
|
|
3314
|
+
prefixed: (prefix) => Object.keys(parsed).filter((key) => startsWithIgnoringCase(key, prefix)).map((key) => parsed[key]),
|
|
3315
|
+
scalars: () => jsonScalars(parsed)
|
|
3316
|
+
};
|
|
613
3317
|
}
|
|
614
3318
|
function fieldReader(fields) {
|
|
615
3319
|
return {
|
|
@@ -619,9 +3323,27 @@ function fieldReader(fields) {
|
|
|
619
3323
|
return hits.length === 1 ? { found: true, value: hits[0].value, field: hits[0] } : { found: false };
|
|
620
3324
|
},
|
|
621
3325
|
variants: (at) => fields.filter((field) => sameName(field.name, at)).map((field) => field.value),
|
|
622
|
-
label: (at) => at
|
|
3326
|
+
label: (at) => at,
|
|
3327
|
+
prefixed: (prefix) => fields.filter((field) => startsWithIgnoringCase(field.name, prefix)).map((field) => field.value),
|
|
3328
|
+
scalars: () => fields.map((field) => field.value)
|
|
623
3329
|
};
|
|
624
3330
|
}
|
|
3331
|
+
function startsWithIgnoringCase(name, prefix) {
|
|
3332
|
+
return !FORBIDDEN_KEYS.has(name) && name.length >= prefix.length && sameName(name.slice(0, prefix.length), prefix);
|
|
3333
|
+
}
|
|
3334
|
+
function jsonScalars(value, out = [], depth = 0) {
|
|
3335
|
+
if (depth > 64)
|
|
3336
|
+
return out;
|
|
3337
|
+
if (typeof value === "string" || typeof value === "number")
|
|
3338
|
+
out.push(value);
|
|
3339
|
+
else if (Array.isArray(value))
|
|
3340
|
+
for (const item of value)
|
|
3341
|
+
jsonScalars(item, out, depth + 1);
|
|
3342
|
+
else if (isPlainObject(value))
|
|
3343
|
+
for (const key of Object.keys(value))
|
|
3344
|
+
jsonScalars(value[key], out, depth + 1);
|
|
3345
|
+
return out;
|
|
3346
|
+
}
|
|
625
3347
|
function readBody(p, body, contentType) {
|
|
626
3348
|
if (typeof body !== "string")
|
|
627
3349
|
return { refusal: "the paused body is not text" };
|
|
@@ -630,7 +3352,7 @@ function readBody(p, body, contentType) {
|
|
|
630
3352
|
const typeRefusal = contentTypeRefusal(p, contentType);
|
|
631
3353
|
if (typeRefusal)
|
|
632
3354
|
return { refusal: typeRefusal };
|
|
633
|
-
if (p
|
|
3355
|
+
if (jsonBody(p)) {
|
|
634
3356
|
let parsed2;
|
|
635
3357
|
try {
|
|
636
3358
|
parsed2 = JSON.parse(body);
|
|
@@ -658,28 +3380,89 @@ var SLOT_NAMES = /* @__PURE__ */ Object.freeze({
|
|
|
658
3380
|
cvv: "the security code",
|
|
659
3381
|
exp_month: "the expiry month",
|
|
660
3382
|
exp_year: "the expiry year",
|
|
3383
|
+
expiry: "the expiry date",
|
|
3384
|
+
last4: "the card number's last four digits",
|
|
3385
|
+
leading: "the card number's leading digits",
|
|
661
3386
|
brand: "the card network code"
|
|
662
3387
|
});
|
|
3388
|
+
var EXPIRY_FORMATS = /* @__PURE__ */ Object.freeze({
|
|
3389
|
+
mmyy: /^(?:0[1-9]|1[0-2])\d{2}$/,
|
|
3390
|
+
"mm/yy": /^(?:0[1-9]|1[0-2])\/\d{2}$/,
|
|
3391
|
+
"mm/yyyy": /^(?:0[1-9]|1[0-2])\/\d{4}$/,
|
|
3392
|
+
"yyyy-mm": /^\d{4}-(?:0[1-9]|1[0-2])$/,
|
|
3393
|
+
"MM/YY": /^(?:0[1-9]|1[0-2])\/\d{2}$/,
|
|
3394
|
+
"MM-YY": /^(?:0[1-9]|1[0-2])-\d{2}$/,
|
|
3395
|
+
MMYYYY: /^(?:0[1-9]|1[0-2])\d{4}$/
|
|
3396
|
+
});
|
|
3397
|
+
var AS_TYPED_EXPIRY_FORMATS = /* @__PURE__ */ Object.freeze(["mmyy", "mm/yy", "mm/yyyy", "yyyy-mm"]);
|
|
3398
|
+
function expiryForm(spec, value) {
|
|
3399
|
+
if (typeof value !== "string")
|
|
3400
|
+
return null;
|
|
3401
|
+
const forms = spec.format === "as_typed" ? AS_TYPED_EXPIRY_FORMATS : [spec.format];
|
|
3402
|
+
return forms.find((form) => Object.prototype.hasOwnProperty.call(EXPIRY_FORMATS, form) && EXPIRY_FORMATS[form].test(value)) ?? null;
|
|
3403
|
+
}
|
|
3404
|
+
function expiryText(form, facts) {
|
|
3405
|
+
const mm = String(facts.month).padStart(2, "0");
|
|
3406
|
+
const yy = String(facts.year % 100).padStart(2, "0");
|
|
3407
|
+
if (form === "mmyy")
|
|
3408
|
+
return `${mm}${yy}`;
|
|
3409
|
+
if (form === "mm/yy" || form === "MM/YY")
|
|
3410
|
+
return `${mm}/${yy}`;
|
|
3411
|
+
if (form === "MM-YY")
|
|
3412
|
+
return `${mm}-${yy}`;
|
|
3413
|
+
if (form === "mm/yyyy")
|
|
3414
|
+
return `${mm}/${facts.year}`;
|
|
3415
|
+
if (form === "MMYYYY")
|
|
3416
|
+
return `${mm}${facts.year}`;
|
|
3417
|
+
return `${facts.year}-${mm}`;
|
|
3418
|
+
}
|
|
663
3419
|
function cardSlots(p) {
|
|
3420
|
+
const c = p.card;
|
|
3421
|
+
const expiry = c.expiry ? [["expiry", c.expiry]] : [["exp_month", c.exp_month], ["exp_year", c.exp_year]];
|
|
664
3422
|
return [
|
|
665
|
-
["pan",
|
|
666
|
-
["cvv",
|
|
667
|
-
|
|
668
|
-
["
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
3423
|
+
["pan", c.pan],
|
|
3424
|
+
["cvv", c.cvv],
|
|
3425
|
+
...expiry,
|
|
3426
|
+
["last4", c.last4],
|
|
3427
|
+
["leading", c.leading],
|
|
3428
|
+
...c.brands.map((brand) => ["brand", brand])
|
|
3429
|
+
].filter(([, spec]) => spec);
|
|
3430
|
+
}
|
|
3431
|
+
function codeNotAsked(reader, spec, variants) {
|
|
3432
|
+
if (spec.format !== "digits_if_asked")
|
|
3433
|
+
return false;
|
|
3434
|
+
if (variants.length === 0)
|
|
3435
|
+
return true;
|
|
3436
|
+
const at = reader.exact(spec.at);
|
|
3437
|
+
return variants.length === 1 && at.found && at.value === "";
|
|
3438
|
+
}
|
|
3439
|
+
function groupedFour(digits) {
|
|
3440
|
+
return digits.replace(/(.{4})/g, "$1 ").trim();
|
|
3441
|
+
}
|
|
3442
|
+
function panDigits(spec, value) {
|
|
3443
|
+
return spec.format === "grouped4" && typeof value === "string" ? value.replace(/ /g, "") : value;
|
|
3444
|
+
}
|
|
3445
|
+
function placeholderFits(p, slot, spec, value) {
|
|
3446
|
+
if (spec.format === "encrypted")
|
|
3447
|
+
return encryptedValueFits(p.merchant_key, value);
|
|
673
3448
|
const text = typeof value === "string";
|
|
674
3449
|
const int = Number.isInteger(value);
|
|
675
3450
|
switch (slot) {
|
|
676
|
-
case "
|
|
677
|
-
return text && /^\d
|
|
3451
|
+
case "leading":
|
|
3452
|
+
return text && value.length === spec.digits && /^\d+$/.test(value);
|
|
3453
|
+
case "pan": {
|
|
3454
|
+
if (!text)
|
|
3455
|
+
return false;
|
|
3456
|
+
const digits = panDigits(spec, value);
|
|
3457
|
+
return /^\d{12,19}$/.test(digits) && (digits === value || value === groupedFour(digits));
|
|
3458
|
+
}
|
|
678
3459
|
case "cvv":
|
|
679
3460
|
return text && /^\d{3,4}$/.test(value);
|
|
680
3461
|
case "exp_month":
|
|
681
3462
|
if (spec.format === "mm")
|
|
682
3463
|
return text && /^(?:0[1-9]|1[0-2])$/.test(value);
|
|
3464
|
+
if (spec.format === "m")
|
|
3465
|
+
return text && /^(?:[1-9]|1[0-2])$/.test(value);
|
|
683
3466
|
if (spec.format === "m_int")
|
|
684
3467
|
return int && value >= 1 && value <= 12;
|
|
685
3468
|
return spec.format === "as_typed" && (text && /^(?:0?[1-9]|1[0-2])$/.test(value) || int && value >= 1 && value <= 12);
|
|
@@ -688,50 +3471,102 @@ function placeholderFits(slot, spec, value) {
|
|
|
688
3471
|
return text && /^\d{4}$/.test(value);
|
|
689
3472
|
if (spec.format === "yyyy_int")
|
|
690
3473
|
return int && value >= 1e3 && value <= 9999;
|
|
3474
|
+
if (spec.format === "yy_int")
|
|
3475
|
+
return int && value >= 0 && value <= 99;
|
|
691
3476
|
return spec.format === "as_typed" && (text && /^(?:\d{2}|\d{4})$/.test(value) || int && (value >= 0 && value <= 99 || value >= 1e3 && value <= 9999));
|
|
3477
|
+
case "expiry":
|
|
3478
|
+
return expiryForm(spec, value) !== null;
|
|
3479
|
+
case "last4":
|
|
3480
|
+
return text && /^\d{4}$/.test(value);
|
|
692
3481
|
case "brand":
|
|
693
3482
|
return spec.map ? Object.values(spec.map).includes(value) : text && value !== "";
|
|
694
3483
|
default:
|
|
695
3484
|
return false;
|
|
696
3485
|
}
|
|
697
3486
|
}
|
|
3487
|
+
var DECIMAL_AMOUNT = /^\d{1,9}(?:\.\d{1,2})?$/;
|
|
698
3488
|
function isNothing(value) {
|
|
699
3489
|
return value === null || value === "" || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
|
|
700
3490
|
}
|
|
3491
|
+
function clearCardNumber(texts) {
|
|
3492
|
+
for (const text of texts) {
|
|
3493
|
+
for (const [run] of text.matchAll(/[0-9](?:[ -]?[0-9])*/g)) {
|
|
3494
|
+
const digits = run.replace(/[ -]/g, "");
|
|
3495
|
+
if (digits.length < 13 || digits.length > 19 || !/^[2-6]/.test(digits))
|
|
3496
|
+
continue;
|
|
3497
|
+
let sum = 0;
|
|
3498
|
+
for (let i = 0; i < digits.length; i += 1) {
|
|
3499
|
+
let digit = Number(digits[digits.length - 1 - i]);
|
|
3500
|
+
if (i % 2 === 1)
|
|
3501
|
+
digit = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;
|
|
3502
|
+
sum += digit;
|
|
3503
|
+
}
|
|
3504
|
+
if (sum % 10 === 0)
|
|
3505
|
+
return digits;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
var bodyTexts = (reader, body) => [body, ...reader.fields ? reader.fields.map((field) => field.value) : []];
|
|
3511
|
+
var pinnedKids = (merchantKey) => merchantKey.keys.map((key) => key.kid);
|
|
701
3512
|
function rulesRefusal(p, reader, body) {
|
|
702
3513
|
const request = `the paused body is not ${p.merchant}'s card request`;
|
|
703
3514
|
for (const [slot, spec] of cardSlots(p)) {
|
|
704
3515
|
const label = reader.label(spec.at);
|
|
705
|
-
const
|
|
3516
|
+
const variants = reader.variants(spec.at);
|
|
3517
|
+
if (slot === "cvv" && codeNotAsked(reader, spec, variants))
|
|
3518
|
+
continue;
|
|
3519
|
+
const once = onlyOnce(variants.length, label);
|
|
706
3520
|
if (once)
|
|
707
3521
|
return `${request}: ${once}`;
|
|
708
3522
|
const at = reader.exact(spec.at);
|
|
709
3523
|
if (!at.found)
|
|
710
3524
|
return `${request}: ${label} is not spelled as reviewed`;
|
|
711
|
-
if (!placeholderFits(slot, spec, at.value))
|
|
3525
|
+
if (!placeholderFits(p, slot, spec, at.value))
|
|
712
3526
|
return `${request}: ${label} is not ${SLOT_NAMES[slot]} in the reviewed form`;
|
|
713
3527
|
}
|
|
714
|
-
|
|
715
|
-
|
|
3528
|
+
const merchantKey = p.merchant_key;
|
|
3529
|
+
if (merchantKey && merchantKey.scheme === "rsa_oaep" && merchantKey.kid_at) {
|
|
3530
|
+
const label = reader.label(merchantKey.kid_at);
|
|
3531
|
+
const once = onlyOnce(reader.variants(merchantKey.kid_at).length, label);
|
|
3532
|
+
if (once)
|
|
3533
|
+
return `${request}: ${once}`;
|
|
3534
|
+
const at = reader.exact(merchantKey.kid_at);
|
|
3535
|
+
const kids = pinnedKids(merchantKey);
|
|
3536
|
+
if (!at.found || typeof at.value !== "string" || at.value === "" || kids.length > 0 && !kids.includes(at.value)) {
|
|
3537
|
+
return `${request}: ${label} does not name a key Agentcard pinned for ${p.merchant}`;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
const typed = p.card.pan && ["digits", "grouped4"].includes(p.card.pan.format) ? panDigits(p.card.pan, reader.exact(p.card.pan.at).value) : null;
|
|
3541
|
+
if (typed !== null) {
|
|
3542
|
+
if (countRuns(body, typed) !== 1) {
|
|
3543
|
+
return `${request}: the card number appears outside ${reader.label(p.card.pan.at)}`;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
else if (clearCardNumber(bodyTexts(reader, body)) !== null) {
|
|
3547
|
+
return `${request}: a card number appears in the clear`;
|
|
3548
|
+
}
|
|
3549
|
+
if (p.card.last4 && (typed === null || reader.exact(p.card.last4.at).value !== typed.slice(-4))) {
|
|
3550
|
+
return `${request}: ${reader.label(p.card.last4.at)} is not the last four digits of ${p.card.pan ? reader.label(p.card.pan.at) : SLOT_NAMES.pan}`;
|
|
716
3551
|
}
|
|
717
3552
|
for (const rule of p.requires) {
|
|
718
3553
|
const label = reader.label(rule.at);
|
|
719
3554
|
const variants = reader.variants(rule.at);
|
|
720
3555
|
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);
|
|
3556
|
+
const holds = (value) => "equals" in rule ? value === rule.equals : "one_of" in rule ? rule.one_of.includes(value) : "matches" in rule ? typeof value === "string" && Object.prototype.hasOwnProperty.call(SHAPES, rule.matches) && SHAPES[rule.matches].test(value) : !isNothing(value);
|
|
722
3557
|
if (!at.found || variants.length !== 1 || !holds(at.value))
|
|
723
3558
|
return `${request}: ${label} is not what the review requires`;
|
|
724
3559
|
}
|
|
725
3560
|
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
|
|
3561
|
+
const values = "prefix" in rule ? reader.prefixed(rule.prefix) : reader.variants(rule.at);
|
|
3562
|
+
const fires = rule.when === "present" ? values.length > 0 : rule.when === "nonempty" ? values.some((value) => !isNothing(value)) : jsonBody(p) ? values.some((value) => value !== false && value !== null) : values.some((value) => value !== "" && value !== "false");
|
|
728
3563
|
if (fires)
|
|
729
|
-
return `the paused body asks ${p.merchant} for more than this purchase: ${reader.label(rule.at)}`;
|
|
3564
|
+
return `the paused body asks ${p.merchant} for more than this purchase: ${"prefix" in rule ? `a field starting ${rule.prefix}` : reader.label(rule.at)}`;
|
|
730
3565
|
}
|
|
731
3566
|
if (p.amount) {
|
|
732
3567
|
const label = reader.label(p.amount.at);
|
|
733
3568
|
const at = reader.exact(p.amount.at);
|
|
734
|
-
const fits = p.amount.kind === "decimal_string" ? typeof at.value === "string" &&
|
|
3569
|
+
const fits = p.amount.kind === "decimal_string" ? typeof at.value === "string" && DECIMAL_AMOUNT.test(at.value) : p.amount.kind === "decimal_number" ? jsonBody(p) && typeof at.value === "number" && DECIMAL_AMOUNT.test(String(at.value)) : jsonBody(p) ? Number.isSafeInteger(at.value) && at.value >= 0 : typeof at.value === "string" && /^\d{1,12}$/.test(at.value);
|
|
735
3570
|
if (!at.found || reader.variants(p.amount.at).length !== 1 || !fits)
|
|
736
3571
|
return `${request}: the amount at ${label} is not a ${p.amount.kind.replace("_", " ")}`;
|
|
737
3572
|
}
|
|
@@ -748,7 +3583,29 @@ function merchantCardEndpointBodyRefusal(profile, body, contentType) {
|
|
|
748
3583
|
const read = readBody(p, body, contentType);
|
|
749
3584
|
return read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
750
3585
|
}
|
|
751
|
-
|
|
3586
|
+
function merchantCardEndpointPasses(profile, body, contentType) {
|
|
3587
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3588
|
+
if (!p.payment_method)
|
|
3589
|
+
return false;
|
|
3590
|
+
const read = readBody(p, body, contentType);
|
|
3591
|
+
if (read.refusal)
|
|
3592
|
+
return false;
|
|
3593
|
+
const at = read.reader.exact(p.payment_method.at);
|
|
3594
|
+
if (read.reader.variants(p.payment_method.at).length !== 1 || !at.found || typeof at.value !== "string" || at.value === "")
|
|
3595
|
+
return false;
|
|
3596
|
+
const method = at.value.toLowerCase();
|
|
3597
|
+
return !p.payment_method.card.some((value) => value.toLowerCase() === method);
|
|
3598
|
+
}
|
|
3599
|
+
function merchantCardEndpointSessionTtl(profile, body, contentType) {
|
|
3600
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3601
|
+
const read = readBody(p, body, contentType);
|
|
3602
|
+
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
3603
|
+
if (refusal)
|
|
3604
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
3605
|
+
if (!p.challenge)
|
|
3606
|
+
return null;
|
|
3607
|
+
return read.reader.variants(p.challenge.at).some((value) => !isNothing(value)) ? p.challenge.ttl_ms : null;
|
|
3608
|
+
}
|
|
752
3609
|
function merchantCardEndpointAmount(profile, body, contentType) {
|
|
753
3610
|
const p = trustedMerchantCardEndpoint(profile);
|
|
754
3611
|
const read = readBody(p, body, contentType);
|
|
@@ -763,9 +3620,91 @@ function merchantCardEndpointAmount(profile, body, contentType) {
|
|
|
763
3620
|
return { amountCents: Number(value), currency };
|
|
764
3621
|
if (!TWO_DECIMAL_CURRENCIES.has(currency))
|
|
765
3622
|
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(".");
|
|
3623
|
+
const [whole, fraction = ""] = String(value).split(".");
|
|
767
3624
|
return { amountCents: Number(whole) * 100 + Number(fraction.padEnd(2, "0")), currency };
|
|
768
3625
|
}
|
|
3626
|
+
var WRITTEN_DIGIT = String.raw `(?:[0-9]|\\u003[0-9]|%3[0-9]|�*(?:4[89]|5[0-7]);|�*3[0-9];)`;
|
|
3627
|
+
var WRITTEN_RUN = /* @__PURE__ */ new RegExp(`${WRITTEN_DIGIT}+(?:${SEPARATOR}+${WRITTEN_DIGIT}+)*`, "gi");
|
|
3628
|
+
var RUN_TOKEN = /* @__PURE__ */ new RegExp(`(${WRITTEN_DIGIT})|${SEPARATOR}`, "giy");
|
|
3629
|
+
function writtenDigit(token) {
|
|
3630
|
+
if (token.length === 1)
|
|
3631
|
+
return token;
|
|
3632
|
+
if (/^&#[0-9]/.test(token))
|
|
3633
|
+
return String.fromCharCode(Number(token.slice(2, -1)));
|
|
3634
|
+
return token.endsWith(";") ? token[token.length - 2] : token[token.length - 1];
|
|
3635
|
+
}
|
|
3636
|
+
function luhnValid(digits) {
|
|
3637
|
+
let sum = 0;
|
|
3638
|
+
for (let i = 0; i < digits.length; i += 1) {
|
|
3639
|
+
let digit = digits.charCodeAt(digits.length - 1 - i) - 48;
|
|
3640
|
+
if (i % 2 === 1) {
|
|
3641
|
+
digit *= 2;
|
|
3642
|
+
if (digit > 9)
|
|
3643
|
+
digit -= 9;
|
|
3644
|
+
}
|
|
3645
|
+
sum += digit;
|
|
3646
|
+
}
|
|
3647
|
+
return sum % 10 === 0;
|
|
3648
|
+
}
|
|
3649
|
+
function writesCardNumber(text) {
|
|
3650
|
+
for (const [run] of text.matchAll(WRITTEN_RUN)) {
|
|
3651
|
+
const groups = [];
|
|
3652
|
+
let group = "";
|
|
3653
|
+
for (const token of run.matchAll(RUN_TOKEN)) {
|
|
3654
|
+
if (token[1] !== void 0)
|
|
3655
|
+
group += writtenDigit(token[1]);
|
|
3656
|
+
else if (group !== "") {
|
|
3657
|
+
groups.push(group);
|
|
3658
|
+
group = "";
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
if (group !== "")
|
|
3662
|
+
groups.push(group);
|
|
3663
|
+
for (let first = 0; first < groups.length; first += 1) {
|
|
3664
|
+
let digits = "";
|
|
3665
|
+
for (let last = first; last < groups.length && digits.length + groups[last].length <= 19; last += 1) {
|
|
3666
|
+
digits += groups[last];
|
|
3667
|
+
if (digits.length >= 12 && luhnValid(digits))
|
|
3668
|
+
return true;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
return false;
|
|
3673
|
+
}
|
|
3674
|
+
function cardNumberValues(p, body, contentType) {
|
|
3675
|
+
const at = (p.card.pan ?? p.card.cvv).at;
|
|
3676
|
+
if (jsonBody(p)) {
|
|
3677
|
+
let parsed2;
|
|
3678
|
+
try {
|
|
3679
|
+
parsed2 = JSON.parse(body);
|
|
3680
|
+
}
|
|
3681
|
+
catch {
|
|
3682
|
+
return [];
|
|
3683
|
+
}
|
|
3684
|
+
if (!isPlainObject(parsed2))
|
|
3685
|
+
return [];
|
|
3686
|
+
return hasSelector(at) ? selectVariants(parsed2, at) : valuesIgnoringCase(parsed2, at);
|
|
3687
|
+
}
|
|
3688
|
+
const parsed = p.encoding === "form" ? parseForm(body) : parseMultipart(body, multipartBoundary(contentType));
|
|
3689
|
+
return parsed.refusal ? [] : parsed.fields.filter((field) => sameName(field.name, at)).map((field) => field.value);
|
|
3690
|
+
}
|
|
3691
|
+
function navigationPostCarriesCard(p, body, contentType) {
|
|
3692
|
+
const read = readBody(p, body, contentType);
|
|
3693
|
+
if (read.refusal)
|
|
3694
|
+
return true;
|
|
3695
|
+
const { reader } = read;
|
|
3696
|
+
if ([p.card.pan, p.card.cvv].some((spec) => spec && reader.variants(spec.at).some((value) => !isNothing(value))))
|
|
3697
|
+
return true;
|
|
3698
|
+
return reader.scalars().some((value) => (String(value).replace(/[\s.-]/g, "").match(/\d+/g) ?? []).some((run) => run.length >= 12 && run.length <= 19 && luhnValid(run)));
|
|
3699
|
+
}
|
|
3700
|
+
function merchantCardEndpointCarriesCard(profile, body, contentType) {
|
|
3701
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3702
|
+
if (typeof body !== "string" || body === "" || utf8Length(body) > MERCHANT_CARD_ENDPOINT_LIMITS.bodyBytes)
|
|
3703
|
+
return true;
|
|
3704
|
+
if (cardNumberValues(p, body, contentType).some((value) => !isNothing(value)) || writesCardNumber(body))
|
|
3705
|
+
return true;
|
|
3706
|
+
return p.navigation !== null && navigationPostCarriesCard(p, body, contentType);
|
|
3707
|
+
}
|
|
769
3708
|
function endpointMatches(p, rawUrl, method, opts) {
|
|
770
3709
|
if (typeof rawUrl !== "string" || rawUrl.length > MERCHANT_CARD_ENDPOINT_LIMITS.urlLength)
|
|
771
3710
|
return false;
|
|
@@ -797,6 +3736,14 @@ function merchantCardEndpointMatches(profile, rawUrl, method, opts) {
|
|
|
797
3736
|
function matchMerchantCardEndpoint(rawUrl, method, opts) {
|
|
798
3737
|
return MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((p) => endpointMatches(p, rawUrl, method, opts)) ?? null;
|
|
799
3738
|
}
|
|
3739
|
+
function matchMerchantCardEndpointResend(rawUrl, method) {
|
|
3740
|
+
if (typeof rawUrl !== "string" || rawUrl.length > MERCHANT_CARD_ENDPOINT_LIMITS.urlLength || typeof method !== "string")
|
|
3741
|
+
return null;
|
|
3742
|
+
const url = strictUrl(rawUrl);
|
|
3743
|
+
if (!url)
|
|
3744
|
+
return null;
|
|
3745
|
+
return MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((p) => url.origin === p.endpoint && (p.resends ?? []).some((resend) => resend.method === method.toUpperCase() && urlTemplateMatch(resend, rawUrl) !== null)) ?? null;
|
|
3746
|
+
}
|
|
800
3747
|
function assertMerchantCardEndpoint(authorization) {
|
|
801
3748
|
if (!isPlainObject(authorization))
|
|
802
3749
|
throw new Error("Refusing to pay: malformed payment endpoint.");
|
|
@@ -838,7 +3785,7 @@ function refererKept(p, referer) {
|
|
|
838
3785
|
catch {
|
|
839
3786
|
return false;
|
|
840
3787
|
}
|
|
841
|
-
return url.href === referer && url.origin === p.checkout && !url.username && !url.password;
|
|
3788
|
+
return url.href === referer && url.origin === (p.frame ?? p.checkout) && !url.username && !url.password;
|
|
842
3789
|
}
|
|
843
3790
|
function lowerHeaders(headers) {
|
|
844
3791
|
const out = /* @__PURE__ */ Object.create(null);
|
|
@@ -850,6 +3797,55 @@ function lowerHeaders(headers) {
|
|
|
850
3797
|
}
|
|
851
3798
|
return out;
|
|
852
3799
|
}
|
|
3800
|
+
function cookieNameAllowed(p, name) {
|
|
3801
|
+
const names = p.session.cookie_names;
|
|
3802
|
+
if (names === null)
|
|
3803
|
+
return true;
|
|
3804
|
+
return names.some((entry) => entry.endsWith("*") ? name.startsWith(entry.slice(0, -1)) && name.length > entry.length - 1 : name === entry);
|
|
3805
|
+
}
|
|
3806
|
+
function sessionCookie(bundle, name) {
|
|
3807
|
+
const hits = (Array.isArray(bundle.cookies) ? bundle.cookies : []).filter((cookie) => cookie.name === name);
|
|
3808
|
+
return hits.length === 1 ? hits[0].value : null;
|
|
3809
|
+
}
|
|
3810
|
+
function checkoutKeyOf(p, bundle, reader) {
|
|
3811
|
+
const once = p.once_per;
|
|
3812
|
+
if (!once)
|
|
3813
|
+
return null;
|
|
3814
|
+
let value = null;
|
|
3815
|
+
if ("at" in once) {
|
|
3816
|
+
const at = reader.exact(once.at);
|
|
3817
|
+
value = at.found && reader.variants(once.at).length === 1 ? at.value : null;
|
|
3818
|
+
}
|
|
3819
|
+
else if ("path" in once) {
|
|
3820
|
+
value = pathPlaceholders(p.path, new URL(bundle.url).pathname)?.[once.path] ?? null;
|
|
3821
|
+
}
|
|
3822
|
+
else if ("query" in once) {
|
|
3823
|
+
const mark = bundle.url.indexOf("?");
|
|
3824
|
+
value = queryPlaceholders(p.query, mark === -1 ? null : bundle.url.slice(mark + 1))?.[once.query] ?? null;
|
|
3825
|
+
}
|
|
3826
|
+
else if ("cookie" in once) {
|
|
3827
|
+
value = sessionCookie(bundle, once.cookie);
|
|
3828
|
+
}
|
|
3829
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
3830
|
+
}
|
|
3831
|
+
function sessionBindingRefusal(p, bundle, contentType) {
|
|
3832
|
+
const { reader } = readBody(p, bundle.body, contentType);
|
|
3833
|
+
for (const rule of p.requires) {
|
|
3834
|
+
if ("equals_cookie" in rule) {
|
|
3835
|
+
const cookie = sessionCookie(bundle, rule.equals_cookie);
|
|
3836
|
+
if (cookie === null || reader.exact(rule.at).value !== cookie)
|
|
3837
|
+
return `${reader.label(rule.at)} is not the checkout the session's ${rule.equals_cookie} cookie names`;
|
|
3838
|
+
}
|
|
3839
|
+
if ("equals_path" in rule) {
|
|
3840
|
+
const segment = pathPlaceholders(p.path, new URL(bundle.url).pathname)?.[rule.equals_path];
|
|
3841
|
+
if (segment === void 0 || reader.exact(rule.at).value !== segment)
|
|
3842
|
+
return `${reader.label(rule.at)} is not the checkout the card request's URL names`;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
if (p.once_per && checkoutKeyOf(p, bundle, reader) === null)
|
|
3846
|
+
return `the session names no checkout for ${p.merchant} to take one payment from`;
|
|
3847
|
+
return null;
|
|
3848
|
+
}
|
|
853
3849
|
function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
854
3850
|
const p = trustedMerchantCardEndpoint(profile);
|
|
855
3851
|
const limits = MERCHANT_CARD_ENDPOINT_LIMITS;
|
|
@@ -861,6 +3857,9 @@ function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
|
861
3857
|
return `the session bundle's URL is not ${p.merchant}'s reviewed card endpoint`;
|
|
862
3858
|
if (bundle.page_origin !== p.checkout)
|
|
863
3859
|
return `the card request was not paused on ${p.checkout}`;
|
|
3860
|
+
if (p.navigation === null ? bundle.navigation !== void 0 : bundle.navigation !== p.navigation) {
|
|
3861
|
+
return p.navigation === null ? `${p.merchant}'s card request is not a page navigation` : `the card request was not the ${p.navigation === "top" ? "page" : "frame"} navigation ${p.merchant}'s form sends`;
|
|
3862
|
+
}
|
|
864
3863
|
const raw = bundle.headers === void 0 ? {} : bundle.headers;
|
|
865
3864
|
const headers = isPlainObject(raw) ? lowerHeaders(raw) : null;
|
|
866
3865
|
if (!headers)
|
|
@@ -892,51 +3891,83 @@ function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
|
892
3891
|
return "a session cookie is not an RFC 6265 name and value";
|
|
893
3892
|
if (cookie.name.length + cookie.value.length + 1 > limits.headerValueBytes)
|
|
894
3893
|
return "a session cookie is larger than the limit";
|
|
3894
|
+
if (!cookieNameAllowed(p, cookie.name))
|
|
3895
|
+
return `the session bundle carries a cookie outside ${p.merchant}'s reviewed list`;
|
|
895
3896
|
cookieBytes += cookie.name.length + cookie.value.length + 3;
|
|
896
3897
|
}
|
|
897
3898
|
if (cookieBytes > limits.headerBytes)
|
|
898
3899
|
return "the session cookies are larger than the limit";
|
|
899
3900
|
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"]);
|
|
3901
|
+
return `the session Referer is not a page on ${p.frame ?? p.checkout}`;
|
|
3902
|
+
return merchantCardEndpointBodyRefusal(p.id, bundle.body, headers["content-type"]) ?? sessionBindingRefusal(p, bundle, headers["content-type"]);
|
|
3903
|
+
}
|
|
3904
|
+
function merchantCardEndpointCheckoutKey(profile, bundle) {
|
|
3905
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3906
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
3907
|
+
if (refusal)
|
|
3908
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
3909
|
+
const headers = lowerHeaders(bundle.headers ?? {});
|
|
3910
|
+
return checkoutKeyOf(p, bundle, readBody(p, bundle.body, headers["content-type"]).reader);
|
|
902
3911
|
}
|
|
903
3912
|
function slotValue(slot, spec, placeholder, facts) {
|
|
904
3913
|
if (slot === "pan")
|
|
905
|
-
return facts.number;
|
|
3914
|
+
return spec.format === "grouped4" && placeholder.includes(" ") ? groupedFour(facts.number) : facts.number;
|
|
906
3915
|
if (slot === "cvv")
|
|
907
3916
|
return facts.cvc;
|
|
3917
|
+
if (slot === "last4")
|
|
3918
|
+
return facts.number.slice(-4);
|
|
3919
|
+
if (slot === "expiry")
|
|
3920
|
+
return expiryText(expiryForm(spec, placeholder), facts);
|
|
3921
|
+
if (slot === "leading")
|
|
3922
|
+
return facts.number.slice(0, spec.digits);
|
|
3923
|
+
const month = String(facts.month).padStart(2, "0");
|
|
908
3924
|
if (slot === "exp_month") {
|
|
909
|
-
const padded = String(facts.month).padStart(2, "0");
|
|
910
3925
|
if (spec.format === "mm")
|
|
911
|
-
return
|
|
3926
|
+
return month;
|
|
3927
|
+
if (spec.format === "m")
|
|
3928
|
+
return String(facts.month);
|
|
912
3929
|
if (spec.format === "m_int" || typeof placeholder === "number")
|
|
913
3930
|
return facts.month;
|
|
914
|
-
return placeholder.length === 2 ?
|
|
3931
|
+
return placeholder.length === 2 ? month : String(facts.month);
|
|
915
3932
|
}
|
|
916
3933
|
if (spec.format === "yyyy")
|
|
917
3934
|
return String(facts.year);
|
|
918
3935
|
if (spec.format === "yyyy_int")
|
|
919
3936
|
return facts.year;
|
|
3937
|
+
if (spec.format === "yy_int")
|
|
3938
|
+
return facts.year % 100;
|
|
920
3939
|
if (typeof placeholder === "number")
|
|
921
3940
|
return placeholder >= 1e3 ? facts.year : facts.year % 100;
|
|
922
3941
|
return placeholder.length === 4 ? String(facts.year) : String(facts.year % 100).padStart(2, "0");
|
|
923
3942
|
}
|
|
3943
|
+
function plaintextFor(slot, spec, facts) {
|
|
3944
|
+
if (spec.plaintext === "digits")
|
|
3945
|
+
return slot === "cvv" ? facts.cvc : facts.number;
|
|
3946
|
+
if (spec.plaintext === "MMYYYY")
|
|
3947
|
+
return `${String(facts.month).padStart(2, "0")}${facts.year}`;
|
|
3948
|
+
let claims = facts.number;
|
|
3949
|
+
for (const name of [...spec.plaintext.claims].reverse())
|
|
3950
|
+
claims = { [name]: claims };
|
|
3951
|
+
return JSON.stringify(claims);
|
|
3952
|
+
}
|
|
924
3953
|
function writeJson(parsed, at, value) {
|
|
925
|
-
const
|
|
926
|
-
|
|
3954
|
+
const path = hasSelector(at) ? selectAt(parsed, at).path : at;
|
|
3955
|
+
if (!path)
|
|
3956
|
+
throw new SubstitutionError(`the paused body has no field at ${describeAt(at)}`);
|
|
3957
|
+
const parent = jsonAt(parsed, path.slice(0, -1));
|
|
3958
|
+
const key = path[path.length - 1];
|
|
927
3959
|
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
3960
|
if (!owned)
|
|
929
|
-
throw new SubstitutionError(`the paused body has no field at ${
|
|
3961
|
+
throw new SubstitutionError(`the paused body has no field at ${describeAt(at)}`);
|
|
930
3962
|
parent.value[key] = value;
|
|
931
3963
|
}
|
|
932
|
-
function
|
|
933
|
-
const p = trustedMerchantCardEndpoint(profile);
|
|
3964
|
+
function substitutionPlan(p, body, contentType, card) {
|
|
934
3965
|
const read = readBody(p, body, contentType);
|
|
935
3966
|
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
936
3967
|
if (refusal)
|
|
937
3968
|
throw new SubstitutionError(refusal);
|
|
938
3969
|
const facts = cardFacts(card);
|
|
939
|
-
const typed = read.reader.exact(p.card.pan.at).value;
|
|
3970
|
+
const typed = p.card.pan && ["digits", "grouped4"].includes(p.card.pan.format) ? panDigits(p.card.pan, read.reader.exact(p.card.pan.at).value) : null;
|
|
940
3971
|
if (typed === facts.number)
|
|
941
3972
|
throw new SubstitutionError("the paused body already carries the real card number");
|
|
942
3973
|
const bins = p.card.bins && facts.brand && Object.prototype.hasOwnProperty.call(p.card.bins, facts.brand) ? p.card.bins[facts.brand] : null;
|
|
@@ -947,56 +3978,155 @@ function substituteMerchantCardEndpoint(body, contentType, profile, card) {
|
|
|
947
3978
|
const at = read.reader.exact(spec.at);
|
|
948
3979
|
if (slot === "brand" && !spec.map)
|
|
949
3980
|
continue;
|
|
950
|
-
|
|
3981
|
+
if (slot === "cvv" && codeNotAsked(read.reader, spec, read.reader.variants(spec.at)))
|
|
3982
|
+
continue;
|
|
951
3983
|
if (slot === "brand") {
|
|
952
|
-
value = facts.brand && Object.prototype.hasOwnProperty.call(spec.map, facts.brand) ? spec.map[facts.brand] : void 0;
|
|
3984
|
+
const value = facts.brand && Object.prototype.hasOwnProperty.call(spec.map, facts.brand) ? spec.map[facts.brand] : void 0;
|
|
953
3985
|
if (value === void 0)
|
|
954
3986
|
throw new SubstitutionError(`${p.merchant} does not take this card's network`);
|
|
3987
|
+
writes.push({ at: spec.at, found: at, value });
|
|
3988
|
+
}
|
|
3989
|
+
else if (spec.format === "encrypted") {
|
|
3990
|
+
writes.push({ at: spec.at, found: at, value: null, plaintext: plaintextFor(slot, spec, facts) });
|
|
955
3991
|
}
|
|
956
3992
|
else {
|
|
957
|
-
value
|
|
3993
|
+
writes.push({ at: spec.at, found: at, value: slotValue(slot, spec, at.value, facts) });
|
|
958
3994
|
}
|
|
959
|
-
|
|
3995
|
+
}
|
|
3996
|
+
return { read, facts, typed, writes };
|
|
3997
|
+
}
|
|
3998
|
+
function writtenBody(p, plan, body) {
|
|
3999
|
+
if (jsonBody(p)) {
|
|
4000
|
+
for (const write of plan.writes)
|
|
4001
|
+
writeJson(plan.read.reader.parsed, write.at, write.value);
|
|
4002
|
+
return JSON.stringify(plan.read.reader.parsed);
|
|
960
4003
|
}
|
|
961
4004
|
let out = body;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
}
|
|
4005
|
+
for (const write of [...plan.writes].sort((a, b) => b.found.field.start - a.found.field.start)) {
|
|
4006
|
+
const text = p.encoding === "form" ? encodeURIComponent(String(write.value)).replace(/%20/g, "+") : String(write.value);
|
|
4007
|
+
out = out.slice(0, write.found.field.start) + text + out.slice(write.found.field.end);
|
|
972
4008
|
}
|
|
973
|
-
|
|
4009
|
+
return out;
|
|
4010
|
+
}
|
|
4011
|
+
function merchantCardEndpointEncrypts(profile) {
|
|
4012
|
+
return trustedMerchantCardEndpoint(profile).merchant_key != null;
|
|
4013
|
+
}
|
|
4014
|
+
function substituteMerchantCardEndpoint(body, contentType, profile, card) {
|
|
4015
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4016
|
+
if (p.merchant_key)
|
|
4017
|
+
throw new SubstitutionError(`${p.merchant} takes the card encrypted under its own key, which this call does not write`);
|
|
4018
|
+
const plan = substitutionPlan(p, body, contentType, card);
|
|
4019
|
+
const out = writtenBody(p, plan, body);
|
|
4020
|
+
if (countRuns(out, plan.typed) !== 0)
|
|
974
4021
|
throw new SubstitutionError("the typed card number would still reach the merchant");
|
|
975
|
-
if (countRuns(out, facts.number) !== 1)
|
|
4022
|
+
if (countRuns(out, plan.facts.number) !== 1)
|
|
976
4023
|
throw new SubstitutionError("the card number would reach the merchant outside its field");
|
|
977
4024
|
if (!merchantCardEndpointLive(p))
|
|
978
4025
|
throw new SubstitutionError(`Agentcard has not turned on payments to ${p.merchant} yet`);
|
|
979
4026
|
return out;
|
|
980
4027
|
}
|
|
981
|
-
function
|
|
4028
|
+
function merchantKeyFor(p, plan) {
|
|
4029
|
+
const merchantKey = p.merchant_key;
|
|
4030
|
+
const encrypted = cardSlots(p).filter(([, spec]) => spec.format === "encrypted").map(([, spec]) => plan.read.reader.exact(spec.at).value);
|
|
4031
|
+
let key = null;
|
|
4032
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
4033
|
+
if (merchantKey.kid_at) {
|
|
4034
|
+
const kid = plan.read.reader.exact(merchantKey.kid_at).value;
|
|
4035
|
+
key = merchantKey.keys.find((entry) => entry.kid === kid) ?? null;
|
|
4036
|
+
}
|
|
4037
|
+
else if (merchantKey.keys.length === 1) {
|
|
4038
|
+
key = merchantKey.keys[0];
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
else if (merchantKey.alg !== null) {
|
|
4042
|
+
const kids = new Set(encrypted.map((value) => readCompactJwe(value)?.header.kid));
|
|
4043
|
+
const [kid] = kids;
|
|
4044
|
+
key = kids.size === 1 ? merchantKey.keys.find((entry) => entry.kid === kid) ?? null : null;
|
|
4045
|
+
}
|
|
4046
|
+
if (!key)
|
|
4047
|
+
throw new SubstitutionError(`Agentcard has not pinned the key ${p.merchant} encrypts the card under`);
|
|
4048
|
+
let size;
|
|
4049
|
+
try {
|
|
4050
|
+
({ size } = parseMerchantKey(key));
|
|
4051
|
+
}
|
|
4052
|
+
catch {
|
|
4053
|
+
throw new SubstitutionError(`the key Agentcard pinned for ${p.merchant} does not match its pin`);
|
|
4054
|
+
}
|
|
4055
|
+
const sizes = encrypted.map((value) => merchantKey.scheme === "rsa_oaep" ? decodeBase64(value)?.length : readCompactJwe(value)?.encryptedKey.length);
|
|
4056
|
+
if (sizes.some((length) => length !== size))
|
|
4057
|
+
throw new SubstitutionError(`the paused body was encrypted under a key Agentcard did not pin for ${p.merchant}`);
|
|
4058
|
+
return key;
|
|
4059
|
+
}
|
|
4060
|
+
async function encryptedValue(merchantKey, key, text, crypto) {
|
|
4061
|
+
const bytes = new TextEncoder().encode(text);
|
|
4062
|
+
try {
|
|
4063
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
4064
|
+
return encodeBase64(await rsaOaepEncrypt(key, bytes, { hash: merchantKey.hash, mgf1: merchantKey.mgf1 }, crypto));
|
|
4065
|
+
}
|
|
4066
|
+
return await jweCompactEncrypt(key, bytes, merchantKey, key.kid, crypto);
|
|
4067
|
+
}
|
|
4068
|
+
catch (error) {
|
|
4069
|
+
if (error instanceof MerchantKeyError)
|
|
4070
|
+
throw new SubstitutionError(`the card could not be encrypted under ${error.reason === "key" ? "the pinned key" : "this device"}`);
|
|
4071
|
+
throw error;
|
|
4072
|
+
}
|
|
4073
|
+
finally {
|
|
4074
|
+
bytes.fill(0);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
async function encryptMerchantCardEndpointBody(body, contentType, profile, card, crypto) {
|
|
982
4078
|
const p = trustedMerchantCardEndpoint(profile);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
|
|
4079
|
+
if (!p.merchant_key)
|
|
4080
|
+
throw new SubstitutionError(`${p.merchant} takes the card in the clear, which this call does not write`);
|
|
4081
|
+
const plan = substitutionPlan(p, body, contentType, card);
|
|
4082
|
+
const key = merchantKeyFor(p, plan);
|
|
4083
|
+
for (const write of plan.writes) {
|
|
4084
|
+
if (write.plaintext === void 0)
|
|
4085
|
+
continue;
|
|
4086
|
+
write.value = await encryptedValue(p.merchant_key, key, write.plaintext, crypto);
|
|
4087
|
+
write.plaintext = void 0;
|
|
4088
|
+
}
|
|
4089
|
+
const out = writtenBody(p, plan, body);
|
|
4090
|
+
if (countRuns(out, plan.facts.number) !== 0 || clearCardNumber([out]) !== null)
|
|
4091
|
+
throw new SubstitutionError("a card number would reach the merchant in the clear");
|
|
4092
|
+
if (!merchantCardEndpointLive(p))
|
|
4093
|
+
throw new SubstitutionError(`Agentcard has not turned on payments to ${p.merchant} yet`);
|
|
4094
|
+
return out;
|
|
4095
|
+
}
|
|
4096
|
+
function endpointRequest(p, bundle, captured, body) {
|
|
4097
|
+
const formType = p.navigation === null ? "application/x-www-form-urlencoded; charset=UTF-8" : NAVIGATION_FORM_TYPE;
|
|
4098
|
+
const headers = { "content-type": p.encoding === "form" ? formType : captured["content-type"] };
|
|
989
4099
|
for (const [name, value] of Object.entries(captured))
|
|
990
4100
|
if (name !== "content-type")
|
|
991
4101
|
headers[name] = value;
|
|
992
4102
|
const cookies = bundle.cookies ?? [];
|
|
993
4103
|
if (p.session.cookies && cookies.length > 0)
|
|
994
4104
|
headers.cookie = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
995
|
-
headers.origin = p.checkout;
|
|
4105
|
+
headers.origin = p.frame ?? p.checkout;
|
|
996
4106
|
if (bundle.referer !== void 0)
|
|
997
4107
|
headers.referer = bundle.referer;
|
|
998
4108
|
return { url: bundle.url, method: p.method, headers, body };
|
|
999
4109
|
}
|
|
4110
|
+
function merchantEndpointRequest(profile, bundle, card) {
|
|
4111
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4112
|
+
if (p.merchant_key)
|
|
4113
|
+
throw new Error(`Refusing to pay: ${p.merchant} takes the card encrypted under its own key, which this call does not write.`);
|
|
4114
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
4115
|
+
if (refusal)
|
|
4116
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
4117
|
+
const captured = lowerHeaders(bundle.headers ?? {});
|
|
4118
|
+
return endpointRequest(p, bundle, captured, substituteMerchantCardEndpoint(bundle.body, captured["content-type"], p.id, card));
|
|
4119
|
+
}
|
|
4120
|
+
async function encryptedMerchantEndpointRequest(profile, bundle, card, crypto) {
|
|
4121
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4122
|
+
if (!p.merchant_key)
|
|
4123
|
+
throw new Error(`Refusing to pay: ${p.merchant} takes the card in the clear, which this call does not write.`);
|
|
4124
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
4125
|
+
if (refusal)
|
|
4126
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
4127
|
+
const captured = lowerHeaders(bundle.headers ?? {});
|
|
4128
|
+
return endpointRequest(p, bundle, captured, await encryptMerchantCardEndpointBody(bundle.body, captured["content-type"], p.id, card, crypto));
|
|
4129
|
+
}
|
|
1000
4130
|
function headerPairs(headers) {
|
|
1001
4131
|
if (headers === void 0)
|
|
1002
4132
|
return [];
|
|
@@ -1026,7 +4156,7 @@ var BASE64_RUN = /[A-Za-z0-9+/_-]{8,}/g;
|
|
|
1026
4156
|
var STREAM_RUN_LENGTH = 16;
|
|
1027
4157
|
var CODE_BASE64_RUN = /[A-Za-z0-9+/_-]{4,}/g;
|
|
1028
4158
|
var BASE64_ESCAPE = /\\\/|\\u002[bBfF]|%2[bBfF]|&#(?:43|47|x2[bBfF]);/g;
|
|
1029
|
-
var
|
|
4159
|
+
var BASE64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1030
4160
|
var unescapeBase64 = (escape) => /43|[bB]/.test(escape) ? "+" : "/";
|
|
1031
4161
|
function base64Bytes(run, skip) {
|
|
1032
4162
|
let bits = 0;
|
|
@@ -1034,7 +4164,7 @@ function base64Bytes(run, skip) {
|
|
|
1034
4164
|
let out = "";
|
|
1035
4165
|
for (let i = skip; i < run.length; i += 1) {
|
|
1036
4166
|
const char = run[i];
|
|
1037
|
-
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 :
|
|
4167
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : BASE64_ALPHABET2.indexOf(char));
|
|
1038
4168
|
bits += 6;
|
|
1039
4169
|
if (bits >= 8) {
|
|
1040
4170
|
bits -= 8;
|
|
@@ -1081,10 +4211,27 @@ function responseCardLeak(card, response) {
|
|
|
1081
4211
|
}
|
|
1082
4212
|
return seen.compressed ? "unreadable_in_response" : null;
|
|
1083
4213
|
}
|
|
4214
|
+
var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
|
|
4215
|
+
function merchantCardEndpointAnswerRefusal(profile, bodyText) {
|
|
4216
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4217
|
+
const rules = p.response.withhold ?? [];
|
|
4218
|
+
if (rules.length === 0 || typeof bodyText !== "string")
|
|
4219
|
+
return null;
|
|
4220
|
+
let parsed;
|
|
4221
|
+
try {
|
|
4222
|
+
parsed = JSON.parse(bodyText.replace(JSON_PROTECTION_PREFIX, ""));
|
|
4223
|
+
}
|
|
4224
|
+
catch {
|
|
4225
|
+
return null;
|
|
4226
|
+
}
|
|
4227
|
+
if (!isPlainObject(parsed))
|
|
4228
|
+
return null;
|
|
4229
|
+
return rules.some((rule) => valuesIgnoringCase(parsed, rule.at).some((value) => value === rule.equals)) ? "refused_step_in_response" : null;
|
|
4230
|
+
}
|
|
1084
4231
|
function merchantCardEndpointAnswer(profile, response, card) {
|
|
1085
4232
|
const p = trustedMerchantCardEndpoint(profile);
|
|
1086
4233
|
const pairs = answerParts(response);
|
|
1087
|
-
const withheld = responseCardLeak(card, response);
|
|
4234
|
+
const withheld = responseCardLeak(card, response) ?? merchantCardEndpointAnswerRefusal(p.id, response.bodyText);
|
|
1088
4235
|
if (withheld)
|
|
1089
4236
|
return { status: response.status, withheld, headers: {}, set_cookies: [], bodyText: null };
|
|
1090
4237
|
const headers = {};
|
|
@@ -1100,4 +4247,26 @@ function merchantCardEndpointAnswer(profile, response, card) {
|
|
|
1100
4247
|
}
|
|
1101
4248
|
return { status: response.status, headers, set_cookies: setCookies, bodyText: response.bodyText };
|
|
1102
4249
|
}
|
|
1103
|
-
|
|
4250
|
+
var GET_REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303]);
|
|
4251
|
+
function merchantCardEndpointNavigationAnswer(profile, answer, requestUrl) {
|
|
4252
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4253
|
+
const status = isPlainObject(answer) ? answer.status : void 0;
|
|
4254
|
+
if (p.navigation === null || !Number.isInteger(status) || status < 300 || status > 399)
|
|
4255
|
+
return { kind: "page" };
|
|
4256
|
+
if (!GET_REDIRECTS.has(status))
|
|
4257
|
+
return { kind: "refused", reason: "redirect_repeats_post" };
|
|
4258
|
+
const location = isPlainObject(answer.headers) ? answer.headers.location : void 0;
|
|
4259
|
+
let target = null;
|
|
4260
|
+
if (typeof location === "string" && location.length <= MERCHANT_CARD_ENDPOINT_LIMITS.urlLength && /^[\x21-\x7e]+$/.test(location)) {
|
|
4261
|
+
try {
|
|
4262
|
+
target = new URL(location, requestUrl);
|
|
4263
|
+
}
|
|
4264
|
+
catch {
|
|
4265
|
+
target = null;
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
if (!target || target.protocol !== "https:" || target.username || target.password)
|
|
4269
|
+
return { kind: "refused", reason: "redirect_location_invalid" };
|
|
4270
|
+
return { kind: "redirect", status, location: target.href };
|
|
4271
|
+
}
|
|
4272
|
+
export { HILTON_CREATE_RESERVATION_DOCUMENTS, MERCHANT_CARD_ENDPOINT_CLIENT_TABLE, MERCHANT_CARD_ENDPOINT_FACTS_TEXT_MAX, MERCHANT_CARD_ENDPOINT_FACT_READERS, MERCHANT_CARD_ENDPOINT_LIMITS, MERCHANT_CARD_ENDPOINT_MODE, MERCHANT_CARD_ENDPOINT_PROFILES, MERCHANT_CARD_ENDPOINT_PSP, MERCHANT_CARD_ENDPOINT_RECOGNIZERS, SubstitutionError, assertMerchantCardEndpoint, encryptMerchantCardEndpointBody, encryptedMerchantEndpointRequest, matchMerchantCardEndpoint, matchMerchantCardEndpointResend, merchantCardEndpointAmount, merchantCardEndpointAnswer, merchantCardEndpointAnswerRefusal, merchantCardEndpointBodyRefusal, merchantCardEndpointById, merchantCardEndpointCarriesCard, merchantCardEndpointCheckoutKey, merchantCardEndpointEncrypts, merchantCardEndpointFactsRefusal, merchantCardEndpointFactsRole, merchantCardEndpointLive, merchantCardEndpointMatches, merchantCardEndpointNavigationAnswer, merchantCardEndpointPasses, merchantCardEndpointRecognizerById, merchantCardEndpointRequestOrigin, merchantCardEndpointSessionRefusal, merchantCardEndpointSessionTtl, merchantEndpointRequest, projectMerchantCardEndpointFacts, readMerchantCardEndpointFacts, responseCardLeak, substituteMerchantCardEndpoint, trustedMerchantCardEndpoint };
|