@agent-cards/sdk 0.23.18 → 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
@@ -1187,8 +1187,20 @@ export declare class CheckoutAuthError extends Error {
1187
1187
  * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1188
1188
  * On Przelewy24's hosts the transaction's token reads `{token}`:
1189
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}`.
1190
1196
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1191
- * 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.
1192
1204
  */
1193
1205
  export declare function redactUrl(raw: string): string;
1194
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';
@@ -1041,9 +1041,12 @@ function maskCreditguardTxId(path) {
1041
1041
  * /v2/payment/{client_secret}/confirm (and /show, /status), and with the page's
1042
1042
  * public token that secret confirms the payment and reads its status.
1043
1043
  */
1044
- const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
1044
+ const PATH_SECRET_HOST = /^(?:proxy(?:-test)?\.wspay\.biz|ipg(?:test)?\.monri\.com)\.?$/i;
1045
1045
  /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) and any spelling of its slashes. */
1046
1046
  const PATH_SECRET = new RegExp(`(${SLASH}payment${SLASH}+)[^/]*`, 'gi');
1047
+ function maskPathSecret(path) {
1048
+ return path.replace(PATH_SECRET, '$1{client_secret}');
1049
+ }
1047
1050
  /**
1048
1051
  * Gravy (Gr4vy) gives every merchant hosts of its own under gr4vy.app, and Secure Fields sends the
1049
1052
  * card to /checkout/sessions/<session id>/fields with no auth header, so the session id is that
@@ -1084,10 +1087,11 @@ function maskStripeSession(path) {
1084
1087
  return path.replace(STRIPE_PAYMENT_PAGE, '$1{session}');
1085
1088
  }
1086
1089
  /**
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}.
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}.
1091
1095
  */
1092
1096
  const ACI_HOST = /^(?:eu-prod\.|eu-test\.|test\.)?oppwa\.com\.?$/i;
1093
1097
  const ACI_CHECKOUT = new RegExp(`(${SLASH}v1${SLASH}checkouts${SLASH}+)[^/]*`, 'gi');
@@ -1162,21 +1166,36 @@ function maskGopayToken(path) {
1162
1166
  * the merchant's publishable key, which is public (payment-core komoju.js): with the session id, anyone
1163
1167
  * can read that session's amount and buyer email and pay it. On komoju.com the segment after /sessions,
1164
1168
  * 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.
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.
1168
1175
  */
1169
1176
  const KOMOJU_HOST = /^komoju\.com\.?$/i;
1170
1177
  const KOMOJU_SESSION = new RegExp(`(${SLASH}sessions)(${SLASH}+)[^/]*`, 'gi');
1171
- const KOMOJU_SESSION_ID = /[a-z0-9]{25}(?![a-z0-9])/gi;
1178
+ const KOMOJU_SESSION_ID = /(%(?:25)*[0-9a-f]{2})?[a-z0-9]{25,}/gi;
1172
1179
  function maskKomojuSession(path) {
1173
- return path.replace(KOMOJU_SESSION, '$1$2{session}').replace(KOMOJU_SESSION_ID, '{session}');
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}');
1174
1193
  }
1175
1194
  /**
1176
1195
  * 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.
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.
1180
1199
  */
1181
1200
  /**
1182
1201
  * Redsys's hosted payment page names its session in the path of every page and post after the entry
@@ -1212,7 +1231,45 @@ const PRZELEWY24_TOKEN = new RegExp(`[0-9a-f]{10}${HYPHEN}[0-9a-f]{6}${HYPHEN}[0
1212
1231
  function maskPrzelewy24Token(path) {
1213
1232
  return path.replace(PRZELEWY24_TOKEN, '{token}');
1214
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
+ }
1215
1271
  const PATH_CREDENTIALS = [
1272
+ { host: PATH_SECRET_HOST, named: /(?:wspay\.biz|monri\.com)/i, mask: maskPathSecret },
1216
1273
  { host: CREDITGUARD_HOST, named: /creditguard\.co\.il/i, mask: maskCreditguardTxId },
1217
1274
  { host: GRAVY_HOST, named: /gr4vy\.app/i, mask: maskGravySession },
1218
1275
  { host: RAPYD_HOST, named: /rapyd\.net/i, mask: maskRapydToken },
@@ -1227,6 +1284,10 @@ const PATH_CREDENTIALS = [
1227
1284
  { host: GOPAY_CARD_FORM_HOST, named: /secure(?:\.sandbox)?\.gopay\.com/i, mask: maskGopayToken },
1228
1285
  { host: KOMOJU_HOST, named: /komoju\.com/i, mask: maskKomojuSession },
1229
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 },
1230
1291
  ];
1231
1292
  /**
1232
1293
  * A URL as it may appear in an error message: origin + path only. A paused
@@ -1254,30 +1315,48 @@ const PATH_CREDENTIALS = [
1254
1315
  * On komoju.com a session id reads `{session}`: https://komoju.com/api/v1/sessions/{session}/pay.
1255
1316
  * On Przelewy24's hosts the transaction's token reads `{token}`:
1256
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}`.
1257
1324
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1258
- * 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.
1259
1332
  */
1260
1333
  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;
1334
+ let url;
1265
1335
  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}`;
1336
+ url = new URL(raw);
1273
1337
  }
1274
1338
  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;
1339
+ url = null;
1280
1340
  }
1341
+ if (!url?.hostname) {
1342
+ const text = raw.split(/[?#]/)[0];
1343
+ return maskNamed(maskPathSecret(text), text);
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;
1281
1360
  }
1282
1361
  /**
1283
1362
  * Refusals a preparation create answers before any preparation exists, surfaced
@@ -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 };
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 7b1296f83822ae81c64835c270ff9048fddb6efe0d7cdbe37ba42b8a81ac03d0
3
+ // artifact-sha256: 30904474bf2bd6c0b0aac97deda8fe133514a241e7060e782118af5fba813fe4
4
4
  // src/json-path.js
5
5
  function roundTrips(parsed, body) {
6
6
  try {
@@ -14,6 +14,7 @@ function roundTrips(parsed, body) {
14
14
  var PELECARD_TRANSACTION_HOSTS = /* @__PURE__ */ Object.freeze(["gateway21.pelecard.biz", "gateway20.pelecard.biz"]);
15
15
  var PELECARD_TRANSACTION_PATH = "/PaymentGW/Transaction";
16
16
  var PELECARD_REFUSAL_REASONS = /* @__PURE__ */ Object.freeze(["unsupported", "keep_on_file"]);
17
+ var PELECARD_SUCCESS_CODES = /* @__PURE__ */ Object.freeze(["000", "302"]);
17
18
  var UNSUPPORTED_REFUSAL = "Refusing to pay: unsupported Pelecard card payment request.";
18
19
  var KEEP_ON_FILE_REFUSAL = "Refusing to pay: this Pelecard payment would keep the card on file without your consent. Nothing was sent.";
19
20
  var TEST_CARD_REFUSAL = "Refusing to pay: a Pelecard checkout takes only a documented test card, because the merchant can have Pelecard keep the card without asking you. Nothing was sent.";
@@ -214,6 +215,9 @@ function landingUrl(value) {
214
215
  return null;
215
216
  }
216
217
  }
218
+ function inPageDebit(value) {
219
+ return absent(value.UrlKashCash) && value.StatusDescription !== "Gama Completion Required" && (value.DisplayIshurPopUp === void 0 || value.DisplayIshurPopUp === null || value.DisplayIshurPopUp === false);
220
+ }
217
221
  function pelecardResponseOutcome(status, body) {
218
222
  if (Number.isInteger(status) && status >= 400 && status < 500)
219
223
  return clientErrorOutcome(status, body);
@@ -225,7 +229,7 @@ function pelecardResponseOutcome(status, body) {
225
229
  const code = value.StatusCode;
226
230
  if (!exact(/^[0-9]{3}$/, code))
227
231
  return { outcome: "unknown", reason: "pelecard_unexpected_response" };
228
- if (code !== "000") {
232
+ if (!PELECARD_SUCCESS_CODES.includes(code)) {
229
233
  return absent(value.ApprovalNo) ? { outcome: "refused", code } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
230
234
  }
231
235
  if (!REDIRECT_KEYS.every((key) => typeof value[key] === "string") || owns(value, "ParamX") && value.ParamX !== null && typeof value.ParamX !== "string") {
@@ -233,6 +237,9 @@ function pelecardResponseOutcome(status, body) {
233
237
  }
234
238
  if (value.DisplayIshurPopUp === true || !absent(value.UrlOpenFinance))
235
239
  return { outcome: "unknown", reason: "pelecard_unconfirmed" };
240
+ if (code === "302" && value.LandingURL === "") {
241
+ return inPageDebit(value) ? { outcome: "approved" } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
242
+ }
236
243
  const landing = landingUrl(value.LandingURL);
237
244
  if (!landing)
238
245
  return { outcome: "unknown", reason: "pelecard_unconfirmed" };
@@ -246,7 +253,7 @@ function clientErrorOutcome(status, body) {
246
253
  if (!value || !owns(value, "StatusCode") && !owns(value, "ApprovalNo"))
247
254
  return { outcome: "refused", code: `http_${status}` };
248
255
  const code = value.StatusCode;
249
- return exact(/^[0-9]{3}$/, code) && code !== "000" && absent(value.ApprovalNo) ? { outcome: "refused", code } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
256
+ return exact(/^[0-9]{3}$/, code) && !PELECARD_SUCCESS_CODES.includes(code) && absent(value.ApprovalNo) ? { outcome: "refused", code } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
250
257
  }
251
258
  function sanitizePelecardResponse(body) {
252
259
  const fail = () => {
@@ -260,7 +267,7 @@ function sanitizePelecardResponse(body) {
260
267
  const safe = {
261
268
  LandingURL: value.LandingURL,
262
269
  Target: value.Target,
263
- StatusCode: "000",
270
+ StatusCode: value.StatusCode,
264
271
  StatusDescription: markupSafe(value.StatusDescription, 256) && !panLike(value.StatusDescription) ? value.StatusDescription : "",
265
272
  ApprovalNo: value.ApprovalNo,
266
273
  Token: "",
@@ -271,6 +278,8 @@ function sanitizePelecardResponse(body) {
271
278
  safe.UserKey = value.UserKey;
272
279
  safe.UseEndOfProcess = value.UseEndOfProcess;
273
280
  safe.FeedbackDataTransferMethod = value.FeedbackDataTransferMethod;
281
+ if (value.LandingURL === "")
282
+ safe.UrlOpenFinance = "";
274
283
  return JSON.stringify(safe);
275
284
  }
276
285
  function pelecardFacts(status, body) {
@@ -280,4 +289,4 @@ function pelecardFacts(status, body) {
280
289
  refusalCode: verdict.outcome === "refused" && /^[0-9]{3}$/.test(verdict.code) ? verdict.code : null
281
290
  };
282
291
  }
283
- export { PELECARD_REFUSAL_REASONS, PELECARD_TRANSACTION_HOSTS, PELECARD_TRANSACTION_PATH, classifyPelecardRequest, injectPelecardCard, isPelecardNewCardBody, isPelecardTransactionRequest, isPelecardTransactionUrl, pelecardBodyRefusal, pelecardFacts, pelecardKeepOnFileRequested, pelecardRefusalMessage, pelecardResponseOutcome, pelecardTransactionRefusal, sanitizePelecardResponse };
292
+ 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 };