@agent-cards/sdk 0.23.18 → 0.23.20

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 (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/PREFLIGHT.md +2 -0
  3. package/README.md +46 -7
  4. package/dist/aci.d.ts +0 -7
  5. package/dist/aci.js +0 -19
  6. package/dist/amadeus.d.ts +4 -2
  7. package/dist/amadeus.js +16 -3
  8. package/dist/buy.d.ts +84 -2
  9. package/dist/buy.js +16 -1
  10. package/dist/card-endpoint-facts.d.ts +2 -1
  11. package/dist/card-endpoint-facts.js +18 -9
  12. package/dist/card-endpoint.d.ts +20 -4
  13. package/dist/card-endpoint.js +38 -4
  14. package/dist/card-fields.generated.js +974 -45
  15. package/dist/cdp.d.ts +29 -0
  16. package/dist/cdp.js +302 -18
  17. package/dist/client.d.ts +43 -5
  18. package/dist/client.js +237 -43
  19. package/dist/datatrans.d.ts +2 -2
  20. package/dist/datatrans.generated.js +16 -11
  21. package/dist/datatrans.js +2 -2
  22. package/dist/hipay.generated.d.ts +56 -0
  23. package/dist/hipay.generated.js +389 -0
  24. package/dist/hold.d.ts +180 -0
  25. package/dist/hold.js +264 -0
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +1 -0
  28. package/dist/lifecycle.d.ts +10 -0
  29. package/dist/lifecycle.js +21 -0
  30. package/dist/merchant-card-endpoints.generated.js +102 -11
  31. package/dist/paddle-vgs.generated.d.ts +3 -2
  32. package/dist/paddle-vgs.generated.js +9 -5
  33. package/dist/pelecard.generated.d.ts +2 -1
  34. package/dist/pelecard.generated.js +14 -5
  35. package/dist/pre-claim.d.ts +14 -0
  36. package/dist/pre-claim.js +76 -3
  37. package/dist/prepared-processor.d.ts +6 -2
  38. package/dist/prepared-processor.js +18 -2
  39. package/dist/primer.generated.js +998 -69
  40. package/dist/quickpay.d.ts +44 -0
  41. package/dist/quickpay.js +65 -0
  42. package/dist/stripe-checkout.generated.js +320 -2
  43. package/dist/twoc2p.d.ts +54 -0
  44. package/dist/twoc2p.generated.d.ts +33 -0
  45. package/dist/twoc2p.generated.js +1145 -0
  46. package/dist/twoc2p.js +85 -0
  47. package/dist/voice.d.ts +16 -13
  48. package/dist/voice.js +13 -10
  49. package/package.json +3 -3
package/dist/client.d.ts CHANGED
@@ -257,6 +257,28 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
257
257
  * that the page then runs in the agent's browser. The cardholder's device sends the card through
258
258
  * the checkout relay. Served on the same terms: only to a build that lists it, for a company
259
259
  * Agentcard turned PayMongo on for.
260
+ *
261
+ * `hipay_vault_token`: the token create HiPay's hosted fields send to HiPay's Secure Vault
262
+ * (secure2-vault.hipay-tpp.com, stage-secure2-vault for a test row), only through a prepared
263
+ * checkout: the merchant's page gives the frame 20 seconds after Pay. The cardholder's device
264
+ * sends the card to HiPay directly and the frame gets back HiPay's token. The number-only post
265
+ * the frame sends while the number is typed, and its /update once it holds a token, fail in the
266
+ * browser (pre-claim.ts hipayPreClaim). Served on the same terms: only to a build that lists it,
267
+ * for a company Agentcard turned HiPay on for.
268
+ *
269
+ * `twoc2p_secure_pay`: the Payment 2C2P's hosted payment page posts to its payment API
270
+ * (pgwv4.2c2p.com), carrying the card only as one field the page encrypts under 2C2P's static
271
+ * key. This build pauses it and writes over the placeholder's field the one the cardholder's
272
+ * device wrote under the key payment-core pins (twoc2p.ts), into that Payment alone; 3-D Secure
273
+ * then runs in the agent's browser. Served only to a build that lists it, and only for Agentcard's
274
+ * proof company, whatever the API's processor lists name: the field is the card under 2C2P's one
275
+ * static key, which any 2C2P merchant could charge.
276
+ *
277
+ * `quickpay_hosted_payment_page`: QuickPay's hosted payment window, whose card-free signed entry
278
+ * this build pauses for the cardholder's device and whose card post, fee lookup, wallet post, 3-D
279
+ * Secure step and card lookup this build never lets the agent's browser send (pre-claim.ts,
280
+ * quickpay.ts). Served on the same terms as NMI's and Redsys's: only to a build that lists it, for a
281
+ * company Agentcard turned QuickPay on for.
260
282
  */
261
283
  export declare const SUPPORTED_CAPABILITIES: readonly string[];
262
284
  /**
@@ -1187,8 +1209,24 @@ export declare class CheckoutAuthError extends Error {
1187
1209
  * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1188
1210
  * On Przelewy24's hosts the transaction's token reads `{token}`:
1189
1211
  * https://secure.przelewy24.pl/trnRequest/{token}.
1212
+ * On Revolut's card payment and hosted checkout hosts an order or payment token reads `{token}`:
1213
+ * https://merchant.revolut.com/api/public/orders/{token}/payments.
1214
+ * On SumUp's API host a checkout id reads `{checkout}`: https://api.sumup.com/v0.2/checkouts/{checkout}.
1215
+ * On Paytrail's hosts a payment's id reads `{transaction}`:
1216
+ * https://services.paytrail.com/payments/{transaction}/click-to-pay/loading-and-redirect, and on Payment
1217
+ * Highway's a card page's session reads `{session}`.
1218
+ * On 2C2P's page hosts everything after the build's folder reads `{token}`:
1219
+ * https://pgw-ui.2c2p.com/payment/4.1/{token}.
1220
+ * On QuickPay's host the window's session reads `{session}`:
1221
+ * https://payment.quickpay.net/{session}/3ds_auth.
1190
1222
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1191
- * same segments masked.
1223
+ * same segments masked for every processor whose host it names.
1224
+ * A URL is read by its host only when the parser finds one. Text before a URL
1225
+ * can read as a scheme: `see:https://api.stripe.com/...` parses as a URL with
1226
+ * the scheme `see`, no host, and the whole Stripe URL as its path, so a parse
1227
+ * with no host is read as a string that is not a URL. A path can carry a URL of
1228
+ * its own, as `http:https://api.stripe.com/...` does once `https` reads as its
1229
+ * host, so every processor whose host the path names masks the URL too.
1192
1230
  */
1193
1231
  export declare function redactUrl(raw: string): string;
1194
1232
  export interface VaultClientOptions {
@@ -1231,8 +1269,8 @@ export interface VaultClientOptions {
1231
1269
  /**
1232
1270
  * The voice module: how this product approves purchases by voice, named
1233
1271
  * once per business, with the Vault return link and the verifier address
1234
- * beside it. Leave it out for a client that approves no purchase by voice;
1235
- * `agentcard.voice.*` then throws `missing_approval_posture`. See `VoiceOptions`.
1272
+ * beside it. Leave it out and the client approves on Agentcard's page
1273
+ * (`approval: 'sheet'`). See `VoiceOptions`.
1236
1274
  */
1237
1275
  voice?: VoiceOptions;
1238
1276
  /**
@@ -1297,8 +1335,8 @@ export declare class VaultClient {
1297
1335
  /**
1298
1336
  * Approve purchases by voice inside your own product: connect a person once,
1299
1337
  * create the purchase, ask for the key word, read the result. Same
1300
- * credentials, same token as the checkout calls. Needs `voice: { approval }`
1301
- * at construction; without it every verb throws `missing_approval_posture`.
1338
+ * credentials, same token as the checkout calls. Approves on Agentcard's
1339
+ * page unless `voice: { approval: 'in_app' }` was named at construction.
1302
1340
  */
1303
1341
  readonly voice: VoiceModule;
1304
1342
  /**
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
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';
4
+ import { ACI_CAPABILITY, ACI_PSP, aciCardLegAnswer } from './aci.js';
5
5
  import { EPROTECT_CAPABILITY, EPROTECT_PSP, eprotectCheckoutRefusal, eprotectScriptAnswer, isEprotectRegistration } from './eprotect.js';
6
6
  import { isMercadoTokenRequest, parseMercadoCheckoutContext, validateMercadoProcessorContext } from './mercado-checkout.generated.js';
7
7
  import { matchesPreparation, preparationMode, validPreparationEnvironment } from './prepared-processor.js';
@@ -16,7 +16,9 @@ import { SHIJI_STS_CAPABILITY, SHIJI_STS_PSP, isShijiStsServedCompanion, isShiji
16
16
  import { AMADEUS_CHECKOUT_CAPABILITY, AMADEUS_CHECKOUT_PSP } from './amadeus-checkout.generated.js';
17
17
  import { PADDLE_VGS_CAPABILITY, PADDLE_VGS_PSP, paddleVgsCardFactsOf } from './paddle-vgs.generated.js';
18
18
  import { PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_PSP, isPaddleTokenexServedFacts, isPaddleTokenexServedSubstitution } from './paddle-tokenex.js';
19
+ import { TWOC2P_CAPABILITY, TWOC2P_PSP } from './twoc2p.js';
19
20
  import { WIX_PAYMENTS_CAPABILITY, WIX_PAYMENTS_PSP } from './wix-payments.generated.js';
21
+ import { HIPAY_CAPABILITY, HIPAY_PSP } from './hipay.generated.js';
20
22
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
21
23
  import { VoiceModule } from './voice.js';
22
24
  import { BuyModule } from './buy.js';
@@ -241,9 +243,31 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
241
243
  * that the page then runs in the agent's browser. The cardholder's device sends the card through
242
244
  * the checkout relay. Served on the same terms: only to a build that lists it, for a company
243
245
  * Agentcard turned PayMongo on for.
246
+ *
247
+ * `hipay_vault_token`: the token create HiPay's hosted fields send to HiPay's Secure Vault
248
+ * (secure2-vault.hipay-tpp.com, stage-secure2-vault for a test row), only through a prepared
249
+ * checkout: the merchant's page gives the frame 20 seconds after Pay. The cardholder's device
250
+ * sends the card to HiPay directly and the frame gets back HiPay's token. The number-only post
251
+ * the frame sends while the number is typed, and its /update once it holds a token, fail in the
252
+ * browser (pre-claim.ts hipayPreClaim). Served on the same terms: only to a build that lists it,
253
+ * for a company Agentcard turned HiPay on for.
254
+ *
255
+ * `twoc2p_secure_pay`: the Payment 2C2P's hosted payment page posts to its payment API
256
+ * (pgwv4.2c2p.com), carrying the card only as one field the page encrypts under 2C2P's static
257
+ * key. This build pauses it and writes over the placeholder's field the one the cardholder's
258
+ * device wrote under the key payment-core pins (twoc2p.ts), into that Payment alone; 3-D Secure
259
+ * then runs in the agent's browser. Served only to a build that lists it, and only for Agentcard's
260
+ * proof company, whatever the API's processor lists name: the field is the card under 2C2P's one
261
+ * static key, which any 2C2P merchant could charge.
262
+ *
263
+ * `quickpay_hosted_payment_page`: QuickPay's hosted payment window, whose card-free signed entry
264
+ * this build pauses for the cardholder's device and whose card post, fee lookup, wallet post, 3-D
265
+ * Secure step and card lookup this build never lets the agent's browser send (pre-claim.ts,
266
+ * quickpay.ts). Served on the same terms as NMI's and Redsys's: only to a build that lists it, for a
267
+ * company Agentcard turned QuickPay on for.
244
268
  */
245
269
  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', SECURE_ACCEPTANCE_CAPABILITY, 'gravy_session_fields_put', 'moneris_hosted_tokenization', PADDLE_VGS_CAPABILITY, PADDLE_TOKENEX_CAPABILITY,
246
- 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay', WIX_PAYMENTS_CAPABILITY, 'paymongo_checkout_pay'];
270
+ 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay', WIX_PAYMENTS_CAPABILITY, 'paymongo_checkout_pay', HIPAY_CAPABILITY, TWOC2P_CAPABILITY, 'quickpay_hosted_payment_page'];
247
271
  /**
248
272
  * Processors whose paused request names no amount at all, so the pair the
249
273
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -275,15 +299,20 @@ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CY
275
299
  * PayMongo's card attach carries none either: the merchant bound the total, always in
276
300
  * Philippine pesos, to the checkout session when it created it, so the agent sends that
277
301
  * total, above zero (PAYMONGO_PSP below), and the API holds it to the session PayMongo's
278
- * own page shows.
302
+ * own page shows. 2C2P's Payment carries none either: the merchant bound the total to the
303
+ * payment token when it created it, so the agent sends the total 2C2P's page shows, above
304
+ * zero (TWOC2P_PSP), and the API holds it to the amount 2C2P's own PaymentOption answer names.
305
+ * HiPay's token create carries none either: the merchant's server charges
306
+ * HiPay's token for its own order, so the agent's pair, which the prepared checkout carries,
307
+ * is the total the cardholder approves, above zero as Moneris's is.
279
308
  */
280
309
  const MONERIS_PSP = 'moneris';
281
310
  const PAYFAST_PSP = 'payfast';
282
311
  const PAYMONGO_PSP = 'paymongo';
283
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP, PAYFAST_PSP, WIX_PAYMENTS_PSP, PAYMONGO_PSP];
312
+ const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP, PAYFAST_PSP, WIX_PAYMENTS_PSP, PAYMONGO_PSP, HIPAY_PSP, TWOC2P_PSP];
284
313
  /** How AmountRequiredError names a processor. */
285
314
  const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay',
286
- [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast', [WIX_PAYMENTS_PSP]: 'Wix', [PAYMONGO_PSP]: 'PayMongo' };
315
+ [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast', [WIX_PAYMENTS_PSP]: 'Wix', [PAYMONGO_PSP]: 'PayMongo', [HIPAY_PSP]: 'HiPay', [TWOC2P_PSP]: '2C2P' };
287
316
  /**
288
317
  * Processors whose request and answer name no amount at all, so the total the
289
318
  * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
@@ -1041,9 +1070,12 @@ function maskCreditguardTxId(path) {
1041
1070
  * /v2/payment/{client_secret}/confirm (and /show, /status), and with the page's
1042
1071
  * public token that secret confirms the payment and reads its status.
1043
1072
  */
1044
- const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
1073
+ const PATH_SECRET_HOST = /^(?:proxy(?:-test)?\.wspay\.biz|ipg(?:test)?\.monri\.com)\.?$/i;
1045
1074
  /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) and any spelling of its slashes. */
1046
1075
  const PATH_SECRET = new RegExp(`(${SLASH}payment${SLASH}+)[^/]*`, 'gi');
1076
+ function maskPathSecret(path) {
1077
+ return path.replace(PATH_SECRET, '$1{client_secret}');
1078
+ }
1047
1079
  /**
1048
1080
  * Gravy (Gr4vy) gives every merchant hosts of its own under gr4vy.app, and Secure Fields sends the
1049
1081
  * card to /checkout/sessions/<session id>/fields with no auth header, so the session id is that
@@ -1084,10 +1116,11 @@ function maskStripeSession(path) {
1084
1116
  return path.replace(STRIPE_PAYMENT_PAGE, '$1{session}');
1085
1117
  }
1086
1118
  /**
1087
- * redactAciCheckoutId (aci.ts) takes the checkout id out of the paths ACI's widget sends. This rule
1088
- * takes any other copy of it out on ACI's hosts: a trailing dot on the host, an escaped or doubled
1089
- * slash, another case, a string that is not a URL. The segment after /v1/checkouts/ and any id of
1090
- * ACI's shape (<32 hex>.<cluster>-vm-tx<nn>) read {checkoutId}.
1119
+ * ACI's checkout id is that checkout's only capability: once the card-number request carried the real
1120
+ * card, anyone holding it can post /payment against that card for the rest of the checkout's 30
1121
+ * minutes. On ACI's hosts, with a trailing dot, an escaped or doubled slash, in another case or in a
1122
+ * string that is not a URL, the segment after /v1/checkouts/ and any id of ACI's shape
1123
+ * (<32 hex>.<cluster>-vm-tx<nn>) read {checkoutId}.
1091
1124
  */
1092
1125
  const ACI_HOST = /^(?:eu-prod\.|eu-test\.|test\.)?oppwa\.com\.?$/i;
1093
1126
  const ACI_CHECKOUT = new RegExp(`(${SLASH}v1${SLASH}checkouts${SLASH}+)[^/]*`, 'gi');
@@ -1162,21 +1195,36 @@ function maskGopayToken(path) {
1162
1195
  * the merchant's publishable key, which is public (payment-core komoju.js): with the session id, anyone
1163
1196
  * can read that session's amount and buyer email and pay it. On komoju.com the segment after /sessions,
1164
1197
  * in any case and after any run of slashes, each written plain or escaped once or more, reads {session}:
1165
- * https://komoju.com/api/v1/sessions/{session}/pay. Then any 25 letters and digits left that end a run read
1166
- * {session} too, so an id after a dot segment, a backslash or an escape KOMOJU's frame never writes is
1167
- * masked as well; no other path KOMOJU's card fields use holds such a run.
1198
+ * https://komoju.com/api/v1/sessions/{session}/pay. Then every run of 25 or more letters and digits left
1199
+ * reads {session}, so an id after a dot segment, a backslash or an escape KOMOJU's frame never writes is
1200
+ * masked as well; a percent escape right before the run stays when 25 or more remain after it. No other
1201
+ * path KOMOJU's card fields use holds such a run. The whole run reads {session}, never only its last 25
1202
+ * characters, so a longer id of another processor in the same text is never cut in two before that
1203
+ * processor's mask reads it.
1168
1204
  */
1169
1205
  const KOMOJU_HOST = /^komoju\.com\.?$/i;
1170
1206
  const KOMOJU_SESSION = new RegExp(`(${SLASH}sessions)(${SLASH}+)[^/]*`, 'gi');
1171
- const KOMOJU_SESSION_ID = /[a-z0-9]{25}(?![a-z0-9])/gi;
1207
+ const KOMOJU_SESSION_ID = /(%(?:25)*[0-9a-f]{2})?[a-z0-9]{25,}/gi;
1172
1208
  function maskKomojuSession(path) {
1173
- return path.replace(KOMOJU_SESSION, '$1$2{session}').replace(KOMOJU_SESSION_ID, '{session}');
1209
+ return path.replace(KOMOJU_SESSION, '$1$2{session}').replace(KOMOJU_SESSION_ID, (_run, escape) => `${escape ?? ''}{session}`);
1210
+ }
1211
+ /**
1212
+ * SumUp's Payment Widget sends the card to https://api.sumup.com/v0.2/checkouts/<checkout id> with no cookie and
1213
+ * no auth header (payment-core sumup.js), so the checkout id is that request's only credential: with it anyone can
1214
+ * read the checkout's amount and merchant and pay it. On api.sumup.com the segment after /checkouts/, in any case
1215
+ * and any spelling of the slashes before it, reads {checkout}, as does any other id shaped like SumUp's UUID
1216
+ * checkout ids, in every spelling maskUuids reads.
1217
+ */
1218
+ const SUMUP_API_HOST = /^api\.sumup\.com\.?$/i;
1219
+ const SUMUP_CHECKOUT = new RegExp(`(${SLASH}checkouts${SLASH}+)[^/]*`, 'gi');
1220
+ function maskSumupCheckout(path) {
1221
+ return maskUuids(path.replace(SUMUP_CHECKOUT, '$1{checkout}'), '{checkout}');
1174
1222
  }
1175
1223
  /**
1176
1224
  * The processors whose card request carries its own credential in the URL path, with the hosts
1177
- * that send it, the text that names those hosts in a string that is not a URL, and the mask. The
1178
- * guard in redact-url.test.mjs fails for a payment-core recognizer that no one has sorted into
1179
- * this list or out of it.
1225
+ * that send it, the text that names those hosts anywhere else (a string that is not a URL, or a
1226
+ * path that carries a URL of its own), and the mask. The guard in redact-url.test.mjs fails for a
1227
+ * payment-core recognizer that no one has sorted into this list or out of it.
1180
1228
  */
1181
1229
  /**
1182
1230
  * Redsys's hosted payment page names its session in the path of every page and post after the entry
@@ -1212,7 +1260,107 @@ const PRZELEWY24_TOKEN = new RegExp(`[0-9a-f]{10}${HYPHEN}[0-9a-f]{6}${HYPHEN}[0
1212
1260
  function maskPrzelewy24Token(path) {
1213
1261
  return path.replace(PRZELEWY24_TOKEN, '{token}');
1214
1262
  }
1263
+ /**
1264
+ * Revolut's card field and card popup send the card payment to /api/public/orders/<order token>/payments on
1265
+ * merchant.revolut.com (sandbox-merchant.revolut.com in test), and the frame polls /api/public/payments/<payment
1266
+ * token> (payment-core revolut.js). Neither carries a header or cookie that binds it: the order token alone
1267
+ * opens that order's checkout, its amount and its merchant, and pays it, and the payment token reads the payment.
1268
+ * Revolut's hosted checkout page names the same order token in its own paths (/payment-link/<order token>), and the
1269
+ * frame reads the order at merchant-mgmt.revolut.com/orders/token/<order token>. On those hosts every id of their
1270
+ * uuid shape, in every spelling maskUuids reads, reads {token}.
1271
+ */
1272
+ const REVOLUT_HOST = /^(?:sandbox-)?(?:merchant|checkout|merchant-mgmt)\.revolut\.com\.?$/i;
1273
+ function maskRevolutToken(path) {
1274
+ return maskUuids(path, '{token}');
1275
+ }
1276
+ /**
1277
+ * Paytrail names its payment in the path of every route of its card method
1278
+ * (/payments/<transaction>/click-to-pay/loading-and-redirect, and its success and cancel routes), and the same id
1279
+ * opens Paytrail's own payment wall for that payment (pay.paytrail.com/pay/<transaction>), its amount, its
1280
+ * merchant and every method's form, to anyone who holds it (payment-core processors/paytrail.js). On every host
1281
+ * Paytrail owns, any id of its shape, with or without its hyphens, in any case, after an escaped slash or with its
1282
+ * hyphens escaped, reads {transaction}. Payment Highway, Paytrail's card platform, names its card page's session
1283
+ * the same way (/form/<session>/pay_with_click_to_pay), and that path is what carries a card post into the payment,
1284
+ * so on its hosts the same shape reads {session}. The pattern reads a percent escape as one unit and keeps it, so
1285
+ * the hex digits of an escaped separator (%2F, %252F, %3D) never start an id and leave part of it showing.
1286
+ */
1287
+ const PAYTRAIL_HOST = /(?:^|\.)paytrail\.com\.?$/i;
1288
+ const PAYMENT_HIGHWAY_HOST = /(?:^|\.)(?:paymenthighway\.io|sph-test-solinor\.com)\.?$/i;
1289
+ const PAYTRAIL_ID_HYPHEN = '(?:-|%(?:25)*2d)?';
1290
+ const PAYTRAIL_ID = new RegExp(`%(?:25)*[0-9a-f]{2}|[0-9a-f]{8}${PAYTRAIL_ID_HYPHEN}[0-9a-f]{4}${PAYTRAIL_ID_HYPHEN}[0-9a-f]{4}${PAYTRAIL_ID_HYPHEN}[0-9a-f]{4}${PAYTRAIL_ID_HYPHEN}[0-9a-f]{12}`, 'gi');
1291
+ function maskPaytrailId(path, label) {
1292
+ return path.replace(PAYTRAIL_ID, (token) => (token.startsWith('%') ? token : label));
1293
+ }
1294
+ function maskPaytrailTransaction(path) {
1295
+ return maskPaytrailId(path, '{transaction}');
1296
+ }
1297
+ function maskPaymentHighwaySession(path) {
1298
+ return maskPaytrailId(path, '{session}');
1299
+ }
1300
+ /**
1301
+ * 2C2P's hosted payment page names its payment token in its hash routes (pgw-ui.2c2p.com/payment/<build>/#/token/
1302
+ * <token>, #/info/<token>, #/mobile/process/<token>), and the token is all a browser needs to open that payment's page
1303
+ * (payment-core twoc2p.js). redactUrl drops a fragment, but a URL whose `#` was escaped carries the route in its path,
1304
+ * and then in any spelling: escaped or doubled separators, escaped letters, parameters. Nothing past the build's folder
1305
+ * on 2C2P's page hosts is worth a log line, so everything after `/payment/<build>/`, its separators in any spelling
1306
+ * SLASH reads, reads {token}, and on any other path everything after its first separator. A string that is not a URL
1307
+ * is cut the same way after a whole page-host authority it names (at the start, or after a space, a quote, a bracket,
1308
+ * a colon such as a `see:` before it, a comma, a semicolon, an equals sign or `//`, and followed by a path, a port or
1309
+ * the end), and only up to the end of that URL, so the message's own words,
1310
+ * another site's path naming the host and a lookalike host stay; a text naming several such URLs has each one cut.
1311
+ * Anchored matches and cuts in one pass: no input makes it backtrack.
1312
+ */
1313
+ const TWOC2P_PAGE_HOST = /^(?:sandbox-)?pgw-ui\.2c2p\.com\.?$/i;
1314
+ const TWOC2P_PAGE_FOLDER = new RegExp(`^${SLASH}payment${SLASH}4\\.[0-9]{1,2}${SLASH}`, 'i');
1315
+ const TWOC2P_FIRST_SEPARATOR = new RegExp(`^${SLASH}`, 'i');
1316
+ const TWOC2P_PAGE_AUTHORITY = `(?:^|[\\s"'(<\\[:,;=]|//)(?:sandbox-)?pgw-ui\\.2c2p\\.com\\.?(?::[0-9]{1,5})?(?=${SLASH}|$)`;
1317
+ /** One page path, from its first separator: the build's folder stays and the rest reads {token}. */
1318
+ function maskTwoC2pPath(path) {
1319
+ const kept = TWOC2P_PAGE_FOLDER.exec(path) ?? TWOC2P_FIRST_SEPARATOR.exec(path);
1320
+ const end = kept ? kept[0].length : 0;
1321
+ return path.length > end ? `${path.slice(0, end)}{token}` : path;
1322
+ }
1323
+ /**
1324
+ * Text that is not a URL: every whole page-host authority it names, each URL cut only to its own end (the first
1325
+ * space, quote or bracket), so the words between and after them stay. A text that starts with a slash is read the
1326
+ * same way, so another site's protocol-relative URL that names the host keeps its own host and path.
1327
+ */
1328
+ function maskTwoC2pToken(path) {
1329
+ const authority = new RegExp(TWOC2P_PAGE_AUTHORITY, 'gi');
1330
+ // A closing bracket or parenthesis ends the URL only when no path or token character follows it, so a URL
1331
+ // a message wraps in [] or () keeps the words after it, and a bracket inside the path still cannot end the cut early.
1332
+ const urlEnd = /[\s"'<>]|[\])](?![A-Za-z0-9+/=%\\])/g;
1333
+ let out = '';
1334
+ let done = 0;
1335
+ for (let host = authority.exec(path); host !== null; host = authority.exec(path)) {
1336
+ const start = host.index + host[0].length;
1337
+ urlEnd.lastIndex = start;
1338
+ const stop = urlEnd.exec(path);
1339
+ const end = stop ? stop.index : path.length;
1340
+ out += path.slice(done, start) + maskTwoC2pPath(path.slice(start, end));
1341
+ done = end;
1342
+ authority.lastIndex = end;
1343
+ }
1344
+ return out + path.slice(done);
1345
+ }
1346
+ /**
1347
+ * QuickPay's window names its session as the first segment of its own path (/<32 hex>, then /3ds_auth or
1348
+ * /poll-response), and that id is what carries a card post into the payment the merchant's signed entry
1349
+ * opened; a payment link's id (/payments/<64 hex>) opens one the same way. On QuickPay's host a run of 32 hex
1350
+ * digits or more after a slash, in any case or escaped, reads {session} whatever follows it: a URL parser drops a
1351
+ * newline after the URL, so words after it can run on into the id. The entry, the host's root, carries none.
1352
+ */
1353
+ const QUICKPAY_HOST = /^payment\.quickpay\.net\.?$/i;
1354
+ const QUICKPAY_SESSION = /(\/|%2f)[0-9a-f]{32,}/gi;
1355
+ function maskQuickpaySession(path) {
1356
+ return path.replace(QUICKPAY_SESSION, '$1{session}');
1357
+ }
1358
+ /**
1359
+ * `mask` reads a text that names the processor's host; `pathMask`, where a processor has one, reads the path of a
1360
+ * URL already parsed on that host, which `mask` reads otherwise.
1361
+ */
1215
1362
  const PATH_CREDENTIALS = [
1363
+ { host: PATH_SECRET_HOST, named: /(?:wspay\.biz|monri\.com)/i, mask: maskPathSecret },
1216
1364
  { host: CREDITGUARD_HOST, named: /creditguard\.co\.il/i, mask: maskCreditguardTxId },
1217
1365
  { host: GRAVY_HOST, named: /gr4vy\.app/i, mask: maskGravySession },
1218
1366
  { host: RAPYD_HOST, named: /rapyd\.net/i, mask: maskRapydToken },
@@ -1227,6 +1375,12 @@ const PATH_CREDENTIALS = [
1227
1375
  { host: GOPAY_CARD_FORM_HOST, named: /secure(?:\.sandbox)?\.gopay\.com/i, mask: maskGopayToken },
1228
1376
  { host: KOMOJU_HOST, named: /komoju\.com/i, mask: maskKomojuSession },
1229
1377
  { host: PRZELEWY24_HOST, named: /przelewy24\.pl/i, mask: maskPrzelewy24Token },
1378
+ { host: REVOLUT_HOST, named: /(?:merchant|checkout|merchant-mgmt)\.revolut\.com/i, mask: maskRevolutToken },
1379
+ { host: SUMUP_API_HOST, named: /api\.sumup\.com/i, mask: maskSumupCheckout },
1380
+ { host: PAYTRAIL_HOST, named: /paytrail\.com/i, mask: maskPaytrailTransaction },
1381
+ { host: PAYMENT_HIGHWAY_HOST, named: /(?:paymenthighway\.io|sph-test-solinor\.com)/i, mask: maskPaymentHighwaySession },
1382
+ { host: TWOC2P_PAGE_HOST, named: /pgw-ui\.2c2p\.com/i, mask: maskTwoC2pToken, pathMask: maskTwoC2pPath },
1383
+ { host: QUICKPAY_HOST, named: /quickpay\.net/i, mask: maskQuickpaySession },
1230
1384
  ];
1231
1385
  /**
1232
1386
  * A URL as it may appear in an error message: origin + path only. A paused
@@ -1254,30 +1408,52 @@ const PATH_CREDENTIALS = [
1254
1408
  * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1255
1409
  * On Przelewy24's hosts the transaction's token reads `{token}`:
1256
1410
  * https://secure.przelewy24.pl/trnRequest/{token}.
1411
+ * On Revolut's card payment and hosted checkout hosts an order or payment token reads `{token}`:
1412
+ * https://merchant.revolut.com/api/public/orders/{token}/payments.
1413
+ * On SumUp's API host a checkout id reads `{checkout}`: https://api.sumup.com/v0.2/checkouts/{checkout}.
1414
+ * On Paytrail's hosts a payment's id reads `{transaction}`:
1415
+ * https://services.paytrail.com/payments/{transaction}/click-to-pay/loading-and-redirect, and on Payment
1416
+ * Highway's a card page's session reads `{session}`.
1417
+ * On 2C2P's page hosts everything after the build's folder reads `{token}`:
1418
+ * https://pgw-ui.2c2p.com/payment/4.1/{token}.
1419
+ * On QuickPay's host the window's session reads `{session}`:
1420
+ * https://payment.quickpay.net/{session}/3ds_auth.
1257
1421
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1258
- * same segments masked.
1422
+ * same segments masked for every processor whose host it names.
1423
+ * A URL is read by its host only when the parser finds one. Text before a URL
1424
+ * can read as a scheme: `see:https://api.stripe.com/...` parses as a URL with
1425
+ * the scheme `see`, no host, and the whole Stripe URL as its path, so a parse
1426
+ * with no host is read as a string that is not a URL. A path can carry a URL of
1427
+ * its own, as `http:https://api.stripe.com/...` does once `https` reads as its
1428
+ * host, so every processor whose host the path names masks the URL too.
1259
1429
  */
1260
1430
  export function redactUrl(raw) {
1261
- // An ACI checkout id is that checkout's only capability; it never rides on an event.
1262
- const aci = redactAciCheckoutId(raw);
1263
- if (aci !== null)
1264
- return aci;
1431
+ let url;
1265
1432
  try {
1266
- const u = new URL(raw);
1267
- // A trailing dot names the same host, so it cannot let the secret through.
1268
- let path = PATH_SECRET_HOSTS.includes(u.hostname.replace(/\.$/, '')) ? u.pathname.replace(PATH_SECRET, '$1{client_secret}') : u.pathname;
1269
- for (const rule of PATH_CREDENTIALS)
1270
- if (rule.host.test(u.hostname))
1271
- path = rule.mask(path);
1272
- return `${u.origin}${path}`;
1433
+ url = new URL(raw);
1273
1434
  }
1274
1435
  catch {
1275
- let bare = raw.split(/[?#]/)[0].replace(PATH_SECRET, '$1{client_secret}');
1276
- for (const rule of PATH_CREDENTIALS)
1277
- if (rule.named.test(bare))
1278
- bare = rule.mask(bare);
1279
- return bare;
1436
+ url = null;
1437
+ }
1438
+ if (!url?.hostname) {
1439
+ const text = raw.split(/[?#]/)[0];
1440
+ return maskNamed(maskPathSecret(text), text);
1280
1441
  }
1442
+ let path = url.pathname;
1443
+ for (const rule of PATH_CREDENTIALS)
1444
+ if (rule.host.test(url.hostname))
1445
+ path = (rule.pathMask ?? rule.mask)(path);
1446
+ return maskNamed(`${url.origin}${path}`, url.pathname);
1447
+ }
1448
+ /**
1449
+ * `text` with the mask of every processor whose host `names` names. `names` is read before any mask
1450
+ * runs, so a mask that rewrites another processor's host cannot keep that processor's mask from running.
1451
+ */
1452
+ function maskNamed(text, names) {
1453
+ for (const rule of PATH_CREDENTIALS)
1454
+ if (rule.named.test(names))
1455
+ text = rule.mask(text);
1456
+ return text;
1281
1457
  }
1282
1458
  /**
1283
1459
  * Refusals a preparation create answers before any preparation exists, surfaced
@@ -1350,6 +1526,16 @@ const PADDLE_VGS_PREPARATION_REFUSALS = [
1350
1526
  * `unsupported_checkout`, which PREPARATION_REFUSALS names for every checkout. Only for a Wix checkout.
1351
1527
  */
1352
1528
  const WIX_PAYMENTS_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandbox_row', 'merchant_profile_mismatch'];
1529
+ /**
1530
+ * The API's refusals of a HiPay preparation, before any preparation exists, that prepareCheckout surfaces by name:
1531
+ * the company is not turned on for HiPay (`processor_unavailable`), the environment is not the caller's key mode's
1532
+ * (`live_host_on_sandbox_row`, `sandbox_host_on_live_row`), a checkout's merchant is not the name the reviewed pin
1533
+ * carries (`merchant_profile_mismatch`), and the price is zero (`amount_required`), since every create after it would be
1534
+ * refused. An unreviewed origin answers `unsupported_checkout`, which PREPARATION_REFUSALS names for every checkout.
1535
+ * Only for a HiPay checkout.
1536
+ */
1537
+ const HIPAY_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandbox_row', 'sandbox_host_on_live_row', 'merchant_profile_mismatch',
1538
+ 'amount_required'];
1353
1539
  /**
1354
1540
  * The reason the cardholder's device declines a bound prepared row with, per processor, when its own
1355
1541
  * send boundary passed before the card left it (the Vault's sendWindow, 12 seconds after it last saw
@@ -1362,6 +1548,7 @@ const PREPARED_SEND_WINDOW_DECLINES = Object.freeze({
1362
1548
  amadeus_checkout: 'amadeus_send_window_closed',
1363
1549
  [SECURE_ACCEPTANCE_PSP]: 'secure_acceptance_send_window_closed',
1364
1550
  [WIX_PAYMENTS_PSP]: 'wix_payments_send_window_closed',
1551
+ [HIPAY_PSP]: 'hipay_send_window_closed',
1365
1552
  });
1366
1553
  /**
1367
1554
  * Refusals a CyberSource Flex preparation that tokenizes before Pay answers (4xx) before any
@@ -1450,8 +1637,8 @@ export class VaultClient {
1450
1637
  /**
1451
1638
  * Approve purchases by voice inside your own product: connect a person once,
1452
1639
  * create the purchase, ask for the key word, read the result. Same
1453
- * credentials, same token as the checkout calls. Needs `voice: { approval }`
1454
- * at construction; without it every verb throws `missing_approval_posture`.
1640
+ * credentials, same token as the checkout calls. Approves on Agentcard's
1641
+ * page unless `voice: { approval: 'in_app' }` was named at construction.
1455
1642
  */
1456
1643
  voice;
1457
1644
  /**
@@ -1565,7 +1752,9 @@ export class VaultClient {
1565
1752
  // wspay_confirm, shiji_sts_pinned_key, amadeus_checkout_actions.v1,
1566
1753
  // worldline_mycheckout_form, verifone_card_capture, ekashu_hosted_payment_page,
1567
1754
  // cybersource_sa_token_create, gravy_session_fields_put,
1568
- // moneris_hosted_tokenization, paddle_vgs_cards, redsys_hosted_payment_page) and
1755
+ // moneris_hosted_tokenization, paddle_vgs_cards, paddle_tokenex_pay,
1756
+ // redsys_hosted_payment_page, lyra_krypton_payment, payfast_card_pay,
1757
+ // quickpay_hosted_payment_page) and
1569
1758
  // the caller's company is one Agentcard turned that processor on for, so this
1570
1759
  // build arms Fiserv's card capture, GMO's getToken, CyberSource Flex's card
1571
1760
  // fields, Juspay's card flow, Datatrans's Secure Fields card post, the
@@ -2044,6 +2233,7 @@ export class VaultClient {
2044
2233
  || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
2045
2234
  || (input.psp === PADDLE_VGS_PSP && PADDLE_VGS_PREPARATION_REFUSALS.includes(error.code))
2046
2235
  || (input.psp === WIX_PAYMENTS_PSP && WIX_PAYMENTS_PREPARATION_REFUSALS.includes(error.code))
2236
+ || (input.psp === HIPAY_PSP && HIPAY_PREPARATION_REFUSALS.includes(error.code))
2047
2237
  || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
2048
2238
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
2049
2239
  throw fail(error.code);
@@ -2327,10 +2517,11 @@ export class VaultClient {
2327
2517
  // amount_required.
2328
2518
  if (!hasAmount && rec && AGENT_AMOUNT_PROCESSORS.includes(rec.psp))
2329
2519
  throw new AmountRequiredError(rec.psp);
2330
- // A Moneris token is charged later for whatever the merchant's server decides, and a Payfast pay or a
2331
- // PayMongo attach charges the total its payment or checkout session already holds, so a zero pair
2332
- // would show the cardholder no price at all: it is refused like a missing one.
2333
- if (hasAmount && rec && (rec.psp === MONERIS_PSP || rec.psp === PAYFAST_PSP || rec.psp === PAYMONGO_PSP) && Number(input.amount) <= 0)
2520
+ // A Moneris or a HiPay token is charged later for whatever the merchant's server decides, and a Payfast pay, a
2521
+ // PayMongo attach or a 2C2P Payment charges the total its payment, checkout session or payment token
2522
+ // already holds, so a zero pair would show the cardholder no price at all: it is refused like a missing one.
2523
+ if (hasAmount && rec && (rec.psp === MONERIS_PSP || rec.psp === PAYFAST_PSP || rec.psp === PAYMONGO_PSP || rec.psp === HIPAY_PSP || rec.psp === TWOC2P_PSP)
2524
+ && Number(input.amount) <= 0)
2334
2525
  throw new AmountRequiredError(rec.psp);
2335
2526
  // Nor is the agent's pair tied to the page by anything but the total the
2336
2527
  // page shows: without it the cardholder could approve one figure while the
@@ -2476,8 +2667,11 @@ export class VaultClient {
2476
2667
  // and the API names 500 ms as such a row's poll interval.
2477
2668
  // A prepared Paddle VGS card request too, for 20 seconds: Paddle's frame aborts it 10 seconds after
2478
2669
  // Pay and sends it once more, and the API names 500 ms as such a row's poll interval.
2670
+ // A prepared HiPay token create too, for 20 seconds: HiPay's hosted fields give it 20 seconds from Pay, so
2671
+ // an answer the cardholder's device records late in that span still reaches the frame before it gives up,
2672
+ // and the API names 500 ms as such a row's poll interval.
2479
2673
  const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
2480
- : preparation?.psp === PADDLE_VGS_PSP ? Date.now() + 20_000
2674
+ : preparation?.psp === PADDLE_VGS_PSP || preparation?.psp === HIPAY_PSP ? Date.now() + 20_000
2481
2675
  : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000
2482
2676
  : preparation?.psp === SECURE_ACCEPTANCE_PSP ? Date.now() + SECURE_ACCEPTANCE_FAST_POLL_MS : 0;
2483
2677
  // A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
@@ -69,8 +69,8 @@ export interface DatatransRepeatAnswer {
69
69
  * The approved Datatrans answers one attachment handed to a card post, per frame. `frame` is the
70
70
  * adapter's identity for the request's frame (the CDP frameId, the Playwright Frame); a request
71
71
  * whose frame is unknown is never remembered and never answered. Only an answer payment-core reads
72
- * as approved ({transactionId, cardInfo}) is kept: an error answer leaves the next post a new
73
- * question, so the page can try again.
72
+ * as approved (the token, with cardInfo when Datatrans sends one) is kept: an error answer leaves
73
+ * the next post a new question, so the page can try again.
74
74
  */
75
75
  export declare class DatatransRepeats {
76
76
  private readonly windowMs;
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 9c00d227ed43d3b5346dba02fe0431f4cc4762fc7d2d9333eeeb34233b0c9844
3
+ // artifact-sha256: 829167e03a7ffe9fc109ababe9755f1263a840fd5294ebec970e08d41aa9741b
4
4
  // src/datatrans.js
5
5
  var DATATRANS_HOSTS = Object.freeze(["pay.datatrans.com", "pay.sandbox.datatrans.com"]);
6
6
  var DATATRANS_TOKENIZE_PATH = "/upp/payment/SecureFields/paymentField";
@@ -346,7 +346,7 @@ function injectDatatransCard(body, card, policy = DATATRANS_REGISTRATION_POLICY)
346
346
  var TRANSACTION_ID = /^[0-9]{18}$/;
347
347
  var CARD_WORD = /^[A-Za-z][A-Za-z _-]{0,31}$/;
348
348
  var COUNTRY = /^[A-Z]{2}$/;
349
- var CARD_INFO_KEYS = ["brand", "type", "usage", "country", "issuer", "coBrand"];
349
+ var CARD_INFO_KEYS = ["brand", "type", "usage", "country", "issuer", "coBrand", "accountType", "binLength"];
350
350
  var UNFOLLOWED_KEYS = ["tds2EnrollmentResult", "redirect", "dcc", "mcp"];
351
351
  var ERROR_CODES = [
352
352
  [/^Invalid merchantId$/, "invalid_merchant_id"],
@@ -368,10 +368,11 @@ function readAnswer(body) {
368
368
  }
369
369
  }
370
370
  var optionalText = (value, pattern) => value === null || exact(pattern, value);
371
+ var optionalBinLength = (value) => value === null || Number.isInteger(value) && value >= 1 && value <= 19;
371
372
  function rebuildCardInfo(info) {
372
- if (!record(info) || !Object.keys(info).every((key) => CARD_INFO_KEYS.includes(key)) || !exact(CARD_WORD, info.brand))
373
+ if (!record(info) || !Object.keys(info).every((key) => CARD_INFO_KEYS.includes(key)))
373
374
  return null;
374
- if (owns(info, "type") && !optionalText(info.type, CARD_WORD) || owns(info, "usage") && !optionalText(info.usage, CARD_WORD) || owns(info, "country") && !optionalText(info.country, COUNTRY) || owns(info, "issuer") && info.issuer !== null && !plain(info.issuer, 128) || owns(info, "coBrand") && info.coBrand !== null && !plain(info.coBrand, 64))
375
+ if (owns(info, "brand") && !optionalText(info.brand, CARD_WORD) || owns(info, "type") && !optionalText(info.type, CARD_WORD) || owns(info, "usage") && !optionalText(info.usage, CARD_WORD) || owns(info, "country") && !optionalText(info.country, COUNTRY) || owns(info, "issuer") && info.issuer !== null && !plain(info.issuer, 128) || owns(info, "coBrand") && info.coBrand !== null && !plain(info.coBrand, 64) || owns(info, "accountType") && !optionalText(info.accountType, CARD_WORD) || owns(info, "binLength") && !optionalBinLength(info.binLength))
375
376
  return null;
376
377
  const safe = {};
377
378
  for (const key of CARD_INFO_KEYS)
@@ -380,10 +381,14 @@ function rebuildCardInfo(info) {
380
381
  return safe;
381
382
  }
382
383
  function rebuildSuccess(value) {
383
- if (Object.keys(value).length !== 2 || !exact(TRANSACTION_ID, value.transactionId))
384
- return null;
384
+ if (!Object.keys(value).every((key) => key === "transactionId" || key === "cardInfo"))
385
+ return { reason: "datatrans_unexpected_key" };
386
+ if (!exact(TRANSACTION_ID, value.transactionId))
387
+ return { reason: "datatrans_unexpected_token" };
388
+ if (!owns(value, "cardInfo") || value.cardInfo === null)
389
+ return { answer: { transactionId: value.transactionId } };
385
390
  const cardInfo = rebuildCardInfo(value.cardInfo);
386
- return cardInfo ? { transactionId: value.transactionId, cardInfo } : null;
391
+ return cardInfo ? { answer: { transactionId: value.transactionId, cardInfo } } : { reason: "datatrans_unexpected_card_info" };
387
392
  }
388
393
  function rebuildError(value) {
389
394
  return Object.keys(value).length === 1 && plain(value.error, 256) && value.error.trim() !== "" ? { error: value.error } : null;
@@ -407,15 +412,15 @@ function datatransResponseOutcome(status, body, card) {
407
412
  if (owns(value, "error")) {
408
413
  const error = rebuildError(value);
409
414
  if (!error)
410
- return { outcome: "unknown", reason: "datatrans_unexpected_response" };
415
+ return { outcome: "unknown", reason: "datatrans_unexpected_error" };
411
416
  const code = datatransErrorCode(error.error);
412
417
  return code === "too_many_requests" ? { outcome: "unknown", reason: "datatrans_repeat_request" } : { outcome: "refused", code };
413
418
  }
414
419
  if (UNFOLLOWED_KEYS.some((key) => owns(value, key)))
415
420
  return { outcome: "unknown", reason: "datatrans_unsupported_flow" };
416
- const safe = rebuildSuccess(value);
421
+ const { answer: safe, reason } = rebuildSuccess(value);
417
422
  if (!safe)
418
- return { outcome: "unknown", reason: "datatrans_unexpected_response" };
423
+ return { outcome: "unknown", reason };
419
424
  if (card && digits(card.number, 12, 19) && safe.transactionId.includes(card.number)) {
420
425
  return { outcome: "unknown", reason: "datatrans_token_mismatch" };
421
426
  }
@@ -432,7 +437,7 @@ function sanitizeDatatransTokenResponse(body) {
432
437
  return JSON.stringify(rebuildError(value) ?? fail());
433
438
  if (UNFOLLOWED_KEYS.some((key) => owns(value, key)))
434
439
  return fail();
435
- return JSON.stringify(rebuildSuccess(value) ?? fail());
440
+ return JSON.stringify(rebuildSuccess(value).answer ?? fail());
436
441
  }
437
442
  function datatransFacts(status, body) {
438
443
  const verdict = datatransResponseOutcome(status, body);
package/dist/datatrans.js CHANGED
@@ -112,8 +112,8 @@ function sameKey(a, b) {
112
112
  * The approved Datatrans answers one attachment handed to a card post, per frame. `frame` is the
113
113
  * adapter's identity for the request's frame (the CDP frameId, the Playwright Frame); a request
114
114
  * whose frame is unknown is never remembered and never answered. Only an answer payment-core reads
115
- * as approved ({transactionId, cardInfo}) is kept: an error answer leaves the next post a new
116
- * question, so the page can try again.
115
+ * as approved (the token, with cardInfo when Datatrans sends one) is kept: an error answer leaves
116
+ * the next post a new question, so the page can try again.
117
117
  */
118
118
  export class DatatransRepeats {
119
119
  windowMs;