@agent-cards/sdk 0.23.28 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/PREFLIGHT.md +7 -1
  3. package/README.md +197 -102
  4. package/dist/aci.d.ts +52 -22
  5. package/dist/aci.js +79 -15
  6. package/dist/adyen-merchant-hosted.generated.d.ts +2 -2
  7. package/dist/adyen-merchant-hosted.generated.js +586 -55
  8. package/dist/agent-amount.generated.js +1 -1
  9. package/dist/agentcard.d.ts +4 -5
  10. package/dist/agentcard.js +4 -5
  11. package/dist/amadeus.d.ts +9 -3
  12. package/dist/amadeus.js +33 -4
  13. package/dist/api-module.d.ts +1 -7
  14. package/dist/api-module.js +3 -5
  15. package/dist/{voice-retry.js → api-retry.js} +1 -1
  16. package/dist/api-types.d.ts +53 -0
  17. package/dist/{voice-types.js → api-types.js} +7 -2
  18. package/dist/approvals.d.ts +166 -0
  19. package/dist/approvals.js +213 -0
  20. package/dist/aurus.d.ts +33 -0
  21. package/dist/aurus.js +75 -0
  22. package/dist/builtin-registry.generated.js +1 -1
  23. package/dist/buy.d.ts +20 -101
  24. package/dist/buy.js +19 -74
  25. package/dist/capabilities.generated.js +1 -1
  26. package/dist/card-fields.generated.js +2864 -249
  27. package/dist/cardlink-vpos.d.ts +43 -0
  28. package/dist/cardlink-vpos.js +90 -0
  29. package/dist/cdp.d.ts +3 -13
  30. package/dist/cdp.js +244 -175
  31. package/dist/checkout-com.generated.js +5 -1
  32. package/dist/client.d.ts +170 -22
  33. package/dist/client.js +281 -40
  34. package/dist/creditguard.generated.d.ts +4 -4
  35. package/dist/cybersource-flex.generated.d.ts +39 -1
  36. package/dist/cybersource-flex.generated.js +100 -11
  37. package/dist/datatrans.generated.js +22 -1
  38. package/dist/eghl.d.ts +40 -0
  39. package/dist/eghl.js +72 -0
  40. package/dist/elavon-converge.generated.js +5 -3
  41. package/dist/gmo.generated.js +7 -2
  42. package/dist/hold.d.ts +8 -0
  43. package/dist/hold.js +43 -2
  44. package/dist/hosted-entry.js +4 -0
  45. package/dist/index.d.ts +9 -15
  46. package/dist/index.js +6 -10
  47. package/dist/lifecycle.d.ts +9 -61
  48. package/dist/lifecycle.js +24 -10
  49. package/dist/merchant-card-endpoints.generated.d.ts +6 -1
  50. package/dist/merchant-card-endpoints.generated.js +344 -19
  51. package/dist/merchant-hosted.d.ts +8 -1
  52. package/dist/nike-order.d.ts +51 -0
  53. package/dist/nike-order.js +81 -0
  54. package/dist/oceanpayment.d.ts +38 -0
  55. package/dist/oceanpayment.js +70 -0
  56. package/dist/paddle-tokenex.generated.js +111 -11
  57. package/dist/payplug.d.ts +41 -0
  58. package/dist/payplug.js +71 -0
  59. package/dist/payrails.generated.js +33 -8
  60. package/dist/pre-claim.d.ts +11 -0
  61. package/dist/pre-claim.js +130 -7
  62. package/dist/preflight-capabilities.generated.d.ts +0 -4
  63. package/dist/preflight-capabilities.generated.js +0 -5
  64. package/dist/preflight-catalog.json +200 -6
  65. package/dist/preflight-playwright.d.ts +2 -2
  66. package/dist/preflight-playwright.js +63 -14
  67. package/dist/preflight-schemas.json +18 -0
  68. package/dist/preflight.generated.d.ts +5 -4
  69. package/dist/preflight.generated.js +63 -3
  70. package/dist/preparation.d.ts +7 -0
  71. package/dist/preparation.js +21 -0
  72. package/dist/prepared-processor.d.ts +18 -5
  73. package/dist/prepared-processor.js +48 -8
  74. package/dist/primer.generated.js +2889 -268
  75. package/dist/processout.generated.d.ts +61 -0
  76. package/dist/processout.generated.js +577 -0
  77. package/dist/requests.js +1 -1
  78. package/dist/shiji-sts.generated.js +111 -11
  79. package/dist/stripe-checkout.generated.js +2761 -319
  80. package/dist/substitutions.generated.js +553 -26
  81. package/dist/tap-payments.generated.js +111 -11
  82. package/dist/twoc2p.generated.js +111 -11
  83. package/dist/url-masks/bluesnap.d.ts +2 -0
  84. package/dist/url-masks/bluesnap.js +15 -0
  85. package/dist/url-masks/oceanpayment.d.ts +2 -0
  86. package/dist/url-masks/oceanpayment.js +13 -0
  87. package/dist/url-masks/payplug.d.ts +2 -0
  88. package/dist/url-masks/payplug.js +13 -0
  89. package/dist/url-masks/vtex_payments.d.ts +2 -0
  90. package/dist/url-masks/vtex_payments.js +12 -0
  91. package/dist/url-masks.generated.d.ts +1 -1
  92. package/dist/url-masks.generated.js +6 -2
  93. package/dist/verifone.generated.js +42 -2
  94. package/dist/vtex-payments.generated.d.ts +127 -0
  95. package/dist/vtex-payments.generated.js +544 -0
  96. package/dist/webhooks.d.ts +2 -2
  97. package/dist/webhooks.js +2 -2
  98. package/examples/preflight/kernel-native/inventory.json +1 -1
  99. package/package.json +6 -8
  100. package/dist/languages.d.ts +0 -23
  101. package/dist/languages.js +0 -76
  102. package/dist/voice-pkce.d.ts +0 -49
  103. package/dist/voice-pkce.js +0 -147
  104. package/dist/voice-stream.generated.d.ts +0 -118
  105. package/dist/voice-stream.generated.js +0 -170
  106. package/dist/voice-types.d.ts +0 -735
  107. package/dist/voice.d.ts +0 -373
  108. package/dist/voice.js +0 -815
  109. package/readme.test.mjs +0 -53
  110. /package/dist/{voice-retry.d.ts → api-retry.d.ts} +0 -0
package/dist/client.js CHANGED
@@ -22,13 +22,14 @@ import { TAP_PAYMENTS_PSP, isTapPaymentsServedSubstitution } from './tap-payment
22
22
  import { WIX_PAYMENTS_PSP } from './wix-payments.generated.js';
23
23
  import { HIPAY_PSP } from './hipay.generated.js';
24
24
  import { OPENPAY_PSP, openpayAnswerOutcome } from './openpay.generated.js';
25
+ import { VTEX_PAYMENTS_PSP } from './vtex-payments.generated.js';
26
+ import { PROCESSOUT_PSP, processoutAnswerOutcome } from './processout.generated.js';
25
27
  import { TOAST_PSP, isToastCiphertext } from './toast.generated.js';
26
28
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
27
- import { VoiceModule } from './voice.js';
28
29
  import { BuyModule } from './buy.js';
29
- import { Languages } from './languages.js';
30
+ import { ApprovalsModule } from './approvals.js';
30
31
  import { RequestRegistry } from './requests.js';
31
- import { AgentcardError } from './voice-types.js';
32
+ import { AgentcardError } from './api-types.js';
32
33
  import { HYPHEN, SLASH, maskUuids } from './url-mask-parts.js';
33
34
  import { URL_MASKS } from './url-masks.generated.js';
34
35
  import { DECLARED_CAPABILITIES } from './capabilities.generated.js';
@@ -114,6 +115,15 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
114
115
  * recognizer is never in BUILTIN_REGISTRY, so a build reaches it only through a
115
116
  * sync that declares its capability, and whether an org may pay through it
116
117
  * stays the API's decision.
118
+ * `bluesnap_hosted_payment_fields`: BlueSnap's Hosted Payment Fields, whose card
119
+ * number, expiry and code frames each PUT their own field as JSON to the
120
+ * merchant's payment-fields token. This build pauses the card number PUT, holds
121
+ * the expiry and code PUTs and answers them with its answer (aci.ts, the JSON
122
+ * split card), and the cardholder's device sends the whole card in that one PUT
123
+ * (payment-core processors/bluesnap.js). The request names no amount, so the
124
+ * agent's pair, above zero, is the one the cardholder approves. Served on the
125
+ * same terms: only to a build that lists it, for a company Agentcard turned
126
+ * BlueSnap on for.
117
127
  * `creditguard_pps_process`: CreditGuard's hosted payment page (Hyp PPS), whose
118
128
  * card submit this build pays only under a prepared checkout that names the
119
129
  * framed page (prepare({ psp: 'creditguard', environment, paymentPageUrl })). The
@@ -452,6 +462,18 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
452
462
  * page the create names. Served on the same terms: only to a build that lists it, for a company
453
463
  * Agentcard turned Pin Payments on for.
454
464
  *
465
+ * `paymentwall_card_token`: the card request Paymentwall's card form (Brick) sends, from its number
466
+ * frame on pwgateway.com inside Paymentwall's hosted widget or from a merchant's own page: a form POST
467
+ * to https://pwgateway.com/api/token (https://api.paymentwall.com/api/brick/token for a test key)
468
+ * under the merchant's public key. This build pauses only the card form's own body at those exact
469
+ * URLs (payment-core processors/paymentwall.js); a stored card's code, a wallet's token, a brand from
470
+ * the page's select or any other body there fails in the browser before anything is created. The
471
+ * cardholder's device sends the request itself with the real card, and the page gets the token answer
472
+ * payment-core's judge rebuilt. The request names no amount, so the agent's pair, above zero, is the
473
+ * one the cardholder approves, and the API pays only a Paymentwall project payment-core pins for the
474
+ * checkout page the create names. Served on the same terms: only to a build that lists it, for a
475
+ * company Agentcard turned Paymentwall on for.
476
+ *
455
477
  * `sbps_token_jsonp`: SB Payment Service's card token request, the JSONP <script>
456
478
  * GET its token script sends to /token/generateToken on token.sps-system.com. This
457
479
  * build pauses it only as the browser writes it, aborts any other request on that
@@ -468,6 +490,56 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
468
490
  * Served on the same terms, and the API holds the lane to Agentcard's own company
469
491
  * until a reviewed change binds the client key to the merchant.
470
492
  *
493
+ * `iyzico_checkout_form`: the card payment iyzico's Checkout Form posts as one JSON request to
494
+ * api.iyzipay.com/payment/iyzipos/checkoutform/auth/ecom, or initialize3ds/ecom for 3-D Secure
495
+ * (sandbox-api.iyzipay.com in test), under the form's X-IYZI-TOKEN. This build pauses only the
496
+ * form's own payment with a new card at those exact URLs (payment-core processors/iyzico.js); a saved
497
+ * card, a card kept on file, installments and a currency conversion fail in the browser before
498
+ * anything is created. The cardholder's device sends the payment itself, and the page gets iyzico's
499
+ * answer as payment-core's judge rebuilt it: the payment done, or the bank's 3-D Secure page, which
500
+ * the form then runs in the agent's browser. The payment names its price but not its currency, so
501
+ * authorize() needs the agent's pair, and the API holds its amount to the price. Served on the same
502
+ * terms, and the API holds the lane to Agentcard's own company.
503
+ *
504
+ * `aurus_one_time_token`: the card request Aurus Pay's ecom card frame (O'Reilly Auto Parts) sends to
505
+ * pecst<NN>.aurusepay.com/storeservices/ecom/oneTimeToken, the card sealed in the frame under a public key its own
506
+ * script carries. This build pauses it only when Aurus's card frame sent it (aurus.ts aurusPreClaim); the same
507
+ * request from the store's page or another frame fails in the browser before anything is created. The request
508
+ * names no amount, so authorize() needs the agent's pair, above zero. The cardholder's device does not seal for
509
+ * Aurus yet (payment-core processors/aurus.js), so a paused request opens no approval that pays.
510
+ *
511
+ * `viva_smart_checkout_chargetoken`: the card request Viva Wallet's Smart Checkout page sends as one
512
+ * JSON request to www.vivapayments.com/web2/checkout/v2/chargetokens (demo.vivapayments.com in test),
513
+ * on Pay, after the page's own reCAPTCHA run. This build pauses only the page's own card body at that
514
+ * exact URL (payment-core processors/viva_wallet.js); a body that keeps the card, pays with a stored
515
+ * card, converts the currency or is the page's second try after a decline fails in the browser before
516
+ * anything is created. The cardholder's device sends the card through Agentcard's checkout relay, since
517
+ * the page's request answers no other origin, and the page gets Viva's single-use charge token as
518
+ * payment-core's judge rebuilt it; the page then charges that token card free. The body names its
519
+ * amount, which the API holds to the agent's pair, and the API pays a live token only for an order
520
+ * payment-core pins. Served on the same terms, and the API holds the lane to Agentcard's own company.
521
+ *
522
+ * `paystack_pinned_key`: the charge Paystack's hosted checkout sends as one form POST to
523
+ * standard.paystack.co/charge/, the card only as `clientdata`, RSA ciphertext under the one key
524
+ * Paystack's page carries. This build pauses only the card page's own charge at that exact URL
525
+ * (payment-core processors/paystack.js); a charge that keeps the card, starts a plan or carries a PIN
526
+ * fails in the browser before anything is created. The cardholder's device encrypts the real card under
527
+ * the same pinned key and sends the charge itself, and the page gets Paystack's answer: paid, declined,
528
+ * or the issuer's 3-D Secure step, which the page then runs in the agent's browser. The charge names
529
+ * no amount, so authorize() needs the agent's pair, above zero. Served on the same terms, and the API
530
+ * holds the lane to Agentcard's own company.
531
+ *
532
+ * `usaepay_payment_key`: the card request USAePay's Pay.js card frame sends as one JSON request to
533
+ * www.usaepay.com/api/v2/pub/payment_keys (secure.usaepay.com alike; sandbox.usaepay.com in test), under
534
+ * the merchant's public key in its Authorization. This build pauses only the frame's own card body at
535
+ * that exact URL (payment-core processors/usaepay.js); a Google Pay or Apple Pay token and a bank
536
+ * account on the same URL fail in the browser before anything is created. The cardholder's device sends
537
+ * the card through Agentcard's checkout relay, since USAePay answers no browser origin, and the frame
538
+ * gets USAePay's single-use payment key as payment-core's judge rebuilt it; the merchant's server
539
+ * charges that key afterwards. The request names no amount, so authorize() needs the agent's pair, and
540
+ * the API pays a live key only to a merchant payment-core pins for the checkout page. Served on the same
541
+ * terms, and the API holds the lane to Agentcard's own company.
542
+ *
471
543
  * `tap_card_sdk_token`: the token request Tap Payments' card frame sends to sdk.tap.company, carrying
472
544
  * the card only as one ciphertext the frame writes under the merchant's own RSA key. This build pauses
473
545
  * it and writes over the placeholder's ciphertext the one the cardholder's device wrote under the key
@@ -495,6 +567,41 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
495
567
  * (agent-amount-processors.json), is the one the cardholder approves. Served on the same terms: only
496
568
  * to a build that lists it, for a company Agentcard turned Openpay on for.
497
569
  *
570
+ * `processout_card_token`: the card request ProcessOut.js's card frame sends from js.processout.com to
571
+ * api.processout.com/cards under the store's project id, only through a prepared checkout: the frame
572
+ * gives the page 20 seconds from the shop's continue press, so the cardholder approves before it. The
573
+ * card URL names the project in its query, so the claim binds the request by its project's environment
574
+ * and its shape, never by the prepared URL. The cardholder's device sends the card to ProcessOut itself
575
+ * and the frame gets back ProcessOut's token alone, rebuilt by payment-core's judge. The request names
576
+ * no amount, so the agent's pair, above zero (agent-amount-processors.json), is the one the cardholder
577
+ * approves. Served on the same terms: only to a build that lists it, for a company Agentcard turned
578
+ * ProcessOut on for.
579
+ *
580
+ * `monei_card_token`: the card request MONEI's card frame sends from MONEI's payment page
581
+ * (secure.monei.com/payments/<id>) to api.monei.com/v1/tokens, with the payment's id and the card. This
582
+ * build pauses only the frame's own body for a payment at that exact URL (payment-core
583
+ * processors/monei.js); MONEI's account flow, Click to Pay and a wallet on the same URL fail in the
584
+ * browser before anything is created. The request names neither the merchant nor the amount, so the API
585
+ * reads MONEI's own record of the payment and holds the agent's pair, above zero
586
+ * (agent-amount-processors.json), to MONEI's amount exactly. The cardholder's device sends the card to
587
+ * MONEI itself and the frame gets back MONEI's payment token alone, rebuilt by payment-core's judge.
588
+ * Served on the same terms: only to a build that lists it, for a company Agentcard turned MONEI on for.
589
+ *
590
+ * `frisbii_card_token`: the card request Frisbii Checkout's hosted payment page (checkout.reepay.com or
591
+ * checkout.frisbii.com, as an overlay, an embedded frame or a full window) sends to
592
+ * card.reepay.com or card.frisbii.com /v1/token/?session=<id>, with the merchant's public key and the
593
+ * card. This build pauses only the page's own body at that exact URL (payment-core
594
+ * processors/frisbii.js); any other body on it, one that keeps the card on file included, fails in the
595
+ * browser before anything is created. The page's two other card paths, a stored card's token
596
+ * (/v1/token/cof/) and Click to Pay (/v1/token/create_click_to_pay), are held and refused whatever
597
+ * their body, so neither leaves the browser either. The request names the amount only when
598
+ * the session carries 3-D Secure data, so the agent's pair is required, above zero
599
+ * (agent-amount-processors.json), and the API refuses it when it differs from the page's own. The
600
+ * cardholder's device sends the card to Frisbii
601
+ * itself and the page gets back Frisbii's card token or 3-D Secure step alone, rebuilt by payment-core's
602
+ * judge. Served on the same terms: only to a build that lists it, for a company Agentcard turned Frisbii
603
+ * on for.
604
+ *
498
605
  * `aps_custom_merchant_page`: Amazon Payment Services' custom merchant page, whose own card form
499
606
  * posts top-level to /FortAPI/paymentPage on checkout.payfort.com (sbcheckout.payfort.com on APS's
500
607
  * sandbox) with service_command=TOKENIZATION. This build pauses that navigation; the cardholder's
@@ -515,6 +622,14 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
515
622
  * the cardholder approves, and the API holds its amount to the body's. Served on the same terms: only to a build
516
623
  * that lists it, for a company Agentcard turned Nexi Checkout on for.
517
624
  *
625
+ * `oceanpayment_hosted_payment_page`: the card post of Oceanpayment's hosted payment page (secure.oceanpayment.com, where
626
+ * Oceanpayment's Shopify redirect app sends the buyer), a native form post to /gateway/sendPay;jsessionid=<session> with
627
+ * the card in clear (test-secure.oceanpayment.com for a test row). This build pauses only that post, exactly as the page's
628
+ * form action writes it, and aborts every other post to Oceanpayment's two hosts (oceanpayment.ts), so the agent's
629
+ * placeholder never reaches Oceanpayment; the cardholder's device writes the real card into the paused post and sends it
630
+ * through the checkout relay. The post names no amount, so the agent's pair, above zero, is the one the cardholder
631
+ * approves. Served on the same terms: only to a build that lists it, for a company Agentcard turned Oceanpayment on for.
632
+ *
518
633
  * `barion_hosted_payment_page`: Barion's Smart Gateway, whose card-free link
519
634
  * (secure.barion.com/Pay?Id=<payment id>, a plain GET the merchant's redirect or script sends
520
635
  * the buyer to) this build pauses for the cardholder's device by its exact link (hosted-entry.ts),
@@ -522,6 +637,27 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
522
637
  * send (pre-claim.ts, barion.ts). Served on the same terms as Przelewy24's: only to a build that
523
638
  * lists it, for a company Agentcard turned Barion on for.
524
639
  *
640
+ * `payplug_hosted_payment_page`: PayPlug's payment page, whose card-free link
641
+ * (secure.payplug.com/pay/<payment id>, or /pay/test/<payment id>, a plain GET the merchant's
642
+ * redirect or script sends the buyer to) this build pauses for the cardholder's device by its exact
643
+ * link (hosted-entry.ts), and whose card post this build never lets the agent's browser send
644
+ * (pre-claim.ts, payplug.ts). The link names no amount, so authorize() asks for the agent's, above
645
+ * zero. Served only to a build that lists it, and the API turns PayPlug on for Agentcard's own
646
+ * company alone.
647
+ *
648
+ * `cardlink_vpos_hosted_payment_page`: Cardlink's VPOS, the payment page of Eurobank, Cardlink and Nexi
649
+ * Greece, whose card-free signed entry (a form post to /vpos/shophandlermpi on one of the three hosts) this build
650
+ * pauses for the cardholder's device, and whose card page's post and card check on the same URL this build never
651
+ * lets the agent's browser send (pre-claim.ts, cardlink-vpos.ts). Served only to a build that lists it, and the API
652
+ * turns the lane on for Agentcard's own company alone.
653
+ *
654
+ * `eghl_hosted_payment_page`: eGHL's payment page (ADAPTIS, formerly eGHL, on securepay.e-ghl.com), whose card-free
655
+ * entry (a GET of /IPG/payment.aspx with the payment in its query, which the merchant's checkout or eGHL's Shopify app
656
+ * sends the buyer to) this build pauses for the cardholder's device by its exact URL (hosted-entry.ts), and whose card
657
+ * page's post to the same address this build never lets the agent's browser send (pre-claim.ts, eghl.ts). The entry
658
+ * names its amount, and the API holds the agent's pair to it exactly. Served only to a build that lists it, and the API
659
+ * turns the lane on for Agentcard's own company alone.
660
+ *
525
661
  * `paymob_checkout_pay`: the card payment Paymob's own checkout sends, from Unified Checkout
526
662
  * (<country>.checkout.paymob.com) or from the Pixel card frame a merchant's page embeds, as JSON to
527
663
  * https://<accept|uae|ksa|oman>.paymob.com/api/acceptance/payments/pay. This build pauses only the
@@ -651,6 +787,8 @@ export const SUPPORTED_CAPABILITIES = DECLARED_CAPABILITIES;
651
787
  const REVOLUT_PSP = 'revolut';
652
788
  /** Nexi Checkout's card payment names the payment's amount in minor units but not its currency, as Revolut's does. */
653
789
  const NEXI_CHECKOUT_PSP = 'nexi_checkout';
790
+ /** iyzico's Checkout Form payment names its price but not its currency, which the form's token holds. */
791
+ const IYZICO_PSP = 'iyzico';
654
792
  const AGENT_AMOUNT_PROCESSORS = AGENT_AMOUNT.processors;
655
793
  /** The ones a zero amount is refused for, like a missing one (authorize()). */
656
794
  const ABOVE_ZERO_PROCESSORS = AGENT_AMOUNT.aboveZero;
@@ -937,11 +1075,12 @@ export class AmountRequiredError extends Error {
937
1075
  constructor(psp) {
938
1076
  const processor = PROCESSOR_NAMES[psp] ?? psp;
939
1077
  // A Payrexx card request names the frame's total, but the merchant's page charges its own total after the token.
940
- const lacks = psp === REVOLUT_PSP || psp === NEXI_CHECKOUT_PSP ? 'names its amount but not its currency'
1078
+ const lacks = psp === REVOLUT_PSP || psp === NEXI_CHECKOUT_PSP || psp === IYZICO_PSP ? 'names its amount but not its currency'
941
1079
  : psp === PAYREXX_PSP ? "only mints a token the merchant's page charges later"
942
1080
  : psp === TAP_PAYMENTS_PSP ? 'does not decide the amount charged' : 'names no amount';
943
- // "An Omise payment request", "A Clover payment request": the article follows the name's first letter.
944
- const article = /^[AEIOU]/.test(processor) ? 'An' : 'A';
1081
+ // "An Omise payment request", "A Clover payment request", "An iyzico payment request": the article follows the
1082
+ // name's first letter, in either case, and "A USAePay payment request" its first sound, a consonant's.
1083
+ const article = /^(?:[AEIO]|U(?!SA))/i.test(processor) ? 'An' : 'A';
945
1084
  super(`${article} ${processor} payment request ${lacks}, so pass amount and currency: that is the amount the cardholder approves. Nothing was created and nothing was charged.`);
946
1085
  this.psp = psp;
947
1086
  this.name = 'AmountRequiredError';
@@ -1365,11 +1504,11 @@ export class CheckoutAuthError extends Error {
1365
1504
  }
1366
1505
  }
1367
1506
  /**
1368
- * A failure as the voice module reports it: an `AgentcardError` carrying the
1369
- * API's code (`invalid_return_url`, `voice_not_enrolled`) or `oauth_failed`;
1507
+ * A failure as the modules report it: an `AgentcardError` carrying the
1508
+ * API's code or `oauth_failed`;
1370
1509
  * a failure to reach Agentcard at all passes through as it came.
1371
1510
  */
1372
- function voiceError(err) {
1511
+ function moduleError(err) {
1373
1512
  if (err instanceof CheckoutAuthError)
1374
1513
  return new AgentcardError('oauth_failed', `Could not get a token for the client (${err.status}${err.code ? ', ' + err.code : ''}).`, err.status);
1375
1514
  if (err instanceof CheckoutApiError) {
@@ -1951,6 +2090,27 @@ const HIPAY_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandb
1951
2090
  */
1952
2091
  const OPENPAY_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandbox_row', 'sandbox_host_on_live_row',
1953
2092
  'merchant_profile_mismatch', 'amount_required'];
2093
+ /**
2094
+ * The API's refusals of a VTEX Payments preparation, before any preparation exists, that prepareCheckout passes on by
2095
+ * name: the company is not turned on for VTEX (`processor_unavailable`), the client is in test mode, which VTEX has no
2096
+ * host for (`live_host_on_sandbox_row`), the merchant is not the name the reviewed pin for the checkout's page carries
2097
+ * (`merchant_profile_mismatch`), the currency is not the store's (`unsupported_currency`), and the price is zero
2098
+ * (`amount_required`). A page no reviewed pin lists answers `unsupported_checkout`, which PREPARATION_REFUSALS names for
2099
+ * every checkout. The API holds the pins, so a store a later release reviews needs no new SDK. Only for a VTEX checkout.
2100
+ */
2101
+ const VTEX_PAYMENTS_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandbox_row', 'merchant_profile_mismatch',
2102
+ 'unsupported_currency', 'amount_required'];
2103
+ /**
2104
+ * The API's refusals of a ProcessOut preparation, before any preparation exists, that prepareCheckout passes on by
2105
+ * name: the company is not turned on for ProcessOut (`processor_unavailable`), the environment is not the caller's key
2106
+ * mode's (`processout_environment_mismatch`, since a live and a sandbox project share one host), a live checkout's
2107
+ * merchant is not the name the reviewed pin for its origin carries (`merchant_profile_mismatch`), and the price is zero
2108
+ * (`amount_required`), since the card request names no amount. An origin no reviewed pin lists answers
2109
+ * `unsupported_checkout`, which PREPARATION_REFUSALS names for every checkout. The API holds the pins, not this build,
2110
+ * so a store a later release reviews needs no new SDK. Only for a ProcessOut checkout.
2111
+ */
2112
+ const PROCESSOUT_PREPARATION_REFUSALS = ['processor_unavailable', 'processout_environment_mismatch', 'merchant_profile_mismatch',
2113
+ 'amount_required'];
1954
2114
  /**
1955
2115
  * The reason the cardholder's device declines a bound prepared row with, per processor, when its own
1956
2116
  * send boundary passed before the card left it (the Vault's sendWindow, 12 seconds after it last saw
@@ -1959,7 +2119,9 @@ const OPENPAY_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_san
1959
2119
  * closes on (amadeus.ts), which can close later, so authorize() ends the attempt the way that window
1960
2120
  * does, as a CheckoutPreparationError with that reason, and never as the cardholder's "no". A prepared
1961
2121
  * HiPay token create too, and a prepared Openpay token create, whose device sends the card at most 25
1962
- * seconds after it last saw the checkout unbound, inside the adapters' 30.
2122
+ * seconds after it last saw the checkout unbound, inside the adapters' 30. And a prepared VTEX card post,
2123
+ * whose device sends the card at most 6 seconds after it last saw the checkout unbound, inside the adapters' 8, and a
2124
+ * prepared ProcessOut card request, whose device keeps HiPay's 10 seconds, inside the adapters' 12.
1963
2125
  */
1964
2126
  const PREPARED_SEND_WINDOW_DECLINES = Object.freeze({
1965
2127
  amadeus_checkout: 'amadeus_send_window_closed',
@@ -1967,7 +2129,17 @@ const PREPARED_SEND_WINDOW_DECLINES = Object.freeze({
1967
2129
  [WIX_PAYMENTS_PSP]: 'wix_payments_send_window_closed',
1968
2130
  [HIPAY_PSP]: 'hipay_send_window_closed',
1969
2131
  [OPENPAY_PSP]: 'openpay_send_window_closed',
2132
+ [VTEX_PAYMENTS_PSP]: 'vtex_payments_send_window_closed',
2133
+ [PROCESSOUT_PSP]: 'processout_send_window_closed',
1970
2134
  });
2135
+ /**
2136
+ * The reason the API declines a bound VTEX card post with when its send-by passed before the card left the cardholder's
2137
+ * device (backend services/commerce/checkout-vtex-payments.ts): Ollie's checkout gives the post 12 seconds, and the API
2138
+ * counts 8 of them from the pause the create named. A known non-payment, as a closed send window is.
2139
+ */
2140
+ const VTEX_PAYMENTS_POST_TIMED_OUT = 'vtex_payments_post_timed_out';
2141
+ /** A pause the create can count `paused_ms` from: a whole, non-negative epoch millisecond on this machine's clock. */
2142
+ const validPausedAt = (value) => Number.isSafeInteger(value) && value >= 0;
1971
2143
  /**
1972
2144
  * Refusals a CyberSource Flex preparation that tokenizes before Pay answers (4xx) before any
1973
2145
  * preparation exists, surfaced as the CheckoutPreparationError reason: the processor is off for
@@ -2050,22 +2222,22 @@ export class VaultClient {
2050
2222
  * false: a live client.
2051
2223
  */
2052
2224
  get sandbox() { return this.sandboxNamed || this.sandboxLearned === true; }
2053
- /** What every module shares: the token, the HTTP path, the poll settings and the language list. */
2225
+ /** What every module shares: the token, the HTTP path and the poll settings. */
2054
2226
  transport;
2055
- /**
2056
- * Approve purchases by voice inside your own product: connect a person once,
2057
- * create the purchase, ask for the key word, read the result. Same
2058
- * credentials, same token as the checkout calls. Approves on Agentcard's
2059
- * page unless `voice: { approval: 'in_app' }` was named at construction.
2060
- */
2061
- voice;
2062
2227
  /**
2063
2228
  * The Purchase API from the same client: ask for a cart on behalf of a
2064
2229
  * person you connected, confirm it by its hash, and name an approval the
2065
- * person already gave by voice so the API places against it and asks for
2066
- * nothing a second time. Same credentials, same token as the voice calls.
2230
+ * person already gave so the API places against it and asks for
2231
+ * nothing a second time. Same credentials, same token as the checkout calls.
2067
2232
  */
2068
2233
  buy;
2234
+ /**
2235
+ * Purchases your server creates for a person, approved on Agentcard's page:
2236
+ * create one (`request: 'sandbox'` under a sandbox client), send the person
2237
+ * its `approval_url`, then read the decision or wait for it. Same
2238
+ * credentials, same token as the checkout calls.
2239
+ */
2240
+ approvals;
2069
2241
  /**
2070
2242
  * The request builders: the processor requests a purchase pauses on, the
2071
2243
  * shipped ones and yours (`requests.register`, `requests.build`).
@@ -2090,7 +2262,7 @@ export class VaultClient {
2090
2262
  });
2091
2263
  }
2092
2264
  catch (err) {
2093
- throw voiceError(err);
2265
+ throw moduleError(err);
2094
2266
  }
2095
2267
  };
2096
2268
  this.transport = {
@@ -2099,18 +2271,14 @@ export class VaultClient {
2099
2271
  return await this.accessToken();
2100
2272
  }
2101
2273
  catch (err) {
2102
- throw voiceError(err);
2274
+ throw moduleError(err);
2103
2275
  }
2104
2276
  },
2105
2277
  request,
2106
2278
  pollIntervalMs: this.pollIntervalMs,
2107
- pollTimeoutMs: opts.voice?.pollTimeoutMs,
2108
- backoffMs: opts.voice?.backoffMs,
2109
- wait: opts.voice?.wait,
2110
- languages: new Languages(request),
2111
2279
  };
2112
- this.voice = new VoiceModule(this.transport, opts.voice, () => this.sandbox);
2113
2280
  this.buy = new BuyModule(this.transport);
2281
+ this.approvals = new ApprovalsModule(this.transport, () => this.sandbox);
2114
2282
  const declared = (opts.sandboxMerchants ?? []).map(declareSandboxMerchant);
2115
2283
  if (new Set(declared.map((profile) => profile.id)).size !== declared.length) {
2116
2284
  throw new TypeError('sandboxMerchants: declare each profile at most once.');
@@ -2660,6 +2828,8 @@ export class VaultClient {
2660
2828
  || (input.psp === PAYGENT_PSP && PAYGENT_PREPARATION_REFUSALS.includes(error.code))
2661
2829
  || (input.psp === HIPAY_PSP && HIPAY_PREPARATION_REFUSALS.includes(error.code))
2662
2830
  || (input.psp === OPENPAY_PSP && OPENPAY_PREPARATION_REFUSALS.includes(error.code))
2831
+ || (input.psp === VTEX_PAYMENTS_PSP && VTEX_PAYMENTS_PREPARATION_REFUSALS.includes(error.code))
2832
+ || (input.psp === PROCESSOUT_PSP && PROCESSOUT_PREPARATION_REFUSALS.includes(error.code))
2663
2833
  || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
2664
2834
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
2665
2835
  throw fail(error.code);
@@ -2778,6 +2948,11 @@ export class VaultClient {
2778
2948
  if (preparation) {
2779
2949
  if (!this.preparations.has(preparation) || this.usedPreparations.has(preparation))
2780
2950
  throw new CheckoutPreparationError(preparation.id ?? null, 'already_used_or_foreign');
2951
+ // VTEX Payments: the API counts the row's send-by from the pause the create names (`paused_ms`) and refuses a create
2952
+ // that names none, so a caller without `pausedAt` is refused here, before the preparation is spent. The adapters fill it.
2953
+ if (preparation.psp === VTEX_PAYMENTS_PSP && !validPausedAt(input.pausedAt)) {
2954
+ throw new Error('A VTEX Payments card post needs pausedAt: when the card request paused, in epoch milliseconds. Nothing was created.');
2955
+ }
2781
2956
  // Consume locally before any await, including OAuth, and never recycle it.
2782
2957
  this.usedPreparations.add(preparation);
2783
2958
  if (Date.parse(preparation.expiresAt) <= Date.now())
@@ -3065,8 +3240,14 @@ export class VaultClient {
3065
3240
  ...(payrailsPayment ? { holder_reference: sidecar.holder_reference } : {}) } } : {}),
3066
3241
  },
3067
3242
  };
3243
+ // VTEX Payments: how long ago the post paused, on this machine's clock as each create request leaves, after any wait
3244
+ // for a token or a refresh after a 401, so the API counts the row's send-by from Ollie's own 12 seconds on its own
3245
+ // clock, whatever the offset between the two machines.
3246
+ const pausedAt = input.pausedAt;
3247
+ const atSend = rec?.psp === VTEX_PAYMENTS_PSP && validPausedAt(pausedAt)
3248
+ ? () => ({ paused_ms: Math.max(0, Date.now() - pausedAt) }) : undefined;
3068
3249
  try {
3069
- created = await this.createAuthorization(payload, input.currency, AbortSignal.any([operationSignal, AbortSignal.timeout(30_000)]), input.merchantSignal);
3250
+ created = await this.createAuthorization(payload, input.currency, AbortSignal.any([operationSignal, AbortSignal.timeout(30_000)]), input.merchantSignal, atSend);
3070
3251
  }
3071
3252
  catch (error) {
3072
3253
  if (error instanceof PaymentOutcomeUnknownError) {
@@ -3095,6 +3276,14 @@ export class VaultClient {
3095
3276
  await this.cancelPreparation(preparation.id).catch(() => { });
3096
3277
  throw new CheckoutPreparationError(preparation?.id ?? null, error.code);
3097
3278
  }
3279
+ // A VTEX create the API refused past its send-by (409 vtex_payments_post_timed_out), before any row, bind or
3280
+ // notification: the card never left the cardholder's device, and Ollie stops waiting for the post before one could.
3281
+ // The same known non-payment as the polled decline below. The preparation, which no later post of this attempt can
3282
+ // use, is retired so the cardholder's page stops waiting for it.
3283
+ if (preparation?.psp === VTEX_PAYMENTS_PSP && error instanceof CheckoutApiError && error.status === 409 && error.code === VTEX_PAYMENTS_POST_TIMED_OUT) {
3284
+ await this.cancelPreparation(preparation.id).catch(() => { });
3285
+ throw new CheckoutPreparationError(preparation.id, error.code);
3286
+ }
3098
3287
  // A missing answer or generic 5xx can hide a committed row and a delivered approval link.
3099
3288
  // Only the documented pre-create read-back errors prove it is safe to retry.
3100
3289
  const safeReadFailure = error instanceof CheckoutApiError
@@ -3127,11 +3316,17 @@ export class VaultClient {
3127
3316
  // A prepared Openpay token create too, for 40 seconds: openpay.js gives it 40 seconds from Pay, so the token
3128
3317
  // reaches the page within half a second of the device's answer, and the API names 500 ms as such a row's poll
3129
3318
  // interval.
3319
+ // A prepared VTEX card post too, for 12 seconds: Ollie's container aborts the post 12 seconds after it starts it,
3320
+ // so VTEX's answer reaches the container within half a second of the device's report, and the API names 500 ms as
3321
+ // such a row's poll interval.
3322
+ // A prepared ProcessOut card request too, for 20 seconds: ProcessOut.js gives its card frame 20 seconds from the
3323
+ // shop's press, and the API names 500 ms as such a row's poll interval.
3130
3324
  const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
3131
- : preparation?.psp === PADDLE_VGS_PSP || preparation?.psp === HIPAY_PSP ? Date.now() + 20_000
3325
+ : preparation?.psp === PADDLE_VGS_PSP || preparation?.psp === HIPAY_PSP || preparation?.psp === PROCESSOUT_PSP ? Date.now() + 20_000
3132
3326
  : preparation?.psp === OPENPAY_PSP ? Date.now() + 40_000
3133
- : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000
3134
- : preparation?.psp === SECURE_ACCEPTANCE_PSP ? Date.now() + SECURE_ACCEPTANCE_FAST_POLL_MS : 0;
3327
+ : preparation?.psp === VTEX_PAYMENTS_PSP ? Date.now() + 12_000
3328
+ : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000
3329
+ : preparation?.psp === SECURE_ACCEPTANCE_PSP ? Date.now() + SECURE_ACCEPTANCE_FAST_POLL_MS : 0;
3135
3330
  // A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
3136
3331
  // token and approves as soon as this bind reaches it, while the page may give createToken only
3137
3332
  // seconds (Alaska Airlines' flight checkout gives it three). Poll every 100 ms for 10 seconds,
@@ -3529,6 +3724,19 @@ export class VaultClient {
3529
3724
  response.headers = { 'content-type': 'application/json' };
3530
3725
  response.body = judged.body;
3531
3726
  }
3727
+ if (rec?.psp === PROCESSOUT_PSP) {
3728
+ // ProcessOut's answer enters the card frame only as payment-core rebuilds an approved token from the card facts
3729
+ // the page reads (processout.js processoutAnswerOutcome: HTTP 200 or 201, success and a card token in shape),
3730
+ // under its approved status and a JSON content type. The frame reads the body of any answer, so anything else, a
3731
+ // refusal ProcessOut answers inside a 2xx or a pending answer included, is never handed over as an approval;
3732
+ // the card may have left the device, so the outcome is unknown rather than declined.
3733
+ const judged = processoutAnswerOutcome(response.status, response.body, null, null);
3734
+ if (judged.outcome !== 'approved' || typeof judged.body !== 'string')
3735
+ throw new PaymentOutcomeUnknownError(authorizationId, 'processout_token_unconfirmed');
3736
+ response.status = response.status === 201 ? 201 : 200;
3737
+ response.headers = { 'content-type': 'application/json' };
3738
+ response.body = judged.body;
3739
+ }
3532
3740
  if (rec?.psp === PRIMER_PSP) {
3533
3741
  // Primer's answer enters the page only as one single-use card token rebuilt from
3534
3742
  // payment-core's allow-list, under a JSON content type (primer.ts primerTokenAnswer).
@@ -3629,6 +3837,10 @@ export class VaultClient {
3629
3837
  if (preparation && typeof s.reason === 'string' && PREPARED_SEND_WINDOW_DECLINES[preparation.psp] === s.reason) {
3630
3838
  throw new CheckoutPreparationError(preparation.id, s.reason);
3631
3839
  }
3840
+ // The API declined a bound VTEX post past its send-by, counted from the pause this create named: the card
3841
+ // never left the cardholder's device, so the checkout failed unsent the same way.
3842
+ if (preparation?.psp === VTEX_PAYMENTS_PSP && s.reason === VTEX_PAYMENTS_POST_TIMED_OUT)
3843
+ throw new CheckoutPreparationError(preparation.id, s.reason);
3632
3844
  throw new ApprovalDeclinedError(s.reason ?? 'no reason given');
3633
3845
  }
3634
3846
  if (s.status === 'cancelled') {
@@ -3766,9 +3978,9 @@ export class VaultClient {
3766
3978
  * exactly the approved payment method (the page's own, or written by the
3767
3979
  * adapters in place of a card the confirm carries again).
3768
3980
  * The API allows a card save for any company, for an approval given on the
3769
- * Vault page, not by voice, with no amount, and a SetupIntent Stripe reports
3770
- * still confirmable on the same Stripe account; any other answer rejects with
3771
- * a CheckoutApiError whose code says why (for example `amount_mismatch`, or
3981
+ * Vault page, with no amount, and a SetupIntent Stripe reports still
3982
+ * confirmable on the same Stripe account; any other answer rejects with a
3983
+ * CheckoutApiError whose code says why (for example `amount_mismatch`, or
3772
3984
  * `continuation_invalid` for a confirm that is not a card-free SetupIntent
3773
3985
  * confirm). Nothing
3774
3986
  * is charged: the adapters continue the page's request once this resolves.
@@ -3973,13 +4185,17 @@ export class VaultClient {
3973
4185
  * (Stripe did not answer the read-back) is retried on a short backoff
3974
4186
  * instead of being left to the page's own retry loop, and a 409
3975
4187
  * `amount_mismatch` becomes an AmountMismatchError at stage 'create' so the
3976
- * adapters treat it as an answered request, not a dead page.
4188
+ * adapters treat it as an answered request, not a dead page. `atSend` adds
4189
+ * fields counted as each request leaves, after any token wait, 401 refresh or
4190
+ * backoff, so a field that measures time (VTEX's paused_ms) measures it then.
3977
4191
  */
3978
- async createAuthorization(payload, currency, signal, stopRetries) {
4192
+ async createAuthorization(payload, currency, signal, stopRetries, atSend) {
3979
4193
  for (let attempt = 0;; attempt++) {
3980
4194
  if (stopRetries?.aborted)
3981
4195
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
3982
4196
  try {
4197
+ if (atSend)
4198
+ return await this.postAtSend('/v2/checkout/authorizations', () => ({ ...payload, ...atSend() }), signal, stopRetries);
3983
4199
  return await this.post('/v2/checkout/authorizations', payload, signal, stopRetries);
3984
4200
  }
3985
4201
  catch (err) {
@@ -4065,8 +4281,10 @@ export class VaultClient {
4065
4281
  * Only fetch and a successful response's body read belong to that retry:
4066
4282
  * OAuth, HTTP errors, invalid JSON and every mutation remain outside it.
4067
4283
  * The existing one-time 401 refresh carries the remaining read budget.
4284
+ * `bodyAt`, when given, builds the body as each request leaves, after the
4285
+ * token and on the 401 refresh's resend, in place of `init.body`.
4068
4286
  */
4069
- async call(path, init = {}, retried = false, stopNewRequests, preparationReadAttempt) {
4287
+ async call(path, init = {}, retried = false, stopNewRequests, preparationReadAttempt, bodyAt) {
4070
4288
  const signal = init.signal ?? AbortSignal.timeout(30_000);
4071
4289
  const token = await withSignal(this.accessToken(), signal);
4072
4290
  // Auth can outlive the merchant request. Stop a create that has not left
@@ -4081,6 +4299,7 @@ export class VaultClient {
4081
4299
  try {
4082
4300
  r = await withSignal(this.fetch(`${this.baseUrl}${path}`, {
4083
4301
  ...init,
4302
+ ...(bodyAt ? { body: bodyAt() } : {}),
4084
4303
  signal,
4085
4304
  headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}`, 'content-type': 'application/json' },
4086
4305
  }), signal);
@@ -4100,7 +4319,7 @@ export class VaultClient {
4100
4319
  if (stopNewRequests?.aborted)
4101
4320
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
4102
4321
  await withSignal(this.accessToken(true), signal);
4103
- return this.call(path, { ...init, signal }, true, stopNewRequests, preparationReadAttempt);
4322
+ return this.call(path, { ...init, signal }, true, stopNewRequests, preparationReadAttempt, bodyAt);
4104
4323
  }
4105
4324
  throw new CheckoutApiError(r.status, path, await withSignal(r.text(), signal));
4106
4325
  }
@@ -4108,6 +4327,10 @@ export class VaultClient {
4108
4327
  post(path, body, signal, stopNewRequests) {
4109
4328
  return this.call(path, { method: 'POST', body: JSON.stringify(body), signal }, false, stopNewRequests);
4110
4329
  }
4330
+ /** A POST whose body is built as each request leaves (call's `bodyAt`), for a body that counts time. */
4331
+ postAtSend(path, body, signal, stopNewRequests) {
4332
+ return this.call(path, { method: 'POST', signal }, false, stopNewRequests, undefined, () => JSON.stringify(body()));
4333
+ }
4111
4334
  get(path, signal) {
4112
4335
  return this.call(path, { signal });
4113
4336
  }
@@ -4209,7 +4432,9 @@ function cardEndpointReplay(s, authorizationId, profile, metadata) {
4209
4432
  const answer = named.answer;
4210
4433
  if (!isRecord(answer) || !Number.isInteger(answer.status) || answer.status < 100 || answer.status > 599)
4211
4434
  throw unknown('merchant_answer_malformed');
4212
- const base = { mode: CARD_ENDPOINT_MODE, authorizationId, profile, status: answer.status, ...metadata };
4435
+ const approved = Number.isSafeInteger(s.amount) && s.amount > 0 && typeof s.currency === 'string' && /^[a-z]{3}$/i.test(s.currency)
4436
+ ? { approved: { amount: s.amount, currency: s.currency.toLowerCase() } } : {};
4437
+ const base = { mode: CARD_ENDPOINT_MODE, authorizationId, profile, status: answer.status, ...approved, ...metadata };
4213
4438
  if (answer.withheld !== undefined) {
4214
4439
  if (answer.withheld !== 'card_in_response' && answer.withheld !== 'unreadable_in_response' && answer.withheld !== 'refused_step_in_response') {
4215
4440
  throw unknown('merchant_answer_malformed');
@@ -4341,6 +4566,22 @@ function jsonPathOf(value) {
4341
4566
  || (typeof step === 'number' && Number.isSafeInteger(step) && step >= 0));
4342
4567
  return ok ? [...value] : null;
4343
4568
  }
4569
+ /** Whether a served step is the step into JSON text, exactly `{text: 'json'}`. */
4570
+ const isTextStep = (step) => isRecord(step) && Object.keys(step).length === 1 && step.text === 'json';
4571
+ /**
4572
+ * A served holder path: a JSON path that may also step into a string holding JSON text, at most twice, never first
4573
+ * or last and never twice in a row (payment-core json-path.js isProfilePath); null for anything else. The substitution
4574
+ * itself checks it against this build's own profile.
4575
+ */
4576
+ function holderPathOf(value) {
4577
+ if (!Array.isArray(value) || value.length > 16)
4578
+ return null;
4579
+ const texts = value.map((step, i) => (isTextStep(step) ? i : -1)).filter((i) => i !== -1);
4580
+ if (texts.length > 2 || texts.some((i) => i === 0 || i === value.length - 1 || texts.includes(i - 1)))
4581
+ return null;
4582
+ const plain = jsonPathOf(value.filter((step) => !isTextStep(step)));
4583
+ return plain ? value.map((step) => (isTextStep(step) ? { text: 'json' } : step)) : null;
4584
+ }
4344
4585
  /** Served card facts: a list of {path, value}; null when anything in it is not one. */
4345
4586
  function factsOf(value) {
4346
4587
  if (!Array.isArray(value))
@@ -4386,7 +4627,7 @@ function merchantHostedApproval(s, authorizationId, profile) {
4386
4627
  if (!isRecord(served) || served.profile !== profile.id || typeof served.body_sha256 !== 'string'
4387
4628
  || !/^[0-9a-f]{64}$/.test(served.body_sha256))
4388
4629
  throw malformed();
4389
- const path = jsonPathOf(sub.path);
4630
+ const path = holderPathOf(sub.path);
4390
4631
  const fields = isRecord(sub.fields) && Object.keys(sub.fields).length > 0
4391
4632
  && Object.values(sub.fields).every((value) => typeof value === 'string' && value.length > 0) ? { ...sub.fields } : null;
4392
4633
  const remove = sub.remove === undefined ? [] : Array.isArray(sub.remove) && sub.remove.every((key) => typeof key === 'string' && key.length > 0)
@@ -14,11 +14,11 @@ declare function isCreditguardPpsDocument(url: any): boolean;
14
14
  declare function creditguardNuveiRefusal(requestUrl: any, initiatorUrl: any): "creditguard_three_d_secure" | null;
15
15
  declare function isCreditguardJweCompact(value: any): boolean;
16
16
  declare function creditguardPpDomainHost(ppDomain: any): string | null;
17
- declare function creditguardBodyRefusal(body: any, txId: any, options: any): "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "wallet" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
17
+ declare function creditguardBodyRefusal(body: any, txId: any, options: any): "wallet" | "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
18
18
  declare function classifyCreditguardRequest(url: any, method: any, body: any, options: any): "card" | "unsupported" | null;
19
- declare function creditguardProcessRefusal(request: any, options: any): "not_process_request" | "headers" | "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "wallet" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
19
+ declare function creditguardProcessRefusal(request: any, options: any): "wallet" | "not_process_request" | "headers" | "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
20
20
  declare function isCreditguardProcessRequest(request: any, options: any): boolean;
21
- declare function creditguardReplayRefusal(request: any, options: any): "not_process_request" | "headers" | "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "wallet" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
21
+ declare function creditguardReplayRefusal(request: any, options: any): "wallet" | "not_process_request" | "headers" | "pp_domain_unbound" | "malformed_body" | "saved_card" | "otp" | "unexpected_field" | "keep_on_file" | "gift_card" | "dcc" | "installments" | "personal_id" | "txid_mismatch" | "pp_domain" | "jwe_fallback" | "jwe_unsupported" | "card_fields" | null;
22
22
  declare function creditguardRefusalMessage(reason: any): string;
23
23
  declare function injectCreditguardCard(body: any, card: any, options: any): {
24
24
  body: string;
@@ -141,7 +141,7 @@ declare function creditguardDeviceThreeDSOutcomeRequest(processUrl: any): {
141
141
  };
142
142
  body: string;
143
143
  };
144
- declare function creditguardDeviceStepRefusal(request: any, processUrl: any): "headers" | "malformed_body" | "wallet" | "unexpected_field" | "gift_card" | "dcc" | "txid_mismatch" | "card_fields" | "not_companion_request" | null;
144
+ declare function creditguardDeviceStepRefusal(request: any, processUrl: any): "wallet" | "headers" | "malformed_body" | "unexpected_field" | "gift_card" | "dcc" | "txid_mismatch" | "card_fields" | "not_companion_request" | null;
145
145
  declare function creditguardCardinalUrl(url: any, purpose: any, environment: any): string | null;
146
146
  declare function isCreditguardCardinalOrigin(origin: any): boolean;
147
147
  declare function creditguardCardinalMessage(data: any): {