@agent-cards/sdk 0.21.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/PREFLIGHT.md +32 -0
  3. package/README.md +1177 -23
  4. package/dist/aci.d.ts +173 -0
  5. package/dist/aci.generated.d.ts +87 -0
  6. package/dist/aci.generated.js +306 -0
  7. package/dist/aci.js +371 -0
  8. package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
  9. package/dist/adyen-merchant-hosted.generated.js +302 -56
  10. package/dist/agentcard.d.ts +9 -2
  11. package/dist/agentcard.js +4 -2
  12. package/dist/amadeus-checkout.generated.d.ts +57 -0
  13. package/dist/amadeus-checkout.generated.js +549 -0
  14. package/dist/amadeus.d.ts +41 -0
  15. package/dist/amadeus.js +81 -0
  16. package/dist/builtin-registry.generated.js +1 -1
  17. package/dist/card-endpoint-facts.d.ts +96 -0
  18. package/dist/card-endpoint-facts.js +275 -0
  19. package/dist/card-endpoint-page.d.ts +54 -0
  20. package/dist/card-endpoint-page.js +69 -0
  21. package/dist/card-endpoint.d.ts +298 -0
  22. package/dist/card-endpoint.js +438 -0
  23. package/dist/card-fields.generated.d.ts +5 -3
  24. package/dist/card-fields.generated.js +284 -7
  25. package/dist/cdp.d.ts +5 -1
  26. package/dist/cdp.js +1155 -141
  27. package/dist/client.d.ts +456 -20
  28. package/dist/client.js +1072 -104
  29. package/dist/creditguard.generated.d.ts +79 -0
  30. package/dist/creditguard.generated.js +662 -0
  31. package/dist/cse-body.js +11 -1
  32. package/dist/cybersource-flex.d.ts +30 -0
  33. package/dist/cybersource-flex.generated.d.ts +27 -1
  34. package/dist/cybersource-flex.generated.js +42 -2
  35. package/dist/cybersource-flex.js +46 -0
  36. package/dist/datatrans.d.ts +96 -0
  37. package/dist/datatrans.generated.d.ts +64 -0
  38. package/dist/datatrans.generated.js +426 -0
  39. package/dist/datatrans.js +178 -0
  40. package/dist/ekashu.d.ts +35 -0
  41. package/dist/ekashu.js +55 -0
  42. package/dist/elavon-converge.d.ts +37 -0
  43. package/dist/elavon-converge.generated.d.ts +49 -0
  44. package/dist/elavon-converge.generated.js +439 -0
  45. package/dist/elavon-converge.js +84 -0
  46. package/dist/eprotect.d.ts +152 -0
  47. package/dist/eprotect.generated.d.ts +77 -0
  48. package/dist/eprotect.generated.js +498 -0
  49. package/dist/eprotect.js +222 -0
  50. package/dist/hosted-form.d.ts +3 -2
  51. package/dist/hosted-form.js +3 -2
  52. package/dist/index.d.ts +17 -3
  53. package/dist/index.js +6 -1
  54. package/dist/juspay-gate.d.ts +82 -0
  55. package/dist/juspay-gate.js +126 -0
  56. package/dist/juspay.generated.d.ts +89 -0
  57. package/dist/juspay.generated.js +705 -0
  58. package/dist/lifecycle.d.ts +60 -6
  59. package/dist/lifecycle.js +122 -8
  60. package/dist/merchant-card-endpoints.generated.d.ts +122 -0
  61. package/dist/merchant-card-endpoints.generated.js +4261 -0
  62. package/dist/merchant-hosted.d.ts +4 -1
  63. package/dist/merchant-hosted.js +5 -2
  64. package/dist/merchant-session-probe.d.ts +48 -0
  65. package/dist/merchant-session-probe.js +53 -0
  66. package/dist/merchant-session.generated.d.ts +69 -0
  67. package/dist/merchant-session.generated.js +919 -0
  68. package/dist/payrails.d.ts +133 -0
  69. package/dist/payrails.generated.d.ts +73 -0
  70. package/dist/payrails.generated.js +596 -0
  71. package/dist/payrails.js +305 -0
  72. package/dist/pelecard.d.ts +32 -0
  73. package/dist/pelecard.generated.d.ts +32 -0
  74. package/dist/pelecard.generated.js +283 -0
  75. package/dist/pelecard.js +42 -0
  76. package/dist/pre-claim.d.ts +81 -2
  77. package/dist/pre-claim.js +1023 -5
  78. package/dist/preflight-catalog.json +2267 -288
  79. package/dist/preflight-schemas.json +183 -6
  80. package/dist/preflight.generated.d.ts +1 -1
  81. package/dist/preflight.generated.js +243 -4
  82. package/dist/preparation.d.ts +12 -3
  83. package/dist/preparation.js +63 -12
  84. package/dist/prepared-processor.d.ts +84 -5
  85. package/dist/prepared-processor.js +147 -6
  86. package/dist/primer.d.ts +53 -0
  87. package/dist/primer.generated.d.ts +38 -0
  88. package/dist/primer.generated.js +624 -0
  89. package/dist/primer.js +82 -0
  90. package/dist/registry.d.ts +100 -8
  91. package/dist/registry.js +85 -11
  92. package/dist/shiji-sts.d.ts +144 -0
  93. package/dist/shiji-sts.generated.d.ts +72 -0
  94. package/dist/shiji-sts.generated.js +913 -0
  95. package/dist/shiji-sts.js +222 -0
  96. package/dist/stripe-checkout.generated.d.ts +1 -1
  97. package/dist/stripe-checkout.generated.js +764 -9
  98. package/dist/stripe-setup.d.ts +16 -0
  99. package/dist/stripe-setup.js +61 -0
  100. package/dist/substitutions.generated.js +187 -29
  101. package/dist/verifone.d.ts +68 -0
  102. package/dist/verifone.generated.d.ts +51 -0
  103. package/dist/verifone.generated.js +1413 -0
  104. package/dist/verifone.js +119 -0
  105. package/dist/voice-types.d.ts +27 -0
  106. package/dist/voice.d.ts +6 -1
  107. package/dist/voice.js +6 -1
  108. package/dist/wspay.d.ts +41 -0
  109. package/dist/wspay.generated.d.ts +41 -0
  110. package/dist/wspay.generated.js +370 -0
  111. package/dist/wspay.js +65 -0
  112. package/examples/preflight/kernel-native/inventory.json +1 -1
  113. package/package.json +3 -3
package/dist/client.js CHANGED
@@ -1,11 +1,19 @@
1
- import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, LOOPBACK_URL_PATTERNS, findRecognizer, recognizerRequiresPreparation, } from './registry.js';
1
+ import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, LOOPBACK_URL_PATTERNS, findRecognizer, isRecognizerRequest, recognizerRequestMethod, recognizerRequiresPreparation, urlsOnProcessorHosts, } from './registry.js';
2
2
  import { armServedProfiles, classifyProfileRequest, declareSandboxMerchant, merchantProfileEnvironment, merchantProfileFor, merchantProfileUrlPatterns, } from './merchant-hosted.js';
3
3
  import { merchantAmountExchanges, merchantAmountFromResponses, merchantProfilePricedBySource, merchantTotalWire, MerchantTotalWait, } from './merchant-total.js';
4
+ import { ACI_CAPABILITY, ACI_PSP, aciCardLegAnswer, redactAciCheckoutId } from './aci.js';
5
+ import { EPROTECT_CAPABILITY, EPROTECT_PSP, eprotectCheckoutRefusal, eprotectScriptAnswer, isEprotectRegistration } from './eprotect.js';
4
6
  import { isMercadoTokenRequest, parseMercadoCheckoutContext, validateMercadoProcessorContext } from './mercado-checkout.generated.js';
5
7
  import { matchesPreparation, preparationMode, validPreparationEnvironment } from './prepared-processor.js';
6
8
  import { fiservKeyPinById, isFiservSubstitution, requiresFiservPreparation } from './fiserv.js';
9
+ import { armServedVerifoneProfiles, classifyVerifoneProfileRequest, isVerifoneProfileEntry, isVerifoneSubstitution, verifoneProfileFor, verifoneProfileUrlPatterns, } from './verifone.js';
7
10
  import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
8
11
  import { requestWithoutCard } from './card-fields.generated.js';
12
+ import { merchantSessionCapture } from './merchant-session-probe.js';
13
+ import { creditguardPageProcessUrl, creditguardPpDomainHost, creditguardPreparationEnvironment, creditguardProcessTxId, sanitizeCreditguardResponse, } from './creditguard.generated.js';
14
+ import { CONVERGE_CAPABILITY } from './elavon-converge.js';
15
+ import { SHIJI_STS_CAPABILITY, SHIJI_STS_PSP, isShijiStsServedCompanion, isShijiStsServedSubstitution } from './shiji-sts.js';
16
+ import { AMADEUS_CHECKOUT_CAPABILITY } from './amadeus-checkout.generated.js';
9
17
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
10
18
  import { VoiceModule } from './voice.js';
11
19
  import { BuyModule } from './buy.js';
@@ -13,14 +21,24 @@ import { Languages } from './languages.js';
13
21
  import { RequestRegistry } from './requests.js';
14
22
  import { AgentcardError } from './voice-types.js';
15
23
  import { GMO_CAPABILITY, GMO_PSP, gmoTokenAnswer } from './gmo.js';
16
- import { isCybersourceFlexTokenShape } from './cybersource-flex.generated.js';
17
- import { CYBERSOURCE_FLEX_CAPABILITY, CYBERSOURCE_FLEX_PSP, validCybersourceFlexSidecar } from './cybersource-flex.js';
24
+ import { admitCybersourceFlexPreparation, isCybersourceFlexTokenShape, sameCybersourceFlexPreparation } from './cybersource-flex.generated.js';
25
+ import { CYBERSOURCE_FLEX_CAPABILITY, CYBERSOURCE_FLEX_PSP, validCybersourceFlexSidecar, } from './cybersource-flex.js';
26
+ import { JUSPAY_CAPABILITY, JUSPAY_PSP, juspayPageAnswer } from './juspay-gate.js';
27
+ import { CARD_ENDPOINT_MODE, CARD_ENDPOINT_PSP, CardEndpointRefusedError, cardEndpointById, cardEndpointLive, cardEndpointSessionRefusal, } from './card-endpoint.js';
28
+ import { merchantCardEndpointFactsRefusal as sharedFactsRefusal, readMerchantCardEndpointFacts as sharedReadFacts, } from './merchant-card-endpoints.generated.js';
29
+ import { DATATRANS_CAPABILITY } from './datatrans.js';
30
+ import { PELECARD_CAPABILITY, PELECARD_PSP } from './pelecard.js';
31
+ import { PRIMER_CAPABILITY, PRIMER_PSP, primerTokenAnswer } from './primer.js';
32
+ import { PAYRAILS_CAPABILITY, payrailsSaveAnswer } from './payrails.js';
33
+ import { WSPAY_CAPABILITY, WSPAY_PSP } from './wspay.js';
18
34
  /**
19
35
  * The modes this SDK can finish. Asked for on syncRegistry (the API serves
20
36
  * only recognizers in these modes, so a request this build cannot complete
21
- * is never paused) and sent on every create.
37
+ * is never paused) and sent on every create. `device_encrypt`: the adapters
38
+ * capture the Payrails session key the page's init payload names (payrails.ts),
39
+ * and the cardholder's device encrypts and sends the save.
22
40
  */
23
- export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form'];
41
+ export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form', 'device_encrypt'];
24
42
  /**
25
43
  * Registry features this SDK honours, asked for on syncRegistry next to the
26
44
  * modes. `card_fields`: it reads a recognizer's cardFields and claims only a
@@ -50,35 +68,243 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
50
68
  * serves Flex's recognizer the same way, only for a company Agentcard turned it on for.
51
69
  * A gated recognizer is never in BUILTIN_REGISTRY, so a build reaches it only through a
52
70
  * sync that declares its capability.
71
+ * `juspay_txns.v1`: Juspay's card flow (juspay-gate.ts), whose page tokenize
72
+ * this build answers locally and whose /txns it pauses, served and refused by
73
+ * the same rule. A gated recognizer is never in BUILTIN_REGISTRY, so a build
74
+ * reaches it only through a sync that declares its capability.
75
+ * `datatrans_secure_fields_tokenize`: Datatrans Secure Fields' TOKENIZE card
76
+ * post, which this build judges before it pauses, under the card-on-file
77
+ * policy served with the entry, and answers again for a repeat of the same
78
+ * form (datatrans.ts). A gated recognizer never sits in BUILTIN_REGISTRY, so
79
+ * it is paused only after a sync.
80
+ * `authorize_net_dpm_form`: the Authorize.Net Direct Post Method and SIM card
81
+ * form, one form navigation to transact.dll that the cardholder's device submits
82
+ * itself (hosted_form). This build finishes it with the hosted_form flow it
83
+ * already speaks, holds the bare endpoint the entry names (bareEndpoint in
84
+ * findRecognizer), and reads the form's keys without regard to case
85
+ * (card-fields.js). The API serves it, like Fiserv's, only to an org Agentcard
86
+ * enabled for that processor; every other org is served the ungated catalog.
87
+ * `pelecard_transaction`: Pelecard's card submission, which this build judges
88
+ * before it pauses (pelecard.ts) and pays only for the amount the agent passes.
89
+ * Declaring it is not enough: the API serves a gated recognizer only to an org
90
+ * Agentcard has enabled for that processor, so for every other org this build
91
+ * syncs the same catalog as one that declares nothing and leaves the processor's
92
+ * requests alone. A gated recognizer never sits in BUILTIN_REGISTRY, so it is
93
+ * paused only after a sync.
94
+ * `aci_split_card.v1`: ACI's COPYandPAY widget, whose card reaches ACI as
95
+ * several requests to one checkout. This build pauses the card-number request
96
+ * for a prepared checkout, holds the page's own CVC and expiry requests and
97
+ * answers them with that request's answer, and continues the page's /payment
98
+ * submission once, with the placeholder expiry removed (aci.ts). A gated
99
+ * recognizer is never in BUILTIN_REGISTRY, so a build reaches it only through a
100
+ * sync that declares its capability, and whether an org may pay through it
101
+ * stays the API's decision.
102
+ * `creditguard_pps_process`: CreditGuard's hosted payment page (Hyp PPS), whose
103
+ * card submit this build pays only under a prepared checkout that names the
104
+ * framed page (prepare({ psp: 'creditguard', environment, paymentPageUrl })). The
105
+ * API serves CreditGuard's recognizer on the same terms as Fiserv's.
106
+ * `primer_payment_instruments`: Primer's card tokenization, which this build
107
+ * judges before it pauses (primer.ts) and hands back to the page only as a
108
+ * single-use token. A merchant's client session can save the card after the
109
+ * payment with no sign in that request, so the API creates a Primer
110
+ * authorization only for a test-mode client and refuses a live one (400
111
+ * invalid_primer_request, live_keep_on_file) until the cardholder can consent
112
+ * to that save; this build then aborts the paused request with nothing sent. A
113
+ * gated recognizer never sits in BUILTIN_REGISTRY, so it is paused only after a
114
+ * sync.
115
+ * `payrails_device_encrypt`: Payrails' card-form save, which this build pauses
116
+ * only once it has bound the save to a captured session (payrails.ts). A gated
117
+ * recognizer never sits in BUILTIN_REGISTRY, so it is paused only after a sync.
118
+ * `converge_hpp_process`: Elavon Converge's Hosted Payment Page card request
119
+ * (elavon-converge.ts), paused in token mode and judged by the pre-claim gate.
120
+ * A gated recognizer never sits in BUILTIN_REGISTRY, so it is paused only after a
121
+ * sync, and the API also refuses a Converge payment for every company the
122
+ * processor is off for.
123
+ * `worldpay_eprotect_jsonp`: Worldpay eProtect, whose card rides in the query of
124
+ * a JSONP <script> GET that this build pauses, binds to a prepared checkout and
125
+ * answers with the registration in the script's own callback (eprotect.ts). A
126
+ * gated recognizer is never in BUILTIN_REGISTRY, so a build reaches either only
127
+ * through a sync that declares its capability, and the API serves it on that
128
+ * sync only for a company it has turned the processor on for.
129
+ * `wspay_confirm`: WSPay Components' card confirm, which this build judges
130
+ * before it pauses (wspay.ts) and pays only for the amount the agent passes.
131
+ * The API serves a gated recognizer only to a build that lists its capability
132
+ * and only for a company Agentcard turned that processor on for, so an older
133
+ * SDK never pauses a request it cannot finish and no SDK pauses one for a
134
+ * company the processor is off for; the API also refuses that processor's
135
+ * payment for every such company. A gated recognizer never sits in
136
+ * BUILTIN_REGISTRY, so it is paused only after a sync.
137
+ * `shiji_sts_pinned_key`: this build swaps Shiji STS's ciphertexts at the body
138
+ * root under the pinned key, binds the getCvvId companion to the getTokenId it
139
+ * continued, and refuses every other Shiji request in the browser (shiji-sts.ts).
140
+ * The API serves it on the same terms, only for a company Agentcard turned Shiji
141
+ * STS on for.
142
+ * `amadeus_checkout_actions.v1`: Amadeus Checkout (the Checkout Web SDK add and
143
+ * the Data Collection Page store), served on the same terms. This build lets every
144
+ * other action on those URLs through without claiming a preparation, so the page's
145
+ * own `load` never spends or blocks one, takes the card request only through a
146
+ * prepared checkout bound to the approved page's host, and fails a paused SDK add
147
+ * the device has not started 20 seconds after the pause (AMADEUS_SEND_WINDOW_MS).
148
+ * `worldline_mycheckout_form`: Worldline MyCheckout's card form (DHL Express),
149
+ * a top-level form POST this build holds like Tranzila's (mode hosted_form) and
150
+ * answers with the submission notice once the cardholder's device submitted the
151
+ * form itself. The page's cancel and its other payment methods on the same URL
152
+ * carry no card and continue untouched. The API serves the recognizer only to a
153
+ * build that lists it and only for a company Agentcard turned it on for.
154
+ * `verifone_card_capture`: a reviewed Verifone merchant's own card request (a
155
+ * verifone.js blob sent to the merchant's server), which this build pauses and pays
156
+ * only through a prepared checkout (verifone.ts); the API serves the Verifone
157
+ * profiles only to a build that lists it, for a company Agentcard turned Verifone on for.
158
+ * `ekashu_hosted_payment_page`: NMI's hosted payment page, whose card-free entry this
159
+ * build pauses for the cardholder's device and whose card page this build never lets
160
+ * the agent's browser post to (pre-claim.ts, ekashu.ts); served on the same terms.
53
161
  */
54
- export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY];
162
+ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY, JUSPAY_CAPABILITY, DATATRANS_CAPABILITY, 'authorize_net_dpm_form', PELECARD_CAPABILITY, ACI_CAPABILITY, 'creditguard_pps_process', PRIMER_CAPABILITY, PAYRAILS_CAPABILITY, CONVERGE_CAPABILITY, EPROTECT_CAPABILITY, WSPAY_CAPABILITY, SHIJI_STS_CAPABILITY, AMADEUS_CHECKOUT_CAPABILITY, 'worldline_mycheckout_form', 'verifone_card_capture', 'ekashu_hosted_payment_page'];
55
163
  /**
56
164
  * Processors whose paused request names no amount at all, so the pair the
57
165
  * agent passes is the amount the cardholder approves: authorize() refuses one
58
166
  * without it (AmountRequiredError), and the API answers 400 amount_required
59
167
  * for the same case. GMO's card token request carries none: the merchant's
60
- * server charges the token later, for the amount it decides.
168
+ * server charges the token later, for the amount it decides. Pelecard's card
169
+ * submission carries none either: the merchant's server bound the total, the
170
+ * currency and the action type to the payment page when it created it, and no
171
+ * Pelecard answer states them. WSPay's confirm carries none either: WSPay fixed
172
+ * the amount when the merchant's server created the payment, so the agent's
173
+ * pair is also the figure the API compares with WSPay's answer when that
174
+ * answer names one. A WSPay 3-D Secure step answers with no amount, so on that
175
+ * path nothing Agentcard sees confirms what WSPay charges once the challenge
176
+ * completes.
61
177
  */
62
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP];
178
+ const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP];
63
179
  /** How AmountRequiredError names a processor. */
64
- const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO' };
180
+ const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay' };
181
+ /**
182
+ * Processors whose request and answer name no amount at all, so the total the
183
+ * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
184
+ * to what the page charges: authorize() refuses one without it
185
+ * (PageAmountRequiredError), and the API answers 400 page_amount_required for the
186
+ * same case. Pelecard only.
187
+ */
188
+ const PAGE_AMOUNT_PROCESSORS = [PELECARD_PSP];
65
189
  /** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
66
190
  export function validAmountInput(amount) {
67
191
  if (typeof amount === 'number')
68
192
  return Number.isSafeInteger(amount) && amount >= 0;
69
193
  return typeof amount === 'string' && /^\d{1,15}\.\d{1,3}$/.test(amount.trim());
70
194
  }
195
+ // The API reads an amount with Stripe's decimal places, which are not always ISO 4217's: MGA has
196
+ // none, IQD and LYD have two. Kept equal to payment-core's stripeMinorUnitDigits by a test.
197
+ const ZERO_DECIMAL_CURRENCIES = ['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'];
198
+ const THREE_DECIMAL_CURRENCIES = ['BHD', 'JOD', 'KWD', 'OMR', 'TND'];
199
+ /** Digits after the point when the API reads an amount in `currency`. */
200
+ export function amountDigits(currency) {
201
+ const code = currency.trim().toUpperCase();
202
+ return ZERO_DECIMAL_CURRENCIES.includes(code) ? 0 : THREE_DECIMAL_CURRENCIES.includes(code) ? 3 : 2;
203
+ }
204
+ /** `minor` in the forms the API accepts, labelled with the amount it means; null when the API refuses it. */
205
+ function amountOption(minor, currency) {
206
+ const digits = amountDigits(currency);
207
+ // A three-decimal amount ends in 0 in the smallest unit (5120 for 5.120 KWD), as the API requires.
208
+ if (!Number.isSafeInteger(minor) || minor < 0 || (digits === 3 && minor % 10 !== 0))
209
+ return null;
210
+ const code = currency.trim().toUpperCase();
211
+ if (digits === 0)
212
+ return `For ${minor} ${code}, send the number ${minor}.`;
213
+ const padded = String(minor).padStart(digits + 1, '0');
214
+ const decimal = `${padded.slice(0, -digits)}.${padded.slice(-digits)}`;
215
+ return `For ${decimal} ${code}, send the number ${minor} or the string "${decimal}".`;
216
+ }
217
+ /** Every amount `figure` can mean in `currency`, each in the forms the API accepts. */
218
+ function amountOptions(figure, currency) {
219
+ const digits = amountDigits(currency);
220
+ const whole = /^\d{1,15}$/.test(figure);
221
+ const decimal = /^(\d{1,15})\.(\d{1,3})$/.exec(figure);
222
+ // Digits alone can mean the smallest unit or normal units, so both readings are offered, each
223
+ // labelled with the amount it means, and the agent picks the one it intended.
224
+ const readings = whole ? (digits === 0 ? [Number(figure)] : [Number(figure), Number(figure) * 10 ** digits])
225
+ // More digits after the point than the currency has (23.065 dollars) is no amount at all.
226
+ : decimal && (decimal[2].length <= digits || Number(decimal[2].slice(digits)) === 0)
227
+ ? [Number(decimal[1] + decimal[2].padEnd(digits, '0').slice(0, digits))] : [];
228
+ return readings.map(minor => amountOption(minor, currency)).filter((option) => option !== null);
229
+ }
230
+ /**
231
+ * Why an amount is refused, worded for the agent that sent it: the rule, the value it passed, and
232
+ * each amount that value can mean, in the forms the API accepts. An agent told only the rule sends
233
+ * the same value again, and a tool harness that serializes every argument as text turns 2667 into
234
+ * "2667" on each try. A value that does not read as a price (long, or more digits than any amount)
235
+ * is described and never echoed, so a card number typed into the wrong field cannot reach a log
236
+ * line. A value below zero is named as such and never offered back as a positive amount.
237
+ */
238
+ export function amountRefusal(amount, currency) {
239
+ const rule = 'amount is an integer in the currency\'s smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06").';
240
+ // Ten digits is $99,999,999.99 in cents, and no more of a card number than its first six and last four.
241
+ const readable = typeof amount === 'string' ? amount.length <= 24 && (amount.match(/\d/g)?.length ?? 0) <= 10
242
+ : typeof amount === 'number' && Number.isFinite(amount) && Math.abs(amount) < 1e10;
243
+ if (!readable) {
244
+ const kind = typeof amount === 'string' ? 'a string' : typeof amount === 'number' ? 'a number' : amount === null ? 'null' : `a value of type ${typeof amount}`;
245
+ return `${rule} You passed ${kind}${typeof amount === 'string' || typeof amount === 'number' ? ' that does not read as an amount' : ''}.`;
246
+ }
247
+ const got = typeof amount === 'string' ? `the string ${JSON.stringify(amount)}` : `the number ${amount}`;
248
+ const code = typeof currency === 'string' && /^[a-z]{3}$/i.test(currency.trim()) ? currency.trim() : null;
249
+ // The figure inside a display string: "$23.06", "USD 23.06" and "23.06 usd" read as 23.06.
250
+ let figure = String(amount).trim().replace(/^[$€£¥₹]\s*/, '');
251
+ if (code)
252
+ figure = figure.replace(new RegExp(`^${code}\\s*|\\s*${code}$`, 'gi'), '');
253
+ if (figure.startsWith('-'))
254
+ return `${rule} You passed ${got}, which is below zero.`;
255
+ const options = code ? amountOptions(figure, code) : [];
256
+ return `${rule} You passed ${got}.${options.length ? ` ${options.join(' ')}` : ''}`;
257
+ }
71
258
  const AMOUNT_AUTHORITIES = ['processor', 'agent', 'page', 'none'];
72
259
  export class CheckoutPreparationError extends Error {
73
260
  preparationId;
74
261
  reason;
75
- constructor(preparationId, reason) {
76
- super(`Checkout preparation unavailable: ${reason}`);
262
+ /**
263
+ * The API's own reason code under `reason`, when it gave one: a CreditGuard
264
+ * transaction refused as `creditguard_transaction_refused` names what the
265
+ * Vault cannot finish here (`three_d_secure`, `otp`, `installments`, ...).
266
+ */
267
+ detail;
268
+ /** `explanation` follows the reason in the message: what the caller passed and what to send instead. */
269
+ constructor(preparationId, reason, detail, explanation) {
270
+ super(`Checkout preparation unavailable: ${reason}${detail ? ` (${detail})` : ''}${explanation ? `. ${explanation}` : ''}`);
77
271
  this.preparationId = preparationId;
78
272
  this.reason = reason;
79
273
  this.name = 'CheckoutPreparationError';
274
+ if (detail)
275
+ this.detail = detail;
80
276
  }
81
277
  }
278
+ /**
279
+ * Why a preparation's payment page is refused before any request, or null. CreditGuard needs the page
280
+ * the checkout framed, on a host of the preparation's environment (payment-core's own reading of it,
281
+ * the one the API applies); no other processor takes one.
282
+ */
283
+ function paymentPageRefusal(input) {
284
+ if (input.psp !== 'creditguard')
285
+ return input.paymentPageUrl === undefined ? null : 'payment_page_unsupported';
286
+ if (input.paymentPageUrl === undefined)
287
+ return 'payment_page_required';
288
+ const processUrl = creditguardPageProcessUrl(input.paymentPageUrl);
289
+ if (processUrl === null)
290
+ return 'unsupported_payment_page';
291
+ return creditguardPreparationEnvironment(new URL(processUrl).hostname) === input.environment ? null : 'payment_page_environment_mismatch';
292
+ }
293
+ /**
294
+ * The API's refusals of a CreditGuard preparation, answered before any row or prompt: each names
295
+ * something the caller can act on (a page the Vault cannot finish, an amount CreditGuard does not
296
+ * hold, a company not enabled), so it is reported as its own reason rather than as an unconfirmed
297
+ * preparation. A transaction refusal carries CreditGuard's reason as `detail`.
298
+ */
299
+ const CREDITGUARD_PREPARATION_REFUSALS = ['payment_page_required', 'unsupported_payment_page',
300
+ 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'creditguard_transaction_refused', 'creditguard_transaction_unavailable',
301
+ 'amount_mismatch', 'processor_unavailable'];
302
+ function creditguardPreparationRefusal(error) {
303
+ if (!(error instanceof CheckoutApiError) || error.code === null || !CREDITGUARD_PREPARATION_REFUSALS.includes(error.code))
304
+ return null;
305
+ const reason = error.details.reason;
306
+ return new CheckoutPreparationError(null, error.code, typeof reason === 'string' && /^[a-z0-9_]{1,64}$/.test(reason) ? reason : undefined);
307
+ }
82
308
  export class CardEncryptedError extends Error {
83
309
  psp;
84
310
  constructor(psp) {
@@ -164,10 +390,15 @@ export class PreparationRequiredError extends Error {
164
390
  /**
165
391
  * The paused request names no amount and none was passed. A GMO card token
166
392
  * request carries no amount (the merchant's server charges the token later),
167
- * so the `amount` and `currency` you pass are what the cardholder approves.
168
- * Thrown locally before any create or prompt, and terminal: the same request
169
- * without an amount fails the same way. The API answers 400 amount_required
170
- * for the same case. A page total (`pageAmount`) does not count.
393
+ * and neither does a Pelecard card submission (the merchant's server bound it
394
+ * to the payment page when it created it) or a WSPay confirm (WSPay fixed it
395
+ * when the merchant's server created the payment), so the `amount` and
396
+ * `currency` you pass are what the cardholder approves, and for WSPay the
397
+ * figure the API compares with WSPay's answer when that answer names one (a
398
+ * 3-D Secure step names none). Thrown locally before any create or
399
+ * prompt, and terminal: the same request without an amount fails the same
400
+ * way. The API answers 400 amount_required for the same case. A page total
401
+ * (`pageAmount`) does not count.
171
402
  */
172
403
  export class AmountRequiredError extends Error {
173
404
  psp;
@@ -179,6 +410,25 @@ export class AmountRequiredError extends Error {
179
410
  this.name = 'AmountRequiredError';
180
411
  }
181
412
  }
413
+ /**
414
+ * The paused request names no amount, and the total its payment page shows
415
+ * (`pageAmount`) was not passed. Pelecard's merchant bound the total to the
416
+ * payment page when it created it, and no Pelecard request or answer states
417
+ * it, so the page's total is the one figure that ties the `amount` the
418
+ * cardholder approves to what the page charges. Thrown locally before any
419
+ * create or prompt; the API answers 400 page_amount_required for the same
420
+ * case. Not terminal: the page's next submission reads the total again.
421
+ */
422
+ export class PageAmountRequiredError extends Error {
423
+ psp;
424
+ code = 'page_amount_required';
425
+ constructor(psp) {
426
+ const processor = psp === PELECARD_PSP ? 'Pelecard' : psp;
427
+ super(`A ${processor} payment request names no amount, so pass the total its payment page shows as pageAmount beside amount and currency. Nothing was created and nothing was charged.`);
428
+ this.psp = psp;
429
+ this.name = 'PageAmountRequiredError';
430
+ }
431
+ }
182
432
  export class ApprovalTimeoutError extends Error {
183
433
  constructor(ms) { super(`user did not approve within ${ms}ms`); this.name = 'ApprovalTimeoutError'; }
184
434
  }
@@ -190,6 +440,15 @@ export const RUNTIME_CANCEL_REASONS = ['merchant_request_aborted', 'merchant_nev
190
440
  export class PaymentOutcomeUnknownError extends Error {
191
441
  authorizationId;
192
442
  reason;
443
+ /**
444
+ * False when the API confirmed it retired this authorization before any processor
445
+ * request started: authorize() cancels an authorization it could not finish (a card
446
+ * endpoint, a prepared or a native checkout, or a request the page abandoned), and the
447
+ * cancel came back with `processor_request_started: false`, so nothing reached the
448
+ * processor or the merchant. Null when that is not known. The controller still reads
449
+ * `outcome_unknown`; a card form navigation's page reads this to say nothing was sent.
450
+ */
451
+ processorRequestStarted = null;
193
452
  constructor(authorizationId, reason) {
194
453
  super(`payment outcome unknown${authorizationId ? ` for ${authorizationId}` : ''}: ${reason}; check the merchant order before retrying`);
195
454
  this.authorizationId = authorizationId;
@@ -522,10 +781,13 @@ export class CheckoutApiError extends Error {
522
781
  * already has, or already answered, the prompt for this submission, and
523
782
  * once that prior authorization is declined or expired the same form is a
524
783
  * new question. The adapters quiet the page's re-post the way they quiet a
525
- * decline instead of latching the attachment.
784
+ * decline instead of latching the attachment. NOT a stale form fingerprint
785
+ * either (isStaleFingerprint): the bytes of that form are refused for good,
786
+ * but the merchant's page renders a fresh fingerprint on reload, so the
787
+ * page's next form is a new question and is quieted the same way.
526
788
  */
527
789
  get permanent() {
528
- if (this.code === 'amount_mismatch' || this.code === 'duplicate_submission')
790
+ if (this.code === 'amount_mismatch' || this.code === 'duplicate_submission' || this.isStaleFingerprint)
529
791
  return false;
530
792
  // Nor a refusal by the company preset (a 403 carrying `preset` in the
531
793
  // envelope): the company's rules refused THIS purchase, at this merchant,
@@ -535,6 +797,16 @@ export class CheckoutApiError extends Error {
535
797
  return false;
536
798
  return this.status >= 400 && this.status < 500 && this.status !== 429;
537
799
  }
800
+ /**
801
+ * True for a 400 `hosted_form_field_refused` whose reason is
802
+ * `stale_fingerprint`: an Authorize.Net DPM or SIM form signed more than 40
803
+ * minutes before the create, or ahead of the API's clock. Authorize.Net
804
+ * would refuse that fingerprint itself, and the page renders a fresh one
805
+ * when it reloads, so the refusal holds for this form only.
806
+ */
807
+ get isStaleFingerprint() {
808
+ return this.status === 400 && this.code === 'hosted_form_field_refused' && this.details.reason === 'stale_fingerprint';
809
+ }
538
810
  }
539
811
  /** The client-credentials exchange was refused: the RFC 6749 error, with the status the token endpoint answered. */
540
812
  export class CheckoutAuthError extends Error {
@@ -582,18 +854,46 @@ function parseErrorEnvelope(bodyText) {
582
854
  }
583
855
  return { code: null, details: {} };
584
856
  }
857
+ const CREDITGUARD_HOST = /(?:^|\.)creditguard\.co\.il\.?$/i;
858
+ function maskCreditguardTxId(path) {
859
+ return path.replace(/(\/txn\/)[^/]+/gi, '$1{txId}')
860
+ .replace(/[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi, '{txId}');
861
+ }
862
+ /**
863
+ * Hosts that put a per-payment secret in the PATH: Monri Components and
864
+ * WSPay's build of it name the payment's client_secret in
865
+ * /v2/payment/{client_secret}/confirm (and /show, /status), and with the page's
866
+ * public token that secret confirms the payment and reads its status.
867
+ */
868
+ const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
869
+ /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) or after an escaped slash. */
870
+ const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
585
871
  /**
586
872
  * A URL as it may appear in an error message: origin + path only. A paused
587
873
  * request's URL can carry a client secret in its query string, and error
588
874
  * messages travel further than anyone intends (onEvent, logs, crash reports).
875
+ * CreditGuard puts one in the path: on its hosts the transaction id after
876
+ * /txn/ is that transaction's only credential (payment-core's creditguard.js),
877
+ * so it reads {txId}, as does any other id-shaped segment there.
878
+ * On the Monri and WSPay hosts the secret is a path segment, so it is replaced
879
+ * with `{client_secret}`: https://proxy.wspay.biz/v2/payment/{client_secret}/confirm.
880
+ * A string that is not a URL keeps the part before any `?` or `#`, with the
881
+ * same segment masked.
589
882
  */
590
883
  export function redactUrl(raw) {
884
+ // An ACI checkout id is that checkout's only capability; it never rides on an event.
885
+ const aci = redactAciCheckoutId(raw);
886
+ if (aci !== null)
887
+ return aci;
591
888
  try {
592
889
  const u = new URL(raw);
593
- return `${u.origin}${u.pathname}`;
890
+ // A trailing dot names the same host, so it cannot let the secret through.
891
+ const path = PATH_SECRET_HOSTS.includes(u.hostname.replace(/\.$/, '')) ? u.pathname.replace(PATH_SECRET, '$1{client_secret}') : u.pathname;
892
+ return `${u.origin}${CREDITGUARD_HOST.test(u.hostname) ? maskCreditguardTxId(path) : path}`;
594
893
  }
595
894
  catch {
596
- return raw.split(/[?#]/)[0];
895
+ const bare = raw.split(/[?#]/)[0].replace(PATH_SECRET, '$1{client_secret}');
896
+ return /creditguard\.co\.il/i.test(bare) ? maskCreditguardTxId(bare) : bare;
597
897
  }
598
898
  }
599
899
  /**
@@ -625,6 +925,33 @@ const FISERV_PREPARATION_REFUSALS = [
625
925
  'processor_unavailable', 'unsupported_checkout', 'merchant_profile_required', 'unknown_merchant_profile',
626
926
  'merchant_profile_key_changed', 'fiserv_key_refused', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row',
627
927
  ];
928
+ /**
929
+ * The API's refusals of a Worldpay eProtect preparation, before any preparation exists, that
930
+ * prepareCheckout surfaces by name: a live checkout's merchant is not the name the reviewed pin for
931
+ * its paypageId carries (`merchant_profile_mismatch`, which this build also refuses itself when it
932
+ * holds that pin). Only for an eProtect checkout.
933
+ */
934
+ const EPROTECT_PREPARATION_REFUSALS = ['merchant_profile_mismatch'];
935
+ /**
936
+ * Refusals a CyberSource Flex preparation that tokenizes before Pay answers (4xx) before any
937
+ * preparation exists, surfaced as the CheckoutPreparationError reason: the processor is off for
938
+ * this company (`processor_unavailable`), the environment is not the client's mode, or the capture
939
+ * context or fields read from the page cannot be verified or rebuilt. Only for such a preparation;
940
+ * a Flex preparation without a capture context keeps `preparation_unconfirmed`, as before.
941
+ */
942
+ const CYBERSOURCE_FLEX_PREPARATION_REFUSALS = [
943
+ 'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'capture_context_invalid', 'microform_unsupported',
944
+ 'cybersource_environment_mismatch',
945
+ ];
946
+ /** Why payment-core refused a Flex preparation's capture context or fields, as a CheckoutPreparationError reason. */
947
+ function cybersourceFlexPreparationRefusal(error) {
948
+ const reason = error?.reason;
949
+ if (reason === 'microform_version' || reason === 'fields')
950
+ return 'microform_unsupported';
951
+ if (reason === 'environment_mismatch')
952
+ return 'cybersource_environment_mismatch';
953
+ return 'capture_context_invalid';
954
+ }
628
955
  /** Default backoff for a 502 amount_unverifiable at create: two retries, then give up. */
629
956
  const UNVERIFIABLE_RETRY_DELAYS_MS = [500, 1500];
630
957
  const PREPARATION_READ_RETRY_DELAYS_MS = [250, 500];
@@ -638,6 +965,20 @@ function transientReadNetworkError(error) {
638
965
  const code = error.cause?.code ?? error.code;
639
966
  return typeof code === 'string' && TRANSIENT_READ_NETWORK_CODES.has(code);
640
967
  }
968
+ /**
969
+ * Processors whose payment without a card can spend a token the processor
970
+ * minted from the approved card. Nuvei answers a card it is handed (Fields
971
+ * getToken, or either leg of createPayment) with a ccTempToken, and can save
972
+ * it for a userPaymentOptionId; a clientPayment.do carrying only one of those
973
+ * pays with the approved card, and nothing binds it to the approved amount.
974
+ * So once the attachment has handed Nuvei the card, such a payment is refused
975
+ * (after the holds) instead of continuing. Before that, the token was issued
976
+ * elsewhere (Air France's server issues it, behind Amadeus) and it continues.
977
+ * Stripe is not listed: a card-free PaymentIntent confirm is bound by the API
978
+ * (checkStripeContinuation), and the confirmations its passWithoutCard names
979
+ * continue by design (payment-core definitions.js).
980
+ */
981
+ const CARD_FREE_AFTER_HANDOFF_REFUSED = new Set(['nuvei']);
641
982
  export class VaultClient {
642
983
  opts;
643
984
  baseUrl;
@@ -653,6 +994,12 @@ export class VaultClient {
653
994
  * in 'observe' until a sync reads that the API enabled it (armServedProfiles).
654
995
  */
655
996
  merchantProfiles = armServedProfiles();
997
+ /**
998
+ * The reviewed Verifone profiles this client arms: only the ones the API served on the
999
+ * last sync, which it serves only to a company Agentcard turned Verifone on for
1000
+ * (armServedVerifoneProfiles). None before a sync.
1001
+ */
1002
+ verifoneProfiles = [];
656
1003
  /** This client's own sandbox declarations (sandboxMerchants), armed at construction, one per profile. */
657
1004
  declaredProfiles;
658
1005
  preparations = new WeakSet();
@@ -756,9 +1103,14 @@ export class VaultClient {
756
1103
  mergedRegistry() {
757
1104
  return this.extraRecognizers.length ? [...this.extraRecognizers, ...this.registry] : this.registry;
758
1105
  }
759
- /** The recogniser that claims a card request (`POST` to a recognised endpoint), or null; what a paused request names as `recognizer`. */
1106
+ /**
1107
+ * The recogniser that claims a card request, or null; what a paused request names as
1108
+ * `recognizer`. A POST to a recognised endpoint, or for a recognizer whose card request is a
1109
+ * GET (Worldpay eProtect) that exact GET, query included.
1110
+ */
760
1111
  recognizerOf(url, method = 'POST') {
761
- return method.toUpperCase() === 'POST' ? findRecognizer(url, this.mergedRegistry(), { sandbox: this.sandbox }) : null;
1112
+ const rec = findRecognizer(url, this.mergedRegistry(), { sandbox: this.sandbox });
1113
+ return rec !== null && isRecognizerRequest(rec, url, method) ? rec : null;
762
1114
  }
763
1115
  /** Refresh recognizers from the API so new PSPs work without a redeploy. */
764
1116
  async syncRegistry() {
@@ -774,13 +1126,30 @@ export class VaultClient {
774
1126
  // ?capabilities= is the dark-launch half of the same negotiation: a
775
1127
  // processor gated behind a capability is served only when this build lists
776
1128
  // it (SUPPORTED_CAPABILITIES: fiserv_card_capture, gmo_shared_pk,
777
- // capture_context) and the caller's company is one Agentcard turned that
778
- // processor on for, so this build arms Fiserv's card capture, GMO's getToken
779
- // and CyberSource Flex's card fields only for such a company, and an older
780
- // build never receives a recognizer it cannot judge. Every other org's
781
- // registry never recognizes a Flex host, so its Microform requests go
782
- // through untouched; the create checks the org again (403
783
- // processor_unavailable).
1129
+ // capture_context, juspay_txns.v1, datatrans_secure_fields_tokenize,
1130
+ // authorize_net_dpm_form, pelecard_transaction, aci_split_card.v1,
1131
+ // creditguard_pps_process, primer_payment_instruments,
1132
+ // payrails_device_encrypt, converge_hpp_process, worldpay_eprotect_jsonp,
1133
+ // wspay_confirm, shiji_sts_pinned_key, amadeus_checkout_actions.v1,
1134
+ // worldline_mycheckout_form, verifone_card_capture) and
1135
+ // the caller's company is one Agentcard turned that processor on for, so this
1136
+ // build arms Fiserv's card capture, GMO's getToken, CyberSource Flex's card
1137
+ // fields, Juspay's card flow, Datatrans's Secure Fields card post, the
1138
+ // Authorize.Net DPM or SIM form, Pelecard's card submission, ACI's split card,
1139
+ // CreditGuard's hosted page submit, Primer's card tokenization, Payrails' card
1140
+ // save, Converge's payment page, Worldpay eProtect's registration, WSPay's
1141
+ // card confirm, Shiji STS's card leg, Amadeus Checkout's card add and
1142
+ // Worldline MyCheckout's card form only for
1143
+ // such a company, and an older build never receives a recognizer it cannot
1144
+ // judge. Every other org's registry never recognizes a Flex, Juspay, Datatrans,
1145
+ // Pelecard, ACI, CreditGuard, Primer, Payrails, Converge, eProtect, WSPay, Shiji STS, Amadeus or
1146
+ // MyCheckout host or the
1147
+ // transact.dll form, so its Microform, Juspay, Datatrans, Authorize.Net DPM or
1148
+ // SIM, Pelecard, ACI, CreditGuard, Primer, Payrails, Converge, eProtect, WSPay,
1149
+ // Shiji STS, Amadeus and MyCheckout requests go through untouched; the create checks the org again (403
1150
+ // processor_unavailable), and the SDK refuses ACI's card-number request,
1151
+ // CreditGuard's submit, an eProtect registration and an Amadeus card add locally
1152
+ // without a prepared checkout.
784
1153
  const capabilities = SUPPORTED_CAPABILITIES.length ? `&capabilities=${SUPPORTED_CAPABILITIES.join(',')}` : '';
785
1154
  // merchant_profiles=1 asks for the reviewed Adyen merchant profiles too, each
786
1155
  // as a `{ merchant_profile }` entry after the recognizers; an API that
@@ -797,12 +1166,31 @@ export class VaultClient {
797
1166
  if (!Array.isArray(raw))
798
1167
  return;
799
1168
  const isProfile = (e) => !!e && typeof e === 'object' && 'merchant_profile' in e;
800
- this.registry = raw.filter((e) => !isProfile(e)).map((e) => ({
1169
+ this.registry = raw.filter((e) => !isProfile(e) && !isVerifoneProfileEntry(e)).map((e) => ({
801
1170
  ...e,
802
1171
  match: new RegExp(e.match, 'i'),
803
1172
  passthroughHeaders: e.passthroughHeaders.map((h) => new RegExp(h, 'i')),
804
1173
  }));
805
1174
  this.merchantProfiles = armServedProfiles(raw.filter(isProfile));
1175
+ this.verifoneProfiles = armServedVerifoneProfiles(raw.filter(isVerifoneProfileEntry));
1176
+ }
1177
+ /**
1178
+ * The armed Verifone profile whose endpoint origin this URL is on, with its status on
1179
+ * this client; null for any other URL, and for every URL before a sync served one. The
1180
+ * adapters pause every request to that origin and judge it with
1181
+ * classifyVerifoneProfileRequest. A raw runtime that pauses one must never continue a
1182
+ * body carrying a verifone.js blob there unless authorize() paid it.
1183
+ */
1184
+ verifoneProfileOf(url) {
1185
+ return verifoneProfileFor(this.verifoneProfiles, url);
1186
+ }
1187
+ /** The armed Verifone profile with this id, or null. */
1188
+ verifoneProfile(id) {
1189
+ return this.verifoneProfiles.find((profile) => profile.id === id) ?? null;
1190
+ }
1191
+ /** Fetch.enable globs for every armed Verifone profile's endpoint origins; none before a sync served one. */
1192
+ verifoneProfileUrlPatterns() {
1193
+ return verifoneProfileUrlPatterns(this.verifoneProfiles);
806
1194
  }
807
1195
  /**
808
1196
  * The reviewed Adyen merchant profile whose card endpoint (or a sibling of it,
@@ -830,28 +1218,73 @@ export class VaultClient {
830
1218
  merchantProfileUrlPatterns() {
831
1219
  return merchantProfileUrlPatterns([...this.declaredProfiles, ...this.merchantProfiles]);
832
1220
  }
833
- /** True when this request is a card tokenization we can take over. */
1221
+ /**
1222
+ * The card-on-file policy the API served with the recognizer for this
1223
+ * request (Recognizer.cardOnFile), or null when no recognizer matches or
1224
+ * its entry names none. The Datatrans gate refuses a post that asks to keep
1225
+ * the card on file only while this is `refuse` or a value it does not know.
1226
+ */
1227
+ cardOnFilePolicy(url, method = 'POST') {
1228
+ if (method.toUpperCase() !== 'POST')
1229
+ return null;
1230
+ const rec = findRecognizer(url, this.registry);
1231
+ return rec && typeof rec.cardOnFile === 'string' ? rec.cardOnFile : null;
1232
+ }
1233
+ /**
1234
+ * True when this request is a card tokenization we can take over: a POST to
1235
+ * a recognized endpoint, or for a recognizer whose card request is a GET
1236
+ * (Worldpay eProtect) that exact GET, query included.
1237
+ */
834
1238
  isCardRequest(url, method = 'POST') {
835
1239
  return this.recognizerOf(url, method) !== null;
836
1240
  }
1241
+ /**
1242
+ * Whether the current registry holds this processor's recognizer. A
1243
+ * processor that ships dark is served only to a build that declares its
1244
+ * capability (syncRegistry does), so before a sync, or against an API that
1245
+ * withheld it, the adapters leave that processor's traffic exactly as they
1246
+ * found it. prepare() asks this for ACI, whose card-number request names a
1247
+ * checkout id the merchant mints only at Pay time, and for a processor whose
1248
+ * card request is a GET (Worldpay eProtect), since that request is recognized
1249
+ * by a query the page has not written yet.
1250
+ */
1251
+ recognizesProcessor(psp) {
1252
+ return this.registry.some((rec) => rec.psp === psp);
1253
+ }
837
1254
  /**
838
1255
  * What happens to a card request (by URL) whose body carries no card: null
839
1256
  * when it carries one, so it is a card request as usual; 'continue' when the
840
1257
  * recognizer marks the endpoint as one where such requests are routine and
841
1258
  * never ours; 'refuse' otherwise, such as a Stripe confirmation paying with
842
- * a method this checkout never approved. The adapters ask only after their
843
- * holds, so a request that would reuse an approved token is refused there
844
- * first. A body that could not be read is not judged here.
1259
+ * a method this checkout never approved. The adapters continue a 'continue'
1260
+ * in every state and judge a 'refuse' after their holds. A body that could
1261
+ * not be read is not judged here.
1262
+ *
1263
+ * `handedTo` lists the requests the attachment has already handed the card
1264
+ * to (CheckoutLifecycle.cardHandoffs). Once one of them went to the same
1265
+ * processor, a processor in CARD_FREE_AFTER_HANDOFF_REFUSED answers 'refuse'
1266
+ * where it would have answered 'continue'.
1267
+ *
1268
+ * A sandbox client's loopback fixture is judged on the processor's own
1269
+ * hosts, as it is recognised (urlsOnProcessorHosts), so a card-free request
1270
+ * the catalog lets through there continues on the fixture too.
845
1271
  */
846
- withoutCard(url, body) {
1272
+ withoutCard(url, body, handedTo = []) {
847
1273
  if (body == null)
848
1274
  return null;
849
- const rec = findRecognizer(url, this.mergedRegistry(), { sandbox: this.sandbox });
850
- return rec ? requestWithoutCard(rec, url, body) : null;
1275
+ const registry = this.mergedRegistry();
1276
+ const rec = findRecognizer(url, registry, { sandbox: this.sandbox });
1277
+ if (!rec)
1278
+ return null;
1279
+ const answers = urlsOnProcessorHosts(url, rec, { sandbox: this.sandbox }).map((judged) => requestWithoutCard(rec, judged, body));
1280
+ const answer = answers.includes(null) ? null : answers.includes('continue') ? 'continue' : 'refuse';
1281
+ if (answer !== 'continue' || !CARD_FREE_AFTER_HANDOFF_REFUSED.has(rec.psp))
1282
+ return answer;
1283
+ return handedTo.some((handed) => findRecognizer(handed, registry, { sandbox: this.sandbox })?.psp === rec.psp) ? 'refuse' : 'continue';
851
1284
  }
852
1285
  /**
853
- * How the card would reach the processor on this request (`token`, `cse`
854
- * or `hosted_form`; absent on the entry means `token`), or null when the
1286
+ * How the card would reach the processor on this request (`token`, `cse`,
1287
+ * `hosted_form` or `device_encrypt`; absent on the entry means `token`), or null when the
855
1288
  * registry does not recognize it. The adapters read it to decide whether an
856
1289
  * approval outlives the page's own request: a hosted form is a navigation
857
1290
  * and cannot.
@@ -915,6 +1348,9 @@ export class VaultClient {
915
1348
  const fail = (reason, id = null) => new CheckoutPreparationError(id, reason);
916
1349
  if (!validPreparationEnvironment(input.psp, input.environment))
917
1350
  throw fail('unsupported_processor');
1351
+ const pageRefusal = paymentPageRefusal(input);
1352
+ if (pageRefusal)
1353
+ throw fail(pageRefusal);
918
1354
  // A merchant-hosted preparation pays one reviewed profile's card request: the
919
1355
  // profile must be armed here, enabled, and paid in its own environment.
920
1356
  // A Fiserv checkout names the key Agentcard pinned for its merchant, in that key's own
@@ -922,7 +1358,21 @@ export class VaultClient {
922
1358
  // this preparation pays must carry an envelope under that key (matchesPreparation).
923
1359
  let profile = null;
924
1360
  let fiservPin = null;
925
- if (input.psp === 'fiserv') {
1361
+ // A Verifone checkout names the reviewed profile of the merchant, armed and enabled here, in
1362
+ // that profile's own environment; the API then names that profile's merchant on the approval.
1363
+ let verifone = null;
1364
+ if (input.psp === 'verifone') {
1365
+ if (input.merchantProfile === undefined)
1366
+ throw fail('merchant_profile_required');
1367
+ verifone = typeof input.merchantProfile === 'string' ? this.verifoneProfile(input.merchantProfile) : null;
1368
+ if (!verifone)
1369
+ throw fail('processor_interception_unavailable');
1370
+ if (verifone.status !== 'enabled')
1371
+ throw fail('unsupported_checkout');
1372
+ if (verifone.environment !== input.environment)
1373
+ throw fail('unsupported_processor');
1374
+ }
1375
+ else if (input.psp === 'fiserv') {
926
1376
  if (input.merchantProfile === undefined)
927
1377
  throw fail('merchant_profile_required');
928
1378
  fiservPin = fiservKeyPinById(input.merchantProfile);
@@ -944,11 +1394,44 @@ export class VaultClient {
944
1394
  throw fail('unsupported_processor');
945
1395
  }
946
1396
  const declaration = profile?.sandboxDeclaration;
947
- if (!validAmountInput(input.amount) || typeof input.currency !== 'string' || !/^[a-z]{3}$/i.test(input.currency))
948
- throw fail('amount_required');
1397
+ // Worldpay eProtect: the preparation names the Worldpay merchant its registration may name
1398
+ // (paypageId) and the paying card, whose expiry the agent types at the merchant because
1399
+ // Worldpay never receives one. The API refuses either missing, and a paypageId on any other
1400
+ // processor; refusing here asks nobody to approve a preparation that could never bind.
1401
+ const eprotect = input.psp === EPROTECT_PSP;
1402
+ if (eprotect ? typeof input.paypageId !== 'string' || !/^[A-Za-z0-9]{1,64}$/.test(input.paypageId) : input.paypageId !== undefined)
1403
+ throw fail('paypage_id_invalid');
1404
+ if (eprotect && !input.cardId)
1405
+ throw fail('card_required');
1406
+ // CyberSource Flex, tokenized before Pay: the loaded card field's capture context and fields,
1407
+ // verified here against the keys payment-core pins before anything is created (the API and the
1408
+ // cardholder's device verify them again).
1409
+ if (input.tokenizeBeforePay !== undefined && typeof input.tokenizeBeforePay !== 'boolean')
1410
+ throw fail('tokenize_before_pay_invalid');
1411
+ const flexContext = input.cybersourceFlex;
1412
+ if (input.tokenizeBeforePay === true && !flexContext)
1413
+ throw fail('microform_not_loaded');
1414
+ if (flexContext) {
1415
+ if (input.psp !== CYBERSOURCE_FLEX_PSP)
1416
+ throw fail('unsupported_processor');
1417
+ try {
1418
+ await admitCybersourceFlexPreparation({ capture_context: flexContext.captureContext,
1419
+ microform: { version: flexContext.microform.version, fields: [...flexContext.microform.fields] }, environment: input.environment }, { crypto: globalThis.crypto });
1420
+ }
1421
+ catch (error) {
1422
+ throw fail(cybersourceFlexPreparationRefusal(error));
1423
+ }
1424
+ }
1425
+ if (!validAmountInput(input.amount) || typeof input.currency !== 'string' || !/^[a-z]{3}$/i.test(input.currency)) {
1426
+ throw new CheckoutPreparationError(null, 'amount_required', undefined, input.amount != null && !validAmountInput(input.amount) ? amountRefusal(input.amount, input.currency) : undefined);
1427
+ }
949
1428
  const origin = new URL(input.merchantOrigin);
950
1429
  if (!(origin.protocol === 'https:' || (origin.protocol === 'http:' && origin.hostname === 'localhost')) || origin.origin !== input.merchantOrigin)
951
1430
  throw fail('merchant_origin_invalid');
1431
+ // CreditGuard's submit names the page that frames it (ppDomain), and only an HTTPS page under a
1432
+ // dotted hostname can be bound to it, so the API would refuse any other merchant page.
1433
+ if (input.psp === 'creditguard' && (origin.protocol !== 'https:' || creditguardPpDomainHost(btoa(origin.hostname)) !== origin.hostname))
1434
+ throw fail('merchant_origin_invalid');
952
1435
  if (!input.checkoutKey || !input.user || !input.merchant)
953
1436
  throw fail('checkout_context_required');
954
1437
  // The cardholder approves a payment to the merchant the key pin belongs to, so a Fiserv
@@ -956,6 +1439,15 @@ export class VaultClient {
956
1439
  // authorization it pays then name one merchant, never a label the caller chose.
957
1440
  if (fiservPin && input.merchant !== fiservPin.merchant)
958
1441
  throw fail('merchant_profile_mismatch');
1442
+ // A live Worldpay eProtect checkout whose paypageId this build pins for the merchant origin names
1443
+ // that pin's merchant, the one the API shows on the approval (eprotectCheckoutRefusal): the
1444
+ // approval, the handle this returns and every authorization it pays then name one merchant, and no
1445
+ // approval is retired over a label. The API refuses a paypageId with no pin (unsupported_checkout);
1446
+ // test mode needs no pin.
1447
+ const eprotectRefusal = eprotect ? eprotectCheckoutRefusal({ environment: input.environment, paypageId: input.paypageId,
1448
+ merchantOrigin: input.merchantOrigin, merchant: input.merchant }) : null;
1449
+ if (eprotectRefusal)
1450
+ throw fail(eprotectRefusal);
959
1451
  const timeoutMs = input.timeoutMs ?? 15 * 60_000;
960
1452
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
961
1453
  throw fail('timeout_invalid');
@@ -973,7 +1465,12 @@ export class VaultClient {
973
1465
  environment: input.environment, checkout_key: input.checkoutKey, merchant_origin: input.merchantOrigin,
974
1466
  ...(profile ? { merchant_profile: profile.id } : {}),
975
1467
  ...(fiservPin ? { merchant_profile: fiservPin.id } : {}),
1468
+ ...(verifone ? { merchant_profile: verifone.id } : {}),
976
1469
  ...(declaration ? { sandbox_declaration: { endpoint: declaration.endpoint, client_key: declaration.clientKey, environment: 'test' } } : {}),
1470
+ ...(input.psp === 'creditguard' ? { payment_page_url: input.paymentPageUrl } : {}),
1471
+ ...(eprotect ? { paypage_id: input.paypageId } : {}),
1472
+ ...(flexContext ? { capture_context: flexContext.captureContext,
1473
+ microform: { version: flexContext.microform.version, fields: [...flexContext.microform.fields] } } : {}),
977
1474
  }, AbortSignal.timeout(30_000), signal);
978
1475
  if (!created || typeof created.id !== 'string' || !/^cprep_[A-Za-z0-9_-]{1,128}$/.test(created.id))
979
1476
  throw fail('create_unconfirmed');
@@ -1004,12 +1501,18 @@ export class VaultClient {
1004
1501
  // A merchant-hosted preparation names the merchant its reviewed profile names, and
1005
1502
  // a declared test endpoint names its host (declareSandboxMerchant), never the caller's label.
1006
1503
  // A Fiserv preparation names the merchant its key pin belongs to (input.merchant, checked above), and that pin.
1007
- || state.user !== input.user || state.merchant !== (profile ? profile.merchant : input.merchant) || state.merchant_origin !== input.merchantOrigin
1008
- || state.merchant_profile !== (profile ? profile.id : fiservPin ? fiservPin.id : undefined)
1504
+ // A Verifone preparation names the merchant its reviewed profile names, as a merchant-hosted one.
1505
+ || state.user !== input.user || state.merchant !== (profile ? profile.merchant : verifone ? verifone.merchant : input.merchant)
1506
+ || state.merchant_origin !== input.merchantOrigin
1507
+ || state.merchant_profile !== (profile ? profile.id : fiservPin ? fiservPin.id : verifone ? verifone.id : undefined)
1009
1508
  || !servesDeclaration(state.sandbox_declaration, declaration)
1010
1509
  || !Number.isSafeInteger(state.amount) || (typeof input.amount === 'number' && state.amount !== input.amount) || state.currency !== input.currency.toLowerCase()
1011
1510
  || state.psp !== input.psp || state.mode !== preparationMode(input.psp) || state.environment !== input.environment
1012
- || state.checkout_key !== input.checkoutKey)
1511
+ || state.checkout_key !== input.checkoutKey
1512
+ // eProtect: the merchant and the card the cardholder approved are the ones asked for.
1513
+ || (eprotect && (state.paypage_id !== input.paypageId || state.card_id !== input.cardId))
1514
+ // The API says a preparation tokenizes before Pay exactly when this one carried a capture context.
1515
+ || (state.tokenize_before_pay === true) !== !!flexContext)
1013
1516
  throw fail('ready_unconfirmed', id);
1014
1517
  const prepared = Object.freeze({
1015
1518
  id: preparationId, status: 'ready', psp: input.psp, environment: input.environment, mode: preparationMode(input.psp), expiresAt: state.ready_expires_at,
@@ -1019,7 +1522,12 @@ export class VaultClient {
1019
1522
  paymentStatus: 'not_started', amountAuthority: 'agent',
1020
1523
  ...(profile ? { merchantProfile: profile.id } : {}),
1021
1524
  ...(fiservPin ? { merchantProfile: fiservPin.id } : {}),
1525
+ ...(verifone ? { merchantProfile: verifone.id } : {}),
1022
1526
  ...(declaration ? { sandboxDeclaration: declaration } : {}),
1527
+ ...(input.psp === 'creditguard' ? { paymentPageUrl: input.paymentPageUrl } : {}),
1528
+ ...(eprotect ? { paypageId: input.paypageId } : {}),
1529
+ ...(flexContext ? { cybersourceFlex: Object.freeze({ captureContext: flexContext.captureContext,
1530
+ microform: Object.freeze({ version: flexContext.microform.version, fields: Object.freeze([...flexContext.microform.fields]) }) }) } : {}),
1023
1531
  });
1024
1532
  this.preparations.add(prepared);
1025
1533
  ready = true;
@@ -1034,13 +1542,18 @@ export class VaultClient {
1034
1542
  catch (error) {
1035
1543
  if (error instanceof CheckoutPreparationError)
1036
1544
  throw error;
1545
+ const refusal = input.psp === 'creditguard' && id === null ? creditguardPreparationRefusal(error) : null;
1546
+ if (refusal)
1547
+ throw refusal;
1037
1548
  // A refusal the API answered before any preparation existed names its rule: one
1038
1549
  // PREPARATION_REFUSALS names for any checkout, and, for a Fiserv checkout only, one
1039
1550
  // FISERV_PREPARATION_REFUSALS names or a 502 cse_key_unavailable. Every other
1040
1551
  // processor's other refusals stay preparation_unconfirmed, as before.
1041
1552
  if (!id && !signal.aborted && error instanceof CheckoutApiError && error.code
1042
1553
  && ((error.status >= 400 && error.status < 500 && (PREPARATION_REFUSALS.includes(error.code)
1043
- || (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))))
1554
+ || (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))
1555
+ || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
1556
+ || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
1044
1557
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
1045
1558
  throw fail(error.code);
1046
1559
  throw fail(signal.aborted ? 'cancelled' : 'preparation_unconfirmed', id);
@@ -1058,6 +1571,30 @@ export class VaultClient {
1058
1571
  if (state?.id !== id || !['cancelled', 'expired'].includes(state.status))
1059
1572
  throw new CheckoutPreparationError(id, 'cancel_unconfirmed');
1060
1573
  }
1574
+ /**
1575
+ * Retire a ready preparation whose authorization create has not answered, and name the request
1576
+ * that already holds it. Resolves null once the API confirms no merchant request can bind the
1577
+ * preparation any more (cancelled or expired: the API takes a cancel and a create's bind under one
1578
+ * lock), or the ID of the authorization it is already bound to, which the caller retires like any
1579
+ * other (cancelAuthorization). Rejects when the API confirms neither. The Amadeus send window uses
1580
+ * it (amadeus.ts).
1581
+ */
1582
+ async retirePreparation(id) {
1583
+ if (!/^cprep_[A-Za-z0-9_-]{1,128}$/.test(id))
1584
+ throw new CheckoutPreparationError(null, 'id_invalid');
1585
+ try {
1586
+ await this.cancelPreparation(id);
1587
+ return null;
1588
+ }
1589
+ catch { /* bound, or unanswered: read it */ }
1590
+ const state = await this.get(`/v2/checkout/preparations/${id}`, AbortSignal.timeout(3_000));
1591
+ if (state?.id === id && (state.status === 'cancelled' || state.status === 'expired'))
1592
+ return null;
1593
+ if (state?.id === id && state.status === 'bound' && typeof state.authorization_id === 'string'
1594
+ && /^cauth_[A-Za-z0-9_-]{1,128}$/.test(state.authorization_id))
1595
+ return state.authorization_id;
1596
+ throw new CheckoutPreparationError(id, 'cancel_unconfirmed');
1597
+ }
1061
1598
  async observePreparation(id, guidance, reason) {
1062
1599
  try {
1063
1600
  await this.post(`/v2/checkout/preparations/${id}/observe`, {
@@ -1084,6 +1621,9 @@ export class VaultClient {
1084
1621
  * response to replay into the browser. Your process never sees a card.
1085
1622
  */
1086
1623
  async authorize(input) {
1624
+ // A reviewed merchant's own card endpoint is checked whole before anything else
1625
+ // (admitCardEndpoint): the profile, the approval, the amount and the session.
1626
+ const cardEndpoint = input.request.merchant_card_endpoint === undefined ? null : admitCardEndpoint(input);
1087
1627
  const mercadoContext = input.request.mercado_checkout === undefined ? undefined : parseMercadoCheckoutContext(input.request.mercado_checkout);
1088
1628
  if (mercadoContext && (!isMercadoTokenRequest(input.request.url, input.request.method)
1089
1629
  || input.executionMode === 'autopilot' || input.grantId))
@@ -1144,14 +1684,16 @@ export class VaultClient {
1144
1684
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
1145
1685
  // An armed merchant profile's endpoint (merchant-hosted.ts): the reviewed
1146
1686
  // merchant's own server, never a processor host, so no recognizer matches it.
1147
- const merchantProfile = nativeCheckout || ownedShop || mercadoContext ? null : this.merchantProfileOf(input.request.url);
1687
+ const merchantProfile = cardEndpoint || nativeCheckout || ownedShop || mercadoContext ? null : this.merchantProfileOf(input.request.url);
1688
+ // An armed Verifone profile's origin (verifone.ts): the reviewed merchant's own server.
1689
+ const verifoneProfile = cardEndpoint || nativeCheckout || ownedShop || mercadoContext || merchantProfile ? null : this.verifoneProfileOf(input.request.url);
1148
1690
  // A preparation pays the profile it was made for and nothing else: a
1149
1691
  // merchant-hosted one never a processor's request or another profile's (two
1150
1692
  // profiles could share an endpoint), and a processor-hosted one never a
1151
1693
  // merchant's own endpoint. Checked here, not left to the matcher above, so the
1152
1694
  // create below can only ever name the profile the cardholder prepared. A Fiserv
1153
1695
  // preparation names a key pin, not a reviewed profile (matchesPreparation binds it).
1154
- if (preparation && (((preparation.psp === 'fiserv' ? null : preparation.merchantProfile) ?? null) !== (merchantProfile?.id ?? null)
1696
+ if (preparation && (((preparation.psp === 'fiserv' || preparation.psp === 'verifone' ? null : preparation.merchantProfile) ?? null) !== (merchantProfile?.id ?? null)
1155
1697
  || (preparation.sandboxDeclaration?.endpoint ?? null) !== (merchantProfile?.sandboxDeclaration?.endpoint ?? null))) {
1156
1698
  throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1157
1699
  }
@@ -1161,46 +1703,64 @@ export class VaultClient {
1161
1703
  }
1162
1704
  if (merchantProfile)
1163
1705
  assertMerchantHostedRequest(input, merchantProfile);
1164
- const rec = merchantProfile ? MERCHANT_HOSTED_RECOGNIZER : nativeCheckout ? this.registry.find(entry => entry.psp === 'stripe' && (entry.mode ?? 'token') === 'token')
1706
+ // A Verifone preparation pays only its profile's reviewed leg (matchesPreparation, above);
1707
+ // here a request at an armed Verifone merchant is held to that profile's own rules.
1708
+ if (verifoneProfile)
1709
+ assertVerifoneRequest(input, verifoneProfile);
1710
+ // A card endpoint is no processor: its profile, never the registry, says where it goes.
1711
+ const rec = cardEndpoint ? null : merchantProfile ? MERCHANT_HOSTED_RECOGNIZER : verifoneProfile ? VERIFONE_RECOGNIZER : nativeCheckout ? this.registry.find(entry => entry.psp === 'stripe' && (entry.mode ?? 'token') === 'token')
1165
1712
  : findRecognizer(input.request.url, this.registry, { sandbox: this.sandbox });
1166
- if (!rec)
1713
+ if (!cardEndpoint && !rec)
1167
1714
  throw new Error(`not a known tokenization endpoint: ${redactUrl(input.request.url)}`);
1168
1715
  // A client-side-encrypted processor is only takeable when its entry says
1169
1716
  // the vault produces that ciphertext itself (mode cse); a bare
1170
1717
  // clientSideEncrypted entry (an older registry, a hand-built one) still
1171
1718
  // refuses here, exactly as before.
1172
- const mode = rec.mode ?? 'token';
1173
- if (preparation && (rec.psp !== preparation.psp || mode !== preparationMode(preparation.psp)))
1174
- throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1175
- // A preparation-required processor may not be taken over on an ordinary
1176
- // authorization: the cardholder must have approved a prepare() first. Refuse
1177
- // locally, before any create or prompt, when none is active, through the
1178
- // same strict rule the API applies (recognizerRequiresPreparation). Fiserv
1179
- // sets it; the API serves Fiserv only on a sync that declares its capability,
1180
- // and only to a company Agentcard turned Fiserv on for. This build carries
1181
- // Fiserv's own rules, so it also requires the preparation for a Fiserv
1182
- // request whatever the served entry says (requiresFiservPreparation): a
1183
- // registry that dropped the flag never opens an ordinary Fiserv authorization.
1184
- if ((recognizerRequiresPreparation(rec) || requiresFiservPreparation(rec.psp, input.request.url)) && !preparation)
1185
- throw new PreparationRequiredError(rec.psp);
1186
- // The merchant's server picks what it charges Adyen, so the agent's amount is
1187
- // what the API holds the body's own amount to: a merchant-hosted payment names one.
1188
- if (merchantProfile && input.amount == null) {
1189
- throw new Error(`A payment to ${merchantProfile.merchant} names its amount: pass amount and currency, the amount the cardholder approves.`);
1719
+ const mode = rec ? rec.mode ?? 'token' : CARD_ENDPOINT_MODE;
1720
+ // The transaction a CreditGuard submit pays; its approved answer is rebuilt for it alone.
1721
+ const creditguardTxId = rec?.psp === 'creditguard' ? creditguardProcessTxId(input.request.url) : null;
1722
+ if (rec) {
1723
+ if (preparation && (rec.psp !== preparation.psp || mode !== preparationMode(preparation.psp)))
1724
+ throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1725
+ // A preparation-required processor may not be taken over on an ordinary
1726
+ // authorization: the cardholder must have approved a prepare() first. Refuse
1727
+ // locally, before any create or prompt, when none is active, through the
1728
+ // same strict rule the API applies (recognizerRequiresPreparation). Fiserv,
1729
+ // aci, CreditGuard and amadeus_checkout set it (a CreditGuard submit is the
1730
+ // sale and names no amount); the API serves each only on a sync that declares
1731
+ // its capability,
1732
+ // and only to a company Agentcard turned it on for. This build carries
1733
+ // Fiserv's own rules, so it also requires the preparation for a Fiserv
1734
+ // request whatever the served entry says (requiresFiservPreparation): a
1735
+ // registry that dropped the flag never opens an ordinary Fiserv authorization.
1736
+ if ((recognizerRequiresPreparation(rec) || requiresFiservPreparation(rec.psp, input.request.url)) && !preparation)
1737
+ throw new PreparationRequiredError(rec.psp);
1738
+ // The merchant's server picks what it charges Adyen, so the agent's amount is
1739
+ // what the API holds the body's own amount to: a merchant-hosted payment names one.
1740
+ if (merchantProfile && input.amount == null) {
1741
+ throw new Error(`A payment to ${merchantProfile.merchant} names its amount: pass amount and currency, the amount the cardholder approves.`);
1742
+ }
1743
+ if (verifoneProfile && input.amount == null) {
1744
+ throw new Error(`A payment to ${verifoneProfile.merchant} names its amount: pass amount and currency, the amount the cardholder approves.`);
1745
+ }
1746
+ // device_encrypt (Payrails) also encrypts in-page; the device rebuilds that
1747
+ // ciphertext under the session key the request carries.
1748
+ if (rec.clientSideEncrypted && mode !== 'cse' && mode !== 'device_encrypt')
1749
+ throw new CardEncryptedError(rec.psp);
1750
+ if (rec.psp === 'creditguard' && creditguardTxId === null)
1751
+ throw new Error(`not a CreditGuard card submit: ${redactUrl(input.request.url)}`);
1752
+ if (mode === CARD_ENDPOINT_MODE || !SUPPORTED_MODES.includes(mode))
1753
+ throw new UnsupportedModeError(mode);
1190
1754
  }
1191
- if (rec.clientSideEncrypted && mode !== 'cse')
1192
- throw new CardEncryptedError(rec.psp);
1193
- if (!SUPPORTED_MODES.includes(mode))
1194
- throw new UnsupportedModeError(mode);
1195
1755
  // A dark-launch recognizer names the capability its create needs. A build that does not
1196
1756
  // declare it cannot send what that create carries, so it refuses here, before any create;
1197
1757
  // a registry the API served never holds such an entry.
1198
- if (rec.capability !== undefined && !SUPPORTED_CAPABILITIES.includes(rec.capability))
1758
+ if (rec && rec.capability !== undefined && !SUPPORTED_CAPABILITIES.includes(rec.capability))
1199
1759
  throw new UnsupportedCapabilityError(rec.capability);
1200
1760
  // CyberSource Flex: the capture context and the Microform fields ride on the request (the
1201
1761
  // adapters read both from the card field iframe) and the API verifies them before anything
1202
1762
  // exists. They belong to a Flex request and nothing else, as the API holds them.
1203
- const flex = rec.psp === CYBERSOURCE_FLEX_PSP;
1763
+ const flex = rec?.psp === CYBERSOURCE_FLEX_PSP;
1204
1764
  if (!flex && (input.request.capture_context !== undefined || input.request.microform !== undefined)) {
1205
1765
  throw new Error('request.capture_context and request.microform belong to a CyberSource Flex card request.');
1206
1766
  }
@@ -1208,15 +1768,39 @@ export class VaultClient {
1208
1768
  throw new Error('A CyberSource Flex card request carries the capture context its card field iframe was loaded with (request.capture_context) '
1209
1769
  + 'and the Microform fields that loaded (request.microform); attachToCdp and attachToPlaywright read both.');
1210
1770
  }
1211
- // Say no rather than coerce. isCardRequest only ever pauses a POST, and the
1212
- // service stores the replay template with method 'POST' hardcoded — so a
1213
- // caller handing in a PUT or GET would get it silently rewritten and the
1214
- // cardholder's device would replay something the caller never asked for.
1215
- // A direct caller deserves to be told, not surprised.
1771
+ // The session key is not in a device_encrypt request: without the one the
1772
+ // page's init payload named, the device has nothing to encrypt under.
1773
+ // Refused before any create or prompt, the way the API refuses
1774
+ // payrails_session_key_required.
1775
+ const sidecar = input.request.payrails;
1776
+ if (mode === 'device_encrypt' && (!sidecar || typeof sidecar.encryption_public_key !== 'string' || !sidecar.encryption_public_key)) {
1777
+ throw new Error('A Payrails card save needs request.payrails.encryption_public_key, the session key from the checkout\'s Payrails init payload (the adapters capture it). Nothing was created.');
1778
+ }
1779
+ // A preparation that tokenized before Pay pays only a field request loaded under the capture
1780
+ // context and fields its cardholder approved: the device holds CyberSource's token for that context.
1781
+ if (preparation?.cybersourceFlex && !(flex && sameCybersourceFlexPreparation({ capture_context: preparation.cybersourceFlex.captureContext, microform: preparation.cybersourceFlex.microform }, { capture_context: input.request.capture_context, microform: input.request.microform }))) {
1782
+ throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1783
+ }
1784
+ // Say no rather than coerce. Each recognizer's card request has exactly one
1785
+ // method (recognizerRequestMethod): a POST body, or for Worldpay eProtect the
1786
+ // GET whose query carries the card. The service stores the replay template
1787
+ // with that method, so a caller handing in another one would get it
1788
+ // silently rewritten and the cardholder's device would replay something the
1789
+ // caller never asked for. A direct caller deserves to be told, not surprised.
1216
1790
  const method = (input.request.method ?? 'POST').toUpperCase();
1217
- if (method !== 'POST') {
1218
- throw new Error(`checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
1219
- 'A non-POST tokenizer needs recognizer support before it can be intercepted.');
1791
+ // A card endpoint has no recognizer: its card request is a POST, as every recognizer's is but eProtect's.
1792
+ const cardMethod = rec ? recognizerRequestMethod(rec) : 'POST';
1793
+ if (method !== cardMethod) {
1794
+ throw new Error(cardMethod === 'GET'
1795
+ ? `a ${rec?.psp} card registration is a GET; got ${method} for ${redactUrl(input.request.url)}.`
1796
+ : `checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
1797
+ 'A non-POST tokenizer needs recognizer support before it can be intercepted.');
1798
+ }
1799
+ // A GET card request is its URL alone. This build completes one: Worldpay
1800
+ // eProtect's registration exactly as payment-core admits it, with no body.
1801
+ const eprotect = rec?.psp === EPROTECT_PSP;
1802
+ if (cardMethod === 'GET' && !(eprotect && isEprotectRegistration(input.request))) {
1803
+ throw new Error(`this ${rec?.psp} GET is not a card registration this version of @agent-cards/checkout can complete: ${redactUrl(input.request.url)}.`);
1220
1804
  }
1221
1805
  // The amount is a hint: a pair or nothing. Refuse half a pair here, before
1222
1806
  // a network call: the API would too, and a retry loop cannot fix a
@@ -1226,15 +1810,27 @@ export class VaultClient {
1226
1810
  if (hasAmount !== hasCurrency) {
1227
1811
  throw new Error('amount and currency go together: pass both or neither.');
1228
1812
  }
1229
- if (hasAmount && !validAmountInput(input.amount)) {
1230
- throw new Error('amount is an integer in the currency\'s smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06").');
1231
- }
1813
+ if (hasAmount && !validAmountInput(input.amount))
1814
+ throw new Error(amountRefusal(input.amount, input.currency));
1232
1815
  // A processor whose request names no amount (AGENT_AMOUNT_PROCESSORS) is
1233
- // paid only for the pair passed here: it is all the approval can show.
1816
+ // paid only for the pair passed here: it is all the approval can show and,
1817
+ // for WSPay, the only figure the processor's answer is checked against.
1234
1818
  // Refused before any create or prompt; the API refuses the same create as
1235
1819
  // amount_required.
1236
- if (!hasAmount && AGENT_AMOUNT_PROCESSORS.includes(rec.psp))
1820
+ if (!hasAmount && rec && AGENT_AMOUNT_PROCESSORS.includes(rec.psp))
1237
1821
  throw new AmountRequiredError(rec.psp);
1822
+ // Nor is the agent's pair tied to the page by anything but the total the
1823
+ // page shows: without it the cardholder could approve one figure while the
1824
+ // page charges another. The API refuses the same create as page_amount_required.
1825
+ if (!input.pageAmount && rec && PAGE_AMOUNT_PROCESSORS.includes(rec.psp))
1826
+ throw new PageAmountRequiredError(rec.psp);
1827
+ // A Payrails save names no amount (the merchant charges the saved card
1828
+ // later, under another session), so the price the cardholder approves is
1829
+ // the one sent here: the agent's pair or else the page total. Refused
1830
+ // before any create or prompt, the way the API refuses amount_required.
1831
+ if (mode === 'device_encrypt' && !hasAmount && !input.pageAmount) {
1832
+ throw new Error('A Payrails card save names no amount, so pass amount and currency (or let the adapter read the page total): that is the amount the cardholder approves. Nothing was created and nothing was charged.');
1833
+ }
1238
1834
  const timeoutMs = input.timeoutMs ?? 15 * 60_000;
1239
1835
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
1240
1836
  throw new Error('timeoutMs must be a positive integer no larger than 2147483647.');
@@ -1246,7 +1842,21 @@ export class VaultClient {
1246
1842
  const priced = merchantProfile && merchantProfilePricedBySource(merchantProfile.id)
1247
1843
  ? await this.readMerchantTotal(input, merchantProfile, preparation, operationSignal) : null;
1248
1844
  let created;
1249
- const payload = {
1845
+ const payload = cardEndpoint ? {
1846
+ // A card endpoint's request travels only in merchant_card_endpoint: `request`
1847
+ // names its URL and nothing else, and the agent's amount is the only authority.
1848
+ user: input.user,
1849
+ merchant: input.merchant,
1850
+ amount: input.amount,
1851
+ currency: input.currency,
1852
+ psp: CARD_ENDPOINT_PSP,
1853
+ mode,
1854
+ ...(input.cardId ? { cardId: input.cardId } : {}),
1855
+ execution_mode: 'user_approval',
1856
+ checkout_origin: cardEndpoint.session.page_origin,
1857
+ request: { url: cardEndpoint.session.url, method: 'POST', headers: {}, body: '' },
1858
+ merchant_card_endpoint: { profile: cardEndpoint.profile, session: cardEndpoint.session, ...(cardEndpoint.facts ? { facts: cardEndpoint.facts } : {}) },
1859
+ } : {
1250
1860
  user: input.user,
1251
1861
  merchant: input.merchant,
1252
1862
  // snake_case on the wire; camelCase is this SDK's convention.
@@ -1269,11 +1879,16 @@ export class VaultClient {
1269
1879
  endpoint: merchantProfile.sandboxDeclaration.endpoint, client_key: merchantProfile.sandboxDeclaration.clientKey, environment: 'test'
1270
1880
  } } : {}),
1271
1881
  ...(priced ? { merchant_total: priced.wire } : {}) } } : {}),
1882
+ // Only the profile's id: the API takes the leg, the key and its environment from the
1883
+ // profile Agentcard reviewed, never from the caller.
1884
+ ...(verifoneProfile ? { verifone: { profile: verifoneProfile.id } } : {}),
1272
1885
  // A prepared checkout already carries the page origin as merchant_origin.
1273
1886
  ...(preparation
1274
1887
  ? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin, form_guidance: 'filled' }
1275
1888
  : input.pageOrigin ? { checkout_origin: input.pageOrigin } : {}),
1276
- request: {
1889
+ // A GET card request (Worldpay eProtect) is its URL alone: no body, and no
1890
+ // header the page sent, which the API would neither store nor replay.
1891
+ request: cardMethod === 'GET' ? { url: input.request.url, method: 'GET', headers: {}, body: '' } : {
1277
1892
  url: input.request.url,
1278
1893
  method: input.request.method,
1279
1894
  headers: { ...pickHeaders(input.request.headers, rec.passthroughHeaders),
@@ -1285,6 +1900,8 @@ export class VaultClient {
1285
1900
  microform: { version: input.request.microform.version, fields: [...input.request.microform.fields],
1286
1901
  ...(input.request.microform.trigger ? { trigger: input.request.microform.trigger } : {}) },
1287
1902
  } : {}),
1903
+ // Only the key: the token already rides in the passthrough authorization header.
1904
+ ...(mode === 'device_encrypt' && sidecar ? { payrails: { encryption_public_key: sidecar.encryption_public_key } } : {}),
1288
1905
  },
1289
1906
  };
1290
1907
  try {
@@ -1324,7 +1941,18 @@ export class VaultClient {
1324
1941
  // 10 seconds has at most 20 reads instead of 5 (15 additional reads).
1325
1942
  // Then restore the caller's interval. Human approval waiting, cancellation,
1326
1943
  // merchant deadlines, and the single processor request remain unchanged.
1327
- const preparedMercadoPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000 : 0;
1944
+ // A prepared Worldpay eProtect registration too, for 15 seconds: its
1945
+ // merchants give up on the paused script 10 to 15 seconds after Pay, so a
1946
+ // registration is read every 500 ms instead of waiting out 2 seconds.
1947
+ const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
1948
+ : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000 : 0;
1949
+ // A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
1950
+ // token and approves as soon as this bind reaches it, while the page may give createToken only
1951
+ // seconds (Alaska Airlines' flight checkout gives it three). Poll every 100 ms for 10 seconds,
1952
+ // then at the caller's interval; at the default 2 seconds the first read alone would miss it.
1953
+ // The API's bind names 100 ms as such a row's poll interval, so slow_down admits these reads
1954
+ // (as an eProtect row names the 500 ms read above).
1955
+ const preparedFlexPollUntil = preparation?.cybersourceFlex ? Date.now() + 10_000 : 0;
1328
1956
  let execution = {};
1329
1957
  let approvalDelivered = false;
1330
1958
  const deliverApproval = (state) => {
@@ -1340,6 +1968,8 @@ export class VaultClient {
1340
1968
  catch { /* observer only */ }
1341
1969
  };
1342
1970
  let failed = false;
1971
+ // What this attempt throws, once it fails: a confirmed retirement below is written onto it.
1972
+ let failure = null;
1343
1973
  let actionRequired = false;
1344
1974
  const checkAutopilotAction = (state) => {
1345
1975
  if (state.autopilot_status !== 'action_required')
@@ -1370,7 +2000,8 @@ export class VaultClient {
1370
2000
  while (Date.now() < deadline) {
1371
2001
  if (stopSignal?.aborted)
1372
2002
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
1373
- const pollInterval = Date.now() < preparedMercadoPollUntil ? Math.min(this.pollIntervalMs, 500) : this.pollIntervalMs;
2003
+ const pollInterval = Date.now() < preparedFastPollUntil ? Math.min(this.pollIntervalMs, 500)
2004
+ : Date.now() < preparedFlexPollUntil ? Math.min(this.pollIntervalMs, 100) : this.pollIntervalMs;
1374
2005
  await interruptibleSleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())), stopSignal);
1375
2006
  if (stopSignal?.aborted)
1376
2007
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
@@ -1405,7 +2036,7 @@ export class VaultClient {
1405
2036
  // database forces these rows to cse. A finished read in any other mode
1406
2037
  // (token, a missing mode, a hosted form) is not one to act on, and never
1407
2038
  // reaches the code that would answer the merchant's paused request with it.
1408
- if (merchantProfile && (s.status === 'approved' || s.status === 'submitted_on_device') && s.mode !== 'cse') {
2039
+ if ((merchantProfile || verifoneProfile) && (s.status === 'approved' || s.status === 'submitted_on_device') && s.mode !== 'cse') {
1409
2040
  throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_hosted_mode_invalid');
1410
2041
  }
1411
2042
  if (s.status === 'submitted_on_device') {
@@ -1426,6 +2057,13 @@ export class VaultClient {
1426
2057
  }
1427
2058
  if (s.status === 'approved') {
1428
2059
  const approvedMode = typeof s.mode === 'string' ? s.mode : 'token';
2060
+ if (cardEndpoint || approvedMode === CARD_ENDPOINT_MODE) {
2061
+ // Only a card endpoint request is answered from a merchant's own answer, and
2062
+ // it is answered from nothing else.
2063
+ if (!cardEndpoint || approvedMode !== CARD_ENDPOINT_MODE)
2064
+ throw new PaymentOutcomeUnknownError(authorizationId, 'approved_mode_unsupported');
2065
+ return cardEndpointReplay(s, authorizationId, cardEndpoint.profile, { ...amountAuthority, ...execution });
2066
+ }
1429
2067
  if (approvedMode === 'hosted_form') {
1430
2068
  // Never: the API finishes a hosted form as submitted_on_device, and
1431
2069
  // its database refuses `approved` on that mode. An answer that says
@@ -1437,19 +2075,30 @@ export class VaultClient {
1437
2075
  throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
1438
2076
  // A merchant-hosted create reads back only a merchant-hosted approval,
1439
2077
  // and a processor-hosted one never does.
2078
+ // A Verifone create reads back only a Verifone approval, and no other create ever does.
2079
+ if (verifoneProfile || (s.substitutions && typeof s.substitutions === 'object' && s.substitutions.kind === 'verifone')) {
2080
+ return { ...verifoneApproval(s, authorizationId, verifoneProfile), ...amountAuthority, ...execution };
2081
+ }
1440
2082
  if (merchantProfile || (s.substitutions && typeof s.substitutions === 'object' && 'kind' in s.substitutions)) {
1441
2083
  return { ...merchantHostedApproval(s, authorizationId, merchantProfile), ...(priced ? { merchantTotal: priced.approved } : {}),
1442
2084
  ...amountAuthority, ...execution };
1443
2085
  }
1444
- // The API serves a Fiserv envelope only briefly after the approval (it carries
1445
- // no timestamp, so whoever holds it could resubmit it). A read after that
1446
- // window says so instead of serving it; the capture was never continued here.
2086
+ // The API serves a Fiserv envelope, or a Shiji STS set, only briefly after the
2087
+ // approval (neither is bound to a session, so whoever holds it could resubmit
2088
+ // it). A read after that window says so instead of serving it; the request was
2089
+ // never continued here, and nothing here can pay with it.
1447
2090
  if (s.substitutions_expired === true)
1448
2091
  throw new PaymentOutcomeUnknownError(authorizationId, 'cse_substitutions_expired');
1449
2092
  const sub = s.substitutions;
1450
- const fieldsOk = sub && typeof sub === 'object' && sub.encoding === 'json' && typeof sub.at === 'string' && sub.at
1451
- && sub.fields && typeof sub.fields === 'object' && !Array.isArray(sub.fields)
1452
- && Object.values(sub.fields).every((v) => typeof v === 'string' && v.length > 0);
2093
+ // Shiji STS carries its fields at the body root (`at` is ''), and
2094
+ // exactly pan, expires and cvv, each a 128-byte ciphertext, beside
2095
+ // the page's reviewed getCvvId keys (no keys, no continuation);
2096
+ // every other processor names a holder path, as before.
2097
+ const shiji = rec?.psp === SHIJI_STS_PSP;
2098
+ const fieldsOk = shiji ? isShijiStsServedSubstitution(sub) && isShijiStsServedCompanion(s.shiji_sts_companion)
2099
+ : sub && typeof sub === 'object' && sub.encoding === 'json' && typeof sub.at === 'string' && sub.at
2100
+ && sub.fields && typeof sub.fields === 'object' && !Array.isArray(sub.fields)
2101
+ && Object.values(sub.fields).every((v) => typeof v === 'string' && v.length > 0);
1453
2102
  if (!fieldsOk)
1454
2103
  throw new PaymentOutcomeUnknownError(authorizationId, 'cse_substitutions_malformed');
1455
2104
  // A Fiserv approval carries exactly its envelope's four members at
@@ -1473,12 +2122,39 @@ export class VaultClient {
1473
2122
  fields: { ...sub.fields },
1474
2123
  ...(removeRaw ? { remove: [...removeRaw] } : {}),
1475
2124
  },
2125
+ ...(shiji ? { shijiStsCompanion: Object.freeze({ keySequences: Object.freeze([...s.shiji_sts_companion.key_sequences]) }) } : {}),
1476
2126
  ...amountAuthority,
1477
2127
  ...execution,
1478
2128
  };
1479
2129
  }
1480
- if (approvedMode !== 'token')
2130
+ if (approvedMode === 'device_encrypt') {
2131
+ // The device sent the card to Payrails itself; this is Payrails' answer to it. It
2132
+ // enters the page only as a 201 for a card instrument, rebuilt from payment-core's
2133
+ // allow-list under a JSON content type (payrails.ts payrailsSaveAnswer). Anything else
2134
+ // is never handed over: the card may be saved, so the outcome is unknown, not declined.
2135
+ // Autopilot never runs this mode, so an autopilot answer is not one to act on either.
2136
+ if (execution.executionMode === 'autopilot')
2137
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
2138
+ if (mode !== 'device_encrypt')
2139
+ throw new PaymentOutcomeUnknownError(authorizationId, 'approved_mode_unsupported');
2140
+ const saved = s.response;
2141
+ const answer = saved && typeof saved === 'object' && Number.isInteger(saved.status) && typeof saved.body === 'string'
2142
+ ? payrailsSaveAnswer(saved.status, saved.body) : null;
2143
+ if (!answer)
2144
+ throw new PaymentOutcomeUnknownError(authorizationId, 'payrails_answer_unconfirmed');
2145
+ if (s.processor_context !== undefined)
2146
+ throw new PaymentOutcomeUnknownError(authorizationId, 'unexpected_processor_context');
2147
+ return { mode: 'device_encrypt', authorizationId, ...answer, ...amountAuthority, ...execution };
2148
+ }
2149
+ // A save created as device_encrypt is answered only as one: Payrails' raw answer never
2150
+ // passes through the token path below.
2151
+ if (approvedMode !== 'token' || mode === 'device_encrypt')
1481
2152
  throw new PaymentOutcomeUnknownError(authorizationId, 'approved_mode_unsupported');
2153
+ // The API serves an approved answer whose landing url carries a processor's card token
2154
+ // (CreditGuard's) only for a window after the approval, this SDK's default timeoutMs; one it
2155
+ // no longer serves is an unknown outcome, never a page to continue.
2156
+ if (s.response_expired === true)
2157
+ throw new PaymentOutcomeUnknownError(authorizationId, 'response_expired');
1482
2158
  const response = s.response;
1483
2159
  if (!response || !Number.isInteger(response.status) || response.status < 100 || response.status > 599
1484
2160
  || typeof response.body !== 'string' || !response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
@@ -1490,6 +2166,41 @@ export class VaultClient {
1490
2166
  if (flex && (response.status < 200 || response.status > 299 || !isCybersourceFlexTokenShape(response.body))) {
1491
2167
  throw new PaymentOutcomeUnknownError(authorizationId, 'cybersource_flex_token_unconfirmed');
1492
2168
  }
2169
+ if (rec?.psp === JUSPAY_PSP) {
2170
+ // The page receives only the allowlisted /txns answer for the paused
2171
+ // order, re-serialized here from the stored one (juspayPageAnswer),
2172
+ // with a JSON content type as its one header. Anything else is not an
2173
+ // answer to hand the page: the device may already have sent the payment.
2174
+ const answer = juspayPageAnswer(response.status, response.body, input.request);
2175
+ if (answer === null)
2176
+ throw new PaymentOutcomeUnknownError(authorizationId, 'juspay_response_unconfirmed');
2177
+ response.body = answer;
2178
+ response.headers = { 'content-type': 'application/json' };
2179
+ }
2180
+ if (rec?.psp === ACI_PSP) {
2181
+ // ACI: the paused card-number request, and the page's CVC and expiry requests the
2182
+ // adapters answer with the same bytes, get exactly ACI's rebuilt success answer under
2183
+ // JSON. Anything else is not an answer to hand the page, and the device may already have
2184
+ // put the real card on the checkout.
2185
+ const answer = aciCardLegAnswer(response);
2186
+ if (!answer)
2187
+ throw new PaymentOutcomeUnknownError(authorizationId, 'aci_answer_unconfirmed');
2188
+ response.status = answer.status;
2189
+ response.headers = answer.headers;
2190
+ response.body = answer.body;
2191
+ }
2192
+ if (eprotect) {
2193
+ // Worldpay eProtect: the paused <script> is answered the way Worldpay answers it,
2194
+ // `<callback>(<registration>)` under its script type, with the callback the paused URL
2195
+ // named and only a registration payment-core's judge accepts for that URL. Anything else
2196
+ // is not an answer to hand the page, and the device may already have registered the card.
2197
+ const answer = eprotectScriptAnswer(input.request.url, response);
2198
+ if (!answer)
2199
+ throw new PaymentOutcomeUnknownError(authorizationId, 'eprotect_registration_unconfirmed');
2200
+ response.status = answer.status;
2201
+ response.headers = answer.headers;
2202
+ response.body = answer.body;
2203
+ }
1493
2204
  if (shopOrder) {
1494
2205
  let receipt;
1495
2206
  try {
@@ -1502,6 +2213,26 @@ export class VaultClient {
1502
2213
  response.body = JSON.stringify(receipt);
1503
2214
  response.headers = { 'content-type': 'application/json' };
1504
2215
  }
2216
+ if (creditguardTxId !== null) {
2217
+ // A CreditGuard payment is always prepared and approved by the cardholder, and no
2218
+ // autopilot run sends one (payment-core and the autopilot authority refuse it), so an
2219
+ // approval the API reports as unattended is not handed to the page.
2220
+ if (execution.executionMode === 'autopilot')
2221
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
2222
+ // CreditGuard's answer to the device's submit reaches the page only as payment-core
2223
+ // rebuilds it from its allow-list, for the paused transaction, as JSON: an answer that
2224
+ // is not an approval of this txId is an unknown outcome, never handed to the page.
2225
+ let rebuilt;
2226
+ try {
2227
+ if (response.status === 200)
2228
+ rebuilt = sanitizeCreditguardResponse(response.body, { txId: creditguardTxId });
2229
+ }
2230
+ catch { /* Never return an unvalidated processor body to the page. */ }
2231
+ if (rebuilt === undefined)
2232
+ throw new PaymentOutcomeUnknownError(authorizationId, 'creditguard_response_unconfirmed');
2233
+ response.body = rebuilt;
2234
+ response.headers = { 'content-type': 'application/json' };
2235
+ }
1505
2236
  if (checkoutContext) {
1506
2237
  let receipt;
1507
2238
  try {
@@ -1514,7 +2245,7 @@ export class VaultClient {
1514
2245
  response.body = JSON.stringify(receipt);
1515
2246
  response.headers = { 'content-type': 'application/json' };
1516
2247
  }
1517
- if (rec.psp === GMO_PSP) {
2248
+ if (rec?.psp === GMO_PSP) {
1518
2249
  // GMO's answer enters the page only as one clean single-use token under a JSON
1519
2250
  // content type (gmo.ts gmoTokenAnswer). Anything else is never handed over; the
1520
2251
  // card may have left the device, so the outcome is unknown rather than declined.
@@ -1525,6 +2256,18 @@ export class VaultClient {
1525
2256
  response.headers = answer.headers;
1526
2257
  response.body = answer.body;
1527
2258
  }
2259
+ if (rec?.psp === PRIMER_PSP) {
2260
+ // Primer's answer enters the page only as one single-use card token rebuilt from
2261
+ // payment-core's allow-list, under a JSON content type (primer.ts primerTokenAnswer).
2262
+ // Anything else, a multi-use token or vaultData included, is never handed over; the card
2263
+ // may have left the device, so the outcome is unknown rather than declined.
2264
+ const answer = primerTokenAnswer(response.status, response.body);
2265
+ if (!answer)
2266
+ throw new PaymentOutcomeUnknownError(authorizationId, 'primer_token_unconfirmed');
2267
+ response.status = answer.status;
2268
+ response.headers = answer.headers;
2269
+ response.body = answer.body;
2270
+ }
1528
2271
  let processorContext;
1529
2272
  if (mercadoContext) {
1530
2273
  try {
@@ -1588,19 +2331,25 @@ export class VaultClient {
1588
2331
  }
1589
2332
  catch (error) {
1590
2333
  failed = true;
1591
- if (input.merchantSignal?.aborted)
1592
- throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
1593
- throw error;
2334
+ failure = input.merchantSignal?.aborted ? new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted') : error;
2335
+ throw failure;
1594
2336
  }
1595
2337
  finally {
1596
2338
  // An attested action-required result retains its claim and reservation.
1597
2339
  // Reporting that state must not request cancellation of the same attempt.
1598
- if (!actionRequired && (input.merchantSignal?.aborted || ((preparation || nativeCheckout) && failed))) {
2340
+ // A card endpoint request that failed here has no page request left to answer, so
2341
+ // it is retired too: the device must not send the card for an answer nobody reads.
2342
+ if (!actionRequired && (input.merchantSignal?.aborted || ((preparation || nativeCheckout || cardEndpoint) && failed))) {
1599
2343
  // Drain a create acknowledgement even after the merchant aborts so its
1600
2344
  // known ID can be retired. An unacknowledged create remains unknown.
1601
2345
  // A started/finalized replay or failed cleanup never becomes a claimed
1602
2346
  // cancellation, and this best-effort cleanup never retries a payment.
1603
- await this.cancelAuthorization(authorizationId).catch(() => { });
2347
+ const retired = await this.cancelAuthorization(authorizationId).catch(() => null);
2348
+ // A retirement whose acknowledgement says no processor request started: the thrown error
2349
+ // says so. The API echoes the reason the row carries, so a row retired earlier as
2350
+ // merchant_never_retried can answer that a processor request did start; then nothing changes.
2351
+ if (retired?.processor_request_started === false && failure instanceof PaymentOutcomeUnknownError)
2352
+ failure.processorRequestStarted = false;
1604
2353
  }
1605
2354
  }
1606
2355
  }
@@ -1688,6 +2437,65 @@ export class VaultClient {
1688
2437
  }
1689
2438
  return { paymentIntentId: pi.id, amount: pi.amount, currency: pi.currency };
1690
2439
  }
2440
+ /**
2441
+ * Ask whether the page may save the card with the Stripe payment method an
2442
+ * approval produced: a SetupIntent confirm that carries no card and pays with
2443
+ * exactly the approved payment method (the page's own, or written by the
2444
+ * adapters in place of a card the confirm carries again).
2445
+ * The API allows a card save only for a company Agentcard turned it on for,
2446
+ * for an approval given on the Vault page, not by voice, with no amount, and
2447
+ * a SetupIntent Stripe reports still confirmable on the same Stripe account;
2448
+ * any other answer rejects with a CheckoutApiError whose code says why (for
2449
+ * example `continuation_invalid` where it is off, `amount_mismatch`). Nothing
2450
+ * is charged: the adapters continue the page's request once this resolves.
2451
+ */
2452
+ async checkStripeSetupContinuation(authorizationId, request) {
2453
+ if (!/^cauth_[A-Za-z0-9_-]{1,128}$/.test(authorizationId))
2454
+ throw new Error('Invalid authorization ID.');
2455
+ const rec = findRecognizer(request.url, this.registry);
2456
+ if (rec?.psp !== 'stripe')
2457
+ throw new Error('Only a Stripe request can continue an approved Stripe payment method.');
2458
+ let confirmed = null;
2459
+ try {
2460
+ confirmed = /^\/v1\/setup_intents\/(seti_[A-Za-z0-9]+)\/confirm$/.exec(new URL(request.url).pathname)?.[1] ?? null;
2461
+ }
2462
+ catch {
2463
+ confirmed = null;
2464
+ }
2465
+ if (!confirmed)
2466
+ throw new Error('Only a Stripe SetupIntent confirmation can continue into a card save.');
2467
+ const result = await this.post(`/v2/checkout/authorizations/${authorizationId}/continuations`, {
2468
+ request: { url: request.url, method: request.method, headers: pickHeaders(request.headers, rec.passthroughHeaders), body: request.body },
2469
+ }, AbortSignal.timeout(15_000));
2470
+ const si = result?.setup_intent;
2471
+ // The answer names the SetupIntent this page confirms, and no payment.
2472
+ if (result?.object !== 'checkout_continuation' || result.continuation !== 'allowed' || result.authorization !== authorizationId
2473
+ || result.payment_intent !== undefined || !si || si.id !== confirmed) {
2474
+ throw new Error('The continuation answer was not recognized.');
2475
+ }
2476
+ return { setupIntentId: confirmed };
2477
+ }
2478
+ /**
2479
+ * Create a merchant-session probe, the phase-0 read of the deterministic merchant total:
2480
+ * `user` is the org's connected user, who opens the returned Vault link and reads the
2481
+ * merchant's own total once through Agentcard's merchant-session relay. The capture is
2482
+ * the merchant's cookie jar as captureMerchantSession read it; the capture rules run
2483
+ * again here, so no bot-manager cookie and no signed-in session ever leaves. No card is
2484
+ * involved. The API answers 404 for every org not on its probe list.
2485
+ */
2486
+ async createMerchantSessionProbe(input) {
2487
+ const capture = merchantSessionCapture(input.capture.profile, input.capture.cookies);
2488
+ const created = await this.post('/v2/checkout/merchant-session-probes', { user: input.user, profile: capture.profile, session: { cookies: capture.cookies } }, AbortSignal.timeout(15_000));
2489
+ return { id: created.id, profile: created.profile, approvalUrl: created.approval_url, expiresAt: created.expires_at };
2490
+ }
2491
+ /**
2492
+ * Record the same read made from the autopilot egress by the rustls runner (the API's
2493
+ * egressResultSchema: statuses, header names, sizes, times, totals, hashes and the
2494
+ * certificate chain's SPKI hashes). Once per probe.
2495
+ */
2496
+ async recordMerchantSessionEgress(probeId, result) {
2497
+ await this.post(`/v2/checkout/merchant-session-probes/${encodeURIComponent(probeId)}/egress-result`, { result }, AbortSignal.timeout(15_000));
2498
+ }
1691
2499
  /**
1692
2500
  * After a merchant-hosted payment priced by the merchant's own total: what the merchant's
1693
2501
  * confirmation says it charged (merchant-total.ts merchantChargeReport builds `report`
@@ -1896,6 +2704,112 @@ export class VaultClient {
1896
2704
  return this.call(`/v2/checkout/preparations/${id}`, { signal }, false, undefined, 0);
1897
2705
  }
1898
2706
  }
2707
+ /**
2708
+ * A card endpoint request, checked whole before any network call: a profile in this
2709
+ * build's table; no other checkout context, Autopilot or preparation (the cardholder
2710
+ * approves every such payment); the agent's amount, the only authority here; a
2711
+ * session the API and the device would take (payment-core
2712
+ * merchantCardEndpointSessionRefusal: the profile's URL, headers, cookies, Referer and
2713
+ * body, paused on its checkout page); for a merchant that keeps its own record of the
2714
+ * order, that record, held to the agent's amount and currency, and refused for an order
2715
+ * that renews or a checkout in test mode (admitCardEndpointFacts); and the profile's
2716
+ * status last, so that refusal means every other check passed. Nothing it throws names
2717
+ * the URL, the body or a session value.
2718
+ */
2719
+ function admitCardEndpoint(input) {
2720
+ const named = input.request.merchant_card_endpoint;
2721
+ const profile = isRecord(named) && typeof named.profile === 'string' ? cardEndpointById(named.profile) : null;
2722
+ if (!profile || !isRecord(named))
2723
+ throw new Error('merchant_card_endpoint names no card endpoint Agentcard reviewed.');
2724
+ if (input.request.mercado_checkout !== undefined || hasStripeCheckoutMarker(input.request) || hasOwnedShopMarker(input.request)) {
2725
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the request carries another checkout\'s context');
2726
+ }
2727
+ if (input.executionMode === 'autopilot' || input.grantId !== undefined)
2728
+ throw new CardEndpointRefusedError(profile.id, 'approval_required');
2729
+ if (input.preparation || input.stripeCheckoutEnvironment !== undefined)
2730
+ throw new CardEndpointRefusedError(profile.id, 'preparation_unsupported');
2731
+ if (input.amount == null || typeof input.currency !== 'string' || !input.currency)
2732
+ throw new CardEndpointRefusedError(profile.id, 'amount_required');
2733
+ const session = named.session;
2734
+ if (!isRecord(session) || (input.request.method ?? 'POST').toUpperCase() !== 'POST' || input.request.url !== session.url) {
2735
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the request is not the session\'s card request');
2736
+ }
2737
+ const refusal = cardEndpointSessionRefusal(profile, session);
2738
+ if (refusal)
2739
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', refusal);
2740
+ const facts = admitCardEndpointFacts(profile, session, named.facts, input);
2741
+ if (!cardEndpointLive(profile))
2742
+ throw new CardEndpointRefusedError(profile.id, 'not_enabled');
2743
+ return { profile: profile.id, session, ...(facts ? { facts } : {}) };
2744
+ }
2745
+ /**
2746
+ * The merchant's own record of the order, for a profile whose merchant keeps one
2747
+ * (profile.facts), read with the rules the API reads it with (payment-core
2748
+ * readMerchantCardEndpointFacts and merchantCardEndpointFactsRefusal): required, bound
2749
+ * to this checkout's session, in the reviewed layout; a total or currency that differs
2750
+ * from the agent's is an AmountMismatchError against the merchant's total, and a
2751
+ * renewing order or a checkout in test mode is refused. Card endpoint payments are live
2752
+ * only, so the record must say the checkout is live. Undefined for any other profile,
2753
+ * which sends none.
2754
+ */
2755
+ function admitCardEndpointFacts(profile, session, sent, input) {
2756
+ if (!profile.facts) {
2757
+ if (sent !== undefined)
2758
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the request carries an order record this merchant has none of');
2759
+ return undefined;
2760
+ }
2761
+ if (sent === undefined)
2762
+ throw new CardEndpointRefusedError(profile.id, 'facts_unavailable', `${profile.merchant}'s own record of this order was not read`);
2763
+ const read = sharedReadFacts(profile.id, session.url, sent);
2764
+ if (!read.ok)
2765
+ throw new CardEndpointRefusedError(profile.id, 'facts_unavailable', read.refusal);
2766
+ const currency = String(input.currency).toLowerCase();
2767
+ const refusal = sharedFactsRefusal(profile.id, read.facts, { amountCents: typeof input.amount === 'number' ? input.amount : Number.NaN, currency, live: true });
2768
+ if (refusal?.code === 'amount' || refusal?.code === 'currency') {
2769
+ // An integer amount is compared here; a decimal string is left to the API, which
2770
+ // reads it in the currency's units and holds it to the same total.
2771
+ if (typeof input.amount === 'number') {
2772
+ throw new AmountMismatchError(null, input.amount, read.facts.amount.amountCents, currency, read.facts.amount.currency, 'create', 'merchant_total');
2773
+ }
2774
+ }
2775
+ else if (refusal) {
2776
+ throw new CardEndpointRefusedError(profile.id, 'facts_refused', refusal.reason);
2777
+ }
2778
+ return sent;
2779
+ }
2780
+ /**
2781
+ * The merchant's answer to the device's card request, as the API serves it once
2782
+ * (merchant_card_endpoint.answer), checked before any of it reaches the page: the
2783
+ * profile the request named, an HTTP status, string headers, Set-Cookie strings and a
2784
+ * body, or `withheld` with none of them. An answer the API no longer holds
2785
+ * (answer_expired) or cannot vouch for leaves the outcome unknown: the card was sent.
2786
+ */
2787
+ function cardEndpointReplay(s, authorizationId, profile, metadata) {
2788
+ const unknown = (reason) => new PaymentOutcomeUnknownError(authorizationId, reason);
2789
+ if (metadata.executionMode === 'autopilot')
2790
+ throw unknown('autopilot_submission_mode_invalid');
2791
+ const named = s.merchant_card_endpoint;
2792
+ if (!isRecord(named) || named.profile !== profile)
2793
+ throw unknown('merchant_answer_malformed');
2794
+ if (named.answer_expired === true)
2795
+ throw unknown('merchant_answer_expired');
2796
+ const answer = named.answer;
2797
+ if (!isRecord(answer) || !Number.isInteger(answer.status) || answer.status < 100 || answer.status > 599)
2798
+ throw unknown('merchant_answer_malformed');
2799
+ const base = { mode: CARD_ENDPOINT_MODE, authorizationId, profile, status: answer.status, ...metadata };
2800
+ if (answer.withheld !== undefined) {
2801
+ if (answer.withheld !== 'card_in_response' && answer.withheld !== 'unreadable_in_response' && answer.withheld !== 'refused_step_in_response') {
2802
+ throw unknown('merchant_answer_malformed');
2803
+ }
2804
+ return { ...base, headers: {}, setCookies: [], body: '', withheld: answer.withheld };
2805
+ }
2806
+ const headers = answer.headers;
2807
+ const setCookies = answer.set_cookies;
2808
+ if (answer.status < 200 || typeof answer.bodyText !== 'string' || !isRecord(headers) || !Object.values(headers).every((value) => typeof value === 'string')
2809
+ || !Array.isArray(setCookies) || !setCookies.every((value) => typeof value === 'string'))
2810
+ throw unknown('merchant_answer_malformed');
2811
+ return { ...base, headers: { ...headers }, setCookies: [...setCookies], body: answer.bodyText, withheld: null };
2812
+ }
1899
2813
  function executionMetadata(state, authorizationId, previous = {}) {
1900
2814
  if (state.execution_mode === undefined)
1901
2815
  return previous;
@@ -1909,6 +2823,15 @@ function executionMetadata(state, authorizationId, previous = {}) {
1909
2823
  throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_changed');
1910
2824
  return { executionMode: 'autopilot', grantId: state.grant_id };
1911
2825
  }
2826
+ /**
2827
+ * What authorize() creates a Verifone authorization as: a cse payment that only a
2828
+ * prepared checkout may make, forwarding no header but content-type. Never matched
2829
+ * against a URL: authorize() uses it only for an armed Verifone profile's request.
2830
+ */
2831
+ const VERIFONE_RECOGNIZER = Object.freeze({
2832
+ psp: 'verifone', match: /(?!)/, encoding: 'json', passthroughHeaders: [],
2833
+ clientSideEncrypted: true, mode: 'cse', preparationRequired: true,
2834
+ });
1912
2835
  /**
1913
2836
  * What authorize() creates a merchant-hosted authorization as: an Adyen cse
1914
2837
  * payment that only a prepared checkout may make (so an unprepared request is
@@ -1939,6 +2862,51 @@ function assertMerchantHostedRequest(input, profile) {
1939
2862
  if (verdict.verdict === 'pass')
1940
2863
  throw new Error(`not a card request Agentcard reviewed for ${profile.merchant}.`);
1941
2864
  }
2865
+ /**
2866
+ * Refuse, before any create or prompt, a Verifone request this client cannot pay: a profile
2867
+ * Agentcard has not turned on (observe), Autopilot (the cardholder approves every one), or a
2868
+ * request its rules do not pause (anything but its reviewed card request).
2869
+ */
2870
+ function assertVerifoneRequest(input, profile) {
2871
+ if (profile.status !== 'enabled') {
2872
+ throw new Error(`Agentcard has not turned on payments to ${profile.merchant} yet. Nothing was charged.`);
2873
+ }
2874
+ if (input.executionMode === 'autopilot' || input.grantId !== undefined) {
2875
+ throw new Error(`A payment to ${profile.merchant} needs the cardholder's approval every time; Autopilot cannot pay it.`);
2876
+ }
2877
+ const verdict = classifyVerifoneProfileRequest(profile, input.request.url, input.request.method ?? 'POST', input.request.body);
2878
+ if (verdict.verdict === 'abort')
2879
+ throw new Error(`Refusing to pay ${profile.merchant}: ${verdict.reason}. Nothing was charged.`);
2880
+ if (verdict.verdict === 'pass')
2881
+ throw new Error(`not a card request Agentcard reviewed for ${profile.merchant}.`);
2882
+ }
2883
+ /**
2884
+ * The approved read of a Verifone authorization, as the replay the adapters continue: the
2885
+ * device's blob for the profile (`kind: 'verifone'`), beside the profile and the SHA-256 of the
2886
+ * body the API checked at create. Anything else, a create that was not Verifone's, another
2887
+ * profile, or a blob the API no longer serves (`substitutions_expired`) is an unknown outcome:
2888
+ * the device may already have encrypted the card, and nothing here can pay with it.
2889
+ */
2890
+ function verifoneApproval(s, authorizationId, profile) {
2891
+ const malformed = () => new PaymentOutcomeUnknownError(authorizationId, 'cse_substitutions_malformed');
2892
+ if (!profile)
2893
+ throw malformed();
2894
+ if (s.substitutions_expired === true)
2895
+ throw new PaymentOutcomeUnknownError(authorizationId, 'substitutions_expired');
2896
+ const served = s.verifone;
2897
+ if (!isVerifoneSubstitution(s.substitutions, profile.id) || !isRecord(served) || served.profile !== profile.id
2898
+ || typeof served.body_sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(served.body_sha256))
2899
+ throw malformed();
2900
+ return {
2901
+ mode: 'cse',
2902
+ kind: 'verifone',
2903
+ authorizationId,
2904
+ profile: profile.id,
2905
+ substitutions: { encoding: 'json', kind: 'verifone', profile: profile.id, blob: s.substitutions.blob },
2906
+ bodySha256: served.body_sha256,
2907
+ substitutionsExpiresAt: typeof s.substitutions_expires_at === 'string' ? s.substitutions_expires_at : null,
2908
+ };
2909
+ }
1942
2910
  const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
1943
2911
  const FORBIDDEN_STEPS = new Set(['__proto__', 'constructor', 'prototype']);
1944
2912
  /**