@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.
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 0149082f3796b4b5bbe843a6ab8e4f34f551de49e3edfc69ccf66d3cba1ba272
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
- if (value.startsWith(":")) {
104
- if (!placeholderValue(written))
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[value.slice(1)] = written;
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 clientProjection(profiles) {
397
- return deepFreeze(profiles.map(({ evidence, ...profile }) => ({ ...profile })));
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
- var MERCHANT_CARD_ENDPOINT_RECOGNIZERS = /* @__PURE__ */ wireProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
400
- var MERCHANT_CARD_ENDPOINT_CLIENT_TABLE = /* @__PURE__ */ clientProjection(MERCHANT_CARD_ENDPOINT_PROFILES);
401
- function merchantCardEndpointById(id) {
402
- return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_CLIENT_TABLE.find((entry) => entry.id === id) ?? null : null;
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 trustedMerchantCardEndpoint(idOrEntry) {
405
- const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
406
- if (!profile)
407
- throw new Error("Refusing to pay: unknown merchant card endpoint.");
408
- return profile;
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 merchantCardEndpointRecognizerById(id) {
411
- return typeof id === "string" ? MERCHANT_CARD_ENDPOINT_RECOGNIZERS.find((entry) => entry.id === id) ?? null : null;
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 merchantCardEndpointLive(idOrEntry) {
414
- const profile = merchantCardEndpointById(typeof idOrEntry === "string" ? idOrEntry : idOrEntry?.id);
415
- return !!profile && profile.status === "enabled" && profile.pending.length === 0 && typeof profile.har === "string" && profile.har !== "" && profile.charge_timing !== "recurring";
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 { parsed, exact: (at) => jsonAt(parsed, at), variants: (at) => valuesIgnoringCase(parsed, at), label: describePath };
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.encoding === "json") {
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", p.card.pan],
666
- ["cvv", p.card.cvv],
667
- ["exp_month", p.card.exp_month],
668
- ["exp_year", p.card.exp_year],
669
- ...p.card.brands.map((brand) => ["brand", brand])
670
- ];
671
- }
672
- function placeholderFits(slot, spec, value) {
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 "pan":
677
- return text && /^\d{12,19}$/.test(value);
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 once = onlyOnce(reader.variants(spec.at).length, label);
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
- if (countRuns(body, reader.exact(p.card.pan.at).value) !== 1) {
715
- return `${request}: the card number appears outside ${reader.label(p.card.pan.at)}`;
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.encoding === "json" ? values.some((value) => value !== false && value !== null) : values.some((value) => value !== "" && value !== "false");
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" && /^\d{1,9}(?:\.\d{1,2})?$/.test(at.value) : p.encoding === "json" ? Number.isSafeInteger(at.value) && at.value >= 0 : typeof at.value === "string" && /^\d{1,12}$/.test(at.value);
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
- var TWO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set(["usd", "eur", "gbp", "cad", "aud"]);
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]|&#0*(?:4[89]|5[0-7]);|&#x0*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 padded;
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 ? padded : String(facts.month);
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 parent = jsonAt(parsed, at.slice(0, -1));
926
- const key = at[at.length - 1];
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 ${describePath(at)}`);
3950
+ throw new SubstitutionError(`the paused body has no field at ${describeAt(at)}`);
930
3951
  parent.value[key] = value;
931
3952
  }
932
- function substituteMerchantCardEndpoint(body, contentType, profile, card) {
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
- let value;
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 = slotValue(slot, spec, at.value, facts);
3982
+ writes.push({ at: spec.at, found: at, value: slotValue(slot, spec, at.value, facts) });
958
3983
  }
959
- writes.push({ at: spec.at, found: at, value });
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
- if (p.encoding === "json") {
963
- for (const write of writes)
964
- writeJson(read.reader.parsed, write.at, write.value);
965
- out = JSON.stringify(read.reader.parsed);
966
- }
967
- else {
968
- for (const write of [...writes].sort((a, b) => b.found.field.start - a.found.field.start)) {
969
- const text = p.encoding === "form" ? encodeURIComponent(String(write.value)).replace(/%20/g, "+") : String(write.value);
970
- out = out.slice(0, write.found.field.start) + text + out.slice(write.found.field.end);
971
- }
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
- if (countRuns(out, typed) !== 0)
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 merchantEndpointRequest(profile, bundle, card) {
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
- const refusal = merchantCardEndpointSessionRefusal(p.id, bundle);
984
- if (refusal)
985
- throw new Error(`Refusing to pay: ${refusal}.`);
986
- const captured = lowerHeaders(bundle.headers ?? {});
987
- const body = substituteMerchantCardEndpoint(bundle.body, captured["content-type"], p.id, card);
988
- const headers = { "content-type": p.encoding === "form" ? "application/x-www-form-urlencoded; charset=UTF-8" : captured["content-type"] };
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 BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
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 : BASE64_ALPHABET.indexOf(char));
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
- export { MERCHANT_CARD_ENDPOINT_CLIENT_TABLE, MERCHANT_CARD_ENDPOINT_LIMITS, MERCHANT_CARD_ENDPOINT_MODE, MERCHANT_CARD_ENDPOINT_PROFILES, MERCHANT_CARD_ENDPOINT_PSP, MERCHANT_CARD_ENDPOINT_RECOGNIZERS, SubstitutionError, assertMerchantCardEndpoint, matchMerchantCardEndpoint, merchantCardEndpointAmount, merchantCardEndpointAnswer, merchantCardEndpointBodyRefusal, merchantCardEndpointById, merchantCardEndpointLive, merchantCardEndpointMatches, merchantCardEndpointRecognizerById, merchantCardEndpointSessionRefusal, merchantEndpointRequest, responseCardLeak, substituteMerchantCardEndpoint, trustedMerchantCardEndpoint };
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 };