@agent-cards/sdk 0.23.9 → 0.23.10

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 (58) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/PREFLIGHT.md +7 -5
  3. package/README.md +181 -92
  4. package/dist/aci.d.ts +7 -5
  5. package/dist/aci.generated.d.ts +2 -1
  6. package/dist/aci.generated.js +14 -4
  7. package/dist/aci.js +28 -7
  8. package/dist/amadeus-checkout.generated.js +2 -2
  9. package/dist/builtin-registry.generated.js +1 -1
  10. package/dist/card-fields.generated.js +163 -9
  11. package/dist/cdp.js +79 -34
  12. package/dist/client.d.ts +61 -31
  13. package/dist/client.js +99 -42
  14. package/dist/creditguard.generated.d.ts +1 -1
  15. package/dist/creditguard.generated.js +1 -3
  16. package/dist/cse-body.d.ts +7 -6
  17. package/dist/cse-body.js +8 -7
  18. package/dist/datatrans.generated.js +8 -5
  19. package/dist/elavon-converge.d.ts +1 -1
  20. package/dist/elavon-converge.generated.js +7 -4
  21. package/dist/elavon-converge.js +4 -3
  22. package/dist/fiserv.d.ts +9 -5
  23. package/dist/fiserv.generated.d.ts +3 -3
  24. package/dist/fiserv.generated.js +50 -20
  25. package/dist/fiserv.js +10 -6
  26. package/dist/payrails.d.ts +78 -18
  27. package/dist/payrails.generated.d.ts +12 -2
  28. package/dist/payrails.generated.js +213 -18
  29. package/dist/payrails.js +165 -44
  30. package/dist/pre-claim.js +51 -29
  31. package/dist/preflight-capabilities.generated.d.ts +0 -106
  32. package/dist/preflight-capabilities.generated.js +0 -153
  33. package/dist/preflight-catalog.json +61 -181
  34. package/dist/preflight-schemas.json +6 -6
  35. package/dist/preflight.generated.d.ts +0 -106
  36. package/dist/preflight.generated.js +27 -15
  37. package/dist/preparation.d.ts +2 -0
  38. package/dist/preparation.js +4 -2
  39. package/dist/prepared-processor.d.ts +19 -5
  40. package/dist/prepared-processor.js +21 -5
  41. package/dist/primer.generated.d.ts +5 -1
  42. package/dist/primer.generated.js +200 -33
  43. package/dist/registry.d.ts +18 -12
  44. package/dist/registry.js +11 -8
  45. package/dist/shiji-sts.d.ts +24 -22
  46. package/dist/shiji-sts.generated.d.ts +1 -1
  47. package/dist/shiji-sts.generated.js +12 -8
  48. package/dist/shiji-sts.js +49 -46
  49. package/dist/stripe-checkout.generated.js +146 -49
  50. package/dist/verifone.generated.js +192 -17
  51. package/dist/verifone.js +1 -1
  52. package/dist/wspay.d.ts +17 -16
  53. package/dist/wspay.generated.d.ts +6 -1
  54. package/dist/wspay.generated.js +13 -3
  55. package/dist/wspay.js +9 -11
  56. package/examples/preflight/kernel-native/README.md +1 -1
  57. package/examples/preflight/kernel-native/inventory.json +1 -19
  58. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 2bf5a6f966e390caa86efaa4be753222abb5606a8689cc8cfa42c2466c65ded9
3
+ // artifact-sha256: 66c54c571ca1294220b070eaca1ca3888feebcd7f2562b761d74ded25a453c57
4
4
  // src/json-path.js
5
5
  function roundTrips(parsed, body) {
6
6
  try {
@@ -237,6 +237,10 @@ var PAYRAILS_CAPABILITY = "payrails_device_encrypt";
237
237
  var PAYRAILS_OPERATION = "payrails.device_encrypted_card";
238
238
  var PAYRAILS_SAVE_PATH = "/public/payment/instruments";
239
239
  var PAYRAILS_SAVE_HOSTS = Object.freeze(["api-pub.payrails.io", "api-pub.staging.payrails.io", "sandbox-api-pub.staging.payrails.io"]);
240
+ var PAYRAILS_MERCHANT_API_HOST = /^api-pub\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.payrails\.io$/;
241
+ var PAYRAILS_AUTHORIZE_WORKFLOW = "payment-acceptance";
242
+ var AUTHORIZE_PATH = /^\/public\/merchant\/workflows\/payment-acceptance\/executions\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/authorize$/;
243
+ var EXECUTION_PATH = /^\/public\/merchant\/workflows\/payment-acceptance\/executions\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
240
244
  var PAYRAILS_JWE_HEADER = '{"alg":"RSA-OAEP-256","enc":"A256CBC-HS512"}';
241
245
  var PAYRAILS_JWE_HEADER_B64 = "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0";
242
246
  var PAYRAILS_KEY_BITS = Object.freeze([2048, 3072, 4096]);
@@ -292,21 +296,33 @@ function base64UrlDecode(text, max) {
292
296
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
293
297
  return base64UrlEncode(bytes) === text ? bytes : null;
294
298
  }
295
- function isPayrailsSaveUrl(url, { exact: exact2 = false } = {}) {
299
+ function isPayrailsApiHost(hostname) {
300
+ return typeof hostname === "string" && (PAYRAILS_SAVE_HOSTS.includes(hostname) || PAYRAILS_MERCHANT_API_HOST.test(hostname));
301
+ }
302
+ function payrailsApiUrl(url, exact2) {
296
303
  if (typeof url !== "string" || url.length > 2048)
297
- return false;
304
+ return null;
298
305
  let parsed;
299
306
  try {
300
307
  parsed = new URL(url);
301
308
  }
302
309
  catch {
303
- return false;
310
+ return null;
304
311
  }
305
- if (parsed.protocol !== "https:" || parsed.port || parsed.username || parsed.password)
306
- return false;
307
- if (!PAYRAILS_SAVE_HOSTS.includes(parsed.hostname) || parsed.pathname !== PAYRAILS_SAVE_PATH)
308
- return false;
309
- return !exact2 || parsed.search === "" && parsed.hash === "" && !/[?#]/.test(url);
312
+ if (parsed.protocol !== "https:" || parsed.port || parsed.username || parsed.password || !isPayrailsApiHost(parsed.hostname))
313
+ return null;
314
+ return !exact2 || parsed.search === "" && parsed.hash === "" && !/[?#]/.test(url) ? parsed : null;
315
+ }
316
+ function isPayrailsSaveUrl(url, { exact: exact2 = false } = {}) {
317
+ return payrailsApiUrl(url, exact2)?.pathname === PAYRAILS_SAVE_PATH;
318
+ }
319
+ function isPayrailsAuthorizeUrl(url, { exact: exact2 = false } = {}) {
320
+ const parsed = payrailsApiUrl(url, exact2);
321
+ return !!parsed && AUTHORIZE_PATH.test(parsed.pathname);
322
+ }
323
+ function payrailsExecutionId(url) {
324
+ const parsed = payrailsApiUrl(url, true);
325
+ return parsed ? AUTHORIZE_PATH.exec(parsed.pathname)?.[1] ?? null : null;
310
326
  }
311
327
  function payrailsJweParts(jwe) {
312
328
  if (typeof jwe !== "string" || jwe.length > 8192)
@@ -386,8 +402,141 @@ function payrailsCardOnFileRefusal(body) {
386
402
  const usage = payrailsKeepsCardFor(save.value);
387
403
  return usage === null || PAYRAILS_CARD_ON_FILE_CONSENTED.includes(usage) ? null : "payrails_card_on_file_consent_required";
388
404
  }
405
+ var AUTHORIZE_LIMIT = 32768;
406
+ var AUTHORIZE_KEYS = Object.freeze(["amount", "paymentComposition", "returnInfo", "meta"]);
407
+ var COMPOSITION_KEYS = Object.freeze([
408
+ "paymentMethodCode",
409
+ "integrationType",
410
+ "amount",
411
+ "storeInstrument",
412
+ "paymentInstrumentData",
413
+ "enrollInstrumentToNetworkOffers"
414
+ ]);
415
+ var RETURN_KEYS = Object.freeze(["success", "cancel", "error", "pending"]);
416
+ var AMOUNT_VALUE = /^[0-9]{1,15}(?:\.[0-9]{1,12})?$/;
417
+ var CURRENCY = /^[A-Z]{3}$/;
418
+ var scalar = (value) => value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= 2048;
419
+ var META_KEYS = Object.freeze(["clientContext", "risk", "order", "customer"]);
420
+ var ORDER_KEYS = Object.freeze(["processingType"]);
421
+ var CUSTOMER_KEYS = Object.freeze([]);
422
+ var caseCollides = (value) => {
423
+ const lower = Object.keys(value).map((key) => key.toLowerCase());
424
+ return new Set(lower).size !== lower.length;
425
+ };
426
+ var onlyKeys = (value, keys) => record(value) && !caseCollides(value) && Object.keys(value).every((key) => keys.includes(key));
427
+ function payrailsMetaRefusal(meta) {
428
+ if (owns(meta, "subscription"))
429
+ return "payrails_future_usage_refused";
430
+ if (!onlyKeys(meta, META_KEYS))
431
+ return "payrails_request_malformed";
432
+ if (owns(meta, "clientContext") && !(record(meta.clientContext) && !caseCollides(meta.clientContext) && Object.values(meta.clientContext).every(scalar))) {
433
+ return "payrails_request_malformed";
434
+ }
435
+ if (owns(meta, "risk")) {
436
+ if (!record(meta.risk) || caseCollides(meta.risk))
437
+ return "payrails_request_malformed";
438
+ for (const [key, value] of Object.entries(meta.risk)) {
439
+ const sound = key === "sessionId" ? plainText(value, 256) : key === "sessions" ? Array.isArray(value) && value.length <= 8 && value.every((session) => record(session) && !caseCollides(session) && Object.values(session).every(scalar)) : key === "threeDSMode" ? value === "Default" : /^skip[A-Z]/.test(key) ? value === false : false;
440
+ if (!sound)
441
+ return "payrails_request_malformed";
442
+ }
443
+ }
444
+ if (owns(meta, "order")) {
445
+ const order = meta.order;
446
+ if (!onlyKeys(order, ORDER_KEYS))
447
+ return "payrails_request_malformed";
448
+ if (owns(order, "processingType") && order.processingType !== "Regular") {
449
+ if (order.processingType === "CardOnFile" || order.processingType === "UnscheduledCardOnFile")
450
+ return "payrails_card_on_file_consent_required";
451
+ return order.processingType === "Subscription" ? "payrails_future_usage_refused" : "payrails_request_malformed";
452
+ }
453
+ }
454
+ if (owns(meta, "customer") && !onlyKeys(meta.customer, CUSTOMER_KEYS))
455
+ return "payrails_request_malformed";
456
+ return null;
457
+ }
458
+ function readAmount(value) {
459
+ if (!record(value) || Object.keys(value).length !== 2 || typeof value.value !== "string" || !AMOUNT_VALUE.test(value.value) || typeof value.currency !== "string" || !CURRENCY.test(value.currency))
460
+ return null;
461
+ return value;
462
+ }
463
+ function readAuthorizeBody(body) {
464
+ const malformed = refusal("payrails_request_malformed");
465
+ if (typeof body !== "string" || body.length === 0 || body.length > AUTHORIZE_LIMIT)
466
+ return malformed;
467
+ let parsed;
468
+ try {
469
+ parsed = JSON.parse(body);
470
+ }
471
+ catch {
472
+ return malformed;
473
+ }
474
+ if (!record(parsed) || !roundTrips(parsed, body) || !sameKeys(parsed, AUTHORIZE_KEYS) || !record(parsed.meta))
475
+ return malformed;
476
+ const amount = readAmount(parsed.amount);
477
+ const parts = parsed.paymentComposition;
478
+ const part = Array.isArray(parts) && parts.length === 1 && record(parts[0]) ? parts[0] : null;
479
+ if (!amount || !part || !sameKeys(part, COMPOSITION_KEYS) || part.paymentMethodCode !== "card" || part.integrationType !== "api")
480
+ return malformed;
481
+ const partAmount = readAmount(part.amount);
482
+ if (!partAmount || partAmount.value !== amount.value || partAmount.currency !== amount.currency)
483
+ return malformed;
484
+ if (typeof part.storeInstrument !== "boolean" || typeof part.enrollInstrumentToNetworkOffers !== "boolean")
485
+ return malformed;
486
+ const data = part.paymentInstrumentData;
487
+ const keys = record(data) ? Object.keys(data) : [];
488
+ if (keys[0] !== "encryptedData" || keys[1] !== "vaultProviderConfigId" || !(keys.length === 2 || keys.length === 3 && keys[2] === "billingAddress" && record(data.billingAddress)))
489
+ return malformed;
490
+ if (typeof data.vaultProviderConfigId !== "string" || !UUID.test(data.vaultProviderConfigId))
491
+ return malformed;
492
+ const back = parsed.returnInfo;
493
+ if (!record(back) || !sameKeys(back, RETURN_KEYS) || !RETURN_KEYS.every((key) => plainText(back[key], 2048)))
494
+ return malformed;
495
+ const metaRefusal = payrailsMetaRefusal(parsed.meta);
496
+ if (metaRefusal)
497
+ return refusal(metaRefusal);
498
+ const jwe = payrailsJweParts(data.encryptedData);
499
+ if (!jwe)
500
+ return refusal("payrails_jwe_malformed");
501
+ return {
502
+ value: Object.freeze({
503
+ amount: Object.freeze({ value: amount.value, currency: amount.currency }),
504
+ storeInstrument: part.storeInstrument,
505
+ enrollInstrumentToNetworkOffers: part.enrollInstrumentToNetworkOffers,
506
+ encryptedData: data.encryptedData,
507
+ vaultProviderConfigId: data.vaultProviderConfigId,
508
+ wrappedKeyBytes: jwe.wrappedKey.length
509
+ })
510
+ };
511
+ }
512
+ function payrailsAuthorizeRefusal(body) {
513
+ const payment = readAuthorizeBody(body);
514
+ if (payment.code)
515
+ return payment.code;
516
+ return payment.value.storeInstrument || payment.value.enrollInstrumentToNetworkOffers ? "payrails_card_on_file_consent_required" : null;
517
+ }
518
+ function parsePayrailsAuthorize(body) {
519
+ return readAuthorizeBody(body).value ?? null;
520
+ }
521
+ function authorizeShaped(body) {
522
+ if (typeof body !== "string" || body.length > AUTHORIZE_LIMIT)
523
+ return false;
524
+ try {
525
+ const parsed = JSON.parse(body);
526
+ return record(parsed) && owns(parsed, "paymentComposition");
527
+ }
528
+ catch {
529
+ return false;
530
+ }
531
+ }
532
+ function compositionCiphertext(parsed) {
533
+ const parts = record(parsed) ? parsed.paymentComposition : null;
534
+ const part = Array.isArray(parts) ? parts[0] : null;
535
+ const data = record(part) ? part.paymentInstrumentData : null;
536
+ return record(data) && owns(data, "encryptedData") ? data.encryptedData : void 0;
537
+ }
389
538
  function classifyPayrailsRequest(url, method, body) {
390
- if (method !== "POST" || !isPayrailsSaveUrl(url))
539
+ if (method !== "POST" || !(isPayrailsSaveUrl(url) || isPayrailsAuthorizeUrl(url)))
391
540
  return null;
392
541
  if (typeof body !== "string" || body === "")
393
542
  return "continue";
@@ -399,8 +548,8 @@ function classifyPayrailsRequest(url, method, body) {
399
548
  return "continue";
400
549
  }
401
550
  const data = record(parsed) ? parsed.data : null;
402
- const value = record(data) && owns(data, "encryptedData") ? data.encryptedData : void 0;
403
- return typeof value === "string" && value !== "" || typeof value === "number" ? "card" : "continue";
551
+ const holds = (value) => typeof value === "string" && value !== "" || typeof value === "number";
552
+ return holds(record(data) && owns(data, "encryptedData") ? data.encryptedData : void 0) || holds(compositionCiphertext(parsed)) ? "card" : "continue";
404
553
  }
405
554
  function payrailsAuthorizeCarriesCard(url, method, body) {
406
555
  if (method !== "POST" || typeof url !== "string" || typeof body !== "string" || body.length > 262144)
@@ -437,13 +586,13 @@ function payrailsSessionKeyBytes(spki) {
437
586
  }
438
587
  }
439
588
  function payrailsSessionKeyRefusal(spki, body) {
440
- const save = readSaveBody(body);
441
- if (save.code)
442
- return save.code;
589
+ const request = authorizeShaped(body) ? readAuthorizeBody(body) : readSaveBody(body);
590
+ if (request.code)
591
+ return request.code;
443
592
  const size = payrailsSessionKeyBytes(spki);
444
593
  if (size === null)
445
594
  return "payrails_key_invalid";
446
- return size === save.value.wrappedKeyBytes ? null : "payrails_key_mismatch";
595
+ return size === request.value.wrappedKeyBytes ? null : "payrails_key_mismatch";
447
596
  }
448
597
  var TOKEN_LIMIT = 8192;
449
598
  var CLAIM_NAME = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;
@@ -571,7 +720,43 @@ function instrumentMatchesCard(instrument, card) {
571
720
  const data = instrument.data;
572
721
  return number.length >= 12 && data.suffix === number.slice(-4) && exact(/^(?:0[1-9]|1[0-2])$/, data.expiryMonth) && Number(data.expiryMonth) === month && exact(/^(?:[0-9]{2}|[0-9]{4})$/, data.expiryYear) && Number(data.expiryYear) % 100 === year % 100;
573
722
  }
574
- function payrailsResponseOutcome(status, text, { card } = {}) {
723
+ var ACTION_TIME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/;
724
+ function payrailsLink(value) {
725
+ if (!plainText(value, 2048))
726
+ return null;
727
+ let parsed;
728
+ try {
729
+ parsed = new URL(value);
730
+ }
731
+ catch {
732
+ return null;
733
+ }
734
+ return parsed.protocol === "https:" && !parsed.port && !parsed.username && !parsed.password && /(?:^|\.)payrails\.io$/.test(parsed.hostname) ? value : null;
735
+ }
736
+ function readAuthorizeAction(text) {
737
+ const value = readJson(text);
738
+ if (!record(value) || value.name !== "authorize" || typeof value.actionId !== "string" || !UUID.test(value.actionId) || !record(value.links))
739
+ return null;
740
+ const execution = payrailsLink(value.links.execution);
741
+ if (!execution)
742
+ return null;
743
+ return compact({ name: "authorize", actionId: value.actionId, executedAt: pick(value.executedAt, ACTION_TIME), links: { execution } });
744
+ }
745
+ function namesPausedExecution(link, url) {
746
+ const paused = payrailsApiUrl(url, true);
747
+ const id = paused ? AUTHORIZE_PATH.exec(paused.pathname)?.[1] : void 0;
748
+ const named = payrailsApiUrl(link, true);
749
+ return !!id && !!named && named.hostname === paused.hostname && EXECUTION_PATH.exec(named.pathname)?.[1] === id;
750
+ }
751
+ function sanitizePayrailsAuthorizeResponse(text, url) {
752
+ const action = readAuthorizeAction(text);
753
+ if (!action)
754
+ throw new Error("Refusing to record: the Payrails answer is not an authorize action.");
755
+ if (!namesPausedExecution(action.links.execution, url))
756
+ throw new Error("Refusing to record: the Payrails answer names another payment.");
757
+ return JSON.stringify(action);
758
+ }
759
+ function payrailsResponseOutcome(status, text, { card, url } = {}) {
575
760
  if (!Number.isInteger(status))
576
761
  return { outcome: "unknown", reason: "payrails_unconfirmed" };
577
762
  if (status === 409 || status === 429)
@@ -580,6 +765,16 @@ function payrailsResponseOutcome(status, text, { card } = {}) {
580
765
  return { outcome: "refused", code: payrailsFacts(text).refusalCode || `http_${status}` };
581
766
  if (status >= 500)
582
767
  return { outcome: "unknown", reason: "server_error" };
768
+ if (isPayrailsAuthorizeUrl(url)) {
769
+ if (status < 200 || status > 299)
770
+ return { outcome: "unknown", reason: "payrails_unconfirmed" };
771
+ const action = readAuthorizeAction(text);
772
+ if (!action)
773
+ return { outcome: "unknown", reason: "payrails_unsafe_response" };
774
+ if (!namesPausedExecution(action.links.execution, url))
775
+ return { outcome: "unknown", reason: "payrails_execution_mismatch" };
776
+ return { outcome: "approved", body: JSON.stringify(action) };
777
+ }
583
778
  if (status !== 201)
584
779
  return { outcome: "unknown", reason: "payrails_unconfirmed" };
585
780
  const instrument = readInstrument(text);
@@ -593,4 +788,4 @@ function payrailsResponseOutcome(status, text, { card } = {}) {
593
788
  return { outcome: "unknown", reason: "payrails_instrument_mismatch" };
594
789
  return { outcome: "approved", body: sanitizePayrailsResponse(text) };
595
790
  }
596
- export { PAYRAILS_CAPABILITY, PAYRAILS_CARD_ON_FILE_CONSENTED, PAYRAILS_FUTURE_USAGES, PAYRAILS_JWE_HEADER, PAYRAILS_JWE_HEADER_B64, PAYRAILS_KEY_BITS, PAYRAILS_OPERATION, PAYRAILS_PSP, PAYRAILS_REFUSALS, PAYRAILS_SAVE_HOSTS, PAYRAILS_SAVE_PATH, PAYRAILS_SERVED_CARD_FACTS, PAYRAILS_SESSION_MIN_SECONDS, PAYRAILS_UNUSABLE_STATUSES, PAYRAILS_USABLE_STATUSES, base64UrlDecode, base64UrlEncode, classifyPayrailsRequest, isPayrailsSaveUrl, parsePayrailsSave, payrailsAuthorizeCarriesCard, payrailsCardOnFileRefusal, payrailsFacts, payrailsJweParts, payrailsKeepsCardFor, payrailsRefusalMessage, payrailsResponseOutcome, payrailsSessionKeyBytes, payrailsSessionKeyRefusal, payrailsTemplateRefusal, payrailsTokenClaims, payrailsWorkspacePinRefusal, sanitizePayrailsResponse };
791
+ export { PAYRAILS_AUTHORIZE_WORKFLOW, PAYRAILS_CAPABILITY, PAYRAILS_CARD_ON_FILE_CONSENTED, PAYRAILS_FUTURE_USAGES, PAYRAILS_JWE_HEADER, PAYRAILS_JWE_HEADER_B64, PAYRAILS_KEY_BITS, PAYRAILS_MERCHANT_API_HOST, PAYRAILS_OPERATION, PAYRAILS_PSP, PAYRAILS_REFUSALS, PAYRAILS_SAVE_HOSTS, PAYRAILS_SAVE_PATH, PAYRAILS_SERVED_CARD_FACTS, PAYRAILS_SESSION_MIN_SECONDS, PAYRAILS_UNUSABLE_STATUSES, PAYRAILS_USABLE_STATUSES, base64UrlDecode, base64UrlEncode, classifyPayrailsRequest, isPayrailsApiHost, isPayrailsAuthorizeUrl, isPayrailsSaveUrl, parsePayrailsAuthorize, parsePayrailsSave, payrailsAuthorizeCarriesCard, payrailsAuthorizeRefusal, payrailsCardOnFileRefusal, payrailsExecutionId, payrailsFacts, payrailsJweParts, payrailsKeepsCardFor, payrailsRefusalMessage, payrailsResponseOutcome, payrailsSessionKeyBytes, payrailsSessionKeyRefusal, payrailsTemplateRefusal, payrailsTokenClaims, payrailsWorkspacePinRefusal, sanitizePayrailsAuthorizeResponse, sanitizePayrailsResponse };
package/dist/payrails.js CHANGED
@@ -1,17 +1,28 @@
1
- // Payrails web SDK v6 secure fields (Blacklane's consumer checkout), as the checkout SDK sees it.
1
+ // Payrails web SDK secure fields (Blacklane's consumer checkout, FlixBus's guest checkout), as the
2
+ // checkout SDK sees it.
2
3
  //
3
4
  // payrails.js 6.5.0 saves a card with one fetch from the merchant page: a POST to the session's
4
5
  // vaultConfiguration.links.saveInstrument.href (https://api-pub.payrails.io/public/payment/instruments;
5
- // test hosts api-pub.staging.payrails.io and sandbox-api-pub.staging.payrails.io). Its body carries
6
- // the whole card as one JWE, made in Payrails' secure frame under a per-session RSA key. That key
7
- // is not in the paused request: it comes in the init payload ({version, data}) the merchant's
8
- // backend hands the page before the card form renders. So the runtime reads the page's own XHR and
9
- // fetch answers for that payload and keeps, per attachment and in memory only, each session's
10
- // token, holderReference, providerConfigId, saveInstrument href and key. When a card save pauses,
11
- // exactly one captured session must match it on all four, seen at most nine minutes before. Its key
12
- // rides on the create (request.payrails.encryption_public_key); the cardholder's device encrypts
13
- // the real card under it and sends the save itself (mode device_encrypt); and the page receives
14
- // only Payrails' 201, rebuilt from an allow-list. The device's ciphertext never reaches this runtime.
6
+ // test hosts api-pub.staging.payrails.io and sandbox-api-pub.staging.payrails.io). The card form's Pay
7
+ // button (web SDK 5.50.0 at FlixBus) pays with the card instead: a POST to the session's execution
8
+ // authorize link, /public/merchant/workflows/payment-acceptance/executions/<id>/authorize. A merchant
9
+ // can have its own public API host, api-pub.<merchant>.payrails.io (FlixBus: api-pub.flix.payrails.io).
10
+ // Either body carries the whole card as one JWE, made in Payrails' secure frame under a per-session RSA
11
+ // key. That key is not in the paused request: it comes in the init payload ({version, data}) the
12
+ // merchant's backend hands the page before the card form renders, at the top of an answer
13
+ // (Blacklane) or deep inside one (FlixBus's /payment/configuration/list, six levels down). So the
14
+ // runtime reads the page's own XHR and fetch answers for that payload and keeps, per attachment and
15
+ // in memory only, each session's token, holderReference, providerConfigId, saveInstrument href,
16
+ // execution id and key. When a card request pauses, exactly one captured session must match it, seen
17
+ // at most nine minutes before: a save on its token, its URL, its holderReference and its
18
+ // providerConfigId; a card-form payment on its token, its execution and host, and its
19
+ // providerConfigId. Its key rides on the create (request.payrails.encryption_public_key), and for a
20
+ // payment its holderReference too (request.payrails.holder_reference), which the payment's JWE
21
+ // carries and its body does not. The cardholder's device encrypts the real card under the key and
22
+ // sends the request itself (mode device_encrypt), and the page receives only Payrails' answer, rebuilt
23
+ // from an allow-list: a save's 201, or the authorize action a payment's 202 names, whose execution
24
+ // link the page's SDK then polls for the issuer's answer. The device's ciphertext never reaches this
25
+ // runtime.
15
26
  //
16
27
  // The registry entry ships dark (capability payrails_device_encrypt). This build declares the
17
28
  // capability and the mode on syncRegistry, and the API serves the entry only to an org Agentcard
@@ -26,33 +37,43 @@
26
37
  // merchant's integration decides that shape, so the page's next save would be the same;
27
38
  // - a card save with no captured session, or whose captured key is not as long as the key the
28
39
  // page's frame wrapped, aborts alone: the agent reloads the checkout and tries again;
29
- // - a payment that carries an encrypted card inside paymentComposition (paying with the card
30
- // form, or a stored card with a CVC field) aborts as an unsupported checkout, so the agent's
31
- // placeholder card never reaches an authorization;
40
+ // - the card form's payment on an authorize link the registry recognizes is judged the same
41
+ // way: payment-core's guard first (a payment that stores the card at the merchant aborts as
42
+ // payrails_card_on_file_consent_required, another shape as unsupported_payrails_card_payment,
43
+ // and the attachment holds), then the captured session, which a miss aborts alone;
44
+ // - any other payment that carries an encrypted card inside paymentComposition (a stored card
45
+ // with a CVC field, a split payment, another link, or a registry that does not recognize the
46
+ // payment) aborts as an unsupported checkout, so the agent's placeholder card never reaches
47
+ // an authorization;
32
48
  // - a POST with no body the gate can read (none, an empty one, or one the adapter could not
33
49
  // read) aborts alone, on both adapters alike (pre-claim.ts says why), except on Payrails'
34
50
  // static host, which serves files and takes no payment;
35
51
  // - every other request continues.
36
- import { PAYRAILS_CAPABILITY, PAYRAILS_PSP, PAYRAILS_SAVE_HOSTS, PAYRAILS_SAVE_PATH, classifyPayrailsRequest, isPayrailsSaveUrl, payrailsAuthorizeCarriesCard, payrailsCardOnFileRefusal, payrailsResponseOutcome, payrailsSessionKeyRefusal, } from './payrails.generated.js';
52
+ import { PAYRAILS_CAPABILITY, PAYRAILS_PSP, PAYRAILS_SAVE_HOSTS, PAYRAILS_SAVE_PATH, classifyPayrailsRequest, isPayrailsAuthorizeUrl, isPayrailsSaveUrl, payrailsAuthorizeCarriesCard, payrailsAuthorizeRefusal, payrailsCardOnFileRefusal, payrailsExecutionId, payrailsResponseOutcome, payrailsSessionKeyRefusal, } from './payrails.generated.js';
37
53
  export { PAYRAILS_CAPABILITY, PAYRAILS_PSP };
38
54
  /**
39
55
  * How the pre-claim gate reads a POST to a Payrails host: `card` for a card save on a save URL
40
56
  * (data.encryptedData present, payment-core's pause predicate), `card_payment` for a payment that
41
- * carries an encrypted card inside paymentComposition, and null for anything else, which continues.
57
+ * carries an encrypted card inside paymentComposition, the card form's included, and null for
58
+ * anything else, which continues.
42
59
  */
43
60
  export function payrailsRequestKind(url, method, body) {
44
61
  if (method.toUpperCase() !== 'POST')
45
62
  return null;
46
- if (classifyPayrailsRequest(url, 'POST', body) === 'card')
63
+ if (isPayrailsSaveUrl(url) && classifyPayrailsRequest(url, 'POST', body) === 'card')
47
64
  return 'card';
48
65
  return payrailsAuthorizeCarriesCard(url, 'POST', body) ? 'card_payment' : null;
49
66
  }
50
- /** Refusals of the save's own shape: the page's next save would be the same, so the attachment holds. */
67
+ /** Whether `url` is exactly a payment-acceptance execution's authorize link on a Payrails API host, where a card-form payment may pause. */
68
+ export function isPayrailsPaymentUrl(url) {
69
+ return isPayrailsAuthorizeUrl(url, { exact: true });
70
+ }
71
+ /** Refusals of the request's own shape: the page's next one would be the same, so the attachment holds. */
51
72
  export const PAYRAILS_HOLDING_REASONS = [
52
73
  'unsupported_payrails_card_payment', 'payrails_request_malformed', 'payrails_not_card_save',
53
74
  'payrails_jwe_malformed', 'payrails_future_usage_refused', 'payrails_card_on_file_consent_required',
54
75
  ];
55
- /** What the agent reads when a save's session cannot be bound. Not shown to the cardholder. */
76
+ /** What the agent reads when a card request's session cannot be bound. Not shown to the cardholder. */
56
77
  export const PAYRAILS_SESSION_MESSAGE = 'Payrails session expired, reload the checkout';
57
78
  /** Captured sessions kept per attachment; the oldest is dropped first. */
58
79
  export const PAYRAILS_CAPTURE_LIMIT = 8;
@@ -81,6 +102,7 @@ function base64Json(value) {
81
102
  return null;
82
103
  }
83
104
  }
105
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
84
106
  /** The binding fields of one init payload `{version, data}`, or null when `candidate` is not one. */
85
107
  function sessionOf(candidate) {
86
108
  if (!record(candidate) || typeof candidate.version !== 'string' || typeof candidate.data !== 'string')
@@ -94,14 +116,22 @@ function sessionOf(candidate) {
94
116
  if (!text(data.token, 8192) || !text(data.holderReference, 256) || !text(vault.providerConfigId, 64)
95
117
  || !text(vault.encryptionPublicKey, 1024) || !text(href, 2048) || !isPayrailsSaveUrl(href, { exact: true }))
96
118
  return null;
119
+ const execution = record(data.execution) && typeof data.execution.id === 'string' && UUID.test(data.execution.id) ? data.execution.id : null;
97
120
  return { token: data.token, holderReference: data.holderReference, providerConfigId: vault.providerConfigId,
98
- saveInstrumentHref: href, encryptionPublicKey: vault.encryptionPublicKey };
121
+ saveInstrumentHref: href, executionId: execution, encryptionPublicKey: vault.encryptionPublicKey };
99
122
  }
123
+ /** How deep inside an answer an init payload is looked for, and how many of its objects at most. */
124
+ export const PAYRAILS_INIT_SEARCH_DEPTH = 8;
125
+ export const PAYRAILS_INIT_SEARCH_NODES = 4096;
100
126
  /**
101
- * Every Payrails init payload one JSON answer body carries: the body itself, or its members one
102
- * level down (an object's values or an array's items). An init payload is `{version, data}` whose
103
- * `data` decodes to JSON with a token, a holderReference and a vaultConfiguration naming its
104
- * providerConfigId, its encryptionPublicKey and a saveInstrument href on a Payrails save URL.
127
+ * Every Payrails init payload one JSON answer body carries: the body itself, or any object or array
128
+ * item inside it at most PAYRAILS_INIT_SEARCH_DEPTH levels down, read breadth first over at most
129
+ * PAYRAILS_INIT_SEARCH_NODES of them. Blacklane's session answer is the payload itself; FlixBus's
130
+ * /payment/configuration/list carries it six levels down
131
+ * (options.initialState.selection.paymentMethods[i].paymentToken). An init payload is
132
+ * `{version, data}` whose `data` decodes to JSON with a token, a holderReference and a
133
+ * vaultConfiguration naming its providerConfigId, its encryptionPublicKey and a saveInstrument href
134
+ * on a Payrails save URL; nothing inside one is read further.
105
135
  */
106
136
  export function payrailsInitSessions(body) {
107
137
  if (typeof body !== 'string' || body.length === 0 || body.length > PAYRAILS_INIT_BODY_LIMIT)
@@ -113,11 +143,23 @@ export function payrailsInitSessions(body) {
113
143
  catch {
114
144
  return [];
115
145
  }
116
- const top = sessionOf(parsed);
117
- if (top)
118
- return [top];
119
- const members = Array.isArray(parsed) ? parsed : record(parsed) ? Object.values(parsed) : [];
120
- return members.map(sessionOf).filter((session) => session !== null);
146
+ const found = [];
147
+ const queue = [{ node: parsed, depth: 0 }];
148
+ for (let next = 0; next < queue.length && next < PAYRAILS_INIT_SEARCH_NODES; next++) {
149
+ const { node, depth } = queue[next];
150
+ const session = sessionOf(node);
151
+ if (session) {
152
+ found.push(session);
153
+ continue;
154
+ }
155
+ if (depth >= PAYRAILS_INIT_SEARCH_DEPTH)
156
+ continue;
157
+ for (const member of Array.isArray(node) ? node : record(node) ? Object.values(node) : []) {
158
+ if (member !== null && typeof member === 'object')
159
+ queue.push({ node: member, depth: depth + 1 });
160
+ }
161
+ }
162
+ return found;
121
163
  }
122
164
  /** Whether an answer may carry an init payload worth reading: a 2xx XHR or fetch with a JSON type. */
123
165
  export function payrailsInitCandidate(resourceType, status, contentType) {
@@ -127,7 +169,16 @@ export function payrailsInitCandidate(resourceType, status, contentType) {
127
169
  }
128
170
  const sameSession = (a, b) => a.token === b.token && a.holderReference === b.holderReference
129
171
  && a.providerConfigId === b.providerConfigId && a.saveInstrumentHref === b.saveInstrumentHref
130
- && a.encryptionPublicKey === b.encryptionPublicKey;
172
+ && a.executionId === b.executionId && a.encryptionPublicKey === b.encryptionPublicKey;
173
+ /** The hostname of a URL, or null. */
174
+ function hostnameOf(url) {
175
+ try {
176
+ return new URL(url).hostname;
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
131
182
  /** The bearer token a paused save carries in `authorization`, any case of the name; null otherwise. */
132
183
  function bearerOf(headers) {
133
184
  const values = Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() === 'authorization').map(([, value]) => value);
@@ -149,6 +200,19 @@ function saveBindingOf(body) {
149
200
  return null;
150
201
  }
151
202
  }
203
+ /** A card-form payment body's paymentComposition[0].paymentInstrumentData.vaultProviderConfigId, read leniently; null when absent. */
204
+ function paymentProviderConfigOf(body) {
205
+ try {
206
+ const parsed = JSON.parse(body);
207
+ const parts = record(parsed) ? parsed.paymentComposition : undefined;
208
+ const part = Array.isArray(parts) ? parts[0] : undefined;
209
+ const data = record(part) ? part.paymentInstrumentData : undefined;
210
+ return record(data) && typeof data.vaultProviderConfigId === 'string' ? data.vaultProviderConfigId : null;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
152
216
  /**
153
217
  * The Payrails sessions one attachment has seen, in memory only: at most PAYRAILS_CAPTURE_LIMIT,
154
218
  * each dropped PAYRAILS_CAPTURE_TTL_MS after it was first seen. Nothing here is ever logged or
@@ -207,18 +271,48 @@ export class PayrailsSessionCapture {
207
271
  * The session key for a paused card save: the key of the captured session whose token is the
208
272
  * save's bearer token, whose saveInstrument href is the save's URL, and whose holderReference and
209
273
  * providerConfigId are the body's, first seen at most PAYRAILS_SESSION_MAX_AGE_MS before `now`.
210
- * Null when none matches, or when matching sessions disagree on the key.
274
+ * Null when none matches, or when matching sessions disagree on the key. bind() reads a card-form
275
+ * payment too.
211
276
  */
212
277
  keyFor(request, now = Date.now()) {
278
+ return isPayrailsSaveUrl(request.url) ? this.bind(request, now)?.key ?? null : null;
279
+ }
280
+ /**
281
+ * The session a paused card request binds to: its key and holderReference. A save binds as keyFor
282
+ * says. A card-form payment binds to the captured session whose token is the payment's bearer
283
+ * token, whose execution is the one the authorize link names, whose saveInstrument href is on the
284
+ * link's host, and whose providerConfigId is the payment's vaultProviderConfigId, first seen at most
285
+ * PAYRAILS_SESSION_MAX_AGE_MS before `now`. Null when none matches, or when matching sessions
286
+ * disagree on the key or the holderReference.
287
+ */
288
+ bind(request, now = Date.now()) {
213
289
  this.prune(now);
214
290
  const token = bearerOf(request.headers);
215
- const binding = typeof request.body === 'string' ? saveBindingOf(request.body) : null;
216
- if (!token || !binding)
291
+ if (!token || typeof request.body !== 'string')
217
292
  return null;
218
- const keys = new Set(this.sessions.filter((session) => session.token === token && session.saveInstrumentHref === request.url
219
- && session.holderReference === binding.holderReference && session.providerConfigId === binding.providerConfigId
220
- && session.seenAt <= now && now - session.seenAt <= PAYRAILS_SESSION_MAX_AGE_MS).map((session) => session.encryptionPublicKey));
221
- return keys.size === 1 ? [...keys][0] : null;
293
+ const fresh = (session) => session.token === token && session.seenAt <= now && now - session.seenAt <= PAYRAILS_SESSION_MAX_AGE_MS;
294
+ let matches;
295
+ if (isPayrailsSaveUrl(request.url)) {
296
+ const binding = saveBindingOf(request.body);
297
+ if (!binding)
298
+ return null;
299
+ matches = this.sessions.filter((session) => fresh(session) && session.saveInstrumentHref === request.url
300
+ && session.holderReference === binding.holderReference && session.providerConfigId === binding.providerConfigId);
301
+ }
302
+ else {
303
+ const executionId = payrailsExecutionId(request.url);
304
+ const host = hostnameOf(request.url);
305
+ const providerConfigId = paymentProviderConfigOf(request.body);
306
+ if (!executionId || !host || !providerConfigId)
307
+ return null;
308
+ matches = this.sessions.filter((session) => fresh(session) && session.executionId === executionId
309
+ && hostnameOf(session.saveInstrumentHref) === host && session.providerConfigId === providerConfigId);
310
+ }
311
+ const bindings = new Map(matches.map((session) => [`${session.encryptionPublicKey}\n${session.holderReference}`, session]));
312
+ if (bindings.size !== 1)
313
+ return null;
314
+ const [session] = bindings.values();
315
+ return { key: session.encryptionPublicKey, holderReference: session.holderReference };
222
316
  }
223
317
  /** How many sessions are held; for tests. */
224
318
  get size() { return this.sessions.length; }
@@ -276,9 +370,27 @@ export function payrailsSaveRefusal(body) {
276
370
  return 'payrails_request_malformed';
277
371
  }
278
372
  }
373
+ /**
374
+ * payment-core's judgement of a card-form payment's own shape, before any session is looked up:
375
+ * null when it is the SDK's own payment that pays once, else the reason. A payment that stores the
376
+ * card at the merchant reads as payrails_card_on_file_consent_required, one that sets up a
377
+ * subscription as payrails_future_usage_refused, and a ciphertext of another shape as
378
+ * payrails_jwe_malformed; every other shape (a stored card's CVC, a split or installment payment, a
379
+ * preferred scheme) is one this build does not take over, unsupported_payrails_card_payment. A guard
380
+ * that throws refuses.
381
+ */
382
+ export function payrailsPaymentRefusal(body) {
383
+ try {
384
+ const code = payrailsAuthorizeRefusal(body);
385
+ return code === 'payrails_request_malformed' ? 'unsupported_payrails_card_payment' : code ?? null;
386
+ }
387
+ catch {
388
+ return 'unsupported_payrails_card_payment';
389
+ }
390
+ }
279
391
  /**
280
392
  * The key check for a bound session: null when the captured key is an RSA-2048, 3072 or 4096 key as
281
- * long as the key the page's frame wrapped for this save, else the refusal code.
393
+ * long as the key the page's frame wrapped for this save or payment, else the refusal code.
282
394
  */
283
395
  export function payrailsKeyRefusal(key, body) {
284
396
  try {
@@ -289,17 +401,26 @@ export function payrailsKeyRefusal(key, body) {
289
401
  }
290
402
  }
291
403
  /**
292
- * The only answer the page may receive for an approved Payrails save: Payrails' 201 for a card
293
- * instrument, rebuilt from payment-core's allow-list (the BIN cut to six digits), under a JSON
294
- * content type. Null for anything else.
404
+ * The only answer the page may receive for an approved Payrails card request, by the paused `url`: for
405
+ * a save, Payrails' 201 for a card instrument, rebuilt from payment-core's allow-list (the BIN cut to
406
+ * six digits); for a card-form payment, Payrails' 2xx for the authorize action it accepted, rebuilt to
407
+ * its name, id, time and execution link, which the page's SDK polls for the issuer's answer. That link
408
+ * must be the paused payment's own execution on its own host, or the page would poll another payment's
409
+ * result. Under a JSON content type; null for anything else.
295
410
  */
296
- export function payrailsSaveAnswer(status, body) {
411
+ export function payrailsAnswer(status, body, url) {
297
412
  try {
298
- const verdict = payrailsResponseOutcome(status, body);
413
+ const verdict = payrailsResponseOutcome(status, body, { url });
299
414
  const answer = verdict.outcome === 'approved' ? verdict.body : undefined;
300
- return typeof answer === 'string' ? { status: 201, headers: { 'content-type': 'application/json' }, body: answer } : null;
415
+ if (typeof answer !== 'string')
416
+ return null;
417
+ return { status: isPayrailsAuthorizeUrl(url) ? status : 201, headers: { 'content-type': 'application/json' }, body: answer };
301
418
  }
302
419
  catch {
303
420
  return null;
304
421
  }
305
422
  }
423
+ /** payrailsAnswer for a save. */
424
+ export function payrailsSaveAnswer(status, body) {
425
+ return payrailsAnswer(status, body, `https://${PAYRAILS_SAVE_HOSTS[0]}${PAYRAILS_SAVE_PATH}`);
426
+ }