@agent-cards/sdk 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -8
- package/PREFLIGHT.md +3 -1
- package/README.md +120 -15
- package/dist/adyen-merchant-hosted.generated.js +20 -8
- 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 +3253 -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: 82c93c756fd934cb52d2d868b73e546d741aebbe53f12d4c38c9173abf7a0c03
|
|
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,2096 @@ 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 and /paymentSetup and put the result in 3dsData, which is refused here; China UnionPay and PayPal set cupTokenId and paymentSource, also refused."
|
|
1121
|
+
],
|
|
1122
|
+
har: null,
|
|
1123
|
+
checkout: "https://www.ihg.com",
|
|
1124
|
+
checkout_frame: false,
|
|
1125
|
+
frame: null,
|
|
1126
|
+
endpoint: "https://apis.ihg.com",
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
path: "/reservations/v3/hotels",
|
|
1129
|
+
query: {},
|
|
1130
|
+
navigation: null,
|
|
1131
|
+
encoding: "json",
|
|
1132
|
+
session: {
|
|
1133
|
+
cookies: true,
|
|
1134
|
+
cookie_names: null,
|
|
1135
|
+
headers: [
|
|
1136
|
+
...BROWSER_HEADERS,
|
|
1137
|
+
"content-type",
|
|
1138
|
+
"x-ihg-api-key",
|
|
1139
|
+
"ihg-language",
|
|
1140
|
+
"x-ihg-sso-token",
|
|
1141
|
+
"x-accept-currency",
|
|
1142
|
+
"ihg-sellsource",
|
|
1143
|
+
"forter-token",
|
|
1144
|
+
"ihg-sessionid",
|
|
1145
|
+
"ihg-transactionid"
|
|
1146
|
+
],
|
|
1147
|
+
required: ["x-ihg-api-key"]
|
|
1148
|
+
},
|
|
1149
|
+
card: {
|
|
1150
|
+
pan: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cardNumber", "token"], format: "digits" },
|
|
1151
|
+
cvv: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cvvNumber", "token"], format: "digits" },
|
|
1152
|
+
exp_month: null,
|
|
1153
|
+
exp_year: null,
|
|
1154
|
+
expiry: { at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "expireDate"], format: "mmyy" },
|
|
1155
|
+
last4: null,
|
|
1156
|
+
leading: null,
|
|
1157
|
+
brands: [{ at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "cardCode"], map: null }],
|
|
1158
|
+
bins: null
|
|
1159
|
+
},
|
|
1160
|
+
merchant_key: null,
|
|
1161
|
+
requires: [
|
|
1162
|
+
{ at: ["imageStatus"], equals: "COMMIT" },
|
|
1163
|
+
{ at: ["segments", 0, "payment", "guaranteeType"], equals: "STANDARD_GUARANTEE" }
|
|
1164
|
+
],
|
|
1165
|
+
refuse: [
|
|
1166
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "paymentCard", "3dsData"], when: "present" },
|
|
1167
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "cupTokenId"], when: "present" },
|
|
1168
|
+
{ at: ["segments", 0, "payment", "paymentInformation", "paymentSource"], when: "present" }
|
|
1169
|
+
],
|
|
1170
|
+
payment_method: null,
|
|
1171
|
+
amount: null,
|
|
1172
|
+
currency: "USD",
|
|
1173
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1174
|
+
resends: [],
|
|
1175
|
+
challenge: null,
|
|
1176
|
+
facts: null,
|
|
1177
|
+
bootstrap: "/robots.txt",
|
|
1178
|
+
timeout_ms: 3e4,
|
|
1179
|
+
charge_timing: "deferred",
|
|
1180
|
+
once_per: null,
|
|
1181
|
+
pending: [
|
|
1182
|
+
"card.brands: the cardCode value for each brand",
|
|
1183
|
+
"card.expiry: that expirationMonth is always two digits, so expireDate is MMYY",
|
|
1184
|
+
"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",
|
|
1185
|
+
GUARANTEE_RATES
|
|
1186
|
+
]
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
id: "aa_process_payment",
|
|
1190
|
+
merchant: "American Airlines",
|
|
1191
|
+
status: "observe",
|
|
1192
|
+
evidence: [
|
|
1193
|
+
"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/.",
|
|
1194
|
+
"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}.",
|
|
1195
|
+
"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.",
|
|
1196
|
+
"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."
|
|
1197
|
+
],
|
|
1198
|
+
har: null,
|
|
1199
|
+
checkout: "https://www.aa.com",
|
|
1200
|
+
checkout_frame: false,
|
|
1201
|
+
frame: null,
|
|
1202
|
+
endpoint: "https://www.aa.com",
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
path: "/ecommerce/checkout-services/processpayment",
|
|
1205
|
+
query: {},
|
|
1206
|
+
navigation: null,
|
|
1207
|
+
encoding: "json",
|
|
1208
|
+
session: {
|
|
1209
|
+
cookies: true,
|
|
1210
|
+
cookie_names: null,
|
|
1211
|
+
headers: [...BROWSER_HEADERS, "content-type", "authorization", "x-xsrf-token"],
|
|
1212
|
+
required: []
|
|
1213
|
+
},
|
|
1214
|
+
card: {
|
|
1215
|
+
pan: { at: ["formOfPayments", 0, "creditCard", "cardNumber"], format: "digits" },
|
|
1216
|
+
cvv: { at: ["formOfPayments", 0, "creditCard", "cvvCode"], format: "digits" },
|
|
1217
|
+
exp_month: { at: ["formOfPayments", 0, "creditCard", "expirationMonth"], format: "as_typed" },
|
|
1218
|
+
exp_year: { at: ["formOfPayments", 0, "creditCard", "expirationYear"], format: "as_typed" },
|
|
1219
|
+
expiry: null,
|
|
1220
|
+
last4: null,
|
|
1221
|
+
leading: null,
|
|
1222
|
+
brands: [{ at: ["formOfPayments", 0, "creditCard", "creditCardType"], map: null }],
|
|
1223
|
+
bins: null
|
|
1224
|
+
},
|
|
1225
|
+
merchant_key: null,
|
|
1226
|
+
requires: [
|
|
1227
|
+
{ at: ["formOfPayments", 0, "formOfPaymentType"], equals: "CREDIT_CARD" },
|
|
1228
|
+
{ at: ["responseVersion"], equals: "v2" },
|
|
1229
|
+
{ at: ["cartId"], present: true }
|
|
1230
|
+
],
|
|
1231
|
+
refuse: [
|
|
1232
|
+
{ at: ["formOfPayments", 0, "threeDSStep"], when: "present" },
|
|
1233
|
+
{ at: ["formOfPayments", 0, "threeDSecureInfo"], when: "present" },
|
|
1234
|
+
{ at: ["formOfPayments", 0, "saveCardAttributes", "saveCardToAccount"], when: "true" },
|
|
1235
|
+
{ at: ["isRecurringPurchaseSelected"], when: "true" }
|
|
1236
|
+
],
|
|
1237
|
+
payment_method: null,
|
|
1238
|
+
amount: null,
|
|
1239
|
+
currency: "USD",
|
|
1240
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1241
|
+
resends: [],
|
|
1242
|
+
challenge: null,
|
|
1243
|
+
facts: null,
|
|
1244
|
+
bootstrap: "/robots.txt",
|
|
1245
|
+
timeout_ms: 3e4,
|
|
1246
|
+
charge_timing: "at_request",
|
|
1247
|
+
once_per: null,
|
|
1248
|
+
pending: [
|
|
1249
|
+
"card.brands: the creditCardType value for each brand",
|
|
1250
|
+
"card.exp_month and card.exp_year: the form the expiry parser writes them in",
|
|
1251
|
+
"which of authorization and x-xsrf-token the call requires",
|
|
1252
|
+
"whether some shoppers check out on checkout.aa.com, which this profile does not cover"
|
|
1253
|
+
]
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
id: "economybookings_vault",
|
|
1257
|
+
merchant: "EconomyBookings",
|
|
1258
|
+
status: "observe",
|
|
1259
|
+
evidence: [
|
|
1260
|
+
"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.",
|
|
1261
|
+
"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, ...}.",
|
|
1262
|
+
"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."
|
|
1263
|
+
],
|
|
1264
|
+
har: null,
|
|
1265
|
+
checkout: "https://www.economybookings.com",
|
|
1266
|
+
checkout_frame: false,
|
|
1267
|
+
frame: "https://vault.economybookings.com",
|
|
1268
|
+
endpoint: "https://vault-api.economybookings.com",
|
|
1269
|
+
method: "POST",
|
|
1270
|
+
path: "/api/v1/vault",
|
|
1271
|
+
query: {},
|
|
1272
|
+
navigation: null,
|
|
1273
|
+
encoding: "json",
|
|
1274
|
+
session: {
|
|
1275
|
+
cookies: false,
|
|
1276
|
+
cookie_names: null,
|
|
1277
|
+
headers: [...BROWSER_HEADERS, "content-type"],
|
|
1278
|
+
required: []
|
|
1279
|
+
},
|
|
1280
|
+
card: {
|
|
1281
|
+
pan: { at: ["cardNumber"], format: "digits" },
|
|
1282
|
+
cvv: { at: ["cvv"], format: "digits" },
|
|
1283
|
+
exp_month: { at: ["ccExpMonth"], format: "m_int" },
|
|
1284
|
+
exp_year: { at: ["ccExpYear"], format: "yy_int" },
|
|
1285
|
+
expiry: null,
|
|
1286
|
+
last4: null,
|
|
1287
|
+
leading: null,
|
|
1288
|
+
brands: [{ at: ["cardType"], map: { visa: "visa", mastercard: "mastercard" } }],
|
|
1289
|
+
bins: null
|
|
1290
|
+
},
|
|
1291
|
+
merchant_key: null,
|
|
1292
|
+
requires: [
|
|
1293
|
+
{ at: ["affiliateId"], equals: "economybookings" },
|
|
1294
|
+
{ at: ["reservationId"], present: true }
|
|
1295
|
+
],
|
|
1296
|
+
refuse: [{ at: ["transactionId"], when: "nonempty" }],
|
|
1297
|
+
payment_method: null,
|
|
1298
|
+
amount: null,
|
|
1299
|
+
currency: "USD",
|
|
1300
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1301
|
+
resends: [],
|
|
1302
|
+
challenge: null,
|
|
1303
|
+
facts: null,
|
|
1304
|
+
bootstrap: "/robots.txt",
|
|
1305
|
+
timeout_ms: 3e4,
|
|
1306
|
+
charge_timing: "at_request",
|
|
1307
|
+
once_per: null,
|
|
1308
|
+
pending: [
|
|
1309
|
+
"whether an AwaitingAddressData answer already holds an authorization, since the resubmission it asks for is refused here",
|
|
1310
|
+
"the currency: the booking is priced in the shopper's chosen currency, which the body does not name"
|
|
1311
|
+
]
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
id: "synxis_the_verb_hotel",
|
|
1315
|
+
merchant: "The Verb Hotel",
|
|
1316
|
+
status: "observe",
|
|
1317
|
+
evidence: [
|
|
1318
|
+
"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.",
|
|
1319
|
+
"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).",
|
|
1320
|
+
'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.',
|
|
1321
|
+
"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.",
|
|
1322
|
+
"A RedirectHtml answer with VendorStatus Redirected sends the page through a vendor or 3DS step and then itinerary/v2/ReservationPaymentCallback, with no card."
|
|
1323
|
+
],
|
|
1324
|
+
har: null,
|
|
1325
|
+
checkout: "https://be.synxis.com",
|
|
1326
|
+
checkout_frame: false,
|
|
1327
|
+
frame: null,
|
|
1328
|
+
endpoint: "https://be.synxis.com",
|
|
1329
|
+
method: "POST",
|
|
1330
|
+
path: "/gw/itinerary/v1/updateReservation",
|
|
1331
|
+
query: {},
|
|
1332
|
+
navigation: null,
|
|
1333
|
+
encoding: "json",
|
|
1334
|
+
session: {
|
|
1335
|
+
cookies: true,
|
|
1336
|
+
cookie_names: null,
|
|
1337
|
+
headers: [...BROWSER_HEADERS, "content-type", "context", "activityid", "authorization"],
|
|
1338
|
+
required: ["context", "activityid"]
|
|
1339
|
+
},
|
|
1340
|
+
card: {
|
|
1341
|
+
pan: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardNumber"], format: "digits" },
|
|
1342
|
+
cvv: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardSecurityCode"], format: "digits_if_asked" },
|
|
1343
|
+
exp_month: null,
|
|
1344
|
+
exp_year: null,
|
|
1345
|
+
expiry: { at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "expireDate"], format: "mm/yy" },
|
|
1346
|
+
last4: null,
|
|
1347
|
+
leading: null,
|
|
1348
|
+
brands: [{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "cardCode"], map: { visa: "VI", mastercard: "MC" } }],
|
|
1349
|
+
bins: null
|
|
1350
|
+
},
|
|
1351
|
+
merchant_key: null,
|
|
1352
|
+
requires: [
|
|
1353
|
+
{ at: ["Chain", "id"], one_of: [16362, "16362"] },
|
|
1354
|
+
{ at: ["Reservation", "Hotel", "id"], one_of: [61148, "61148"] },
|
|
1355
|
+
{ at: ["Reservation", "action"], equals: "Confirmed" },
|
|
1356
|
+
{ at: ["Reservation", "GuestList", 0, "Role"], equals: "Primary" },
|
|
1357
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "type"], equals: "CreditCard" }
|
|
1358
|
+
],
|
|
1359
|
+
refuse: [
|
|
1360
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "PaymentToken"], when: "nonempty" },
|
|
1361
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "consentToStoreCardData"], when: "true" },
|
|
1362
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "FintechPaymentProvider"], when: "nonempty" },
|
|
1363
|
+
{ at: ["Reservation", "GuestList", 0, "PaymentList", 0, "PaymentCard", "InstallmentOptionId"], when: "nonempty" }
|
|
1364
|
+
],
|
|
1365
|
+
payment_method: null,
|
|
1366
|
+
amount: null,
|
|
1367
|
+
currency: "USD",
|
|
1368
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1369
|
+
resends: [],
|
|
1370
|
+
challenge: null,
|
|
1371
|
+
facts: null,
|
|
1372
|
+
bootstrap: "/robots.txt",
|
|
1373
|
+
timeout_ms: 3e4,
|
|
1374
|
+
charge_timing: "deferred",
|
|
1375
|
+
once_per: null,
|
|
1376
|
+
pending: [
|
|
1377
|
+
"that the primary guest is GuestList[0] and its card PaymentList[0]",
|
|
1378
|
+
"whether the hotel's rates take a deposit at booking or only hold the card",
|
|
1379
|
+
GUARANTEE_RATES
|
|
1380
|
+
]
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
id: "runsignup_register_pay",
|
|
1384
|
+
merchant: "RunSignUp",
|
|
1385
|
+
status: "observe",
|
|
1386
|
+
evidence: [
|
|
1387
|
+
"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.",
|
|
1388
|
+
"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.",
|
|
1389
|
+
"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."
|
|
1390
|
+
],
|
|
1391
|
+
har: null,
|
|
1392
|
+
checkout: "https://runsignup.com",
|
|
1393
|
+
checkout_frame: false,
|
|
1394
|
+
frame: null,
|
|
1395
|
+
endpoint: "https://runsignup.com",
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
path: "/Race/Register/",
|
|
1398
|
+
query: { regToken: ":regToken", raceId: ":raceId", track: "pay" },
|
|
1399
|
+
navigation: null,
|
|
1400
|
+
encoding: "form",
|
|
1401
|
+
session: {
|
|
1402
|
+
cookies: true,
|
|
1403
|
+
cookie_names: null,
|
|
1404
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-requested-with"],
|
|
1405
|
+
required: []
|
|
1406
|
+
},
|
|
1407
|
+
card: {
|
|
1408
|
+
pan: { at: "payment[cardNumber]", format: "digits" },
|
|
1409
|
+
cvv: { at: "payment[cvv]", format: "digits" },
|
|
1410
|
+
exp_month: { at: "payment[cardExpiresMonth]", format: "as_typed" },
|
|
1411
|
+
exp_year: { at: "payment[cardExpiresYear]", format: "as_typed" },
|
|
1412
|
+
expiry: null,
|
|
1413
|
+
last4: null,
|
|
1414
|
+
leading: null,
|
|
1415
|
+
brands: [],
|
|
1416
|
+
bins: null
|
|
1417
|
+
},
|
|
1418
|
+
merchant_key: null,
|
|
1419
|
+
requires: [],
|
|
1420
|
+
refuse: [],
|
|
1421
|
+
payment_method: null,
|
|
1422
|
+
amount: null,
|
|
1423
|
+
currency: "USD",
|
|
1424
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1425
|
+
resends: [],
|
|
1426
|
+
challenge: null,
|
|
1427
|
+
facts: null,
|
|
1428
|
+
bootstrap: "/robots.txt",
|
|
1429
|
+
timeout_ms: 3e4,
|
|
1430
|
+
charge_timing: "at_request",
|
|
1431
|
+
once_per: null,
|
|
1432
|
+
pending: [
|
|
1433
|
+
"that Confirm Payment posts the form as an XHR to its own pay step URL, not as a document",
|
|
1434
|
+
"the card field names today: the four above come from an archived form",
|
|
1435
|
+
"the name of the Save my credit card field, for a refuse rule",
|
|
1436
|
+
"card.exp_month and card.exp_year: the values the Month and Year menus send"
|
|
1437
|
+
]
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
id: "mindbody_explore_billing",
|
|
1441
|
+
merchant: "Mindbody",
|
|
1442
|
+
status: "observe",
|
|
1443
|
+
evidence: [
|
|
1444
|
+
"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.",
|
|
1445
|
+
"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.",
|
|
1446
|
+
"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.",
|
|
1447
|
+
"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."
|
|
1448
|
+
],
|
|
1449
|
+
har: null,
|
|
1450
|
+
checkout: "https://www.mindbodyonline.com",
|
|
1451
|
+
checkout_frame: false,
|
|
1452
|
+
frame: null,
|
|
1453
|
+
endpoint: "https://prod-mkt-gateway.mindbody.io",
|
|
1454
|
+
method: "POST",
|
|
1455
|
+
path: "/v1/user/billing",
|
|
1456
|
+
query: {},
|
|
1457
|
+
navigation: null,
|
|
1458
|
+
encoding: "json_api",
|
|
1459
|
+
session: {
|
|
1460
|
+
cookies: false,
|
|
1461
|
+
cookie_names: null,
|
|
1462
|
+
headers: [
|
|
1463
|
+
...BROWSER_HEADERS,
|
|
1464
|
+
"content-type",
|
|
1465
|
+
"authorization",
|
|
1466
|
+
"x-mb-app-build",
|
|
1467
|
+
"x-mb-app-name",
|
|
1468
|
+
"x-mb-app-version",
|
|
1469
|
+
"x-mb-user-session-id",
|
|
1470
|
+
"x-mb-on-behalf-of"
|
|
1471
|
+
],
|
|
1472
|
+
required: ["authorization", "x-mb-app-name", "x-mb-user-session-id"]
|
|
1473
|
+
},
|
|
1474
|
+
card: {
|
|
1475
|
+
pan: { at: ["data", "attributes", "card_number"], format: "digits" },
|
|
1476
|
+
cvv: { at: ["data", "attributes", "cvv"], format: "digits" },
|
|
1477
|
+
exp_month: { at: ["data", "attributes", "expiration_month"], format: "as_typed" },
|
|
1478
|
+
exp_year: { at: ["data", "attributes", "expiration_year"], format: "as_typed" },
|
|
1479
|
+
expiry: null,
|
|
1480
|
+
last4: null,
|
|
1481
|
+
leading: null,
|
|
1482
|
+
brands: [],
|
|
1483
|
+
bins: null
|
|
1484
|
+
},
|
|
1485
|
+
merchant_key: null,
|
|
1486
|
+
requires: [
|
|
1487
|
+
{ at: ["data", "type"], equals: "userBilling" },
|
|
1488
|
+
{ at: ["data", "attributes", "primary_card"], equals: false }
|
|
1489
|
+
],
|
|
1490
|
+
refuse: [],
|
|
1491
|
+
payment_method: null,
|
|
1492
|
+
amount: null,
|
|
1493
|
+
currency: "USD",
|
|
1494
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1495
|
+
resends: [],
|
|
1496
|
+
challenge: null,
|
|
1497
|
+
facts: null,
|
|
1498
|
+
bootstrap: "/robots.txt",
|
|
1499
|
+
timeout_ms: 3e4,
|
|
1500
|
+
charge_timing: "deferred",
|
|
1501
|
+
once_per: null,
|
|
1502
|
+
pending: [
|
|
1503
|
+
"the attribute names addBillingInfo writes, and whether card_number is sent without the spaces the input shows",
|
|
1504
|
+
"card.exp_month and card.exp_year: the values the expiry menus send",
|
|
1505
|
+
"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"
|
|
1506
|
+
]
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
id: "lookfantastic_place_order",
|
|
1510
|
+
merchant: "LookFantastic",
|
|
1511
|
+
status: "observe",
|
|
1512
|
+
evidence: [
|
|
1513
|
+
"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.",
|
|
1514
|
+
"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.",
|
|
1515
|
+
"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}.",
|
|
1516
|
+
"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."
|
|
1517
|
+
],
|
|
1518
|
+
har: null,
|
|
1519
|
+
checkout: "https://checkout.lookfantastic.com",
|
|
1520
|
+
checkout_frame: false,
|
|
1521
|
+
frame: null,
|
|
1522
|
+
endpoint: "https://checkout.lookfantastic.com",
|
|
1523
|
+
method: "POST",
|
|
1524
|
+
path: "/checkout-api/v2/order/place-order",
|
|
1525
|
+
query: {},
|
|
1526
|
+
navigation: null,
|
|
1527
|
+
encoding: "json",
|
|
1528
|
+
session: {
|
|
1529
|
+
cookies: true,
|
|
1530
|
+
cookie_names: null,
|
|
1531
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-thg-client", "sessionid"],
|
|
1532
|
+
required: ["x-thg-client", "sessionid"]
|
|
1533
|
+
},
|
|
1534
|
+
card: {
|
|
1535
|
+
pan: { at: ["payment", "creditCardNumber"], format: "digits" },
|
|
1536
|
+
cvv: { at: ["payment", "cv2"], format: "digits" },
|
|
1537
|
+
exp_month: { at: ["payment", "expirationMonth"], format: "as_typed" },
|
|
1538
|
+
exp_year: { at: ["payment", "expirationYear"], format: "as_typed" },
|
|
1539
|
+
expiry: null,
|
|
1540
|
+
last4: null,
|
|
1541
|
+
leading: null,
|
|
1542
|
+
brands: [],
|
|
1543
|
+
bins: null
|
|
1544
|
+
},
|
|
1545
|
+
merchant_key: null,
|
|
1546
|
+
requires: [{ at: ["payment", "method"], equals: "new-credit-card" }],
|
|
1547
|
+
refuse: [
|
|
1548
|
+
{ at: ["payment", "consentToReuse"], when: "true" },
|
|
1549
|
+
{ at: ["payment", "cardDataProvider"], when: "present" },
|
|
1550
|
+
{ at: ["reCaptchaToken"], when: "nonempty" }
|
|
1551
|
+
],
|
|
1552
|
+
payment_method: null,
|
|
1553
|
+
amount: null,
|
|
1554
|
+
currency: "GBP",
|
|
1555
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1556
|
+
resends: [],
|
|
1557
|
+
challenge: null,
|
|
1558
|
+
facts: null,
|
|
1559
|
+
bootstrap: "/robots.txt",
|
|
1560
|
+
timeout_ms: 3e4,
|
|
1561
|
+
charge_timing: "at_request",
|
|
1562
|
+
once_per: null,
|
|
1563
|
+
pending: [
|
|
1564
|
+
"which card path lookfantastic.com has on: its own card fields (this profile), BR-DGE hosted fields or Adyen V2",
|
|
1565
|
+
"card.exp_month and card.exp_year: the form expirationMonth and expirationYear are sent in",
|
|
1566
|
+
"the currency: the order is priced in the shopper's currency_V6 cookie, which the body does not name",
|
|
1567
|
+
"whether the site fills reCaptchaToken, and a policy for a captcha token in the card request"
|
|
1568
|
+
]
|
|
1569
|
+
},
|
|
1570
|
+
// ── Merchants whose own page encrypts the card under the merchant's key ──────
|
|
1571
|
+
// Each ships in observe with pending items: until a HAR pins a key the device has
|
|
1572
|
+
// no key to encrypt under, and the requests around the card request (a second
|
|
1573
|
+
// token call, a page navigation, a re-sent ciphertext) are not in this mode yet.
|
|
1574
|
+
{
|
|
1575
|
+
id: "hyatt_card_token",
|
|
1576
|
+
merchant: "Hyatt",
|
|
1577
|
+
status: "observe",
|
|
1578
|
+
evidence: [
|
|
1579
|
+
"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.",
|
|
1580
|
+
"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}.",
|
|
1581
|
+
"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)."
|
|
1582
|
+
],
|
|
1583
|
+
har: null,
|
|
1584
|
+
checkout: "https://www.hyatt.com",
|
|
1585
|
+
checkout_frame: false,
|
|
1586
|
+
frame: null,
|
|
1587
|
+
endpoint: "https://www.hyatt.com",
|
|
1588
|
+
method: "POST",
|
|
1589
|
+
path: "/v1/cc/secure/tokens",
|
|
1590
|
+
query: {},
|
|
1591
|
+
navigation: null,
|
|
1592
|
+
encoding: "json",
|
|
1593
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type"], required: [] },
|
|
1594
|
+
card: {
|
|
1595
|
+
pan: { at: ["pan"], format: "encrypted", plaintext: "digits" },
|
|
1596
|
+
cvv: null,
|
|
1597
|
+
exp_month: { at: ["expMonth"], format: "mm" },
|
|
1598
|
+
exp_year: { at: ["expYear"], format: "yyyy" },
|
|
1599
|
+
expiry: null,
|
|
1600
|
+
last4: null,
|
|
1601
|
+
leading: null,
|
|
1602
|
+
brands: [{ at: ["type"], map: { visa: "VS", mastercard: "MC", amex: "AX", discover: "DS" } }],
|
|
1603
|
+
bins: null
|
|
1604
|
+
},
|
|
1605
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-256", mgf1: "SHA-1", kid_at: null, keys: [] },
|
|
1606
|
+
requires: [],
|
|
1607
|
+
refuse: [],
|
|
1608
|
+
payment_method: null,
|
|
1609
|
+
amount: null,
|
|
1610
|
+
currency: "USD",
|
|
1611
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1612
|
+
resends: [],
|
|
1613
|
+
challenge: null,
|
|
1614
|
+
facts: null,
|
|
1615
|
+
bootstrap: "/robots.txt",
|
|
1616
|
+
timeout_ms: 3e4,
|
|
1617
|
+
charge_timing: "deferred",
|
|
1618
|
+
once_per: null,
|
|
1619
|
+
pending: [
|
|
1620
|
+
"merchant_key.keys: the key /v1/cc/secure/key serves, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1621
|
+
"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",
|
|
1622
|
+
"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",
|
|
1623
|
+
"whether the card number is encrypted as typed, with spaces",
|
|
1624
|
+
"the booking currency: the request names none, so the profile takes stays priced in US dollars only"
|
|
1625
|
+
]
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
id: "hyatt_cvv_token",
|
|
1629
|
+
merchant: "Hyatt",
|
|
1630
|
+
status: "observe",
|
|
1631
|
+
evidence: [
|
|
1632
|
+
"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.",
|
|
1633
|
+
"A saved-card booking posts the same body to the same path, for a card Hyatt already holds."
|
|
1634
|
+
],
|
|
1635
|
+
har: null,
|
|
1636
|
+
checkout: "https://www.hyatt.com",
|
|
1637
|
+
checkout_frame: false,
|
|
1638
|
+
frame: null,
|
|
1639
|
+
endpoint: "https://www.hyatt.com",
|
|
1640
|
+
method: "POST",
|
|
1641
|
+
path: "/v1/cvv/secure/tokens",
|
|
1642
|
+
query: {},
|
|
1643
|
+
navigation: null,
|
|
1644
|
+
encoding: "json",
|
|
1645
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type"], required: [] },
|
|
1646
|
+
card: {
|
|
1647
|
+
pan: null,
|
|
1648
|
+
cvv: { at: ["cvv"], format: "encrypted", plaintext: "digits" },
|
|
1649
|
+
exp_month: null,
|
|
1650
|
+
exp_year: null,
|
|
1651
|
+
expiry: null,
|
|
1652
|
+
last4: null,
|
|
1653
|
+
leading: null,
|
|
1654
|
+
brands: [],
|
|
1655
|
+
bins: null
|
|
1656
|
+
},
|
|
1657
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-256", mgf1: "SHA-1", kid_at: null, keys: [] },
|
|
1658
|
+
requires: [],
|
|
1659
|
+
refuse: [],
|
|
1660
|
+
payment_method: null,
|
|
1661
|
+
amount: null,
|
|
1662
|
+
currency: "USD",
|
|
1663
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1664
|
+
resends: [],
|
|
1665
|
+
challenge: null,
|
|
1666
|
+
facts: null,
|
|
1667
|
+
bootstrap: "/robots.txt",
|
|
1668
|
+
timeout_ms: 3e4,
|
|
1669
|
+
charge_timing: "deferred",
|
|
1670
|
+
once_per: null,
|
|
1671
|
+
pending: [
|
|
1672
|
+
"merchant_key.keys: the key /v1/cc/secure/key serves, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1673
|
+
"one approval for both token calls: this one carries the security code of the card hyatt_card_token carries",
|
|
1674
|
+
"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",
|
|
1675
|
+
"makeReservation is a page navigation this mode does not rewrite (see hyatt_card_token)"
|
|
1676
|
+
]
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
id: "lifemiles_process_payment",
|
|
1680
|
+
merchant: "LifeMiles",
|
|
1681
|
+
status: "observe",
|
|
1682
|
+
evidence: [
|
|
1683
|
+
"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>.",
|
|
1684
|
+
'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.',
|
|
1685
|
+
"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.",
|
|
1686
|
+
"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."
|
|
1687
|
+
],
|
|
1688
|
+
har: null,
|
|
1689
|
+
checkout: "https://www.lifemiles.com",
|
|
1690
|
+
checkout_frame: false,
|
|
1691
|
+
frame: null,
|
|
1692
|
+
endpoint: "https://api.lifemiles.com",
|
|
1693
|
+
method: "POST",
|
|
1694
|
+
path: "/svc/checkout-new-flow-process-payment",
|
|
1695
|
+
query: {},
|
|
1696
|
+
navigation: null,
|
|
1697
|
+
encoding: "json",
|
|
1698
|
+
session: { cookies: false, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type", "authorization", "realm"], required: ["authorization", "realm"] },
|
|
1699
|
+
card: {
|
|
1700
|
+
pan: { at: ["fop", "parameter", { code: "CCNUMCARD" }, "value"], format: "encrypted", plaintext: "digits" },
|
|
1701
|
+
cvv: { at: ["fop", "parameter", { code: "CCCODCVV" }, "value"], format: "encrypted", plaintext: "digits" },
|
|
1702
|
+
exp_month: null,
|
|
1703
|
+
exp_year: null,
|
|
1704
|
+
expiry: { at: ["fop", "parameter", { code: "CCFECEXP" }, "value"], format: "encrypted", plaintext: "MMYYYY" },
|
|
1705
|
+
last4: null,
|
|
1706
|
+
leading: { at: ["fop", "parameter", { code: "CCBIN" }, "value"], digits: 7 },
|
|
1707
|
+
brands: [{ at: ["fop", "parameter", { code: "CCFRANCHISE" }, "value"], map: { visa: "VI", mastercard: "CA", amex: "AX", discover: "DS" } }],
|
|
1708
|
+
bins: null
|
|
1709
|
+
},
|
|
1710
|
+
merchant_key: {
|
|
1711
|
+
scheme: "rsa_oaep",
|
|
1712
|
+
hash: "SHA-512",
|
|
1713
|
+
mgf1: "SHA-512",
|
|
1714
|
+
kid_at: null,
|
|
1715
|
+
keys: [{
|
|
1716
|
+
kid: null,
|
|
1717
|
+
spki: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdiBpp7T3C7Ac6R+FTYimtCNzmQLRrCPVLrUZ2wjhjrGMsfeLt4EPbqw168mtJahzysvB+bjB5XZPiAci//nn+pDQK+x9iXS2Lb0RrTDwS8ey39THybEGw0295g4kQO8ZAw5uc9p+hkVcKwgOIkG5fe8GyQlFMhoySQhj/L75BGsbIbojybsW9AJ9K1KKy0CGCiCre6L5x9kuKfkFR0Q7khWTncmKenbkPOvgQXCg5r4+6Nk15imaiR4O1HKHInkPRSdu19kBYBqVXxXm3lYtY3NCOtPS0irRrAdi4UfT46/81ugvEm7y2xuFv32MSJIAzpAC4jHbB+HUD4sJQpVsQIDAQAB",
|
|
1718
|
+
sha256: "dc2a6ee45bf21c652dc3286244fa80e455200181e71a6b34d9d9a525a4ecfe79"
|
|
1719
|
+
}]
|
|
1720
|
+
},
|
|
1721
|
+
requires: [
|
|
1722
|
+
{ at: ["fop", "type"], equals: "CC" },
|
|
1723
|
+
{ at: ["partner"], one_of: ["lmbex", "avanc", "buylm", "tsflm"] }
|
|
1724
|
+
],
|
|
1725
|
+
refuse: [
|
|
1726
|
+
{ at: ["saveCard"], when: "true" },
|
|
1727
|
+
{ at: ["fop", "parameter", { code: "CCTOKEN" }], when: "present" }
|
|
1728
|
+
],
|
|
1729
|
+
payment_method: null,
|
|
1730
|
+
amount: null,
|
|
1731
|
+
currency: { at: ["currency"] },
|
|
1732
|
+
response: { headers: ["content-type"], withhold: [] },
|
|
1733
|
+
resends: [],
|
|
1734
|
+
challenge: null,
|
|
1735
|
+
facts: null,
|
|
1736
|
+
bootstrap: "/robots.txt",
|
|
1737
|
+
timeout_ms: 3e4,
|
|
1738
|
+
charge_timing: "at_request",
|
|
1739
|
+
once_per: null,
|
|
1740
|
+
pending: [
|
|
1741
|
+
"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",
|
|
1742
|
+
"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)"
|
|
1743
|
+
]
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
id: "southwest_booking_confirmation",
|
|
1747
|
+
merchant: "Southwest Airlines",
|
|
1748
|
+
status: "observe",
|
|
1749
|
+
evidence: [
|
|
1750
|
+
"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.",
|
|
1751
|
+
"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.",
|
|
1752
|
+
"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."
|
|
1753
|
+
],
|
|
1754
|
+
har: null,
|
|
1755
|
+
checkout: "https://www.southwest.com",
|
|
1756
|
+
checkout_frame: false,
|
|
1757
|
+
frame: null,
|
|
1758
|
+
endpoint: "https://www.southwest.com",
|
|
1759
|
+
method: "POST",
|
|
1760
|
+
path: "/api/air-booking/v1/air-booking/page/air/booking/confirmation",
|
|
1761
|
+
query: {},
|
|
1762
|
+
navigation: null,
|
|
1763
|
+
encoding: "json",
|
|
1764
|
+
session: {
|
|
1765
|
+
cookies: true,
|
|
1766
|
+
cookie_names: null,
|
|
1767
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-api-key", "x-app-id", "x-app-version", "x-channel-id", "x-user-experience-id"],
|
|
1768
|
+
required: ["x-api-key", "x-channel-id"]
|
|
1769
|
+
},
|
|
1770
|
+
card: {
|
|
1771
|
+
pan: { at: ["creditCard", "encryptedCardNumber"], format: "encrypted", plaintext: { claims: ["creditCardNumbers", "cardNumber1"] } },
|
|
1772
|
+
cvv: { at: ["creditCard", "securityCode"], format: "digits" },
|
|
1773
|
+
exp_month: null,
|
|
1774
|
+
exp_year: null,
|
|
1775
|
+
expiry: { at: ["creditCard", "expiration"], format: "MM-YY" },
|
|
1776
|
+
last4: null,
|
|
1777
|
+
leading: null,
|
|
1778
|
+
brands: [{ at: ["creditCard", "creditCardType"], map: { visa: "VISA", mastercard: "MASTERCARD", amex: "AMEX", discover: "DISCOVER" } }],
|
|
1779
|
+
bins: null
|
|
1780
|
+
},
|
|
1781
|
+
merchant_key: { scheme: "jwe", alg: null, enc: "A256CBC-HS512", header: { typ: "JWT" }, keys: [] },
|
|
1782
|
+
requires: [{ at: ["paymentMethodSelected"], equals: "creditcard" }],
|
|
1783
|
+
refuse: [
|
|
1784
|
+
{ at: ["creditCard", "descriptionEnabled"], when: "true" },
|
|
1785
|
+
{ at: ["creditCard", "primary"], when: "true" },
|
|
1786
|
+
{ at: ["savedCreditCardSelected"], when: "true" },
|
|
1787
|
+
{ at: ["savedCreditCardToken"], when: "nonempty" },
|
|
1788
|
+
{ at: ["selectedCardInfo"], when: "nonempty" },
|
|
1789
|
+
{ at: ["fundsCoverTotalFlight"], when: "true" }
|
|
1790
|
+
],
|
|
1791
|
+
payment_method: null,
|
|
1792
|
+
amount: null,
|
|
1793
|
+
currency: "USD",
|
|
1794
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1795
|
+
resends: [],
|
|
1796
|
+
challenge: null,
|
|
1797
|
+
facts: null,
|
|
1798
|
+
bootstrap: "/robots.txt",
|
|
1799
|
+
timeout_ms: 3e4,
|
|
1800
|
+
charge_timing: "at_request",
|
|
1801
|
+
once_per: null,
|
|
1802
|
+
pending: [
|
|
1803
|
+
"merchant_key.keys: the key the cyberJwks JWKS marks use enc, pinned with its kid by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1804
|
+
"merchant_key.alg: the alg the JWK names, which the page writes into the protected header",
|
|
1805
|
+
"card.expiry: whether expiration carries MM-YY or MM-YYYY",
|
|
1806
|
+
"card.brands: whether the form sends RRVISA for a Rapid Rewards Visa, where the map writes VISA",
|
|
1807
|
+
"a trip insurance purchase sends the same encrypted number to /api/payments/v2/fund-tokens, a second card request this profile does not take",
|
|
1808
|
+
"the page re-encrypts and posts again when the answer names an expired key, which asks for a second approval"
|
|
1809
|
+
]
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
id: "united_card_token",
|
|
1813
|
+
merchant: "United Airlines",
|
|
1814
|
+
status: "observe",
|
|
1815
|
+
evidence: [
|
|
1816
|
+
"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.",
|
|
1817
|
+
"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.",
|
|
1818
|
+
"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."
|
|
1819
|
+
],
|
|
1820
|
+
har: null,
|
|
1821
|
+
checkout: "https://www.united.com",
|
|
1822
|
+
checkout_frame: false,
|
|
1823
|
+
frame: null,
|
|
1824
|
+
endpoint: "https://www.united.com",
|
|
1825
|
+
method: "POST",
|
|
1826
|
+
path: "/api/user/accountNumberTokenOAEP",
|
|
1827
|
+
query: {},
|
|
1828
|
+
navigation: null,
|
|
1829
|
+
encoding: "json",
|
|
1830
|
+
session: { cookies: true, cookie_names: null, headers: [...BROWSER_HEADERS, "content-type", "x-authorization-api"], required: ["x-authorization-api"] },
|
|
1831
|
+
card: {
|
|
1832
|
+
pan: { at: ["ProfileCreditCard", "AccountNumberEncrypted"], format: "encrypted", plaintext: "digits" },
|
|
1833
|
+
cvv: { at: ["ProfileCreditCard", "SecurityCode"], format: "digits" },
|
|
1834
|
+
exp_month: null,
|
|
1835
|
+
exp_year: null,
|
|
1836
|
+
expiry: { at: ["ProfileCreditCard", "ExpirationDate"], format: "MM/YY" },
|
|
1837
|
+
last4: null,
|
|
1838
|
+
leading: null,
|
|
1839
|
+
brands: [{ at: ["ProfileCreditCard", "Code"], map: null }],
|
|
1840
|
+
bins: null
|
|
1841
|
+
},
|
|
1842
|
+
merchant_key: { scheme: "rsa_oaep", hash: "SHA-1", mgf1: "SHA-1", kid_at: ["Kid"], keys: [] },
|
|
1843
|
+
requires: [],
|
|
1844
|
+
refuse: [],
|
|
1845
|
+
payment_method: null,
|
|
1846
|
+
amount: null,
|
|
1847
|
+
currency: "USD",
|
|
1848
|
+
response: { headers: ["content-type", "set-cookie"], withhold: [] },
|
|
1849
|
+
resends: [],
|
|
1850
|
+
challenge: null,
|
|
1851
|
+
facts: null,
|
|
1852
|
+
bootstrap: "/robots.txt",
|
|
1853
|
+
timeout_ms: 3e4,
|
|
1854
|
+
charge_timing: "deferred",
|
|
1855
|
+
once_per: null,
|
|
1856
|
+
pending: [
|
|
1857
|
+
"merchant_key.keys: the key /api/User/PublicKeyNonPCI serves and its Kid, pinned by the SHA-256 of its SubjectPublicKeyInfo",
|
|
1858
|
+
"card.brands: the Code GetCreditCardType answers for each network",
|
|
1859
|
+
"card.expiry: whether ExpirationDate carries MM/YY or MM/YYYY",
|
|
1860
|
+
"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",
|
|
1861
|
+
"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"
|
|
1862
|
+
]
|
|
1863
|
+
},
|
|
1864
|
+
// ── Document form navigations: the page submits a form that navigates ──────
|
|
1865
|
+
{
|
|
1866
|
+
id: "myrec_athens_clarke_payment",
|
|
1867
|
+
merchant: "Athens-Clarke County Leisure Services",
|
|
1868
|
+
status: "observe",
|
|
1869
|
+
evidence: [
|
|
1870
|
+
`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.`,
|
|
1871
|
+
"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.",
|
|
1872
|
+
"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.",
|
|
1873
|
+
"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."
|
|
1874
|
+
],
|
|
1875
|
+
har: null,
|
|
1876
|
+
checkout: "https://secure.myrecdept.com",
|
|
1877
|
+
checkout_frame: false,
|
|
1878
|
+
frame: null,
|
|
1879
|
+
endpoint: "https://secure.myrecdept.com",
|
|
1880
|
+
method: "POST",
|
|
1881
|
+
path: "/auth/default.aspx",
|
|
1882
|
+
query: { did: "54048", authkey: ":authkey" },
|
|
1883
|
+
navigation: "top",
|
|
1884
|
+
encoding: "form",
|
|
1885
|
+
session: {
|
|
1886
|
+
cookies: true,
|
|
1887
|
+
cookie_names: ["ASP.NET_SessionId"],
|
|
1888
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
1889
|
+
required: []
|
|
1890
|
+
},
|
|
1891
|
+
card: {
|
|
1892
|
+
pan: { at: "ctl00$Content$txtAccountNumber", format: "digits" },
|
|
1893
|
+
cvv: { at: "ctl00$Content$txtCvvNumber", format: "digits" },
|
|
1894
|
+
exp_month: { at: "ctl00$Content$lstExpMonth", format: "mm" },
|
|
1895
|
+
exp_year: { at: "ctl00$Content$lstExpYear", format: "yyyy" },
|
|
1896
|
+
expiry: null,
|
|
1897
|
+
last4: null,
|
|
1898
|
+
leading: null,
|
|
1899
|
+
brands: [],
|
|
1900
|
+
bins: null
|
|
1901
|
+
},
|
|
1902
|
+
merchant_key: null,
|
|
1903
|
+
requires: [
|
|
1904
|
+
{ at: "ctl00$Content$btnSubmitPayment", present: true },
|
|
1905
|
+
{ at: "__EVENTTARGET", equals: "" },
|
|
1906
|
+
{ at: "__VIEWSTATE", present: true },
|
|
1907
|
+
{ at: "__EVENTVALIDATION", present: true }
|
|
1908
|
+
],
|
|
1909
|
+
refuse: [
|
|
1910
|
+
{ at: "ctl00$Content$btnCancel", when: "present" },
|
|
1911
|
+
{ at: "g-recaptcha-response", when: "nonempty" }
|
|
1912
|
+
],
|
|
1913
|
+
payment_method: null,
|
|
1914
|
+
amount: null,
|
|
1915
|
+
currency: "USD",
|
|
1916
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
1917
|
+
resends: [],
|
|
1918
|
+
challenge: null,
|
|
1919
|
+
facts: null,
|
|
1920
|
+
bootstrap: "/robots.txt",
|
|
1921
|
+
timeout_ms: 6e4,
|
|
1922
|
+
charge_timing: "at_request",
|
|
1923
|
+
once_per: { query: "authkey" },
|
|
1924
|
+
pending: [
|
|
1925
|
+
"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",
|
|
1926
|
+
"which cookies the postback needs: ASP.NET_SessionId, another, or none",
|
|
1927
|
+
"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",
|
|
1928
|
+
"that the bootstrap path answers without a server error"
|
|
1929
|
+
]
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
id: "ladot_etims_guest_permit",
|
|
1933
|
+
merchant: "City of Los Angeles guest parking permits",
|
|
1934
|
+
status: "observe",
|
|
1935
|
+
evidence: [
|
|
1936
|
+
"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.",
|
|
1937
|
+
"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.",
|
|
1938
|
+
"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.",
|
|
1939
|
+
"Every eTIMS form posts as a document navigation, bound to the HttpOnly JSESSIONID cookie each page sets."
|
|
1940
|
+
],
|
|
1941
|
+
har: null,
|
|
1942
|
+
checkout: "https://wmq.etimspayments.com",
|
|
1943
|
+
checkout_frame: false,
|
|
1944
|
+
frame: null,
|
|
1945
|
+
endpoint: "https://wmq.etimspayments.com",
|
|
1946
|
+
method: "POST",
|
|
1947
|
+
path: "/pbw/rppVirtualMerchantSubmitAction.doh",
|
|
1948
|
+
query: {},
|
|
1949
|
+
navigation: "top",
|
|
1950
|
+
encoding: "form",
|
|
1951
|
+
session: {
|
|
1952
|
+
cookies: true,
|
|
1953
|
+
cookie_names: ["JSESSIONID"],
|
|
1954
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
1955
|
+
required: []
|
|
1956
|
+
},
|
|
1957
|
+
card: {
|
|
1958
|
+
pan: { at: "cardNumber", format: "digits" },
|
|
1959
|
+
cvv: { at: "cvv", format: "digits" },
|
|
1960
|
+
exp_month: { at: "expMonth", format: "as_typed" },
|
|
1961
|
+
exp_year: { at: "expYear", format: "as_typed" },
|
|
1962
|
+
expiry: null,
|
|
1963
|
+
last4: null,
|
|
1964
|
+
leading: null,
|
|
1965
|
+
brands: [],
|
|
1966
|
+
bins: null
|
|
1967
|
+
},
|
|
1968
|
+
merchant_key: null,
|
|
1969
|
+
requires: [{ at: "clientcode", equals: "17" }],
|
|
1970
|
+
refuse: [],
|
|
1971
|
+
payment_method: null,
|
|
1972
|
+
amount: null,
|
|
1973
|
+
currency: "USD",
|
|
1974
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
1975
|
+
resends: [],
|
|
1976
|
+
challenge: null,
|
|
1977
|
+
facts: null,
|
|
1978
|
+
bootstrap: "/robots.txt",
|
|
1979
|
+
timeout_ms: 6e4,
|
|
1980
|
+
charge_timing: "at_request",
|
|
1981
|
+
once_per: { cookie: "JSESSIONID" },
|
|
1982
|
+
pending: [
|
|
1983
|
+
"the guest permit card action: rppVirtualMerchantSubmitAction.doh is the renewal action on the same template, and the guest flow's own is not archived",
|
|
1984
|
+
"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",
|
|
1985
|
+
"whether the card form carries clientcode 17, and which of its buttons post to the same action",
|
|
1986
|
+
"which request carries the card: the submit action or the confirmAction.doh step after it",
|
|
1987
|
+
"that the Virtual Merchant step does not hand the browser to Converge's hosted page, which would make this Converge processor work instead",
|
|
1988
|
+
"the answer to the card post, and whether it echoes the card",
|
|
1989
|
+
"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",
|
|
1990
|
+
"that the bootstrap path answers without a server error"
|
|
1991
|
+
]
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
id: "sfmta_etims_citation",
|
|
1995
|
+
merchant: "San Francisco parking citations",
|
|
1996
|
+
status: "observe",
|
|
1997
|
+
evidence: [
|
|
1998
|
+
"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.",
|
|
1999
|
+
"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.",
|
|
2000
|
+
"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.",
|
|
2001
|
+
"Every eTIMS form posts as a document navigation, bound to the HttpOnly JSESSIONID cookie each page sets."
|
|
2002
|
+
],
|
|
2003
|
+
har: null,
|
|
2004
|
+
checkout: "https://wmq.etimspayments.com",
|
|
2005
|
+
checkout_frame: false,
|
|
2006
|
+
frame: null,
|
|
2007
|
+
endpoint: "https://wmq.etimspayments.com",
|
|
2008
|
+
method: "POST",
|
|
2009
|
+
path: "/pbw/transFirstPBWMultiStepSubmitPage.doh",
|
|
2010
|
+
query: {},
|
|
2011
|
+
navigation: "top",
|
|
2012
|
+
encoding: "form",
|
|
2013
|
+
session: {
|
|
2014
|
+
cookies: true,
|
|
2015
|
+
cookie_names: ["JSESSIONID"],
|
|
2016
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2017
|
+
required: []
|
|
2018
|
+
},
|
|
2019
|
+
card: {
|
|
2020
|
+
pan: { at: "cardNumber", format: "digits" },
|
|
2021
|
+
cvv: { at: "cvv", format: "digits" },
|
|
2022
|
+
exp_month: { at: "expMonth", format: "as_typed" },
|
|
2023
|
+
exp_year: { at: "expYear", format: "as_typed" },
|
|
2024
|
+
expiry: null,
|
|
2025
|
+
last4: null,
|
|
2026
|
+
leading: null,
|
|
2027
|
+
brands: [],
|
|
2028
|
+
bins: null
|
|
2029
|
+
},
|
|
2030
|
+
merchant_key: null,
|
|
2031
|
+
requires: [{ at: "clientcode", equals: "19" }],
|
|
2032
|
+
refuse: [],
|
|
2033
|
+
payment_method: null,
|
|
2034
|
+
amount: null,
|
|
2035
|
+
currency: "USD",
|
|
2036
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2037
|
+
resends: [],
|
|
2038
|
+
challenge: null,
|
|
2039
|
+
facts: null,
|
|
2040
|
+
bootstrap: "/robots.txt",
|
|
2041
|
+
timeout_ms: 6e4,
|
|
2042
|
+
charge_timing: "deferred",
|
|
2043
|
+
once_per: { cookie: "JSESSIONID" },
|
|
2044
|
+
pending: [
|
|
2045
|
+
"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",
|
|
2046
|
+
"whether the card form carries clientcode 19, and which of its buttons post to the same action",
|
|
2047
|
+
"whether the review page or confirmAction.doh, which the agent submits after the card, carries or echoes the card",
|
|
2048
|
+
"the answer to the card post, and whether it echoes the card",
|
|
2049
|
+
"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",
|
|
2050
|
+
"that the bootstrap path answers without a server error"
|
|
2051
|
+
]
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
id: "dedge_hotel_card_form",
|
|
2055
|
+
merchant: "D-EDGE hotel booking",
|
|
2056
|
+
status: "observe",
|
|
2057
|
+
evidence: [
|
|
2058
|
+
"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>.",
|
|
2059
|
+
"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).",
|
|
2060
|
+
"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.",
|
|
2061
|
+
"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."
|
|
2062
|
+
],
|
|
2063
|
+
har: null,
|
|
2064
|
+
checkout: "https://card.secure-hotel-booking.com",
|
|
2065
|
+
checkout_frame: false,
|
|
2066
|
+
frame: null,
|
|
2067
|
+
endpoint: "https://card.secure-hotel-booking.com",
|
|
2068
|
+
method: "POST",
|
|
2069
|
+
path: "/validate/:session/Default",
|
|
2070
|
+
query: {},
|
|
2071
|
+
navigation: "frame",
|
|
2072
|
+
encoding: "form",
|
|
2073
|
+
session: {
|
|
2074
|
+
cookies: true,
|
|
2075
|
+
cookie_names: ["datadome"],
|
|
2076
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2077
|
+
required: []
|
|
2078
|
+
},
|
|
2079
|
+
card: {
|
|
2080
|
+
pan: { at: "CardNumber", format: "digits" },
|
|
2081
|
+
cvv: { at: "Cvv", format: "digits" },
|
|
2082
|
+
exp_month: { at: "Month", format: "m" },
|
|
2083
|
+
exp_year: { at: "Year", format: "yyyy" },
|
|
2084
|
+
expiry: null,
|
|
2085
|
+
last4: null,
|
|
2086
|
+
leading: null,
|
|
2087
|
+
brands: [],
|
|
2088
|
+
bins: null
|
|
2089
|
+
},
|
|
2090
|
+
merchant_key: null,
|
|
2091
|
+
requires: [{ at: "Id", equals_path: "session" }],
|
|
2092
|
+
refuse: [],
|
|
2093
|
+
payment_method: null,
|
|
2094
|
+
amount: null,
|
|
2095
|
+
currency: "EUR",
|
|
2096
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2097
|
+
resends: [],
|
|
2098
|
+
challenge: null,
|
|
2099
|
+
facts: null,
|
|
2100
|
+
bootstrap: "/robots.txt",
|
|
2101
|
+
timeout_ms: 6e4,
|
|
2102
|
+
charge_timing: "at_request",
|
|
2103
|
+
once_per: { path: "session" },
|
|
2104
|
+
pending: [
|
|
2105
|
+
"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)",
|
|
2106
|
+
"which cookies the card post needs, with DataDome in front of the host",
|
|
2107
|
+
"whether CardNumber is posted as digits or with the spaces the field allows",
|
|
2108
|
+
"the answer to the card post (a redirect, a 3DS step or a page), and whether a 3DS step sends the card again",
|
|
2109
|
+
"whether D-EDGE charges the card at this step or holds it for the hotel to charge later",
|
|
2110
|
+
"that the bootstrap path answers without a server error"
|
|
2111
|
+
]
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
id: "nic_common_checkout_payment",
|
|
2115
|
+
merchant: "NIC government checkout (Tyler Technologies)",
|
|
2116
|
+
status: "observe",
|
|
2117
|
+
evidence: [
|
|
2118
|
+
"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).",
|
|
2119
|
+
"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.",
|
|
2120
|
+
"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.",
|
|
2121
|
+
"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."
|
|
2122
|
+
],
|
|
2123
|
+
har: null,
|
|
2124
|
+
checkout: "https://securecheckout.cdc.nicusa.com",
|
|
2125
|
+
checkout_frame: false,
|
|
2126
|
+
frame: null,
|
|
2127
|
+
endpoint: "https://securecheckout.cdc.nicusa.com",
|
|
2128
|
+
method: "POST",
|
|
2129
|
+
path: "/Checkout/Payment/ProcessPaymentEntry",
|
|
2130
|
+
query: {},
|
|
2131
|
+
navigation: "top",
|
|
2132
|
+
encoding: "form",
|
|
2133
|
+
session: {
|
|
2134
|
+
cookies: true,
|
|
2135
|
+
cookie_names: ["token", "lang", "ASP.NET_SessionId", "TS01*"],
|
|
2136
|
+
headers: [...BROWSER_HEADERS, ...NAVIGATION_HEADERS],
|
|
2137
|
+
required: []
|
|
2138
|
+
},
|
|
2139
|
+
card: {
|
|
2140
|
+
pan: { at: "CCCardNumber", format: "digits" },
|
|
2141
|
+
cvv: { at: "CCCardCVV", format: "digits" },
|
|
2142
|
+
exp_month: { at: "CCExpirationMonth", format: "mm" },
|
|
2143
|
+
exp_year: { at: "CCExpirationYear", format: "yyyy" },
|
|
2144
|
+
expiry: null,
|
|
2145
|
+
last4: null,
|
|
2146
|
+
leading: null,
|
|
2147
|
+
brands: [{ at: "CCCardType", map: { visa: "VISA", mastercard: "MAST", amex: "AMEX", discover: "DISC" } }],
|
|
2148
|
+
bins: null
|
|
2149
|
+
},
|
|
2150
|
+
merchant_key: null,
|
|
2151
|
+
requires: [
|
|
2152
|
+
{ at: "PayType", equals: "CC" },
|
|
2153
|
+
{ at: "Token", matches: "guid" },
|
|
2154
|
+
{ at: "Token", equals_cookie: "token" },
|
|
2155
|
+
{ at: "submitPayment", equals: "" }
|
|
2156
|
+
],
|
|
2157
|
+
refuse: [
|
|
2158
|
+
{ at: "cancelPayment", when: "present" },
|
|
2159
|
+
{ at: "launchPayPal", when: "present" },
|
|
2160
|
+
{ at: "launchGov2Go", when: "present" },
|
|
2161
|
+
{ at: "launchGov2GoPay", when: "present" },
|
|
2162
|
+
{ at: "ait", when: "present" },
|
|
2163
|
+
{ at: "g-recaptcha-response", when: "present" },
|
|
2164
|
+
{ at: "CCCardNumberBIN", when: "present" },
|
|
2165
|
+
{ at: "SubUserName", when: "nonempty" },
|
|
2166
|
+
{ at: "SubPassword", when: "nonempty" },
|
|
2167
|
+
{ at: "ACHRoutingNumber", when: "nonempty" },
|
|
2168
|
+
{ at: "ACHAccountNumber", when: "nonempty" },
|
|
2169
|
+
{ at: "ACHAccountNumberVerify", when: "nonempty" },
|
|
2170
|
+
{ at: "ACHNameOnAccount", when: "nonempty" },
|
|
2171
|
+
{ at: "ACHBankType", when: "present" },
|
|
2172
|
+
{ at: "AuthorizeAch", when: "true" },
|
|
2173
|
+
{ at: "PayPalToken", when: "nonempty" },
|
|
2174
|
+
{ at: "PaymentApiToken", when: "nonempty" },
|
|
2175
|
+
{ at: "Gov2GoPaymentApiPaymentType", when: "nonempty" },
|
|
2176
|
+
{ prefix: "ApplePay.PaymentAccount.", when: "nonempty" },
|
|
2177
|
+
{ prefix: "EmbeddedCheckoutPayment", when: "present" },
|
|
2178
|
+
{ prefix: "braintree", when: "present" }
|
|
2179
|
+
],
|
|
2180
|
+
payment_method: null,
|
|
2181
|
+
amount: null,
|
|
2182
|
+
currency: "USD",
|
|
2183
|
+
response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
|
|
2184
|
+
resends: [],
|
|
2185
|
+
challenge: null,
|
|
2186
|
+
facts: null,
|
|
2187
|
+
bootstrap: "/Checkout/bundles/html5SiteLayout",
|
|
2188
|
+
timeout_ms: 6e4,
|
|
2189
|
+
charge_timing: "at_request",
|
|
2190
|
+
once_per: { at: "Token" },
|
|
2191
|
+
pending: [
|
|
2192
|
+
"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)",
|
|
2193
|
+
"every cookie the checkout sets after its cookie check (each TS01 name, whether lang matters)",
|
|
2194
|
+
"the CCCardType codes NIC's BIN lookup answers for Visa, Mastercard, Amex and Discover cards, credit and debit",
|
|
2195
|
+
"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",
|
|
2196
|
+
"the session's idle timeout, which bounds the approval",
|
|
2197
|
+
"Tyler Technologies' PCI attestation of compliance for this checkout host",
|
|
2198
|
+
"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"
|
|
2199
|
+
]
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
id: "fastspring_popup_rekordcloud",
|
|
2203
|
+
merchant: "Lexicon",
|
|
2204
|
+
status: "observe",
|
|
2205
|
+
evidence: [
|
|
2206
|
+
"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}.",
|
|
2207
|
+
"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.",
|
|
2208
|
+
"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.",
|
|
2209
|
+
"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.",
|
|
2210
|
+
"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).",
|
|
2211
|
+
"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.",
|
|
2212
|
+
"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.",
|
|
2213
|
+
"Research: processors-wave2/items/fastspring/DESIGN.md; the popup HTML itself needs a live session and was not read."
|
|
2214
|
+
],
|
|
2215
|
+
har: null,
|
|
2216
|
+
checkout: "https://rekordcloud.onfastspring.com",
|
|
2217
|
+
checkout_frame: true,
|
|
2218
|
+
frame: null,
|
|
2219
|
+
endpoint: "https://rekordcloud.onfastspring.com",
|
|
2220
|
+
method: "POST",
|
|
2221
|
+
path: "/popup-lexicon/session/:sessionId/payment",
|
|
2222
|
+
query: {},
|
|
2223
|
+
navigation: null,
|
|
2224
|
+
encoding: "json",
|
|
2225
|
+
session: {
|
|
2226
|
+
cookies: false,
|
|
2227
|
+
cookie_names: null,
|
|
2228
|
+
headers: [...BROWSER_HEADERS, "content-type", "x-session-token"],
|
|
2229
|
+
required: ["x-session-token"]
|
|
2230
|
+
},
|
|
2231
|
+
card: {
|
|
2232
|
+
pan: { at: ["card", "number"], format: "grouped4" },
|
|
2233
|
+
cvv: { at: ["card", "security"], format: "digits" },
|
|
2234
|
+
exp_month: { at: ["card", "month"], format: "as_typed" },
|
|
2235
|
+
exp_year: { at: ["card", "year"], format: "as_typed" },
|
|
2236
|
+
expiry: null,
|
|
2237
|
+
last4: null,
|
|
2238
|
+
leading: null,
|
|
2239
|
+
brands: [],
|
|
2240
|
+
bins: null
|
|
2241
|
+
},
|
|
2242
|
+
merchant_key: null,
|
|
2243
|
+
requires: [{ at: ["paymentType"], equals: "card" }],
|
|
2244
|
+
refuse: [
|
|
2245
|
+
// Brazil's EBANX route sends the typed card a second time.
|
|
2246
|
+
{ at: ["cpfNumber"], when: "nonempty" },
|
|
2247
|
+
{ at: ["dniNumber"], when: "nonempty" },
|
|
2248
|
+
// A gift licenses someone else.
|
|
2249
|
+
{ at: ["recipientSelected"], when: "true" },
|
|
2250
|
+
{ at: ["recipient"], when: "present" },
|
|
2251
|
+
// FastSpring keeps the card for renewals, which waits on the card-on-file decision.
|
|
2252
|
+
{ at: ["autoRenew"], when: "true" },
|
|
2253
|
+
// An invoiced buyer's purchase order, another checkout than this one.
|
|
2254
|
+
{ at: ["customerReference"], when: "present" },
|
|
2255
|
+
// Another payment method's details beside the card.
|
|
2256
|
+
{ at: ["sepa", "iban"], when: "nonempty" },
|
|
2257
|
+
{ at: ["ach", "accountNum"], when: "nonempty" },
|
|
2258
|
+
{ at: ["pad", "accountNum"], when: "nonempty" },
|
|
2259
|
+
{ at: ["bacs", "accountNumber"], when: "nonempty" },
|
|
2260
|
+
{ at: ["googlepay"], when: "nonempty" },
|
|
2261
|
+
{ at: ["paymentKlarna"], when: "nonempty" },
|
|
2262
|
+
{ at: ["mercadopago"], when: "nonempty" }
|
|
2263
|
+
],
|
|
2264
|
+
payment_method: { at: ["paymentType"], card: ["card"] },
|
|
2265
|
+
amount: null,
|
|
2266
|
+
currency: "USD",
|
|
2267
|
+
response: { headers: ["content-type"], withhold: [{ at: ["next"], equals: "ebanx-3ds" }] },
|
|
2268
|
+
resends: [{ method: "POST", path: "/popup-lexicon/session/:sessionId/3ds/return-ebanx", query: {} }],
|
|
2269
|
+
// The device may send a body holding a Turnstile token for 240 seconds after the create. The
|
|
2270
|
+
// page minted the token when the card option rendered, before the pause, so this cannot keep
|
|
2271
|
+
// a slow approval from sending an expired one (pending below).
|
|
2272
|
+
challenge: { at: ["captcha", "token"], ttl_ms: 24e4 },
|
|
2273
|
+
facts: {
|
|
2274
|
+
reader: "fastspring_session",
|
|
2275
|
+
// Every answer FastSpring's page reads its order from (refreshPageResponse and
|
|
2276
|
+
// refreshOrderResponse): the popup document, the one-segment calls (/tax, /coupons,
|
|
2277
|
+
// /locale, /exempt), an item's quantity, renewal or add-on change, and a verified discount.
|
|
2278
|
+
responses: [
|
|
2279
|
+
{ method: "GET", path: "/popup-lexicon/session/:sessionId", query: {}, format: "html" },
|
|
2280
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/:step", query: {}, format: "json" },
|
|
2281
|
+
{ method: "GET", path: "/popup-lexicon/session/:sessionId/:step", query: {}, format: "json" },
|
|
2282
|
+
{ method: "PUT", path: "/popup-lexicon/session/:sessionId/items/:item", query: {}, format: "json" },
|
|
2283
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/items/:item", query: {}, format: "json" },
|
|
2284
|
+
{ method: "POST", path: "/popup-lexicon/session/:sessionId/spi/sheerid", query: {}, format: "json" }
|
|
2285
|
+
],
|
|
2286
|
+
// Customer recognition, posted when a contact field loses focus: the page reads nothing from its answer.
|
|
2287
|
+
inert: [{ method: "POST", path: "/popup-lexicon/session/:sessionId/customer/recognize", query: {} }],
|
|
2288
|
+
bind: "sessionId"
|
|
2289
|
+
},
|
|
2290
|
+
bootstrap: "/robots.txt",
|
|
2291
|
+
timeout_ms: 3e4,
|
|
2292
|
+
// Most of Lexicon's catalog renews monthly. The facts read refuses each renewing
|
|
2293
|
+
// order, and the worst case stays here until a purchase proves that read.
|
|
2294
|
+
charge_timing: "recurring",
|
|
2295
|
+
once_per: null,
|
|
2296
|
+
pending: [
|
|
2297
|
+
"which controller the popup runs (foundation or pinhole) and the exact /payment body it sends",
|
|
2298
|
+
"the popup document URL and the paths of the JSON answers that carry its session state",
|
|
2299
|
+
"whether /payment needs the AWSALB and AWSALBCORS cookies, Origin or Referer",
|
|
2300
|
+
"whether an X-XSRF-TOKEN header rides along",
|
|
2301
|
+
"whether rekordcloud sets session.captchaRequired",
|
|
2302
|
+
"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",
|
|
2303
|
+
"card.number sent grouped or plain; month and year widths",
|
|
2304
|
+
"the session currency for US buyers",
|
|
2305
|
+
"a card-on-file policy for the monthly plans and the add-on, before any order that renews"
|
|
2306
|
+
]
|
|
2307
|
+
}
|
|
2308
|
+
]);
|
|
2309
|
+
function wireProjection(profiles) {
|
|
2310
|
+
return deepFreeze(profiles.map(({ evidence, bootstrap, timeout_ms, ...profile }) => ({ ...profile })));
|
|
2311
|
+
}
|
|
2312
|
+
function clientProjection(profiles) {
|
|
2313
|
+
return deepFreeze(profiles.map(({ evidence, ...profile }) => ({ ...profile })));
|
|
2314
|
+
}
|
|
2315
|
+
var MERCHANT_CARD_ENDPOINT_RECOGNIZERS = /* @__PURE__ */ wireProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
2316
|
+
var MERCHANT_CARD_ENDPOINT_CLIENT_TABLE = /* @__PURE__ */ clientProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
|
|
2317
|
+
function merchantCardEndpointById(id) {
|
|
2318
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((entry) => entry.id === id) ?? null : null;
|
|
2319
|
+
}
|
|
2320
|
+
function trustedMerchantCardEndpoint(idOrEntry) {
|
|
2321
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
2322
|
+
if (!profile)
|
|
2323
|
+
throw new Error("Refusing to pay: unknown merchant card endpoint.");
|
|
2324
|
+
return profile;
|
|
2325
|
+
}
|
|
2326
|
+
function merchantCardEndpointRecognizerById(id) {
|
|
2327
|
+
return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_RECOGNIZERS.find((entry) => entry.id === id) ?? null : null;
|
|
2328
|
+
}
|
|
2329
|
+
function merchantCardEndpointLive(idOrEntry) {
|
|
2330
|
+
const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
|
|
2331
|
+
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);
|
|
2332
|
+
}
|
|
2333
|
+
// src/sha256.js
|
|
2334
|
+
var K = /* @__PURE__ */ new Uint32Array([
|
|
2335
|
+
1116352408,
|
|
2336
|
+
1899447441,
|
|
2337
|
+
3049323471,
|
|
2338
|
+
3921009573,
|
|
2339
|
+
961987163,
|
|
2340
|
+
1508970993,
|
|
2341
|
+
2453635748,
|
|
2342
|
+
2870763221,
|
|
2343
|
+
3624381080,
|
|
2344
|
+
310598401,
|
|
2345
|
+
607225278,
|
|
2346
|
+
1426881987,
|
|
2347
|
+
1925078388,
|
|
2348
|
+
2162078206,
|
|
2349
|
+
2614888103,
|
|
2350
|
+
3248222580,
|
|
2351
|
+
3835390401,
|
|
2352
|
+
4022224774,
|
|
2353
|
+
264347078,
|
|
2354
|
+
604807628,
|
|
2355
|
+
770255983,
|
|
2356
|
+
1249150122,
|
|
2357
|
+
1555081692,
|
|
2358
|
+
1996064986,
|
|
2359
|
+
2554220882,
|
|
2360
|
+
2821834349,
|
|
2361
|
+
2952996808,
|
|
2362
|
+
3210313671,
|
|
2363
|
+
3336571891,
|
|
2364
|
+
3584528711,
|
|
2365
|
+
113926993,
|
|
2366
|
+
338241895,
|
|
2367
|
+
666307205,
|
|
2368
|
+
773529912,
|
|
2369
|
+
1294757372,
|
|
2370
|
+
1396182291,
|
|
2371
|
+
1695183700,
|
|
2372
|
+
1986661051,
|
|
2373
|
+
2177026350,
|
|
2374
|
+
2456956037,
|
|
2375
|
+
2730485921,
|
|
2376
|
+
2820302411,
|
|
2377
|
+
3259730800,
|
|
2378
|
+
3345764771,
|
|
2379
|
+
3516065817,
|
|
2380
|
+
3600352804,
|
|
2381
|
+
4094571909,
|
|
2382
|
+
275423344,
|
|
2383
|
+
430227734,
|
|
2384
|
+
506948616,
|
|
2385
|
+
659060556,
|
|
2386
|
+
883997877,
|
|
2387
|
+
958139571,
|
|
2388
|
+
1322822218,
|
|
2389
|
+
1537002063,
|
|
2390
|
+
1747873779,
|
|
2391
|
+
1955562222,
|
|
2392
|
+
2024104815,
|
|
2393
|
+
2227730452,
|
|
2394
|
+
2361852424,
|
|
2395
|
+
2428436474,
|
|
2396
|
+
2756734187,
|
|
2397
|
+
3204031479,
|
|
2398
|
+
3329325298
|
|
2399
|
+
]);
|
|
2400
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
2401
|
+
function utf8(text) {
|
|
2402
|
+
const out = [];
|
|
2403
|
+
for (const char of text) {
|
|
2404
|
+
let cp = char.codePointAt(0);
|
|
2405
|
+
if (cp >= 55296 && cp <= 57343)
|
|
2406
|
+
cp = 65533;
|
|
2407
|
+
if (cp < 128)
|
|
2408
|
+
out.push(cp);
|
|
2409
|
+
else if (cp < 2048)
|
|
2410
|
+
out.push(192 | cp >> 6, 128 | cp & 63);
|
|
2411
|
+
else if (cp < 65536)
|
|
2412
|
+
out.push(224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63);
|
|
2413
|
+
else
|
|
2414
|
+
out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
|
|
2415
|
+
}
|
|
2416
|
+
return Uint8Array.from(out);
|
|
2417
|
+
}
|
|
2418
|
+
function sha256Hex(input) {
|
|
2419
|
+
const isBytes2 = Object.prototype.toString.call(input) === "[object Uint8Array]";
|
|
2420
|
+
if (typeof input !== "string" && !isBytes2)
|
|
2421
|
+
throw new TypeError("sha256Hex takes a string or a Uint8Array.");
|
|
2422
|
+
const bytes = typeof input === "string" ? utf8(input) : input;
|
|
2423
|
+
const length = bytes.length;
|
|
2424
|
+
const padded = new Uint8Array((length + 8 >> 6) + 1 << 6);
|
|
2425
|
+
padded.set(bytes);
|
|
2426
|
+
padded[length] = 128;
|
|
2427
|
+
const view = new DataView(padded.buffer);
|
|
2428
|
+
view.setUint32(padded.length - 8, Math.floor(length / 536870912));
|
|
2429
|
+
view.setUint32(padded.length - 4, length << 3 >>> 0);
|
|
2430
|
+
const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
2431
|
+
const W = new Uint32Array(64);
|
|
2432
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
2433
|
+
for (let t = 0; t < 16; t++)
|
|
2434
|
+
W[t] = view.getUint32(offset + t * 4);
|
|
2435
|
+
for (let t = 16; t < 64; t++) {
|
|
2436
|
+
const s0 = rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ W[t - 15] >>> 3;
|
|
2437
|
+
const s1 = rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ W[t - 2] >>> 10;
|
|
2438
|
+
W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
|
|
2439
|
+
}
|
|
2440
|
+
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];
|
|
2441
|
+
for (let t = 0; t < 64; t++) {
|
|
2442
|
+
const t1 = h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[t] + W[t] >>> 0;
|
|
2443
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
2444
|
+
h = g;
|
|
2445
|
+
g = f;
|
|
2446
|
+
f = e;
|
|
2447
|
+
e = d + t1 >>> 0;
|
|
2448
|
+
d = c;
|
|
2449
|
+
c = b;
|
|
2450
|
+
b = a;
|
|
2451
|
+
a = t1 + t2 >>> 0;
|
|
2452
|
+
}
|
|
2453
|
+
H[0] += a;
|
|
2454
|
+
H[1] += b;
|
|
2455
|
+
H[2] += c;
|
|
2456
|
+
H[3] += d;
|
|
2457
|
+
H[4] += e;
|
|
2458
|
+
H[5] += f;
|
|
2459
|
+
H[6] += g;
|
|
2460
|
+
H[7] += h;
|
|
2461
|
+
}
|
|
2462
|
+
return Array.from(H, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
2463
|
+
}
|
|
2464
|
+
// src/merchant-card-endpoint-keys.js
|
|
2465
|
+
var MerchantKeyError = class extends Error {
|
|
2466
|
+
constructor(reason) {
|
|
2467
|
+
super("Refusing to encrypt the card under the merchant's key.");
|
|
2468
|
+
this.name = "MerchantKeyError";
|
|
2469
|
+
this.code = "merchant_key_refused";
|
|
2470
|
+
this.reason = reason;
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
var refuse = (reason) => new MerchantKeyError(reason);
|
|
2474
|
+
var MERCHANT_KEY_HASHES = /* @__PURE__ */ Object.freeze({ "SHA-1": 20, "SHA-256": 32, "SHA-512": 64 });
|
|
2475
|
+
var MERCHANT_KEY_JWE_ALGS = /* @__PURE__ */ Object.freeze({ "RSA-OAEP": "SHA-1", "RSA-OAEP-256": "SHA-256" });
|
|
2476
|
+
var MERCHANT_KEY_JWE_ENCS = /* @__PURE__ */ Object.freeze({
|
|
2477
|
+
"A256CBC-HS512": Object.freeze({ key: 64, iv: 16, tag: 32 }),
|
|
2478
|
+
A256GCM: Object.freeze({ key: 32, iv: 12, tag: 16 })
|
|
2479
|
+
});
|
|
2480
|
+
var MERCHANT_KEY_SIZES = /* @__PURE__ */ Object.freeze([256, 384, 512]);
|
|
2481
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2482
|
+
var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
2483
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
2484
|
+
var RSA_ALGORITHM = [48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0];
|
|
2485
|
+
var MAX_ENCRYPTED_LENGTH = 4096;
|
|
2486
|
+
var isBytes = (value) => Object.prototype.toString.call(value) === "[object Uint8Array]";
|
|
2487
|
+
function encodeBase64(bytes, url = false) {
|
|
2488
|
+
const alphabet = url ? BASE64URL_ALPHABET : BASE64_ALPHABET;
|
|
2489
|
+
let out = "";
|
|
2490
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
2491
|
+
const a = bytes[i];
|
|
2492
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
2493
|
+
const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
2494
|
+
const triple = a << 16 | b << 8 | c;
|
|
2495
|
+
out += alphabet[triple >> 18 & 63] + alphabet[triple >> 12 & 63];
|
|
2496
|
+
if (i + 1 < bytes.length)
|
|
2497
|
+
out += alphabet[triple >> 6 & 63];
|
|
2498
|
+
else if (!url)
|
|
2499
|
+
out += "=";
|
|
2500
|
+
if (i + 2 < bytes.length)
|
|
2501
|
+
out += alphabet[triple & 63];
|
|
2502
|
+
else if (!url)
|
|
2503
|
+
out += "=";
|
|
2504
|
+
}
|
|
2505
|
+
return out;
|
|
2506
|
+
}
|
|
2507
|
+
function decodeBase64(text, url = false) {
|
|
2508
|
+
if (typeof text !== "string" || text.length === 0 || text.length > MAX_ENCRYPTED_LENGTH * 2)
|
|
2509
|
+
return null;
|
|
2510
|
+
const alphabet = url ? BASE64URL_ALPHABET : BASE64_ALPHABET;
|
|
2511
|
+
const body = url ? text : text.replace(/={1,2}$/, "");
|
|
2512
|
+
if (!url && text.length % 4 !== 0)
|
|
2513
|
+
return null;
|
|
2514
|
+
if (body.length % 4 === 1)
|
|
2515
|
+
return null;
|
|
2516
|
+
const out = new Uint8Array(Math.floor(body.length * 3 / 4));
|
|
2517
|
+
let bits = 0;
|
|
2518
|
+
let value = 0;
|
|
2519
|
+
let at = 0;
|
|
2520
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
2521
|
+
const digit = alphabet.indexOf(body[i]);
|
|
2522
|
+
if (digit === -1)
|
|
2523
|
+
return null;
|
|
2524
|
+
value = value << 6 | digit;
|
|
2525
|
+
bits += 6;
|
|
2526
|
+
if (bits >= 8) {
|
|
2527
|
+
bits -= 8;
|
|
2528
|
+
out[at++] = value >> bits & 255;
|
|
2529
|
+
value &= (1 << bits) - 1;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return encodeBase64(out, url) === text ? out : null;
|
|
2533
|
+
}
|
|
2534
|
+
var utf8Bytes = (text) => new TextEncoder().encode(text);
|
|
2535
|
+
function readElement(der, at, tag) {
|
|
2536
|
+
if (at + 2 > der.length || der[at] !== tag)
|
|
2537
|
+
throw refuse("key");
|
|
2538
|
+
let length = der[at + 1];
|
|
2539
|
+
let start = at + 2;
|
|
2540
|
+
if (length & 128) {
|
|
2541
|
+
const count = length & 127;
|
|
2542
|
+
if (count === 0 || count > 2 || start + count > der.length || der[start] === 0)
|
|
2543
|
+
throw refuse("key");
|
|
2544
|
+
length = count === 1 ? der[start] : der[start] * 256 + der[start + 1];
|
|
2545
|
+
if (length < 128)
|
|
2546
|
+
throw refuse("key");
|
|
2547
|
+
start += count;
|
|
2548
|
+
}
|
|
2549
|
+
if (start + length > der.length)
|
|
2550
|
+
throw refuse("key");
|
|
2551
|
+
return { start, end: start + length };
|
|
2552
|
+
}
|
|
2553
|
+
function readPositiveInteger(der, at) {
|
|
2554
|
+
const { start, end } = readElement(der, at, 2);
|
|
2555
|
+
if (end === start || der[start] & 128)
|
|
2556
|
+
throw refuse("key");
|
|
2557
|
+
if (der[start] === 0 && (end - start === 1 || !(der[start + 1] & 128)))
|
|
2558
|
+
throw refuse("key");
|
|
2559
|
+
return { bytes: der.subarray(der[start] === 0 ? start + 1 : start, end), end };
|
|
2560
|
+
}
|
|
2561
|
+
function toBigInt(bytes) {
|
|
2562
|
+
let hex = "";
|
|
2563
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
2564
|
+
hex += (bytes[i] < 16 ? "0" : "") + bytes[i].toString(16);
|
|
2565
|
+
return BigInt(`0x${hex || "0"}`);
|
|
2566
|
+
}
|
|
2567
|
+
function toBytes(value, size) {
|
|
2568
|
+
const hex = value.toString(16).padStart(size * 2, "0");
|
|
2569
|
+
if (hex.length !== size * 2)
|
|
2570
|
+
throw refuse("text");
|
|
2571
|
+
const out = new Uint8Array(size);
|
|
2572
|
+
for (let i = 0; i < size; i += 1)
|
|
2573
|
+
out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
|
|
2574
|
+
return out;
|
|
2575
|
+
}
|
|
2576
|
+
function modPow(base, exponent, modulus) {
|
|
2577
|
+
let result = 1n;
|
|
2578
|
+
let square = base % modulus;
|
|
2579
|
+
for (let rest = exponent; rest > 0n; rest >>= 1n) {
|
|
2580
|
+
if (rest & 1n)
|
|
2581
|
+
result = result * square % modulus;
|
|
2582
|
+
square = square * square % modulus;
|
|
2583
|
+
}
|
|
2584
|
+
return result;
|
|
2585
|
+
}
|
|
2586
|
+
function parseMerchantKey(key) {
|
|
2587
|
+
const spki = key && typeof key === "object" ? key.spki : void 0;
|
|
2588
|
+
const pin = key && typeof key === "object" ? key.sha256 : void 0;
|
|
2589
|
+
if (typeof spki !== "string" || spki.length > 1024 || typeof pin !== "string" || !SHA256_HEX.test(pin))
|
|
2590
|
+
throw refuse("key");
|
|
2591
|
+
const der = decodeBase64(spki);
|
|
2592
|
+
if (!der || sha256Hex(der) !== pin)
|
|
2593
|
+
throw refuse("key");
|
|
2594
|
+
const outer = readElement(der, 0, 48);
|
|
2595
|
+
if (outer.end !== der.length || outer.start + RSA_ALGORITHM.length > der.length)
|
|
2596
|
+
throw refuse("key");
|
|
2597
|
+
if (!RSA_ALGORITHM.every((byte, i) => der[outer.start + i] === byte))
|
|
2598
|
+
throw refuse("key");
|
|
2599
|
+
const bits = readElement(der, outer.start + RSA_ALGORITHM.length, 3);
|
|
2600
|
+
if (bits.end !== outer.end || bits.start === bits.end || der[bits.start] !== 0)
|
|
2601
|
+
throw refuse("key");
|
|
2602
|
+
const rsa = readElement(der, bits.start + 1, 48);
|
|
2603
|
+
if (rsa.end !== bits.end)
|
|
2604
|
+
throw refuse("key");
|
|
2605
|
+
const modulus = readPositiveInteger(der, rsa.start);
|
|
2606
|
+
const exponent = readPositiveInteger(der, modulus.end);
|
|
2607
|
+
if (exponent.end !== rsa.end)
|
|
2608
|
+
throw refuse("key");
|
|
2609
|
+
const n = modulus.bytes;
|
|
2610
|
+
if (!MERCHANT_KEY_SIZES.includes(n.length) || !(n[0] & 128) || !(n[n.length - 1] & 1))
|
|
2611
|
+
throw refuse("key");
|
|
2612
|
+
const e = exponent.bytes;
|
|
2613
|
+
if (e.length !== 3 || e[0] !== 1 || e[1] !== 0 || e[2] !== 1)
|
|
2614
|
+
throw refuse("key");
|
|
2615
|
+
return Object.freeze({ n: toBigInt(n), size: n.length });
|
|
2616
|
+
}
|
|
2617
|
+
function subtleOf(crypto) {
|
|
2618
|
+
if (!crypto || !crypto.subtle || typeof crypto.getRandomValues !== "function")
|
|
2619
|
+
throw refuse("crypto");
|
|
2620
|
+
return crypto.subtle;
|
|
2621
|
+
}
|
|
2622
|
+
async function digest(subtle, hash, bytes) {
|
|
2623
|
+
try {
|
|
2624
|
+
return new Uint8Array(await subtle.digest(hash, bytes));
|
|
2625
|
+
}
|
|
2626
|
+
catch {
|
|
2627
|
+
throw refuse("crypto");
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async function mgf1(subtle, hash, seed, length) {
|
|
2631
|
+
const out = new Uint8Array(length);
|
|
2632
|
+
const input = new Uint8Array(seed.length + 4);
|
|
2633
|
+
input.set(seed);
|
|
2634
|
+
try {
|
|
2635
|
+
for (let counter = 0, filled = 0; filled < length; counter += 1) {
|
|
2636
|
+
input[seed.length] = counter >>> 24 & 255;
|
|
2637
|
+
input[seed.length + 1] = counter >>> 16 & 255;
|
|
2638
|
+
input[seed.length + 2] = counter >>> 8 & 255;
|
|
2639
|
+
input[seed.length + 3] = counter & 255;
|
|
2640
|
+
const block = await digest(subtle, hash, input);
|
|
2641
|
+
out.set(block.subarray(0, Math.min(block.length, length - filled)), filled);
|
|
2642
|
+
filled += block.length;
|
|
2643
|
+
block.fill(0);
|
|
2644
|
+
}
|
|
2645
|
+
return out;
|
|
2646
|
+
}
|
|
2647
|
+
finally {
|
|
2648
|
+
input.fill(0);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
function randomBytes(crypto, length) {
|
|
2652
|
+
const out = new Uint8Array(length);
|
|
2653
|
+
try {
|
|
2654
|
+
crypto.getRandomValues(out);
|
|
2655
|
+
}
|
|
2656
|
+
catch {
|
|
2657
|
+
throw refuse("random");
|
|
2658
|
+
}
|
|
2659
|
+
return out;
|
|
2660
|
+
}
|
|
2661
|
+
async function rsaOaepEncrypt(key, message, { hash, mgf1: maskHash }, crypto, seed) {
|
|
2662
|
+
const subtle = subtleOf(crypto);
|
|
2663
|
+
const { n, size } = parseMerchantKey(key);
|
|
2664
|
+
const hLen = MERCHANT_KEY_HASHES[hash];
|
|
2665
|
+
if (!hLen || !MERCHANT_KEY_HASHES[maskHash])
|
|
2666
|
+
throw refuse("key");
|
|
2667
|
+
if (!isBytes(message) || message.length > size - 2 * hLen - 2)
|
|
2668
|
+
throw refuse("text");
|
|
2669
|
+
const db = new Uint8Array(size - hLen - 1);
|
|
2670
|
+
const em = new Uint8Array(size);
|
|
2671
|
+
let seedBytes = null;
|
|
2672
|
+
let dbMask = null;
|
|
2673
|
+
let seedMask = null;
|
|
2674
|
+
try {
|
|
2675
|
+
db.set(await digest(subtle, hash, new Uint8Array(0)));
|
|
2676
|
+
db[db.length - message.length - 1] = 1;
|
|
2677
|
+
db.set(message, db.length - message.length);
|
|
2678
|
+
if (seed !== void 0) {
|
|
2679
|
+
if (!isBytes(seed) || seed.length !== hLen)
|
|
2680
|
+
throw refuse("random");
|
|
2681
|
+
seedBytes = Uint8Array.from(seed);
|
|
2682
|
+
}
|
|
2683
|
+
else {
|
|
2684
|
+
seedBytes = randomBytes(crypto, hLen);
|
|
2685
|
+
}
|
|
2686
|
+
dbMask = await mgf1(subtle, maskHash, seedBytes, db.length);
|
|
2687
|
+
for (let i = 0; i < db.length; i += 1)
|
|
2688
|
+
db[i] ^= dbMask[i];
|
|
2689
|
+
seedMask = await mgf1(subtle, maskHash, db, hLen);
|
|
2690
|
+
for (let i = 0; i < hLen; i += 1)
|
|
2691
|
+
seedBytes[i] ^= seedMask[i];
|
|
2692
|
+
em.set(seedBytes, 1);
|
|
2693
|
+
em.set(db, 1 + hLen);
|
|
2694
|
+
return toBytes(modPow(toBigInt(em), 65537n, n), size);
|
|
2695
|
+
}
|
|
2696
|
+
finally {
|
|
2697
|
+
db.fill(0);
|
|
2698
|
+
em.fill(0);
|
|
2699
|
+
if (seedBytes)
|
|
2700
|
+
seedBytes.fill(0);
|
|
2701
|
+
if (dbMask)
|
|
2702
|
+
dbMask.fill(0);
|
|
2703
|
+
if (seedMask)
|
|
2704
|
+
seedMask.fill(0);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
function merchantJweHeader(spec, kid) {
|
|
2708
|
+
return JSON.stringify({ alg: spec.alg, enc: spec.enc, kid, ...spec.header });
|
|
2709
|
+
}
|
|
2710
|
+
async function aesCbcHmacSeal(subtle, contentKey, iv, text, aad) {
|
|
2711
|
+
let macKey;
|
|
2712
|
+
let encKey;
|
|
2713
|
+
try {
|
|
2714
|
+
macKey = await subtle.importKey("raw", contentKey.subarray(0, 32), { name: "HMAC", hash: "SHA-512" }, false, ["sign"]);
|
|
2715
|
+
encKey = await subtle.importKey("raw", contentKey.subarray(32, 64), { name: "AES-CBC" }, false, ["encrypt"]);
|
|
2716
|
+
}
|
|
2717
|
+
catch {
|
|
2718
|
+
throw refuse("crypto");
|
|
2719
|
+
}
|
|
2720
|
+
let ciphertext;
|
|
2721
|
+
try {
|
|
2722
|
+
ciphertext = new Uint8Array(await subtle.encrypt({ name: "AES-CBC", iv }, encKey, text));
|
|
2723
|
+
}
|
|
2724
|
+
catch {
|
|
2725
|
+
throw refuse("crypto");
|
|
2726
|
+
}
|
|
2727
|
+
const al = new Uint8Array(8);
|
|
2728
|
+
const bits = aad.length * 8;
|
|
2729
|
+
new DataView(al.buffer).setUint32(0, Math.floor(bits / 4294967296));
|
|
2730
|
+
new DataView(al.buffer).setUint32(4, bits >>> 0);
|
|
2731
|
+
const macInput = new Uint8Array(aad.length + iv.length + ciphertext.length + 8);
|
|
2732
|
+
macInput.set(aad, 0);
|
|
2733
|
+
macInput.set(iv, aad.length);
|
|
2734
|
+
macInput.set(ciphertext, aad.length + iv.length);
|
|
2735
|
+
macInput.set(al, aad.length + iv.length + ciphertext.length);
|
|
2736
|
+
let mac;
|
|
2737
|
+
try {
|
|
2738
|
+
mac = new Uint8Array(await subtle.sign("HMAC", macKey, macInput));
|
|
2739
|
+
}
|
|
2740
|
+
catch {
|
|
2741
|
+
throw refuse("crypto");
|
|
2742
|
+
}
|
|
2743
|
+
const tag = mac.slice(0, 32);
|
|
2744
|
+
mac.fill(0);
|
|
2745
|
+
return { ciphertext, tag };
|
|
2746
|
+
}
|
|
2747
|
+
async function aesGcmSeal(subtle, contentKey, iv, text, aad) {
|
|
2748
|
+
try {
|
|
2749
|
+
const key = await subtle.importKey("raw", contentKey, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
2750
|
+
const sealed = new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 128 }, key, text));
|
|
2751
|
+
return { ciphertext: sealed.slice(0, sealed.length - 16), tag: sealed.slice(sealed.length - 16) };
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
throw refuse("crypto");
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
async function jweCompactEncrypt(key, text, spec, kid, crypto) {
|
|
2758
|
+
const subtle = subtleOf(crypto);
|
|
2759
|
+
const hash = MERCHANT_KEY_JWE_ALGS[spec && spec.alg];
|
|
2760
|
+
const enc = MERCHANT_KEY_JWE_ENCS[spec && spec.enc];
|
|
2761
|
+
if (!hash || !enc || typeof kid !== "string" || kid === "")
|
|
2762
|
+
throw refuse("key");
|
|
2763
|
+
if (!isBytes(text))
|
|
2764
|
+
throw refuse("text");
|
|
2765
|
+
const header = encodeBase64(utf8Bytes(merchantJweHeader(spec, kid)), true);
|
|
2766
|
+
const aad = utf8Bytes(header);
|
|
2767
|
+
const contentKey = randomBytes(crypto, enc.key);
|
|
2768
|
+
try {
|
|
2769
|
+
const wrapped = await rsaOaepEncrypt(key, contentKey, { hash, mgf1: hash }, crypto);
|
|
2770
|
+
const iv = randomBytes(crypto, enc.iv);
|
|
2771
|
+
const { ciphertext, tag } = spec.enc === "A256GCM" ? await aesGcmSeal(subtle, contentKey, iv, text, aad) : await aesCbcHmacSeal(subtle, contentKey, iv, text, aad);
|
|
2772
|
+
return [header, encodeBase64(wrapped, true), encodeBase64(iv, true), encodeBase64(ciphertext, true), encodeBase64(tag, true)].join(".");
|
|
2773
|
+
}
|
|
2774
|
+
finally {
|
|
2775
|
+
contentKey.fill(0);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
function readCompactJwe(value) {
|
|
2779
|
+
if (typeof value !== "string" || value.length > MAX_ENCRYPTED_LENGTH)
|
|
2780
|
+
return null;
|
|
2781
|
+
const parts = value.split(".");
|
|
2782
|
+
if (parts.length !== 5)
|
|
2783
|
+
return null;
|
|
2784
|
+
const bytes = parts.map((part) => decodeBase64(part, true));
|
|
2785
|
+
if (bytes.some((part) => part === null))
|
|
2786
|
+
return null;
|
|
2787
|
+
let headerText;
|
|
2788
|
+
let header;
|
|
2789
|
+
try {
|
|
2790
|
+
headerText = new TextDecoder("utf-8", { fatal: true }).decode(bytes[0]);
|
|
2791
|
+
header = JSON.parse(headerText);
|
|
2792
|
+
}
|
|
2793
|
+
catch {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
if (!header || typeof header !== "object" || Array.isArray(header) || JSON.stringify(header) !== headerText)
|
|
2797
|
+
return null;
|
|
2798
|
+
return { header, headerText, encryptedKey: bytes[1], iv: bytes[2], ciphertext: bytes[3], tag: bytes[4] };
|
|
2799
|
+
}
|
|
2800
|
+
function allowedSizes(merchantKey) {
|
|
2801
|
+
const sizes = [];
|
|
2802
|
+
for (const key of merchantKey.keys) {
|
|
2803
|
+
try {
|
|
2804
|
+
sizes.push(parseMerchantKey(key).size);
|
|
2805
|
+
}
|
|
2806
|
+
catch {
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return merchantKey.keys.length ? sizes : MERCHANT_KEY_SIZES;
|
|
2810
|
+
}
|
|
2811
|
+
function encryptedValueFits(merchantKey, value) {
|
|
2812
|
+
if (!merchantKey || typeof value !== "string" || value.length > MAX_ENCRYPTED_LENGTH)
|
|
2813
|
+
return false;
|
|
2814
|
+
const sizes = allowedSizes(merchantKey);
|
|
2815
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
2816
|
+
const bytes = decodeBase64(value);
|
|
2817
|
+
return !!bytes && sizes.includes(bytes.length);
|
|
2818
|
+
}
|
|
2819
|
+
if (merchantKey.scheme !== "jwe")
|
|
2820
|
+
return false;
|
|
2821
|
+
const jwe = readCompactJwe(value);
|
|
2822
|
+
const enc = MERCHANT_KEY_JWE_ENCS[merchantKey.enc];
|
|
2823
|
+
if (!jwe || !enc)
|
|
2824
|
+
return false;
|
|
2825
|
+
const { header } = jwe;
|
|
2826
|
+
const kids = merchantKey.keys.map((key) => key.kid);
|
|
2827
|
+
const alg = merchantKey.alg === null ? header.alg : merchantKey.alg;
|
|
2828
|
+
if (!Object.prototype.hasOwnProperty.call(MERCHANT_KEY_JWE_ALGS, alg) || typeof header.kid !== "string" || header.kid === "" || kids.length && !kids.includes(header.kid))
|
|
2829
|
+
return false;
|
|
2830
|
+
if (jwe.headerText !== merchantJweHeader({ ...merchantKey, alg }, header.kid))
|
|
2831
|
+
return false;
|
|
2832
|
+
if (!sizes.includes(jwe.encryptedKey.length) || jwe.iv.length !== enc.iv || jwe.tag.length !== enc.tag || jwe.ciphertext.length === 0)
|
|
2833
|
+
return false;
|
|
2834
|
+
return merchantKey.enc === "A256GCM" || jwe.ciphertext.length % 16 === 0;
|
|
2835
|
+
}
|
|
2836
|
+
// src/merchant-card-endpoint-facts.js
|
|
2837
|
+
var TWO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["usd", "eur", "gbp", "cad", "aud"]);
|
|
2838
|
+
var MERCHANT_CARD_ENDPOINT_FACTS_TEXT_MAX = 1024 * 1024;
|
|
2839
|
+
var MAX_DEPTH = 8;
|
|
2840
|
+
var MAX_ITEMS = 256;
|
|
2841
|
+
var ROUNDING_MARGIN = 1;
|
|
2842
|
+
var fail = (refusal) => ({ ok: false, refusal });
|
|
2843
|
+
var VIEWDATA = /<script\b[^>]*\bid\s*=\s*["']viewdata["'][^>]*>([\s\S]*?)<\/script\s*>/gi;
|
|
2844
|
+
var ANGULAR_PREFIX = /^\)\]\}',?\n/;
|
|
2845
|
+
function parseJson(text) {
|
|
2846
|
+
try {
|
|
2847
|
+
return JSON.parse(text);
|
|
2848
|
+
}
|
|
2849
|
+
catch {
|
|
2850
|
+
return void 0;
|
|
391
2851
|
}
|
|
392
|
-
]);
|
|
393
|
-
function wireProjection(profiles) {
|
|
394
|
-
return deepFreeze(profiles.map(({ evidence, bootstrap, timeout_ms, ...profile }) => ({ ...profile })));
|
|
395
2852
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
2853
|
+
function fastspringState(format, text) {
|
|
2854
|
+
if (format === "html") {
|
|
2855
|
+
const scripts = [...text.matchAll(VIEWDATA)];
|
|
2856
|
+
if (scripts.length !== 1)
|
|
2857
|
+
return { refusal: "the checkout page does not hold exactly one order record" };
|
|
2858
|
+
const state2 = parseJson(scripts[0][1]);
|
|
2859
|
+
return isPlainObject(state2) ? { state: state2 } : { refusal: "the checkout page's order record is not a JSON object" };
|
|
2860
|
+
}
|
|
2861
|
+
const state = parseJson(text.replace(ANGULAR_PREFIX, ""));
|
|
2862
|
+
if (!isPlainObject(state))
|
|
2863
|
+
return { none: true };
|
|
2864
|
+
return isPlainObject(state.order) && isPlainObject(state.session) ? { state } : { none: true };
|
|
398
2865
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
2866
|
+
function selectedRenewals(groups, depth, counter) {
|
|
2867
|
+
if (!Array.isArray(groups) || depth > MAX_DEPTH)
|
|
2868
|
+
return null;
|
|
2869
|
+
const out = [];
|
|
2870
|
+
for (const group of groups) {
|
|
2871
|
+
if (!isPlainObject(group) || group.items !== void 0 && !Array.isArray(group.items))
|
|
2872
|
+
return null;
|
|
2873
|
+
for (const item of group.items ?? []) {
|
|
2874
|
+
counter.items += 1;
|
|
2875
|
+
if (!isPlainObject(item) || counter.items > MAX_ITEMS)
|
|
2876
|
+
return null;
|
|
2877
|
+
const nested = selectedRenewals(item.groups ?? [], depth + 1, counter);
|
|
2878
|
+
if (nested === null)
|
|
2879
|
+
return null;
|
|
2880
|
+
out.push({ selected: item.selected === true, future: item.future ? true : null, groups: nested });
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return [{ items: out }];
|
|
403
2884
|
}
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
|
|
408
|
-
|
|
2885
|
+
function fastspringProjection(state) {
|
|
2886
|
+
const order = state.order;
|
|
2887
|
+
if (!isPlainObject(order))
|
|
2888
|
+
return null;
|
|
2889
|
+
const groups = selectedRenewals(order.groups ?? [], 0, { items: 0 });
|
|
2890
|
+
if (groups === null)
|
|
2891
|
+
return null;
|
|
2892
|
+
return {
|
|
2893
|
+
order: { totalWithTaxValue: order.totalWithTaxValue, currency: order.currency, groups },
|
|
2894
|
+
production: state.production,
|
|
2895
|
+
hasRecurring: state.hasRecurring === void 0 ? false : state.hasRecurring,
|
|
2896
|
+
forceFuture: state.forceFuture === void 0 ? false : state.forceFuture
|
|
2897
|
+
};
|
|
409
2898
|
}
|
|
410
|
-
function
|
|
411
|
-
return
|
|
2899
|
+
function anySelectedRenewal(groups) {
|
|
2900
|
+
return groups.some((group) => group.items.some((item) => item.selected && item.future === true || anySelectedRenewal(item.groups)));
|
|
412
2901
|
}
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
2902
|
+
function minorUnits(value) {
|
|
2903
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
2904
|
+
return null;
|
|
2905
|
+
const match = /^(\d{1,12})(?:\.(\d{1,2}))?$/.exec(String(value));
|
|
2906
|
+
return match ? Number(match[1]) * 100 + Number((match[2] ?? "").padEnd(2, "0")) : null;
|
|
2907
|
+
}
|
|
2908
|
+
function fastspringFacts(state, merchant) {
|
|
2909
|
+
const unreadable = `${merchant}'s order record is not in the layout Agentcard reviewed`;
|
|
2910
|
+
const projection = isPlainObject(state) ? fastspringProjection(state) : null;
|
|
2911
|
+
if (!projection || JSON.stringify(projection) !== JSON.stringify(state))
|
|
2912
|
+
return fail(unreadable);
|
|
2913
|
+
const { totalWithTaxValue, currency, groups } = projection.order;
|
|
2914
|
+
if (typeof currency !== "string" || !/^[A-Z]{3}$/.test(currency))
|
|
2915
|
+
return fail(unreadable);
|
|
2916
|
+
if (!TWO_DECIMAL_CURRENCIES.has(currency.toLowerCase()))
|
|
2917
|
+
return fail(`${merchant}'s order is in a currency this build does not read`);
|
|
2918
|
+
const amountCents = minorUnits(totalWithTaxValue);
|
|
2919
|
+
if (amountCents === null)
|
|
2920
|
+
return fail(`${merchant}'s order total is not a whole number of cents`);
|
|
2921
|
+
if (typeof projection.production !== "boolean" || typeof projection.hasRecurring !== "boolean" || typeof projection.forceFuture !== "boolean") {
|
|
2922
|
+
return fail(unreadable);
|
|
2923
|
+
}
|
|
2924
|
+
const recurring = projection.hasRecurring || projection.forceFuture || anySelectedRenewal(groups);
|
|
2925
|
+
return { ok: true, facts: { amount: { amountCents, currency: currency.toLowerCase() }, live: projection.production, recurring } };
|
|
2926
|
+
}
|
|
2927
|
+
var READERS = /* @__PURE__ */ Object.freeze({
|
|
2928
|
+
fastspring_session: Object.freeze({ state: fastspringState, project: fastspringProjection, facts: fastspringFacts })
|
|
2929
|
+
});
|
|
2930
|
+
var MERCHANT_CARD_ENDPOINT_FACT_READERS = /* @__PURE__ */ Object.freeze(Object.keys(READERS));
|
|
2931
|
+
function cardPlaceholders(p, cardUrl) {
|
|
2932
|
+
const match = urlTemplateMatch({ path: p.path, query: p.query }, cardUrl);
|
|
2933
|
+
return match && match.origin === p.endpoint ? match.placeholders : null;
|
|
2934
|
+
}
|
|
2935
|
+
function boundShape(p, card, shapes, exchange) {
|
|
2936
|
+
const bind = p.facts.bind;
|
|
2937
|
+
for (const shape of shapes) {
|
|
2938
|
+
if (shape.method !== exchange.method.toUpperCase())
|
|
2939
|
+
continue;
|
|
2940
|
+
const match = urlTemplateMatch(shape, exchange.url);
|
|
2941
|
+
if (match && typeof card[bind] === "string" && match.placeholders[bind] === card[bind])
|
|
2942
|
+
return shape;
|
|
2943
|
+
}
|
|
2944
|
+
return null;
|
|
2945
|
+
}
|
|
2946
|
+
function shapeFor(p, cardUrl, exchange) {
|
|
2947
|
+
if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
|
|
2948
|
+
return null;
|
|
2949
|
+
const card = cardPlaceholders(p, cardUrl);
|
|
2950
|
+
const url = strictUrl(exchange.url);
|
|
2951
|
+
if (!card || !url || url.origin !== p.endpoint)
|
|
2952
|
+
return null;
|
|
2953
|
+
return boundShape(p, card, p.facts.responses, exchange);
|
|
2954
|
+
}
|
|
2955
|
+
function sessionPath(p, card) {
|
|
2956
|
+
const segments = p.path.split("/");
|
|
2957
|
+
const at = segments.indexOf(`:${p.facts.bind}`);
|
|
2958
|
+
if (at < 1)
|
|
2959
|
+
return null;
|
|
2960
|
+
const written = segments.slice(0, at + 1).map((segment) => segment.startsWith(":") ? card[segment.slice(1)] : segment);
|
|
2961
|
+
return written.every((segment) => typeof segment === "string") ? written.join("/") : null;
|
|
2962
|
+
}
|
|
2963
|
+
function merchantCardEndpointFactsRole(profile, cardUrl, exchange) {
|
|
2964
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
2965
|
+
if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
|
|
2966
|
+
return null;
|
|
2967
|
+
const card = cardPlaceholders(p, cardUrl);
|
|
2968
|
+
const session = card ? sessionPath(p, card) : null;
|
|
2969
|
+
let url;
|
|
2970
|
+
try {
|
|
2971
|
+
url = new URL(exchange.url);
|
|
2972
|
+
}
|
|
2973
|
+
catch {
|
|
2974
|
+
return null;
|
|
2975
|
+
}
|
|
2976
|
+
if (!session || url.origin !== p.endpoint || url.pathname !== session && !url.pathname.startsWith(`${session}/`))
|
|
2977
|
+
return null;
|
|
2978
|
+
if (exchange.method.toUpperCase() === p.method && exchange.url === cardUrl)
|
|
2979
|
+
return "card";
|
|
2980
|
+
if (shapeFor(p, cardUrl, exchange))
|
|
2981
|
+
return "record";
|
|
2982
|
+
if (boundShape(p, card, p.facts.inert, exchange))
|
|
2983
|
+
return "inert";
|
|
2984
|
+
return "unread";
|
|
2985
|
+
}
|
|
2986
|
+
function projectMerchantCardEndpointFacts(profile, cardUrl, exchange) {
|
|
2987
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
2988
|
+
const shape = shapeFor(p, cardUrl, exchange);
|
|
2989
|
+
if (!shape)
|
|
2990
|
+
return { refusal: `the response is not ${p.merchant}'s record of this checkout` };
|
|
2991
|
+
if (!Number.isInteger(exchange.status) || exchange.status < 200 || exchange.status > 299)
|
|
2992
|
+
return { refusal: `${p.merchant}'s record of this checkout did not load` };
|
|
2993
|
+
if (typeof exchange.body !== "string" || exchange.body.length > MERCHANT_CARD_ENDPOINT_FACTS_TEXT_MAX)
|
|
2994
|
+
return { refusal: `${p.merchant}'s record of this checkout could not be read` };
|
|
2995
|
+
const reader = READERS[p.facts.reader];
|
|
2996
|
+
const read = reader.state(shape.format, exchange.body);
|
|
2997
|
+
if (read.none)
|
|
2998
|
+
return { none: true };
|
|
2999
|
+
if (read.refusal)
|
|
3000
|
+
return { refusal: read.refusal };
|
|
3001
|
+
const state = reader.project(read.state);
|
|
3002
|
+
if (!state)
|
|
3003
|
+
return { refusal: `${p.merchant}'s order record is not in the layout Agentcard reviewed` };
|
|
3004
|
+
return { record: { method: shape.method, url: exchange.url, state } };
|
|
3005
|
+
}
|
|
3006
|
+
function readMerchantCardEndpointFacts(profile, cardUrl, record) {
|
|
3007
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3008
|
+
if (!p.facts)
|
|
3009
|
+
return fail(`Agentcard reads no order record for ${p.merchant}`);
|
|
3010
|
+
if (!isPlainObject(record) || Object.keys(record).sort().join(",") !== "method,state,url")
|
|
3011
|
+
return fail(`the order record for ${p.merchant} is malformed`);
|
|
3012
|
+
if (!shapeFor(p, cardUrl, record))
|
|
3013
|
+
return fail(`the order record is not ${p.merchant}'s record of this checkout`);
|
|
3014
|
+
return READERS[p.facts.reader].facts(record.state, p.merchant);
|
|
3015
|
+
}
|
|
3016
|
+
function merchantCardEndpointFactsRefusal(profile, facts, agent) {
|
|
3017
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3018
|
+
const refuse2 = (code, reason) => ({ code, reason });
|
|
3019
|
+
if (!isPlainObject(facts) || !isPlainObject(facts.amount) || !isPlainObject(agent))
|
|
3020
|
+
return refuse2("malformed", `the order record for ${p.merchant} is malformed`);
|
|
3021
|
+
if (facts.recurring !== false)
|
|
3022
|
+
return refuse2("recurring", `${p.merchant}'s order renews on a schedule, and each renewal would charge the card with no approval of its own`);
|
|
3023
|
+
if (facts.live !== agent.live)
|
|
3024
|
+
return agent.live ? refuse2("test_checkout", `${p.merchant}'s checkout is in test mode`) : refuse2("live_checkout", `${p.merchant}'s checkout is live`);
|
|
3025
|
+
if (typeof agent.currency !== "string" || facts.amount.currency !== agent.currency.toLowerCase()) {
|
|
3026
|
+
return refuse2("currency", `${p.merchant}'s order is in another currency than the one approved`);
|
|
3027
|
+
}
|
|
3028
|
+
if (!Number.isSafeInteger(agent.amountCents) || Math.abs(facts.amount.amountCents - agent.amountCents) > ROUNDING_MARGIN) {
|
|
3029
|
+
return refuse2("amount", `${p.merchant}'s own total is not the amount the cardholder would be asked to approve`);
|
|
3030
|
+
}
|
|
3031
|
+
return null;
|
|
416
3032
|
}
|
|
417
3033
|
// src/merchant-card-endpoints.js
|
|
418
3034
|
var MERCHANT_CARD_ENDPOINT_LIMITS = /* @__PURE__ */ Object.freeze({
|
|
@@ -428,7 +3044,9 @@ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
|
428
3044
|
var COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,256}$/;
|
|
429
3045
|
var COOKIE_VALUE = /^(?:[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*|"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*")$/;
|
|
430
3046
|
var BOUNDARY = /^[A-Za-z0-9'()+_,\-./:=?]{1,70}$/;
|
|
431
|
-
var BUNDLE_MEMBERS = /* @__PURE__ */ new Set(["url", "page_origin", "headers", "cookies", "referer", "body"]);
|
|
3047
|
+
var BUNDLE_MEMBERS = /* @__PURE__ */ new Set(["url", "page_origin", "navigation", "headers", "cookies", "referer", "body"]);
|
|
3048
|
+
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}$/ });
|
|
3049
|
+
var NAVIGATION_FORM_TYPE = "application/x-www-form-urlencoded";
|
|
432
3050
|
function utf8Length(text) {
|
|
433
3051
|
let bytes = 0;
|
|
434
3052
|
for (let i = 0; i < text.length; i += 1) {
|
|
@@ -522,12 +3140,19 @@ function multipartBoundary(contentType) {
|
|
|
522
3140
|
const boundary = match ? match[1] ?? match[2] : null;
|
|
523
3141
|
return boundary && BOUNDARY.test(boundary) ? boundary : null;
|
|
524
3142
|
}
|
|
3143
|
+
function jsonBody(p) {
|
|
3144
|
+
return p.encoding === "json" || p.encoding === "json_api";
|
|
3145
|
+
}
|
|
525
3146
|
function contentTypeRefusal(p, contentType) {
|
|
526
3147
|
if (typeof contentType !== "string")
|
|
527
3148
|
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;
|
|
3149
|
+
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
3150
|
return ok ? null : `the card request to ${p.merchant} is not sent as ${p.encoding}`;
|
|
530
3151
|
}
|
|
3152
|
+
function merchantCardEndpointRequestOrigin(profile) {
|
|
3153
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3154
|
+
return p.frame ?? p.checkout;
|
|
3155
|
+
}
|
|
531
3156
|
function forbiddenKeyIn(value, depth = 0) {
|
|
532
3157
|
if (depth > 64)
|
|
533
3158
|
return true;
|
|
@@ -608,8 +3233,76 @@ function parseMultipart(body, boundary) {
|
|
|
608
3233
|
function sameName(name, wanted) {
|
|
609
3234
|
return !FORBIDDEN_KEYS.has(name) && (name.toLowerCase() === wanted.toLowerCase() || name.toUpperCase() === wanted.toUpperCase());
|
|
610
3235
|
}
|
|
3236
|
+
function isSelector(step) {
|
|
3237
|
+
if (!isPlainObject(step))
|
|
3238
|
+
return false;
|
|
3239
|
+
const keys = Object.keys(step);
|
|
3240
|
+
return keys.length === 1 && !FORBIDDEN_KEYS.has(keys[0]) && typeof step[keys[0]] === "string" && step[keys[0]] !== "";
|
|
3241
|
+
}
|
|
3242
|
+
var hasSelector = (at) => Array.isArray(at) && at.some(isSelector);
|
|
3243
|
+
function selectAt(root, at) {
|
|
3244
|
+
let node = root;
|
|
3245
|
+
const path = [];
|
|
3246
|
+
for (const step of at) {
|
|
3247
|
+
if (isSelector(step)) {
|
|
3248
|
+
const [[name, wanted]] = Object.entries(step);
|
|
3249
|
+
if (!Array.isArray(node))
|
|
3250
|
+
return { found: false };
|
|
3251
|
+
const hits = [];
|
|
3252
|
+
node.forEach((item, index) => {
|
|
3253
|
+
if (isPlainObject(item) && Object.prototype.hasOwnProperty.call(item, name) && item[name] === wanted)
|
|
3254
|
+
hits.push(index);
|
|
3255
|
+
});
|
|
3256
|
+
if (hits.length !== 1)
|
|
3257
|
+
return { found: false };
|
|
3258
|
+
path.push(hits[0]);
|
|
3259
|
+
node = node[hits[0]];
|
|
3260
|
+
}
|
|
3261
|
+
else {
|
|
3262
|
+
const next = jsonAt(node, [step]);
|
|
3263
|
+
if (!next.found)
|
|
3264
|
+
return { found: false };
|
|
3265
|
+
path.push(step);
|
|
3266
|
+
node = next.value;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
return { found: true, value: node, path };
|
|
3270
|
+
}
|
|
3271
|
+
function selectVariants(root, at) {
|
|
3272
|
+
let nodes = [root];
|
|
3273
|
+
for (const step of at) {
|
|
3274
|
+
if (!isSelector(step)) {
|
|
3275
|
+
nodes = nodes.flatMap((node) => valuesIgnoringCase(node, [step]));
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
const [[name, wanted]] = Object.entries(step);
|
|
3279
|
+
const next = [];
|
|
3280
|
+
for (const node of nodes) {
|
|
3281
|
+
if (!Array.isArray(node))
|
|
3282
|
+
continue;
|
|
3283
|
+
for (const item of node) {
|
|
3284
|
+
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())))
|
|
3285
|
+
next.push(item);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
nodes = next;
|
|
3289
|
+
}
|
|
3290
|
+
return nodes;
|
|
3291
|
+
}
|
|
3292
|
+
function describeAt(at) {
|
|
3293
|
+
if (!hasSelector(at))
|
|
3294
|
+
return describePath(at);
|
|
3295
|
+
return at.map((step) => isSelector(step) ? `[${Object.keys(step)[0]}=${Object.values(step)[0]}]` : typeof step === "number" ? `[${step}]` : `.${step}`).join("").replace(/^\./, "");
|
|
3296
|
+
}
|
|
611
3297
|
function jsonReader(parsed) {
|
|
612
|
-
return {
|
|
3298
|
+
return {
|
|
3299
|
+
parsed,
|
|
3300
|
+
exact: (at) => hasSelector(at) ? selectAt(parsed, at) : jsonAt(parsed, at),
|
|
3301
|
+
variants: (at) => hasSelector(at) ? selectVariants(parsed, at) : valuesIgnoringCase(parsed, at),
|
|
3302
|
+
label: describeAt,
|
|
3303
|
+
prefixed: (prefix) => Object.keys(parsed).filter((key) => startsWithIgnoringCase(key, prefix)).map((key) => parsed[key]),
|
|
3304
|
+
scalars: () => jsonScalars(parsed)
|
|
3305
|
+
};
|
|
613
3306
|
}
|
|
614
3307
|
function fieldReader(fields) {
|
|
615
3308
|
return {
|
|
@@ -619,9 +3312,27 @@ function fieldReader(fields) {
|
|
|
619
3312
|
return hits.length === 1 ? { found: true, value: hits[0].value, field: hits[0] } : { found: false };
|
|
620
3313
|
},
|
|
621
3314
|
variants: (at) => fields.filter((field) => sameName(field.name, at)).map((field) => field.value),
|
|
622
|
-
label: (at) => at
|
|
3315
|
+
label: (at) => at,
|
|
3316
|
+
prefixed: (prefix) => fields.filter((field) => startsWithIgnoringCase(field.name, prefix)).map((field) => field.value),
|
|
3317
|
+
scalars: () => fields.map((field) => field.value)
|
|
623
3318
|
};
|
|
624
3319
|
}
|
|
3320
|
+
function startsWithIgnoringCase(name, prefix) {
|
|
3321
|
+
return !FORBIDDEN_KEYS.has(name) && name.length >= prefix.length && sameName(name.slice(0, prefix.length), prefix);
|
|
3322
|
+
}
|
|
3323
|
+
function jsonScalars(value, out = [], depth = 0) {
|
|
3324
|
+
if (depth > 64)
|
|
3325
|
+
return out;
|
|
3326
|
+
if (typeof value === "string" || typeof value === "number")
|
|
3327
|
+
out.push(value);
|
|
3328
|
+
else if (Array.isArray(value))
|
|
3329
|
+
for (const item of value)
|
|
3330
|
+
jsonScalars(item, out, depth + 1);
|
|
3331
|
+
else if (isPlainObject(value))
|
|
3332
|
+
for (const key of Object.keys(value))
|
|
3333
|
+
jsonScalars(value[key], out, depth + 1);
|
|
3334
|
+
return out;
|
|
3335
|
+
}
|
|
625
3336
|
function readBody(p, body, contentType) {
|
|
626
3337
|
if (typeof body !== "string")
|
|
627
3338
|
return { refusal: "the paused body is not text" };
|
|
@@ -630,7 +3341,7 @@ function readBody(p, body, contentType) {
|
|
|
630
3341
|
const typeRefusal = contentTypeRefusal(p, contentType);
|
|
631
3342
|
if (typeRefusal)
|
|
632
3343
|
return { refusal: typeRefusal };
|
|
633
|
-
if (p
|
|
3344
|
+
if (jsonBody(p)) {
|
|
634
3345
|
let parsed2;
|
|
635
3346
|
try {
|
|
636
3347
|
parsed2 = JSON.parse(body);
|
|
@@ -658,28 +3369,89 @@ var SLOT_NAMES = /* @__PURE__ */ Object.freeze({
|
|
|
658
3369
|
cvv: "the security code",
|
|
659
3370
|
exp_month: "the expiry month",
|
|
660
3371
|
exp_year: "the expiry year",
|
|
3372
|
+
expiry: "the expiry date",
|
|
3373
|
+
last4: "the card number's last four digits",
|
|
3374
|
+
leading: "the card number's leading digits",
|
|
661
3375
|
brand: "the card network code"
|
|
662
3376
|
});
|
|
3377
|
+
var EXPIRY_FORMATS = /* @__PURE__ */ Object.freeze({
|
|
3378
|
+
mmyy: /^(?:0[1-9]|1[0-2])\d{2}$/,
|
|
3379
|
+
"mm/yy": /^(?:0[1-9]|1[0-2])\/\d{2}$/,
|
|
3380
|
+
"mm/yyyy": /^(?:0[1-9]|1[0-2])\/\d{4}$/,
|
|
3381
|
+
"yyyy-mm": /^\d{4}-(?:0[1-9]|1[0-2])$/,
|
|
3382
|
+
"MM/YY": /^(?:0[1-9]|1[0-2])\/\d{2}$/,
|
|
3383
|
+
"MM-YY": /^(?:0[1-9]|1[0-2])-\d{2}$/,
|
|
3384
|
+
MMYYYY: /^(?:0[1-9]|1[0-2])\d{4}$/
|
|
3385
|
+
});
|
|
3386
|
+
var AS_TYPED_EXPIRY_FORMATS = /* @__PURE__ */ Object.freeze(["mmyy", "mm/yy", "mm/yyyy", "yyyy-mm"]);
|
|
3387
|
+
function expiryForm(spec, value) {
|
|
3388
|
+
if (typeof value !== "string")
|
|
3389
|
+
return null;
|
|
3390
|
+
const forms = spec.format === "as_typed" ? AS_TYPED_EXPIRY_FORMATS : [spec.format];
|
|
3391
|
+
return forms.find((form) => Object.prototype.hasOwnProperty.call(EXPIRY_FORMATS, form) && EXPIRY_FORMATS[form].test(value)) ?? null;
|
|
3392
|
+
}
|
|
3393
|
+
function expiryText(form, facts) {
|
|
3394
|
+
const mm = String(facts.month).padStart(2, "0");
|
|
3395
|
+
const yy = String(facts.year % 100).padStart(2, "0");
|
|
3396
|
+
if (form === "mmyy")
|
|
3397
|
+
return `${mm}${yy}`;
|
|
3398
|
+
if (form === "mm/yy" || form === "MM/YY")
|
|
3399
|
+
return `${mm}/${yy}`;
|
|
3400
|
+
if (form === "MM-YY")
|
|
3401
|
+
return `${mm}-${yy}`;
|
|
3402
|
+
if (form === "mm/yyyy")
|
|
3403
|
+
return `${mm}/${facts.year}`;
|
|
3404
|
+
if (form === "MMYYYY")
|
|
3405
|
+
return `${mm}${facts.year}`;
|
|
3406
|
+
return `${facts.year}-${mm}`;
|
|
3407
|
+
}
|
|
663
3408
|
function cardSlots(p) {
|
|
3409
|
+
const c = p.card;
|
|
3410
|
+
const expiry = c.expiry ? [["expiry", c.expiry]] : [["exp_month", c.exp_month], ["exp_year", c.exp_year]];
|
|
664
3411
|
return [
|
|
665
|
-
["pan",
|
|
666
|
-
["cvv",
|
|
667
|
-
|
|
668
|
-
["
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
3412
|
+
["pan", c.pan],
|
|
3413
|
+
["cvv", c.cvv],
|
|
3414
|
+
...expiry,
|
|
3415
|
+
["last4", c.last4],
|
|
3416
|
+
["leading", c.leading],
|
|
3417
|
+
...c.brands.map((brand) => ["brand", brand])
|
|
3418
|
+
].filter(([, spec]) => spec);
|
|
3419
|
+
}
|
|
3420
|
+
function codeNotAsked(reader, spec, variants) {
|
|
3421
|
+
if (spec.format !== "digits_if_asked")
|
|
3422
|
+
return false;
|
|
3423
|
+
if (variants.length === 0)
|
|
3424
|
+
return true;
|
|
3425
|
+
const at = reader.exact(spec.at);
|
|
3426
|
+
return variants.length === 1 && at.found && at.value === "";
|
|
3427
|
+
}
|
|
3428
|
+
function groupedFour(digits) {
|
|
3429
|
+
return digits.replace(/(.{4})/g, "$1 ").trim();
|
|
3430
|
+
}
|
|
3431
|
+
function panDigits(spec, value) {
|
|
3432
|
+
return spec.format === "grouped4" && typeof value === "string" ? value.replace(/ /g, "") : value;
|
|
3433
|
+
}
|
|
3434
|
+
function placeholderFits(p, slot, spec, value) {
|
|
3435
|
+
if (spec.format === "encrypted")
|
|
3436
|
+
return encryptedValueFits(p.merchant_key, value);
|
|
673
3437
|
const text = typeof value === "string";
|
|
674
3438
|
const int = Number.isInteger(value);
|
|
675
3439
|
switch (slot) {
|
|
676
|
-
case "
|
|
677
|
-
return text && /^\d
|
|
3440
|
+
case "leading":
|
|
3441
|
+
return text && value.length === spec.digits && /^\d+$/.test(value);
|
|
3442
|
+
case "pan": {
|
|
3443
|
+
if (!text)
|
|
3444
|
+
return false;
|
|
3445
|
+
const digits = panDigits(spec, value);
|
|
3446
|
+
return /^\d{12,19}$/.test(digits) && (digits === value || value === groupedFour(digits));
|
|
3447
|
+
}
|
|
678
3448
|
case "cvv":
|
|
679
3449
|
return text && /^\d{3,4}$/.test(value);
|
|
680
3450
|
case "exp_month":
|
|
681
3451
|
if (spec.format === "mm")
|
|
682
3452
|
return text && /^(?:0[1-9]|1[0-2])$/.test(value);
|
|
3453
|
+
if (spec.format === "m")
|
|
3454
|
+
return text && /^(?:[1-9]|1[0-2])$/.test(value);
|
|
683
3455
|
if (spec.format === "m_int")
|
|
684
3456
|
return int && value >= 1 && value <= 12;
|
|
685
3457
|
return spec.format === "as_typed" && (text && /^(?:0?[1-9]|1[0-2])$/.test(value) || int && value >= 1 && value <= 12);
|
|
@@ -688,50 +3460,102 @@ function placeholderFits(slot, spec, value) {
|
|
|
688
3460
|
return text && /^\d{4}$/.test(value);
|
|
689
3461
|
if (spec.format === "yyyy_int")
|
|
690
3462
|
return int && value >= 1e3 && value <= 9999;
|
|
3463
|
+
if (spec.format === "yy_int")
|
|
3464
|
+
return int && value >= 0 && value <= 99;
|
|
691
3465
|
return spec.format === "as_typed" && (text && /^(?:\d{2}|\d{4})$/.test(value) || int && (value >= 0 && value <= 99 || value >= 1e3 && value <= 9999));
|
|
3466
|
+
case "expiry":
|
|
3467
|
+
return expiryForm(spec, value) !== null;
|
|
3468
|
+
case "last4":
|
|
3469
|
+
return text && /^\d{4}$/.test(value);
|
|
692
3470
|
case "brand":
|
|
693
3471
|
return spec.map ? Object.values(spec.map).includes(value) : text && value !== "";
|
|
694
3472
|
default:
|
|
695
3473
|
return false;
|
|
696
3474
|
}
|
|
697
3475
|
}
|
|
3476
|
+
var DECIMAL_AMOUNT = /^\d{1,9}(?:\.\d{1,2})?$/;
|
|
698
3477
|
function isNothing(value) {
|
|
699
3478
|
return value === null || value === "" || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
|
|
700
3479
|
}
|
|
3480
|
+
function clearCardNumber(texts) {
|
|
3481
|
+
for (const text of texts) {
|
|
3482
|
+
for (const [run] of text.matchAll(/[0-9](?:[ -]?[0-9])*/g)) {
|
|
3483
|
+
const digits = run.replace(/[ -]/g, "");
|
|
3484
|
+
if (digits.length < 13 || digits.length > 19 || !/^[2-6]/.test(digits))
|
|
3485
|
+
continue;
|
|
3486
|
+
let sum = 0;
|
|
3487
|
+
for (let i = 0; i < digits.length; i += 1) {
|
|
3488
|
+
let digit = Number(digits[digits.length - 1 - i]);
|
|
3489
|
+
if (i % 2 === 1)
|
|
3490
|
+
digit = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;
|
|
3491
|
+
sum += digit;
|
|
3492
|
+
}
|
|
3493
|
+
if (sum % 10 === 0)
|
|
3494
|
+
return digits;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return null;
|
|
3498
|
+
}
|
|
3499
|
+
var bodyTexts = (reader, body) => [body, ...reader.fields ? reader.fields.map((field) => field.value) : []];
|
|
3500
|
+
var pinnedKids = (merchantKey) => merchantKey.keys.map((key) => key.kid);
|
|
701
3501
|
function rulesRefusal(p, reader, body) {
|
|
702
3502
|
const request = `the paused body is not ${p.merchant}'s card request`;
|
|
703
3503
|
for (const [slot, spec] of cardSlots(p)) {
|
|
704
3504
|
const label = reader.label(spec.at);
|
|
705
|
-
const
|
|
3505
|
+
const variants = reader.variants(spec.at);
|
|
3506
|
+
if (slot === "cvv" && codeNotAsked(reader, spec, variants))
|
|
3507
|
+
continue;
|
|
3508
|
+
const once = onlyOnce(variants.length, label);
|
|
706
3509
|
if (once)
|
|
707
3510
|
return `${request}: ${once}`;
|
|
708
3511
|
const at = reader.exact(spec.at);
|
|
709
3512
|
if (!at.found)
|
|
710
3513
|
return `${request}: ${label} is not spelled as reviewed`;
|
|
711
|
-
if (!placeholderFits(slot, spec, at.value))
|
|
3514
|
+
if (!placeholderFits(p, slot, spec, at.value))
|
|
712
3515
|
return `${request}: ${label} is not ${SLOT_NAMES[slot]} in the reviewed form`;
|
|
713
3516
|
}
|
|
714
|
-
|
|
715
|
-
|
|
3517
|
+
const merchantKey = p.merchant_key;
|
|
3518
|
+
if (merchantKey && merchantKey.scheme === "rsa_oaep" && merchantKey.kid_at) {
|
|
3519
|
+
const label = reader.label(merchantKey.kid_at);
|
|
3520
|
+
const once = onlyOnce(reader.variants(merchantKey.kid_at).length, label);
|
|
3521
|
+
if (once)
|
|
3522
|
+
return `${request}: ${once}`;
|
|
3523
|
+
const at = reader.exact(merchantKey.kid_at);
|
|
3524
|
+
const kids = pinnedKids(merchantKey);
|
|
3525
|
+
if (!at.found || typeof at.value !== "string" || at.value === "" || kids.length > 0 && !kids.includes(at.value)) {
|
|
3526
|
+
return `${request}: ${label} does not name a key Agentcard pinned for ${p.merchant}`;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const typed = p.card.pan && ["digits", "grouped4"].includes(p.card.pan.format) ? panDigits(p.card.pan, reader.exact(p.card.pan.at).value) : null;
|
|
3530
|
+
if (typed !== null) {
|
|
3531
|
+
if (countRuns(body, typed) !== 1) {
|
|
3532
|
+
return `${request}: the card number appears outside ${reader.label(p.card.pan.at)}`;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
else if (clearCardNumber(bodyTexts(reader, body)) !== null) {
|
|
3536
|
+
return `${request}: a card number appears in the clear`;
|
|
3537
|
+
}
|
|
3538
|
+
if (p.card.last4 && (typed === null || reader.exact(p.card.last4.at).value !== typed.slice(-4))) {
|
|
3539
|
+
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
3540
|
}
|
|
717
3541
|
for (const rule of p.requires) {
|
|
718
3542
|
const label = reader.label(rule.at);
|
|
719
3543
|
const variants = reader.variants(rule.at);
|
|
720
3544
|
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);
|
|
3545
|
+
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
3546
|
if (!at.found || variants.length !== 1 || !holds(at.value))
|
|
723
3547
|
return `${request}: ${label} is not what the review requires`;
|
|
724
3548
|
}
|
|
725
3549
|
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
|
|
3550
|
+
const values = "prefix" in rule ? reader.prefixed(rule.prefix) : reader.variants(rule.at);
|
|
3551
|
+
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
3552
|
if (fires)
|
|
729
|
-
return `the paused body asks ${p.merchant} for more than this purchase: ${reader.label(rule.at)}`;
|
|
3553
|
+
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
3554
|
}
|
|
731
3555
|
if (p.amount) {
|
|
732
3556
|
const label = reader.label(p.amount.at);
|
|
733
3557
|
const at = reader.exact(p.amount.at);
|
|
734
|
-
const fits = p.amount.kind === "decimal_string" ? typeof at.value === "string" &&
|
|
3558
|
+
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
3559
|
if (!at.found || reader.variants(p.amount.at).length !== 1 || !fits)
|
|
736
3560
|
return `${request}: the amount at ${label} is not a ${p.amount.kind.replace("_", " ")}`;
|
|
737
3561
|
}
|
|
@@ -748,7 +3572,29 @@ function merchantCardEndpointBodyRefusal(profile, body, contentType) {
|
|
|
748
3572
|
const read = readBody(p, body, contentType);
|
|
749
3573
|
return read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
750
3574
|
}
|
|
751
|
-
|
|
3575
|
+
function merchantCardEndpointPasses(profile, body, contentType) {
|
|
3576
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3577
|
+
if (!p.payment_method)
|
|
3578
|
+
return false;
|
|
3579
|
+
const read = readBody(p, body, contentType);
|
|
3580
|
+
if (read.refusal)
|
|
3581
|
+
return false;
|
|
3582
|
+
const at = read.reader.exact(p.payment_method.at);
|
|
3583
|
+
if (read.reader.variants(p.payment_method.at).length !== 1 || !at.found || typeof at.value !== "string" || at.value === "")
|
|
3584
|
+
return false;
|
|
3585
|
+
const method = at.value.toLowerCase();
|
|
3586
|
+
return !p.payment_method.card.some((value) => value.toLowerCase() === method);
|
|
3587
|
+
}
|
|
3588
|
+
function merchantCardEndpointSessionTtl(profile, body, contentType) {
|
|
3589
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3590
|
+
const read = readBody(p, body, contentType);
|
|
3591
|
+
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
3592
|
+
if (refusal)
|
|
3593
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
3594
|
+
if (!p.challenge)
|
|
3595
|
+
return null;
|
|
3596
|
+
return read.reader.variants(p.challenge.at).some((value) => !isNothing(value)) ? p.challenge.ttl_ms : null;
|
|
3597
|
+
}
|
|
752
3598
|
function merchantCardEndpointAmount(profile, body, contentType) {
|
|
753
3599
|
const p = trustedMerchantCardEndpoint(profile);
|
|
754
3600
|
const read = readBody(p, body, contentType);
|
|
@@ -763,9 +3609,91 @@ function merchantCardEndpointAmount(profile, body, contentType) {
|
|
|
763
3609
|
return { amountCents: Number(value), currency };
|
|
764
3610
|
if (!TWO_DECIMAL_CURRENCIES.has(currency))
|
|
765
3611
|
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(".");
|
|
3612
|
+
const [whole, fraction = ""] = String(value).split(".");
|
|
767
3613
|
return { amountCents: Number(whole) * 100 + Number(fraction.padEnd(2, "0")), currency };
|
|
768
3614
|
}
|
|
3615
|
+
var WRITTEN_DIGIT = String.raw `(?:[0-9]|\\u003[0-9]|%3[0-9]|�*(?:4[89]|5[0-7]);|�*3[0-9];)`;
|
|
3616
|
+
var WRITTEN_RUN = /* @__PURE__ */ new RegExp(`${WRITTEN_DIGIT}+(?:${SEPARATOR}+${WRITTEN_DIGIT}+)*`, "gi");
|
|
3617
|
+
var RUN_TOKEN = /* @__PURE__ */ new RegExp(`(${WRITTEN_DIGIT})|${SEPARATOR}`, "giy");
|
|
3618
|
+
function writtenDigit(token) {
|
|
3619
|
+
if (token.length === 1)
|
|
3620
|
+
return token;
|
|
3621
|
+
if (/^&#[0-9]/.test(token))
|
|
3622
|
+
return String.fromCharCode(Number(token.slice(2, -1)));
|
|
3623
|
+
return token.endsWith(";") ? token[token.length - 2] : token[token.length - 1];
|
|
3624
|
+
}
|
|
3625
|
+
function luhnValid(digits) {
|
|
3626
|
+
let sum = 0;
|
|
3627
|
+
for (let i = 0; i < digits.length; i += 1) {
|
|
3628
|
+
let digit = digits.charCodeAt(digits.length - 1 - i) - 48;
|
|
3629
|
+
if (i % 2 === 1) {
|
|
3630
|
+
digit *= 2;
|
|
3631
|
+
if (digit > 9)
|
|
3632
|
+
digit -= 9;
|
|
3633
|
+
}
|
|
3634
|
+
sum += digit;
|
|
3635
|
+
}
|
|
3636
|
+
return sum % 10 === 0;
|
|
3637
|
+
}
|
|
3638
|
+
function writesCardNumber(text) {
|
|
3639
|
+
for (const [run] of text.matchAll(WRITTEN_RUN)) {
|
|
3640
|
+
const groups = [];
|
|
3641
|
+
let group = "";
|
|
3642
|
+
for (const token of run.matchAll(RUN_TOKEN)) {
|
|
3643
|
+
if (token[1] !== void 0)
|
|
3644
|
+
group += writtenDigit(token[1]);
|
|
3645
|
+
else if (group !== "") {
|
|
3646
|
+
groups.push(group);
|
|
3647
|
+
group = "";
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
if (group !== "")
|
|
3651
|
+
groups.push(group);
|
|
3652
|
+
for (let first = 0; first < groups.length; first += 1) {
|
|
3653
|
+
let digits = "";
|
|
3654
|
+
for (let last = first; last < groups.length && digits.length + groups[last].length <= 19; last += 1) {
|
|
3655
|
+
digits += groups[last];
|
|
3656
|
+
if (digits.length >= 12 && luhnValid(digits))
|
|
3657
|
+
return true;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
return false;
|
|
3662
|
+
}
|
|
3663
|
+
function cardNumberValues(p, body, contentType) {
|
|
3664
|
+
const at = (p.card.pan ?? p.card.cvv).at;
|
|
3665
|
+
if (jsonBody(p)) {
|
|
3666
|
+
let parsed2;
|
|
3667
|
+
try {
|
|
3668
|
+
parsed2 = JSON.parse(body);
|
|
3669
|
+
}
|
|
3670
|
+
catch {
|
|
3671
|
+
return [];
|
|
3672
|
+
}
|
|
3673
|
+
if (!isPlainObject(parsed2))
|
|
3674
|
+
return [];
|
|
3675
|
+
return hasSelector(at) ? selectVariants(parsed2, at) : valuesIgnoringCase(parsed2, at);
|
|
3676
|
+
}
|
|
3677
|
+
const parsed = p.encoding === "form" ? parseForm(body) : parseMultipart(body, multipartBoundary(contentType));
|
|
3678
|
+
return parsed.refusal ? [] : parsed.fields.filter((field) => sameName(field.name, at)).map((field) => field.value);
|
|
3679
|
+
}
|
|
3680
|
+
function navigationPostCarriesCard(p, body, contentType) {
|
|
3681
|
+
const read = readBody(p, body, contentType);
|
|
3682
|
+
if (read.refusal)
|
|
3683
|
+
return true;
|
|
3684
|
+
const { reader } = read;
|
|
3685
|
+
if ([p.card.pan, p.card.cvv].some((spec) => spec && reader.variants(spec.at).some((value) => !isNothing(value))))
|
|
3686
|
+
return true;
|
|
3687
|
+
return reader.scalars().some((value) => (String(value).replace(/[\s.-]/g, "").match(/\d+/g) ?? []).some((run) => run.length >= 12 && run.length <= 19 && luhnValid(run)));
|
|
3688
|
+
}
|
|
3689
|
+
function merchantCardEndpointCarriesCard(profile, body, contentType) {
|
|
3690
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3691
|
+
if (typeof body !== "string" || body === "" || utf8Length(body) > MERCHANT_CARD_ENDPOINT_LIMITS.bodyBytes)
|
|
3692
|
+
return true;
|
|
3693
|
+
if (cardNumberValues(p, body, contentType).some((value) => !isNothing(value)) || writesCardNumber(body))
|
|
3694
|
+
return true;
|
|
3695
|
+
return p.navigation !== null && navigationPostCarriesCard(p, body, contentType);
|
|
3696
|
+
}
|
|
769
3697
|
function endpointMatches(p, rawUrl, method, opts) {
|
|
770
3698
|
if (typeof rawUrl !== "string" || rawUrl.length > MERCHANT_CARD_ENDPOINT_LIMITS.urlLength)
|
|
771
3699
|
return false;
|
|
@@ -797,6 +3725,14 @@ function merchantCardEndpointMatches(profile, rawUrl, method, opts) {
|
|
|
797
3725
|
function matchMerchantCardEndpoint(rawUrl, method, opts) {
|
|
798
3726
|
return MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((p) => endpointMatches(p, rawUrl, method, opts)) ?? null;
|
|
799
3727
|
}
|
|
3728
|
+
function matchMerchantCardEndpointResend(rawUrl, method) {
|
|
3729
|
+
if (typeof rawUrl !== "string" || rawUrl.length > MERCHANT_CARD_ENDPOINT_LIMITS.urlLength || typeof method !== "string")
|
|
3730
|
+
return null;
|
|
3731
|
+
const url = strictUrl(rawUrl);
|
|
3732
|
+
if (!url)
|
|
3733
|
+
return null;
|
|
3734
|
+
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;
|
|
3735
|
+
}
|
|
800
3736
|
function assertMerchantCardEndpoint(authorization) {
|
|
801
3737
|
if (!isPlainObject(authorization))
|
|
802
3738
|
throw new Error("Refusing to pay: malformed payment endpoint.");
|
|
@@ -838,7 +3774,7 @@ function refererKept(p, referer) {
|
|
|
838
3774
|
catch {
|
|
839
3775
|
return false;
|
|
840
3776
|
}
|
|
841
|
-
return url.href === referer && url.origin === p.checkout && !url.username && !url.password;
|
|
3777
|
+
return url.href === referer && url.origin === (p.frame ?? p.checkout) && !url.username && !url.password;
|
|
842
3778
|
}
|
|
843
3779
|
function lowerHeaders(headers) {
|
|
844
3780
|
const out = /* @__PURE__ */ Object.create(null);
|
|
@@ -850,6 +3786,55 @@ function lowerHeaders(headers) {
|
|
|
850
3786
|
}
|
|
851
3787
|
return out;
|
|
852
3788
|
}
|
|
3789
|
+
function cookieNameAllowed(p, name) {
|
|
3790
|
+
const names = p.session.cookie_names;
|
|
3791
|
+
if (names === null)
|
|
3792
|
+
return true;
|
|
3793
|
+
return names.some((entry) => entry.endsWith("*") ? name.startsWith(entry.slice(0, -1)) && name.length > entry.length - 1 : name === entry);
|
|
3794
|
+
}
|
|
3795
|
+
function sessionCookie(bundle, name) {
|
|
3796
|
+
const hits = (Array.isArray(bundle.cookies) ? bundle.cookies : []).filter((cookie) => cookie.name === name);
|
|
3797
|
+
return hits.length === 1 ? hits[0].value : null;
|
|
3798
|
+
}
|
|
3799
|
+
function checkoutKeyOf(p, bundle, reader) {
|
|
3800
|
+
const once = p.once_per;
|
|
3801
|
+
if (!once)
|
|
3802
|
+
return null;
|
|
3803
|
+
let value = null;
|
|
3804
|
+
if ("at" in once) {
|
|
3805
|
+
const at = reader.exact(once.at);
|
|
3806
|
+
value = at.found && reader.variants(once.at).length === 1 ? at.value : null;
|
|
3807
|
+
}
|
|
3808
|
+
else if ("path" in once) {
|
|
3809
|
+
value = pathPlaceholders(p.path, new URL(bundle.url).pathname)?.[once.path] ?? null;
|
|
3810
|
+
}
|
|
3811
|
+
else if ("query" in once) {
|
|
3812
|
+
const mark = bundle.url.indexOf("?");
|
|
3813
|
+
value = queryPlaceholders(p.query, mark === -1 ? null : bundle.url.slice(mark + 1))?.[once.query] ?? null;
|
|
3814
|
+
}
|
|
3815
|
+
else if ("cookie" in once) {
|
|
3816
|
+
value = sessionCookie(bundle, once.cookie);
|
|
3817
|
+
}
|
|
3818
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
3819
|
+
}
|
|
3820
|
+
function sessionBindingRefusal(p, bundle, contentType) {
|
|
3821
|
+
const { reader } = readBody(p, bundle.body, contentType);
|
|
3822
|
+
for (const rule of p.requires) {
|
|
3823
|
+
if ("equals_cookie" in rule) {
|
|
3824
|
+
const cookie = sessionCookie(bundle, rule.equals_cookie);
|
|
3825
|
+
if (cookie === null || reader.exact(rule.at).value !== cookie)
|
|
3826
|
+
return `${reader.label(rule.at)} is not the checkout the session's ${rule.equals_cookie} cookie names`;
|
|
3827
|
+
}
|
|
3828
|
+
if ("equals_path" in rule) {
|
|
3829
|
+
const segment = pathPlaceholders(p.path, new URL(bundle.url).pathname)?.[rule.equals_path];
|
|
3830
|
+
if (segment === void 0 || reader.exact(rule.at).value !== segment)
|
|
3831
|
+
return `${reader.label(rule.at)} is not the checkout the card request's URL names`;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
if (p.once_per && checkoutKeyOf(p, bundle, reader) === null)
|
|
3835
|
+
return `the session names no checkout for ${p.merchant} to take one payment from`;
|
|
3836
|
+
return null;
|
|
3837
|
+
}
|
|
853
3838
|
function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
854
3839
|
const p = trustedMerchantCardEndpoint(profile);
|
|
855
3840
|
const limits = MERCHANT_CARD_ENDPOINT_LIMITS;
|
|
@@ -861,6 +3846,9 @@ function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
|
861
3846
|
return `the session bundle's URL is not ${p.merchant}'s reviewed card endpoint`;
|
|
862
3847
|
if (bundle.page_origin !== p.checkout)
|
|
863
3848
|
return `the card request was not paused on ${p.checkout}`;
|
|
3849
|
+
if (p.navigation === null ? bundle.navigation !== void 0 : bundle.navigation !== p.navigation) {
|
|
3850
|
+
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`;
|
|
3851
|
+
}
|
|
864
3852
|
const raw = bundle.headers === void 0 ? {} : bundle.headers;
|
|
865
3853
|
const headers = isPlainObject(raw) ? lowerHeaders(raw) : null;
|
|
866
3854
|
if (!headers)
|
|
@@ -892,51 +3880,83 @@ function merchantCardEndpointSessionRefusal(profile, bundle) {
|
|
|
892
3880
|
return "a session cookie is not an RFC 6265 name and value";
|
|
893
3881
|
if (cookie.name.length + cookie.value.length + 1 > limits.headerValueBytes)
|
|
894
3882
|
return "a session cookie is larger than the limit";
|
|
3883
|
+
if (!cookieNameAllowed(p, cookie.name))
|
|
3884
|
+
return `the session bundle carries a cookie outside ${p.merchant}'s reviewed list`;
|
|
895
3885
|
cookieBytes += cookie.name.length + cookie.value.length + 3;
|
|
896
3886
|
}
|
|
897
3887
|
if (cookieBytes > limits.headerBytes)
|
|
898
3888
|
return "the session cookies are larger than the limit";
|
|
899
3889
|
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"]);
|
|
3890
|
+
return `the session Referer is not a page on ${p.frame ?? p.checkout}`;
|
|
3891
|
+
return merchantCardEndpointBodyRefusal(p.id, bundle.body, headers["content-type"]) ?? sessionBindingRefusal(p, bundle, headers["content-type"]);
|
|
3892
|
+
}
|
|
3893
|
+
function merchantCardEndpointCheckoutKey(profile, bundle) {
|
|
3894
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
3895
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
3896
|
+
if (refusal)
|
|
3897
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
3898
|
+
const headers = lowerHeaders(bundle.headers ?? {});
|
|
3899
|
+
return checkoutKeyOf(p, bundle, readBody(p, bundle.body, headers["content-type"]).reader);
|
|
902
3900
|
}
|
|
903
3901
|
function slotValue(slot, spec, placeholder, facts) {
|
|
904
3902
|
if (slot === "pan")
|
|
905
|
-
return facts.number;
|
|
3903
|
+
return spec.format === "grouped4" && placeholder.includes(" ") ? groupedFour(facts.number) : facts.number;
|
|
906
3904
|
if (slot === "cvv")
|
|
907
3905
|
return facts.cvc;
|
|
3906
|
+
if (slot === "last4")
|
|
3907
|
+
return facts.number.slice(-4);
|
|
3908
|
+
if (slot === "expiry")
|
|
3909
|
+
return expiryText(expiryForm(spec, placeholder), facts);
|
|
3910
|
+
if (slot === "leading")
|
|
3911
|
+
return facts.number.slice(0, spec.digits);
|
|
3912
|
+
const month = String(facts.month).padStart(2, "0");
|
|
908
3913
|
if (slot === "exp_month") {
|
|
909
|
-
const padded = String(facts.month).padStart(2, "0");
|
|
910
3914
|
if (spec.format === "mm")
|
|
911
|
-
return
|
|
3915
|
+
return month;
|
|
3916
|
+
if (spec.format === "m")
|
|
3917
|
+
return String(facts.month);
|
|
912
3918
|
if (spec.format === "m_int" || typeof placeholder === "number")
|
|
913
3919
|
return facts.month;
|
|
914
|
-
return placeholder.length === 2 ?
|
|
3920
|
+
return placeholder.length === 2 ? month : String(facts.month);
|
|
915
3921
|
}
|
|
916
3922
|
if (spec.format === "yyyy")
|
|
917
3923
|
return String(facts.year);
|
|
918
3924
|
if (spec.format === "yyyy_int")
|
|
919
3925
|
return facts.year;
|
|
3926
|
+
if (spec.format === "yy_int")
|
|
3927
|
+
return facts.year % 100;
|
|
920
3928
|
if (typeof placeholder === "number")
|
|
921
3929
|
return placeholder >= 1e3 ? facts.year : facts.year % 100;
|
|
922
3930
|
return placeholder.length === 4 ? String(facts.year) : String(facts.year % 100).padStart(2, "0");
|
|
923
3931
|
}
|
|
3932
|
+
function plaintextFor(slot, spec, facts) {
|
|
3933
|
+
if (spec.plaintext === "digits")
|
|
3934
|
+
return slot === "cvv" ? facts.cvc : facts.number;
|
|
3935
|
+
if (spec.plaintext === "MMYYYY")
|
|
3936
|
+
return `${String(facts.month).padStart(2, "0")}${facts.year}`;
|
|
3937
|
+
let claims = facts.number;
|
|
3938
|
+
for (const name of [...spec.plaintext.claims].reverse())
|
|
3939
|
+
claims = { [name]: claims };
|
|
3940
|
+
return JSON.stringify(claims);
|
|
3941
|
+
}
|
|
924
3942
|
function writeJson(parsed, at, value) {
|
|
925
|
-
const
|
|
926
|
-
|
|
3943
|
+
const path = hasSelector(at) ? selectAt(parsed, at).path : at;
|
|
3944
|
+
if (!path)
|
|
3945
|
+
throw new SubstitutionError(`the paused body has no field at ${describeAt(at)}`);
|
|
3946
|
+
const parent = jsonAt(parsed, path.slice(0, -1));
|
|
3947
|
+
const key = path[path.length - 1];
|
|
927
3948
|
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
3949
|
if (!owned)
|
|
929
|
-
throw new SubstitutionError(`the paused body has no field at ${
|
|
3950
|
+
throw new SubstitutionError(`the paused body has no field at ${describeAt(at)}`);
|
|
930
3951
|
parent.value[key] = value;
|
|
931
3952
|
}
|
|
932
|
-
function
|
|
933
|
-
const p = trustedMerchantCardEndpoint(profile);
|
|
3953
|
+
function substitutionPlan(p, body, contentType, card) {
|
|
934
3954
|
const read = readBody(p, body, contentType);
|
|
935
3955
|
const refusal = read.refusal ?? rulesRefusal(p, read.reader, body);
|
|
936
3956
|
if (refusal)
|
|
937
3957
|
throw new SubstitutionError(refusal);
|
|
938
3958
|
const facts = cardFacts(card);
|
|
939
|
-
const typed = read.reader.exact(p.card.pan.at).value;
|
|
3959
|
+
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
3960
|
if (typed === facts.number)
|
|
941
3961
|
throw new SubstitutionError("the paused body already carries the real card number");
|
|
942
3962
|
const bins = p.card.bins && facts.brand && Object.prototype.hasOwnProperty.call(p.card.bins, facts.brand) ? p.card.bins[facts.brand] : null;
|
|
@@ -947,56 +3967,155 @@ function substituteMerchantCardEndpoint(body, contentType, profile, card) {
|
|
|
947
3967
|
const at = read.reader.exact(spec.at);
|
|
948
3968
|
if (slot === "brand" && !spec.map)
|
|
949
3969
|
continue;
|
|
950
|
-
|
|
3970
|
+
if (slot === "cvv" && codeNotAsked(read.reader, spec, read.reader.variants(spec.at)))
|
|
3971
|
+
continue;
|
|
951
3972
|
if (slot === "brand") {
|
|
952
|
-
value = facts.brand && Object.prototype.hasOwnProperty.call(spec.map, facts.brand) ? spec.map[facts.brand] : void 0;
|
|
3973
|
+
const value = facts.brand && Object.prototype.hasOwnProperty.call(spec.map, facts.brand) ? spec.map[facts.brand] : void 0;
|
|
953
3974
|
if (value === void 0)
|
|
954
3975
|
throw new SubstitutionError(`${p.merchant} does not take this card's network`);
|
|
3976
|
+
writes.push({ at: spec.at, found: at, value });
|
|
3977
|
+
}
|
|
3978
|
+
else if (spec.format === "encrypted") {
|
|
3979
|
+
writes.push({ at: spec.at, found: at, value: null, plaintext: plaintextFor(slot, spec, facts) });
|
|
955
3980
|
}
|
|
956
3981
|
else {
|
|
957
|
-
value
|
|
3982
|
+
writes.push({ at: spec.at, found: at, value: slotValue(slot, spec, at.value, facts) });
|
|
958
3983
|
}
|
|
959
|
-
|
|
3984
|
+
}
|
|
3985
|
+
return { read, facts, typed, writes };
|
|
3986
|
+
}
|
|
3987
|
+
function writtenBody(p, plan, body) {
|
|
3988
|
+
if (jsonBody(p)) {
|
|
3989
|
+
for (const write of plan.writes)
|
|
3990
|
+
writeJson(plan.read.reader.parsed, write.at, write.value);
|
|
3991
|
+
return JSON.stringify(plan.read.reader.parsed);
|
|
960
3992
|
}
|
|
961
3993
|
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
|
-
}
|
|
3994
|
+
for (const write of [...plan.writes].sort((a, b) => b.found.field.start - a.found.field.start)) {
|
|
3995
|
+
const text = p.encoding === "form" ? encodeURIComponent(String(write.value)).replace(/%20/g, "+") : String(write.value);
|
|
3996
|
+
out = out.slice(0, write.found.field.start) + text + out.slice(write.found.field.end);
|
|
972
3997
|
}
|
|
973
|
-
|
|
3998
|
+
return out;
|
|
3999
|
+
}
|
|
4000
|
+
function merchantCardEndpointEncrypts(profile) {
|
|
4001
|
+
return trustedMerchantCardEndpoint(profile).merchant_key != null;
|
|
4002
|
+
}
|
|
4003
|
+
function substituteMerchantCardEndpoint(body, contentType, profile, card) {
|
|
4004
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4005
|
+
if (p.merchant_key)
|
|
4006
|
+
throw new SubstitutionError(`${p.merchant} takes the card encrypted under its own key, which this call does not write`);
|
|
4007
|
+
const plan = substitutionPlan(p, body, contentType, card);
|
|
4008
|
+
const out = writtenBody(p, plan, body);
|
|
4009
|
+
if (countRuns(out, plan.typed) !== 0)
|
|
974
4010
|
throw new SubstitutionError("the typed card number would still reach the merchant");
|
|
975
|
-
if (countRuns(out, facts.number) !== 1)
|
|
4011
|
+
if (countRuns(out, plan.facts.number) !== 1)
|
|
976
4012
|
throw new SubstitutionError("the card number would reach the merchant outside its field");
|
|
977
4013
|
if (!merchantCardEndpointLive(p))
|
|
978
4014
|
throw new SubstitutionError(`Agentcard has not turned on payments to ${p.merchant} yet`);
|
|
979
4015
|
return out;
|
|
980
4016
|
}
|
|
981
|
-
function
|
|
4017
|
+
function merchantKeyFor(p, plan) {
|
|
4018
|
+
const merchantKey = p.merchant_key;
|
|
4019
|
+
const encrypted = cardSlots(p).filter(([, spec]) => spec.format === "encrypted").map(([, spec]) => plan.read.reader.exact(spec.at).value);
|
|
4020
|
+
let key = null;
|
|
4021
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
4022
|
+
if (merchantKey.kid_at) {
|
|
4023
|
+
const kid = plan.read.reader.exact(merchantKey.kid_at).value;
|
|
4024
|
+
key = merchantKey.keys.find((entry) => entry.kid === kid) ?? null;
|
|
4025
|
+
}
|
|
4026
|
+
else if (merchantKey.keys.length === 1) {
|
|
4027
|
+
key = merchantKey.keys[0];
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
else if (merchantKey.alg !== null) {
|
|
4031
|
+
const kids = new Set(encrypted.map((value) => readCompactJwe(value)?.header.kid));
|
|
4032
|
+
const [kid] = kids;
|
|
4033
|
+
key = kids.size === 1 ? merchantKey.keys.find((entry) => entry.kid === kid) ?? null : null;
|
|
4034
|
+
}
|
|
4035
|
+
if (!key)
|
|
4036
|
+
throw new SubstitutionError(`Agentcard has not pinned the key ${p.merchant} encrypts the card under`);
|
|
4037
|
+
let size;
|
|
4038
|
+
try {
|
|
4039
|
+
({ size } = parseMerchantKey(key));
|
|
4040
|
+
}
|
|
4041
|
+
catch {
|
|
4042
|
+
throw new SubstitutionError(`the key Agentcard pinned for ${p.merchant} does not match its pin`);
|
|
4043
|
+
}
|
|
4044
|
+
const sizes = encrypted.map((value) => merchantKey.scheme === "rsa_oaep" ? decodeBase64(value)?.length : readCompactJwe(value)?.encryptedKey.length);
|
|
4045
|
+
if (sizes.some((length) => length !== size))
|
|
4046
|
+
throw new SubstitutionError(`the paused body was encrypted under a key Agentcard did not pin for ${p.merchant}`);
|
|
4047
|
+
return key;
|
|
4048
|
+
}
|
|
4049
|
+
async function encryptedValue(merchantKey, key, text, crypto) {
|
|
4050
|
+
const bytes = new TextEncoder().encode(text);
|
|
4051
|
+
try {
|
|
4052
|
+
if (merchantKey.scheme === "rsa_oaep") {
|
|
4053
|
+
return encodeBase64(await rsaOaepEncrypt(key, bytes, { hash: merchantKey.hash, mgf1: merchantKey.mgf1 }, crypto));
|
|
4054
|
+
}
|
|
4055
|
+
return await jweCompactEncrypt(key, bytes, merchantKey, key.kid, crypto);
|
|
4056
|
+
}
|
|
4057
|
+
catch (error) {
|
|
4058
|
+
if (error instanceof MerchantKeyError)
|
|
4059
|
+
throw new SubstitutionError(`the card could not be encrypted under ${error.reason === "key" ? "the pinned key" : "this device"}`);
|
|
4060
|
+
throw error;
|
|
4061
|
+
}
|
|
4062
|
+
finally {
|
|
4063
|
+
bytes.fill(0);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
async function encryptMerchantCardEndpointBody(body, contentType, profile, card, crypto) {
|
|
982
4067
|
const p = trustedMerchantCardEndpoint(profile);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
|
|
4068
|
+
if (!p.merchant_key)
|
|
4069
|
+
throw new SubstitutionError(`${p.merchant} takes the card in the clear, which this call does not write`);
|
|
4070
|
+
const plan = substitutionPlan(p, body, contentType, card);
|
|
4071
|
+
const key = merchantKeyFor(p, plan);
|
|
4072
|
+
for (const write of plan.writes) {
|
|
4073
|
+
if (write.plaintext === void 0)
|
|
4074
|
+
continue;
|
|
4075
|
+
write.value = await encryptedValue(p.merchant_key, key, write.plaintext, crypto);
|
|
4076
|
+
write.plaintext = void 0;
|
|
4077
|
+
}
|
|
4078
|
+
const out = writtenBody(p, plan, body);
|
|
4079
|
+
if (countRuns(out, plan.facts.number) !== 0 || clearCardNumber([out]) !== null)
|
|
4080
|
+
throw new SubstitutionError("a card number would reach the merchant in the clear");
|
|
4081
|
+
if (!merchantCardEndpointLive(p))
|
|
4082
|
+
throw new SubstitutionError(`Agentcard has not turned on payments to ${p.merchant} yet`);
|
|
4083
|
+
return out;
|
|
4084
|
+
}
|
|
4085
|
+
function endpointRequest(p, bundle, captured, body) {
|
|
4086
|
+
const formType = p.navigation === null ? "application/x-www-form-urlencoded; charset=UTF-8" : NAVIGATION_FORM_TYPE;
|
|
4087
|
+
const headers = { "content-type": p.encoding === "form" ? formType : captured["content-type"] };
|
|
989
4088
|
for (const [name, value] of Object.entries(captured))
|
|
990
4089
|
if (name !== "content-type")
|
|
991
4090
|
headers[name] = value;
|
|
992
4091
|
const cookies = bundle.cookies ?? [];
|
|
993
4092
|
if (p.session.cookies && cookies.length > 0)
|
|
994
4093
|
headers.cookie = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
995
|
-
headers.origin = p.checkout;
|
|
4094
|
+
headers.origin = p.frame ?? p.checkout;
|
|
996
4095
|
if (bundle.referer !== void 0)
|
|
997
4096
|
headers.referer = bundle.referer;
|
|
998
4097
|
return { url: bundle.url, method: p.method, headers, body };
|
|
999
4098
|
}
|
|
4099
|
+
function merchantEndpointRequest(profile, bundle, card) {
|
|
4100
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4101
|
+
if (p.merchant_key)
|
|
4102
|
+
throw new Error(`Refusing to pay: ${p.merchant} takes the card encrypted under its own key, which this call does not write.`);
|
|
4103
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
4104
|
+
if (refusal)
|
|
4105
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
4106
|
+
const captured = lowerHeaders(bundle.headers ?? {});
|
|
4107
|
+
return endpointRequest(p, bundle, captured, substituteMerchantCardEndpoint(bundle.body, captured["content-type"], p.id, card));
|
|
4108
|
+
}
|
|
4109
|
+
async function encryptedMerchantEndpointRequest(profile, bundle, card, crypto) {
|
|
4110
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4111
|
+
if (!p.merchant_key)
|
|
4112
|
+
throw new Error(`Refusing to pay: ${p.merchant} takes the card in the clear, which this call does not write.`);
|
|
4113
|
+
const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
|
|
4114
|
+
if (refusal)
|
|
4115
|
+
throw new Error(`Refusing to pay: ${refusal}.`);
|
|
4116
|
+
const captured = lowerHeaders(bundle.headers ?? {});
|
|
4117
|
+
return endpointRequest(p, bundle, captured, await encryptMerchantCardEndpointBody(bundle.body, captured["content-type"], p.id, card, crypto));
|
|
4118
|
+
}
|
|
1000
4119
|
function headerPairs(headers) {
|
|
1001
4120
|
if (headers === void 0)
|
|
1002
4121
|
return [];
|
|
@@ -1026,7 +4145,7 @@ var BASE64_RUN = /[A-Za-z0-9+/_-]{8,}/g;
|
|
|
1026
4145
|
var STREAM_RUN_LENGTH = 16;
|
|
1027
4146
|
var CODE_BASE64_RUN = /[A-Za-z0-9+/_-]{4,}/g;
|
|
1028
4147
|
var BASE64_ESCAPE = /\\\/|\\u002[bBfF]|%2[bBfF]|&#(?:43|47|x2[bBfF]);/g;
|
|
1029
|
-
var
|
|
4148
|
+
var BASE64_ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1030
4149
|
var unescapeBase64 = (escape) => /43|[bB]/.test(escape) ? "+" : "/";
|
|
1031
4150
|
function base64Bytes(run, skip) {
|
|
1032
4151
|
let bits = 0;
|
|
@@ -1034,7 +4153,7 @@ function base64Bytes(run, skip) {
|
|
|
1034
4153
|
let out = "";
|
|
1035
4154
|
for (let i = skip; i < run.length; i += 1) {
|
|
1036
4155
|
const char = run[i];
|
|
1037
|
-
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 :
|
|
4156
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : BASE64_ALPHABET2.indexOf(char));
|
|
1038
4157
|
bits += 6;
|
|
1039
4158
|
if (bits >= 8) {
|
|
1040
4159
|
bits -= 8;
|
|
@@ -1081,10 +4200,27 @@ function responseCardLeak(card, response) {
|
|
|
1081
4200
|
}
|
|
1082
4201
|
return seen.compressed ? "unreadable_in_response" : null;
|
|
1083
4202
|
}
|
|
4203
|
+
var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
|
|
4204
|
+
function merchantCardEndpointAnswerRefusal(profile, bodyText) {
|
|
4205
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4206
|
+
const rules = p.response.withhold ?? [];
|
|
4207
|
+
if (rules.length === 0 || typeof bodyText !== "string")
|
|
4208
|
+
return null;
|
|
4209
|
+
let parsed;
|
|
4210
|
+
try {
|
|
4211
|
+
parsed = JSON.parse(bodyText.replace(JSON_PROTECTION_PREFIX, ""));
|
|
4212
|
+
}
|
|
4213
|
+
catch {
|
|
4214
|
+
return null;
|
|
4215
|
+
}
|
|
4216
|
+
if (!isPlainObject(parsed))
|
|
4217
|
+
return null;
|
|
4218
|
+
return rules.some((rule) => valuesIgnoringCase(parsed, rule.at).some((value) => value === rule.equals)) ? "refused_step_in_response" : null;
|
|
4219
|
+
}
|
|
1084
4220
|
function merchantCardEndpointAnswer(profile, response, card) {
|
|
1085
4221
|
const p = trustedMerchantCardEndpoint(profile);
|
|
1086
4222
|
const pairs = answerParts(response);
|
|
1087
|
-
const withheld = responseCardLeak(card, response);
|
|
4223
|
+
const withheld = responseCardLeak(card, response) ?? merchantCardEndpointAnswerRefusal(p.id, response.bodyText);
|
|
1088
4224
|
if (withheld)
|
|
1089
4225
|
return { status: response.status, withheld, headers: {}, set_cookies: [], bodyText: null };
|
|
1090
4226
|
const headers = {};
|
|
@@ -1100,4 +4236,26 @@ function merchantCardEndpointAnswer(profile, response, card) {
|
|
|
1100
4236
|
}
|
|
1101
4237
|
return { status: response.status, headers, set_cookies: setCookies, bodyText: response.bodyText };
|
|
1102
4238
|
}
|
|
1103
|
-
|
|
4239
|
+
var GET_REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303]);
|
|
4240
|
+
function merchantCardEndpointNavigationAnswer(profile, answer, requestUrl) {
|
|
4241
|
+
const p = trustedMerchantCardEndpoint(profile);
|
|
4242
|
+
const status = isPlainObject(answer) ? answer.status : void 0;
|
|
4243
|
+
if (p.navigation === null || !Number.isInteger(status) || status < 300 || status > 399)
|
|
4244
|
+
return { kind: "page" };
|
|
4245
|
+
if (!GET_REDIRECTS.has(status))
|
|
4246
|
+
return { kind: "refused", reason: "redirect_repeats_post" };
|
|
4247
|
+
const location = isPlainObject(answer.headers) ? answer.headers.location : void 0;
|
|
4248
|
+
let target = null;
|
|
4249
|
+
if (typeof location === "string" && location.length <= MERCHANT_CARD_ENDPOINT_LIMITS.urlLength && /^[\x21-\x7e]+$/.test(location)) {
|
|
4250
|
+
try {
|
|
4251
|
+
target = new URL(location, requestUrl);
|
|
4252
|
+
}
|
|
4253
|
+
catch {
|
|
4254
|
+
target = null;
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
if (!target || target.protocol !== "https:" || target.username || target.password)
|
|
4258
|
+
return { kind: "refused", reason: "redirect_location_invalid" };
|
|
4259
|
+
return { kind: "redirect", status, location: target.href };
|
|
4260
|
+
}
|
|
4261
|
+
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 };
|