@agent-cards/sdk 0.23.17 → 0.23.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -249,6 +249,14 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
249
249
  * Wix's token. A second card request from the frame, which Wix's frame sends after a 401
250
250
  * or 403 under a fresh instance, is never answered from the same approval. Served on the
251
251
  * same terms: only to a build that lists it, for a company Agentcard turned Wix on for.
252
+ *
253
+ * `paymongo_checkout_pay`: the card attach PayMongo's hosted checkout sends to its own page
254
+ * URL (checkout.paymongo.com/<session>) as a Next.js Server Action. This build pauses only the
255
+ * page's own card attach and answers it with PayMongo's answer as the API recorded it, under the
256
+ * RSC type the page reads: a paid payment intent, or the step toward PayMongo's 3-D Secure page
257
+ * that the page then runs in the agent's browser. The cardholder's device sends the card through
258
+ * the checkout relay. Served on the same terms: only to a build that lists it, for a company
259
+ * Agentcard turned PayMongo on for.
252
260
  */
253
261
  export declare const SUPPORTED_CAPABILITIES: readonly string[];
254
262
  /**
@@ -1176,8 +1184,23 @@ export declare class CheckoutAuthError extends Error {
1176
1184
  * https://www.paypal.com/v2/checkout/orders/{order}/confirm-payment-source.
1177
1185
  * On PayMongo's checkout host the session id reads `{session}`: https://checkout.paymongo.com/{session}.
1178
1186
  * On GoPay's card frame hosts the card form's token reads `{token}`: https://secure.gopay.com/gp-card-comm/h/{token}.
1187
+ * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1188
+ * On Przelewy24's hosts the transaction's token reads `{token}`:
1189
+ * https://secure.przelewy24.pl/trnRequest/{token}.
1190
+ * On Revolut's card payment and hosted checkout hosts an order or payment token reads `{token}`:
1191
+ * https://merchant.revolut.com/api/public/orders/{token}/payments.
1192
+ * On SumUp's API host a checkout id reads `{checkout}`: https://api.sumup.com/v0.2/checkouts/{checkout}.
1193
+ * On Paytrail's hosts a payment's id reads `{transaction}`:
1194
+ * https://services.paytrail.com/payments/{transaction}/click-to-pay/loading-and-redirect, and on Payment
1195
+ * Highway's a card page's session reads `{session}`.
1179
1196
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1180
- * same segments masked.
1197
+ * same segments masked for every processor whose host it names.
1198
+ * A URL is read by its host only when the parser finds one. Text before a URL
1199
+ * can read as a scheme: `see:https://api.stripe.com/...` parses as a URL with
1200
+ * the scheme `see`, no host, and the whole Stripe URL as its path, so a parse
1201
+ * with no host is read as a string that is not a URL. A path can carry a URL of
1202
+ * its own, as `http:https://api.stripe.com/...` does once `https` reads as its
1203
+ * host, so every processor whose host the path names masks the URL too.
1181
1204
  */
1182
1205
  export declare function redactUrl(raw: string): string;
1183
1206
  export interface VaultClientOptions {
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';
@@ -233,9 +233,17 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
233
233
  * Wix's token. A second card request from the frame, which Wix's frame sends after a 401
234
234
  * or 403 under a fresh instance, is never answered from the same approval. Served on the
235
235
  * same terms: only to a build that lists it, for a company Agentcard turned Wix on for.
236
+ *
237
+ * `paymongo_checkout_pay`: the card attach PayMongo's hosted checkout sends to its own page
238
+ * URL (checkout.paymongo.com/<session>) as a Next.js Server Action. This build pauses only the
239
+ * page's own card attach and answers it with PayMongo's answer as the API recorded it, under the
240
+ * RSC type the page reads: a paid payment intent, or the step toward PayMongo's 3-D Secure page
241
+ * that the page then runs in the agent's browser. The cardholder's device sends the card through
242
+ * the checkout relay. Served on the same terms: only to a build that lists it, for a company
243
+ * Agentcard turned PayMongo on for.
236
244
  */
237
245
  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,
238
- 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay', WIX_PAYMENTS_CAPABILITY];
246
+ 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay', WIX_PAYMENTS_CAPABILITY, 'paymongo_checkout_pay'];
239
247
  /**
240
248
  * Processors whose paused request names no amount at all, so the pair the
241
249
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -264,13 +272,18 @@ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CY
264
272
  * and the API holds it to the payment Payfast itself reports. Wix's tokenize request
265
273
  * carries none either: the merchant's page charges Wix's token for its own order, so the
266
274
  * agent's pair, which the prepared checkout carries, is the total the cardholder approves.
275
+ * PayMongo's card attach carries none either: the merchant bound the total, always in
276
+ * Philippine pesos, to the checkout session when it created it, so the agent sends that
277
+ * total, above zero (PAYMONGO_PSP below), and the API holds it to the session PayMongo's
278
+ * own page shows.
267
279
  */
268
280
  const MONERIS_PSP = 'moneris';
269
281
  const PAYFAST_PSP = 'payfast';
270
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP, PAYFAST_PSP, WIX_PAYMENTS_PSP];
282
+ 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];
271
284
  /** How AmountRequiredError names a processor. */
272
285
  const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay',
273
- [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast', [WIX_PAYMENTS_PSP]: 'Wix' };
286
+ [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast', [WIX_PAYMENTS_PSP]: 'Wix', [PAYMONGO_PSP]: 'PayMongo' };
274
287
  /**
275
288
  * Processors whose request and answer name no amount at all, so the total the
276
289
  * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
@@ -1000,10 +1013,27 @@ function parseErrorEnvelope(bodyText) {
1000
1013
  }
1001
1014
  return { code: null, details: {} };
1002
1015
  }
1016
+ /**
1017
+ * A slash as the masks below read one: plain, or escaped once or more in either case (%2F, %2f, %252F). A path keeps
1018
+ * an escape as it was written, and the id behind it is the same id, so every slash a mask names reads in any of these
1019
+ * spellings. The slashes right before an id read as a run, and a doubled slash hides nothing either.
1020
+ */
1021
+ const SLASH = '(?:/|%(?:25)*2f)';
1022
+ /** A hyphen inside an id: plain, or escaped once or more in either case (%2D, %252d). */
1023
+ const HYPHEN = '(?:-|%(?:25)*2d)';
1024
+ /**
1025
+ * The masks that find a uuid by its shape take it in any case, with or without its hyphens, each hyphen in any
1026
+ * spelling HYPHEN reads. The pattern reads a percent escape as one unit and keeps it, so the hex digits of an escaped
1027
+ * separator before the id (%2F, %252F, %3D) never start a match and leave the id's last digits showing.
1028
+ */
1029
+ const UUID_OR_ESCAPE = new RegExp(`%(?:25)*[0-9a-f]{2}|[0-9a-f]{8}${HYPHEN}?[0-9a-f]{4}${HYPHEN}?[0-9a-f]{4}${HYPHEN}?[0-9a-f]{4}${HYPHEN}?[0-9a-f]{12}`, 'gi');
1030
+ function maskUuids(path, label) {
1031
+ return path.replace(UUID_OR_ESCAPE, (token) => (token.startsWith('%') ? token : label));
1032
+ }
1003
1033
  const CREDITGUARD_HOST = /(?:^|\.)creditguard\.co\.il\.?$/i;
1034
+ const CREDITGUARD_TXN = new RegExp(`(${SLASH}txn${SLASH}+)[^/]+`, 'gi');
1004
1035
  function maskCreditguardTxId(path) {
1005
- return path.replace(/(\/txn\/)[^/]+/gi, '$1{txId}')
1006
- .replace(/[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi, '{txId}');
1036
+ return maskUuids(path.replace(CREDITGUARD_TXN, '$1{txId}'), '{txId}');
1007
1037
  }
1008
1038
  /**
1009
1039
  * Hosts that put a per-payment secret in the PATH: Monri Components and
@@ -1011,9 +1041,12 @@ function maskCreditguardTxId(path) {
1011
1041
  * /v2/payment/{client_secret}/confirm (and /show, /status), and with the page's
1012
1042
  * public token that secret confirms the payment and reads its status.
1013
1043
  */
1014
- const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
1015
- /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) or after an escaped slash. */
1016
- const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
1044
+ const PATH_SECRET_HOST = /^(?:proxy(?:-test)?\.wspay\.biz|ipg(?:test)?\.monri\.com)\.?$/i;
1045
+ /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) and any spelling of its slashes. */
1046
+ const PATH_SECRET = new RegExp(`(${SLASH}payment${SLASH}+)[^/]*`, 'gi');
1047
+ function maskPathSecret(path) {
1048
+ return path.replace(PATH_SECRET, '$1{client_secret}');
1049
+ }
1017
1050
  /**
1018
1051
  * Gravy (Gr4vy) gives every merchant hosts of its own under gr4vy.app, and Secure Fields sends the
1019
1052
  * card to /checkout/sessions/<session id>/fields with no auth header, so the session id is that
@@ -1023,10 +1056,9 @@ const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
1023
1056
  * session ids.
1024
1057
  */
1025
1058
  const GRAVY_HOST = /(?:^|\.)gr4vy\.app\.?$/i;
1026
- const GRAVY_SESSION = /(\/checkout\/sessions(?:\/|%2f))[^/]*/gi;
1027
- const GRAVY_SESSION_ID = /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi;
1059
+ const GRAVY_SESSION = new RegExp(`(${SLASH}checkout${SLASH}sessions${SLASH}+)[^/]*`, 'gi');
1028
1060
  function maskGravySession(path) {
1029
- return path.replace(GRAVY_SESSION, '$1{session}').replace(GRAVY_SESSION_ID, '{session}');
1061
+ return maskUuids(path.replace(GRAVY_SESSION, '$1{session}'), '{session}');
1030
1062
  }
1031
1063
  /**
1032
1064
  * Rapyd's hosted pages send the card to api.rapyd.net (sandboxapi.rapyd.net in test) with no auth
@@ -1037,7 +1069,7 @@ function maskGravySession(path) {
1037
1069
  * after an escaped slash, read {token}.
1038
1070
  */
1039
1071
  const RAPYD_HOST = /(?:^|\.)rapyd\.net\.?$/i;
1040
- const RAPYD_CARD_PAGE = /(\/hosted\/collect\/card(?:\/|%2f))[^/]*/gi;
1072
+ const RAPYD_CARD_PAGE = new RegExp(`(${SLASH}hosted${SLASH}collect${SLASH}card${SLASH}+)[^/]*`, 'gi');
1041
1073
  const RAPYD_PAGE_ID = /(?:checkout|hp_[a-z]+)_[a-z0-9]+/gi;
1042
1074
  function maskRapydToken(path) {
1043
1075
  return path.replace(RAPYD_CARD_PAGE, '$1{token}').replace(RAPYD_PAGE_ID, '{token}');
@@ -1050,18 +1082,19 @@ function maskRapydToken(path) {
1050
1082
  * SetupIntent id stays: confirming one also needs its client_secret, which rides in the body.
1051
1083
  */
1052
1084
  const STRIPE_API_HOST = /^api\.stripe\.com\.?$/i;
1053
- const STRIPE_PAYMENT_PAGE = /(\/payment_pages(?:\/|%2f))[^/]*/gi;
1085
+ const STRIPE_PAYMENT_PAGE = new RegExp(`(${SLASH}payment_pages${SLASH}+)[^/]*`, 'gi');
1054
1086
  function maskStripeSession(path) {
1055
1087
  return path.replace(STRIPE_PAYMENT_PAGE, '$1{session}');
1056
1088
  }
1057
1089
  /**
1058
- * redactAciCheckoutId (aci.ts) takes the checkout id out of the paths ACI's widget sends. This rule
1059
- * takes any other copy of it out on ACI's hosts: a trailing dot on the host, an escaped or doubled
1060
- * slash, another case, a string that is not a URL. The segment after /v1/checkouts/ and any id of
1061
- * ACI's shape (<32 hex>.<cluster>-vm-tx<nn>) read {checkoutId}.
1090
+ * ACI's checkout id is that checkout's only capability: once the card-number request carried the real
1091
+ * card, anyone holding it can post /payment against that card for the rest of the checkout's 30
1092
+ * minutes. On ACI's hosts, with a trailing dot, an escaped or doubled slash, in another case or in a
1093
+ * string that is not a URL, the segment after /v1/checkouts/ and any id of ACI's shape
1094
+ * (<32 hex>.<cluster>-vm-tx<nn>) read {checkoutId}.
1062
1095
  */
1063
1096
  const ACI_HOST = /^(?:eu-prod\.|eu-test\.|test\.)?oppwa\.com\.?$/i;
1064
- const ACI_CHECKOUT = /(\/v1\/checkouts(?:\/|%2f))[^/]*/gi;
1097
+ const ACI_CHECKOUT = new RegExp(`(${SLASH}v1${SLASH}checkouts${SLASH}+)[^/]*`, 'gi');
1065
1098
  const ACI_CHECKOUT_ID = /[0-9a-f]{32}\.[a-z0-9]{1,16}-vm-tx[0-9]{1,3}/gi;
1066
1099
  function maskAciCheckout(path) {
1067
1100
  return path.replace(ACI_CHECKOUT, '$1{checkoutId}').replace(ACI_CHECKOUT_ID, '{checkoutId}');
@@ -1071,34 +1104,35 @@ function maskAciCheckout(path) {
1071
1104
  * with no auth header and no CSRF token (payment-core payfast.js): the payment's uuid is the
1072
1105
  * request's only credential, and with it anyone can read that payment page, its amount and its
1073
1106
  * merchant. Every route of the page carries the same uuid, so on every host Payfast owns (payfast.io and
1074
- * payfast.co.za and any name under them) any id of its shape, in any case or after an escaped slash,
1075
- * reads {uuid}.
1107
+ * payfast.co.za and any name under them) any id of its shape, in every spelling maskUuids reads, reads {uuid}.
1076
1108
  */
1077
1109
  const PAYFAST_HOST = /(?:^|\.)payfast\.(?:io|co\.za)\.?$/i;
1078
- const PAYFAST_UUID = /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi;
1079
1110
  function maskPayfastUuid(path) {
1080
- return path.replace(PAYFAST_UUID, '{uuid}');
1111
+ return maskUuids(path, '{uuid}');
1081
1112
  }
1082
1113
  /**
1083
1114
  * Paddle's checkout frame pays a TokenEx checkout with its own /pay on checkout-service.paddle.com
1084
1115
  * (sandbox-checkout-service in test), and the checkout id in that path is the checkout's only
1085
1116
  * credential there, no cookies (payment-core paddle-tokenex.js): with it, and the card in the body,
1086
1117
  * a payment reaches that checkout. On those hosts the segment between /checkout/ or
1087
- * /transaction-checkout/ and /pay, in any case or after an escaped slash, reads {checkout}.
1118
+ * /transaction-checkout/ and /pay, in any case and any spelling of the slashes around it, reads {checkout}.
1119
+ * The segment ends at the first slash in any spelling, so a long run of them cannot make the pattern try
1120
+ * every way of splitting the run between the segment and the slashes.
1088
1121
  */
1089
1122
  const PADDLE_CHECKOUT_HOST = /^(?:sandbox-)?checkout-service\.paddle\.com\.?$/i;
1090
- const PADDLE_PAY = /(\/(?:checkout|transaction-checkout)(?:\/|%2f))[^/]*(\/pay)/gi;
1123
+ const PADDLE_PAY = new RegExp(`(${SLASH}(?:checkout|transaction-checkout)${SLASH}+)(?:[^/%]|%(?!(?:25)*2f))+(${SLASH}+pay)`, 'gi');
1091
1124
  function maskPaddleCheckout(path) {
1092
1125
  return path.replace(PADDLE_PAY, '$1{checkout}$2');
1093
1126
  }
1094
1127
  /**
1095
1128
  * Lyra's Krypton frame sends its payment to ProcessPayment/2 on the brand's API host with the form
1096
1129
  * token's session id as a `;jsessionid=` path parameter (payment-core lyra.js). With the form token the
1097
- * body carries, that id continues the payment on Lyra's side, so on Lyra's hosts it reads `{session}`,
1098
- * in any case: https://api.systempay.fr/api-payment/V4/Charge/Public/ProcessPayment/2;jsessionid={session}.
1130
+ * body carries, that id continues the payment on Lyra's side, so on Lyra's hosts it reads `{session}`, in any
1131
+ * case and with its `;` or `=` escaped once or more:
1132
+ * https://api.systempay.fr/api-payment/V4/Charge/Public/ProcessPayment/2;jsessionid={session}.
1099
1133
  */
1100
1134
  const LYRA_HOST = /^(?:api\.payzen\.eu|api\.systempay\.fr|api-sogecommerce\.societegenerale\.eu|api\.lyra\.com)\.?$/i;
1101
- const LYRA_SESSION = /;jsessionid=[^/;?#]*/gi;
1135
+ const LYRA_SESSION = /(?:;|%(?:25)*3b)jsessionid(?:=|%(?:25)*3d)[^/;?#]*/gi;
1102
1136
  function maskLyraSession(path) {
1103
1137
  return path.replace(LYRA_SESSION, ';jsessionid={session}');
1104
1138
  }
@@ -1123,24 +1157,54 @@ function maskPaymongoSession(path) {
1123
1157
  * case or after an escaped slash, reads {token}: https://secure.gopay.com/gp-card-comm/h/{token}.
1124
1158
  */
1125
1159
  const GOPAY_CARD_FORM_HOST = /^secure(?:\.sandbox)?\.gopay\.com\.?$/i;
1126
- const GOPAY_CARD_FORM_TOKEN = /(\/h(?:\/|%2f))[^/?#]*/gi;
1160
+ const GOPAY_CARD_FORM_TOKEN = new RegExp(`(${SLASH}h${SLASH}+)[^/?#]*`, 'gi');
1127
1161
  function maskGopayToken(path) {
1128
1162
  return path.replace(GOPAY_CARD_FORM_TOKEN, '$1{token}');
1129
1163
  }
1164
+ /**
1165
+ * KOMOJU's Hosted Fields frame pays a session with POST /api/v1/sessions/<id>/pay on komoju.com under
1166
+ * the merchant's publishable key, which is public (payment-core komoju.js): with the session id, anyone
1167
+ * can read that session's amount and buyer email and pay it. On komoju.com the segment after /sessions,
1168
+ * in any case and after any run of slashes, each written plain or escaped once or more, reads {session}:
1169
+ * https://komoju.com/api/v1/sessions/{session}/pay. Then every run of 25 or more letters and digits left
1170
+ * reads {session}, so an id after a dot segment, a backslash or an escape KOMOJU's frame never writes is
1171
+ * masked as well; a percent escape right before the run stays when 25 or more remain after it. No other
1172
+ * path KOMOJU's card fields use holds such a run. The whole run reads {session}, never only its last 25
1173
+ * characters, so a longer id of another processor in the same text is never cut in two before that
1174
+ * processor's mask reads it.
1175
+ */
1176
+ const KOMOJU_HOST = /^komoju\.com\.?$/i;
1177
+ const KOMOJU_SESSION = new RegExp(`(${SLASH}sessions)(${SLASH}+)[^/]*`, 'gi');
1178
+ const KOMOJU_SESSION_ID = /(%(?:25)*[0-9a-f]{2})?[a-z0-9]{25,}/gi;
1179
+ function maskKomojuSession(path) {
1180
+ return path.replace(KOMOJU_SESSION, '$1$2{session}').replace(KOMOJU_SESSION_ID, (_run, escape) => `${escape ?? ''}{session}`);
1181
+ }
1182
+ /**
1183
+ * SumUp's Payment Widget sends the card to https://api.sumup.com/v0.2/checkouts/<checkout id> with no cookie and
1184
+ * no auth header (payment-core sumup.js), so the checkout id is that request's only credential: with it anyone can
1185
+ * read the checkout's amount and merchant and pay it. On api.sumup.com the segment after /checkouts/, in any case
1186
+ * and any spelling of the slashes before it, reads {checkout}, as does any other id shaped like SumUp's UUID
1187
+ * checkout ids, in every spelling maskUuids reads.
1188
+ */
1189
+ const SUMUP_API_HOST = /^api\.sumup\.com\.?$/i;
1190
+ const SUMUP_CHECKOUT = new RegExp(`(${SLASH}checkouts${SLASH}+)[^/]*`, 'gi');
1191
+ function maskSumupCheckout(path) {
1192
+ return maskUuids(path.replace(SUMUP_CHECKOUT, '$1{checkout}'), '{checkout}');
1193
+ }
1130
1194
  /**
1131
1195
  * The processors whose card request carries its own credential in the URL path, with the hosts
1132
- * that send it, the text that names those hosts in a string that is not a URL, and the mask. The
1133
- * guard in redact-url.test.mjs fails for a payment-core recognizer that no one has sorted into
1134
- * this list or out of it.
1196
+ * that send it, the text that names those hosts anywhere else (a string that is not a URL, or a
1197
+ * path that carries a URL of its own), and the mask. The guard in redact-url.test.mjs fails for a
1198
+ * payment-core recognizer that no one has sorted into this list or out of it.
1135
1199
  */
1136
1200
  /**
1137
1201
  * Redsys's hosted payment page names its session in the path of every page and post after the entry
1138
1202
  * (/sis/pagoTarjeta;jsessionid=<id>), and that id is what carries a card post into the session the
1139
- * merchant's signed entry opened. On Redsys's hosts the session id, in any case or escaped, reads
1140
- * {session}. The entry itself carries none.
1203
+ * merchant's signed entry opened. On Redsys's hosts the session id, in any case or escaped once or more,
1204
+ * reads {session}. The entry itself carries none.
1141
1205
  */
1142
1206
  const REDSYS_HOST = /^sis(?:-t)?\.redsys\.es\.?$/i;
1143
- const REDSYS_SESSION = /((?:;|%3b)jsessionid(?:=|%3d))[^/?#;]*/gi;
1207
+ const REDSYS_SESSION = /((?:;|%(?:25)*3b)jsessionid(?:=|%(?:25)*3d))[^/?#;]*/gi;
1144
1208
  function maskRedsysSession(path) {
1145
1209
  return path.replace(REDSYS_SESSION, '$1{session}');
1146
1210
  }
@@ -1151,11 +1215,61 @@ function maskRedsysSession(path) {
1151
1215
  * segment after /orders/, in any case or after an escaped slash, reads {order}.
1152
1216
  */
1153
1217
  const PAYPAL_HOST = /(?:^|\.)paypal\.com\.?$/i;
1154
- const PAYPAL_ORDER = /(\/orders(?:\/|%2f))[^/]*/gi;
1218
+ const PAYPAL_ORDER = new RegExp(`(${SLASH}orders${SLASH}+)[^/]*`, 'gi');
1155
1219
  function maskPaypalOrder(path) {
1156
1220
  return path.replace(PAYPAL_ORDER, '$1{order}');
1157
1221
  }
1222
+ /**
1223
+ * Przelewy24's pay wall names its transaction by one token in the path of every page and post
1224
+ * (/trnRequest/<token>, its method links, /card/p24form/<token>, /trnResult/<token>), and that token
1225
+ * is all a browser needs to open the transaction's pay wall (payment-core processors/przelewy24.js).
1226
+ * On Przelewy24's hosts a token-shaped segment, in any case, after an escaped slash or with its hyphens
1227
+ * escaped, reads {token}.
1228
+ */
1229
+ const PRZELEWY24_HOST = /(?:^|\.)przelewy24\.pl\.?$/i;
1230
+ const PRZELEWY24_TOKEN = new RegExp(`[0-9a-f]{10}${HYPHEN}[0-9a-f]{6}${HYPHEN}[0-9a-f]{6}${HYPHEN}[0-9a-f]{10}`, 'gi');
1231
+ function maskPrzelewy24Token(path) {
1232
+ return path.replace(PRZELEWY24_TOKEN, '{token}');
1233
+ }
1234
+ /**
1235
+ * Revolut's card field and card popup send the card payment to /api/public/orders/<order token>/payments on
1236
+ * merchant.revolut.com (sandbox-merchant.revolut.com in test), and the frame polls /api/public/payments/<payment
1237
+ * token> (payment-core revolut.js). Neither carries a header or cookie that binds it: the order token alone
1238
+ * opens that order's checkout, its amount and its merchant, and pays it, and the payment token reads the payment.
1239
+ * Revolut's hosted checkout page names the same order token in its own paths (/payment-link/<order token>), and the
1240
+ * frame reads the order at merchant-mgmt.revolut.com/orders/token/<order token>. On those hosts every id of their
1241
+ * uuid shape, in every spelling maskUuids reads, reads {token}.
1242
+ */
1243
+ const REVOLUT_HOST = /^(?:sandbox-)?(?:merchant|checkout|merchant-mgmt)\.revolut\.com\.?$/i;
1244
+ function maskRevolutToken(path) {
1245
+ return maskUuids(path, '{token}');
1246
+ }
1247
+ /**
1248
+ * Paytrail names its payment in the path of every route of its card method
1249
+ * (/payments/<transaction>/click-to-pay/loading-and-redirect, and its success and cancel routes), and the same id
1250
+ * opens Paytrail's own payment wall for that payment (pay.paytrail.com/pay/<transaction>), its amount, its
1251
+ * merchant and every method's form, to anyone who holds it (payment-core processors/paytrail.js). On every host
1252
+ * Paytrail owns, any id of its shape, with or without its hyphens, in any case, after an escaped slash or with its
1253
+ * hyphens escaped, reads {transaction}. Payment Highway, Paytrail's card platform, names its card page's session
1254
+ * the same way (/form/<session>/pay_with_click_to_pay), and that path is what carries a card post into the payment,
1255
+ * so on its hosts the same shape reads {session}. The pattern reads a percent escape as one unit and keeps it, so
1256
+ * the hex digits of an escaped separator (%2F, %252F, %3D) never start an id and leave part of it showing.
1257
+ */
1258
+ const PAYTRAIL_HOST = /(?:^|\.)paytrail\.com\.?$/i;
1259
+ const PAYMENT_HIGHWAY_HOST = /(?:^|\.)(?:paymenthighway\.io|sph-test-solinor\.com)\.?$/i;
1260
+ const PAYTRAIL_ID_HYPHEN = '(?:-|%(?:25)*2d)?';
1261
+ 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');
1262
+ function maskPaytrailId(path, label) {
1263
+ return path.replace(PAYTRAIL_ID, (token) => (token.startsWith('%') ? token : label));
1264
+ }
1265
+ function maskPaytrailTransaction(path) {
1266
+ return maskPaytrailId(path, '{transaction}');
1267
+ }
1268
+ function maskPaymentHighwaySession(path) {
1269
+ return maskPaytrailId(path, '{session}');
1270
+ }
1158
1271
  const PATH_CREDENTIALS = [
1272
+ { host: PATH_SECRET_HOST, named: /(?:wspay\.biz|monri\.com)/i, mask: maskPathSecret },
1159
1273
  { host: CREDITGUARD_HOST, named: /creditguard\.co\.il/i, mask: maskCreditguardTxId },
1160
1274
  { host: GRAVY_HOST, named: /gr4vy\.app/i, mask: maskGravySession },
1161
1275
  { host: RAPYD_HOST, named: /rapyd\.net/i, mask: maskRapydToken },
@@ -1168,6 +1282,12 @@ const PATH_CREDENTIALS = [
1168
1282
  { host: PAYPAL_HOST, named: /paypal\.com/i, mask: maskPaypalOrder },
1169
1283
  { host: PAYMONGO_CHECKOUT_HOST, named: /checkout\.paymongo\.com/i, mask: maskPaymongoSession },
1170
1284
  { host: GOPAY_CARD_FORM_HOST, named: /secure(?:\.sandbox)?\.gopay\.com/i, mask: maskGopayToken },
1285
+ { host: KOMOJU_HOST, named: /komoju\.com/i, mask: maskKomojuSession },
1286
+ { host: PRZELEWY24_HOST, named: /przelewy24\.pl/i, mask: maskPrzelewy24Token },
1287
+ { host: REVOLUT_HOST, named: /(?:merchant|checkout|merchant-mgmt)\.revolut\.com/i, mask: maskRevolutToken },
1288
+ { host: SUMUP_API_HOST, named: /api\.sumup\.com/i, mask: maskSumupCheckout },
1289
+ { host: PAYTRAIL_HOST, named: /paytrail\.com/i, mask: maskPaytrailTransaction },
1290
+ { host: PAYMENT_HIGHWAY_HOST, named: /(?:paymenthighway\.io|sph-test-solinor\.com)/i, mask: maskPaymentHighwaySession },
1171
1291
  ];
1172
1292
  /**
1173
1293
  * A URL as it may appear in an error message: origin + path only. A paused
@@ -1192,30 +1312,51 @@ const PATH_CREDENTIALS = [
1192
1312
  * https://www.paypal.com/v2/checkout/orders/{order}/confirm-payment-source.
1193
1313
  * On PayMongo's checkout host the session id reads `{session}`: https://checkout.paymongo.com/{session}.
1194
1314
  * On GoPay's card frame hosts the card form's token reads `{token}`: https://secure.gopay.com/gp-card-comm/h/{token}.
1315
+ * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1316
+ * On Przelewy24's hosts the transaction's token reads `{token}`:
1317
+ * https://secure.przelewy24.pl/trnRequest/{token}.
1318
+ * On Revolut's card payment and hosted checkout hosts an order or payment token reads `{token}`:
1319
+ * https://merchant.revolut.com/api/public/orders/{token}/payments.
1320
+ * On SumUp's API host a checkout id reads `{checkout}`: https://api.sumup.com/v0.2/checkouts/{checkout}.
1321
+ * On Paytrail's hosts a payment's id reads `{transaction}`:
1322
+ * https://services.paytrail.com/payments/{transaction}/click-to-pay/loading-and-redirect, and on Payment
1323
+ * Highway's a card page's session reads `{session}`.
1195
1324
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1196
- * same segments masked.
1325
+ * same segments masked for every processor whose host it names.
1326
+ * A URL is read by its host only when the parser finds one. Text before a URL
1327
+ * can read as a scheme: `see:https://api.stripe.com/...` parses as a URL with
1328
+ * the scheme `see`, no host, and the whole Stripe URL as its path, so a parse
1329
+ * with no host is read as a string that is not a URL. A path can carry a URL of
1330
+ * its own, as `http:https://api.stripe.com/...` does once `https` reads as its
1331
+ * host, so every processor whose host the path names masks the URL too.
1197
1332
  */
1198
1333
  export function redactUrl(raw) {
1199
- // An ACI checkout id is that checkout's only capability; it never rides on an event.
1200
- const aci = redactAciCheckoutId(raw);
1201
- if (aci !== null)
1202
- return aci;
1334
+ let url;
1203
1335
  try {
1204
- const u = new URL(raw);
1205
- // A trailing dot names the same host, so it cannot let the secret through.
1206
- let path = PATH_SECRET_HOSTS.includes(u.hostname.replace(/\.$/, '')) ? u.pathname.replace(PATH_SECRET, '$1{client_secret}') : u.pathname;
1207
- for (const rule of PATH_CREDENTIALS)
1208
- if (rule.host.test(u.hostname))
1209
- path = rule.mask(path);
1210
- return `${u.origin}${path}`;
1336
+ url = new URL(raw);
1211
1337
  }
1212
1338
  catch {
1213
- let bare = raw.split(/[?#]/)[0].replace(PATH_SECRET, '$1{client_secret}');
1214
- for (const rule of PATH_CREDENTIALS)
1215
- if (rule.named.test(bare))
1216
- bare = rule.mask(bare);
1217
- return bare;
1339
+ url = null;
1340
+ }
1341
+ if (!url?.hostname) {
1342
+ const text = raw.split(/[?#]/)[0];
1343
+ return maskNamed(maskPathSecret(text), text);
1218
1344
  }
1345
+ let path = url.pathname;
1346
+ for (const rule of PATH_CREDENTIALS)
1347
+ if (rule.host.test(url.hostname))
1348
+ path = rule.mask(path);
1349
+ return maskNamed(`${url.origin}${path}`, url.pathname);
1350
+ }
1351
+ /**
1352
+ * `text` with the mask of every processor whose host `names` names. `names` is read before any mask
1353
+ * runs, so a mask that rewrites another processor's host cannot keep that processor's mask from running.
1354
+ */
1355
+ function maskNamed(text, names) {
1356
+ for (const rule of PATH_CREDENTIALS)
1357
+ if (rule.named.test(names))
1358
+ text = rule.mask(text);
1359
+ return text;
1219
1360
  }
1220
1361
  /**
1221
1362
  * Refusals a preparation create answers before any preparation exists, surfaced
@@ -2265,10 +2406,10 @@ export class VaultClient {
2265
2406
  // amount_required.
2266
2407
  if (!hasAmount && rec && AGENT_AMOUNT_PROCESSORS.includes(rec.psp))
2267
2408
  throw new AmountRequiredError(rec.psp);
2268
- // A Moneris token is charged later for whatever the merchant's server decides, and a Payfast pay
2269
- // charges the total its payment already holds, so a zero pair would show the cardholder no price
2270
- // at all: it is refused like a missing one.
2271
- if (hasAmount && rec && (rec.psp === MONERIS_PSP || rec.psp === PAYFAST_PSP) && Number(input.amount) <= 0)
2409
+ // A Moneris token is charged later for whatever the merchant's server decides, and a Payfast pay or a
2410
+ // PayMongo attach charges the total its payment or checkout session already holds, so a zero pair
2411
+ // would show the cardholder no price at all: it is refused like a missing one.
2412
+ if (hasAmount && rec && (rec.psp === MONERIS_PSP || rec.psp === PAYFAST_PSP || rec.psp === PAYMONGO_PSP) && Number(input.amount) <= 0)
2272
2413
  throw new AmountRequiredError(rec.psp);
2273
2414
  // Nor is the agent's pair tied to the page by anything but the total the
2274
2415
  // page shows: without it the cardholder could approve one figure while the
@@ -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;
@@ -1,6 +1,7 @@
1
1
  declare var PELECARD_TRANSACTION_HOSTS: readonly string[];
2
2
  declare var PELECARD_TRANSACTION_PATH: string;
3
3
  declare var PELECARD_REFUSAL_REASONS: readonly string[];
4
+ declare var PELECARD_SUCCESS_CODES: readonly string[];
4
5
  declare function isPelecardTransactionUrl(url: any, method?: string): boolean;
5
6
  declare function pelecardBodyRefusal(body: any): "unsupported" | "keep_on_file" | null;
6
7
  declare function isPelecardNewCardBody(body: any): boolean;
@@ -29,4 +30,4 @@ declare function pelecardFacts(status: any, body: any): {
29
30
  outcome: string;
30
31
  refusalCode: any;
31
32
  };
32
- export { PELECARD_REFUSAL_REASONS, PELECARD_TRANSACTION_HOSTS, PELECARD_TRANSACTION_PATH, classifyPelecardRequest, injectPelecardCard, isPelecardNewCardBody, isPelecardTransactionRequest, isPelecardTransactionUrl, pelecardBodyRefusal, pelecardFacts, pelecardKeepOnFileRequested, pelecardRefusalMessage, pelecardResponseOutcome, pelecardTransactionRefusal, sanitizePelecardResponse };
33
+ export { PELECARD_REFUSAL_REASONS, PELECARD_SUCCESS_CODES, PELECARD_TRANSACTION_HOSTS, PELECARD_TRANSACTION_PATH, classifyPelecardRequest, injectPelecardCard, isPelecardNewCardBody, isPelecardTransactionRequest, isPelecardTransactionUrl, pelecardBodyRefusal, pelecardFacts, pelecardKeepOnFileRequested, pelecardRefusalMessage, pelecardResponseOutcome, pelecardTransactionRefusal, sanitizePelecardResponse };