@agent-cards/sdk 0.23.28 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/PREFLIGHT.md +7 -1
  3. package/README.md +137 -99
  4. package/dist/aci.d.ts +52 -22
  5. package/dist/aci.js +79 -15
  6. package/dist/adyen-merchant-hosted.generated.d.ts +1 -1
  7. package/dist/adyen-merchant-hosted.generated.js +459 -21
  8. package/dist/agent-amount.generated.js +1 -1
  9. package/dist/agentcard.d.ts +4 -5
  10. package/dist/agentcard.js +4 -5
  11. package/dist/amadeus.d.ts +9 -3
  12. package/dist/amadeus.js +33 -4
  13. package/dist/api-module.d.ts +1 -7
  14. package/dist/api-module.js +3 -5
  15. package/dist/{voice-retry.js → api-retry.js} +1 -1
  16. package/dist/api-types.d.ts +53 -0
  17. package/dist/{voice-types.js → api-types.js} +7 -2
  18. package/dist/approvals.d.ts +166 -0
  19. package/dist/approvals.js +213 -0
  20. package/dist/buy.d.ts +20 -101
  21. package/dist/buy.js +19 -74
  22. package/dist/capabilities.generated.js +1 -1
  23. package/dist/card-fields.generated.js +1398 -106
  24. package/dist/cdp.d.ts +3 -13
  25. package/dist/cdp.js +231 -175
  26. package/dist/client.d.ts +104 -22
  27. package/dist/client.js +215 -40
  28. package/dist/creditguard.generated.d.ts +4 -4
  29. package/dist/datatrans.generated.js +22 -1
  30. package/dist/elavon-converge.generated.js +5 -3
  31. package/dist/hold.d.ts +8 -0
  32. package/dist/hold.js +43 -2
  33. package/dist/hosted-entry.js +2 -0
  34. package/dist/index.d.ts +9 -15
  35. package/dist/index.js +6 -10
  36. package/dist/lifecycle.d.ts +9 -61
  37. package/dist/lifecycle.js +24 -10
  38. package/dist/merchant-card-endpoints.generated.d.ts +6 -1
  39. package/dist/merchant-card-endpoints.generated.js +344 -19
  40. package/dist/merchant-hosted.d.ts +8 -1
  41. package/dist/nike-order.d.ts +51 -0
  42. package/dist/nike-order.js +81 -0
  43. package/dist/oceanpayment.d.ts +38 -0
  44. package/dist/oceanpayment.js +70 -0
  45. package/dist/paddle-tokenex.generated.js +105 -10
  46. package/dist/payplug.d.ts +41 -0
  47. package/dist/payplug.js +71 -0
  48. package/dist/payrails.generated.js +27 -7
  49. package/dist/pre-claim.d.ts +9 -0
  50. package/dist/pre-claim.js +86 -7
  51. package/dist/preflight-capabilities.generated.d.ts +0 -4
  52. package/dist/preflight-capabilities.generated.js +0 -5
  53. package/dist/preflight-catalog.json +46 -5
  54. package/dist/preflight-playwright.d.ts +2 -2
  55. package/dist/preflight-playwright.js +63 -14
  56. package/dist/preflight-schemas.json +4 -0
  57. package/dist/preflight.generated.d.ts +0 -4
  58. package/dist/preflight.generated.js +24 -1
  59. package/dist/prepared-processor.d.ts +18 -5
  60. package/dist/prepared-processor.js +48 -8
  61. package/dist/primer.generated.js +1418 -120
  62. package/dist/processout.generated.d.ts +61 -0
  63. package/dist/processout.generated.js +577 -0
  64. package/dist/requests.js +1 -1
  65. package/dist/shiji-sts.generated.js +105 -10
  66. package/dist/stripe-checkout.generated.js +1405 -234
  67. package/dist/substitutions.generated.js +460 -26
  68. package/dist/tap-payments.generated.js +105 -10
  69. package/dist/twoc2p.generated.js +105 -10
  70. package/dist/url-masks/bluesnap.d.ts +2 -0
  71. package/dist/url-masks/bluesnap.js +15 -0
  72. package/dist/url-masks/oceanpayment.d.ts +2 -0
  73. package/dist/url-masks/oceanpayment.js +13 -0
  74. package/dist/url-masks/payplug.d.ts +2 -0
  75. package/dist/url-masks/payplug.js +13 -0
  76. package/dist/url-masks/vtex_payments.d.ts +2 -0
  77. package/dist/url-masks/vtex_payments.js +12 -0
  78. package/dist/url-masks.generated.d.ts +1 -1
  79. package/dist/url-masks.generated.js +6 -2
  80. package/dist/verifone.generated.js +42 -2
  81. package/dist/vtex-payments.generated.d.ts +127 -0
  82. package/dist/vtex-payments.generated.js +544 -0
  83. package/dist/webhooks.d.ts +2 -2
  84. package/dist/webhooks.js +2 -2
  85. package/examples/preflight/kernel-native/inventory.json +1 -1
  86. package/package.json +6 -8
  87. package/dist/languages.d.ts +0 -23
  88. package/dist/languages.js +0 -76
  89. package/dist/voice-pkce.d.ts +0 -49
  90. package/dist/voice-pkce.js +0 -147
  91. package/dist/voice-stream.generated.d.ts +0 -118
  92. package/dist/voice-stream.generated.js +0 -170
  93. package/dist/voice-types.d.ts +0 -735
  94. package/dist/voice.d.ts +0 -373
  95. package/dist/voice.js +0 -815
  96. package/readme.test.mjs +0 -53
  97. /package/dist/{voice-retry.d.ts → api-retry.d.ts} +0 -0
package/dist/pre-claim.js CHANGED
@@ -66,7 +66,7 @@ import { datatransPauseDecision, isDatatransHost } from './datatrans.js';
66
66
  import { DATATRANS_INVALID_REQUEST } from './datatrans.generated.js';
67
67
  import { INVALID_TOKENEX_IFRAME_REQUEST, TOKENEX_IFRAME_PSP, isTokenexIframePath, servesTokenexIframe, tokenexIframePauseDecision, } from './tokenex-iframe.js';
68
68
  import { isPelecardHost, pelecardRefusal } from './pelecard.js';
69
- import { ACI_CHECKOUT_REUSED } from './aci.js';
69
+ import { ACI_CHECKOUT_REUSED, SPLIT_CARD_REUSED } from './aci.js';
70
70
  import { ACI_EXPIRY_UNSTRIPPABLE } from './aci.generated.js';
71
71
  import { CREDITGUARD_PPS_HOSTS, classifyCreditguardRequest, creditguardCompanionAnswer, creditguardCompanionCall, creditguardNuveiRefusal, creditguardProcessRefusal, creditguardProcessTxId, } from './creditguard.generated.js';
72
72
  import { isPrimerPciHost, primerRequestVerdict } from './primer.js';
@@ -78,6 +78,7 @@ import { isFiservCaptureHost, isFiservPausableBody } from './fiserv.js';
78
78
  import { CYBERSOURCE_FLEX_PSP, isCybersourceFlexHost } from './cybersource-flex.js';
79
79
  import { cardEndpointCarriesCard, cardEndpointDetail, cardEndpointFor, cardEndpointLive, cardEndpointPasses, cardEndpointResendFor, cardEndpointReviewed, } from './card-endpoint.js';
80
80
  import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoint-page.js';
81
+ import { NIKE_ORDER_DETAIL } from './nike-order.js';
81
82
  import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isPayrailsPaymentUrl, isPayrailsStaticHost, payrailsKeyRefusal, payrailsPaymentRefusal, payrailsRequestKind, payrailsSaveRefusal, } from './payrails.js';
82
83
  import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
83
84
  import { isPaymentHighwayCardLookup, isPaymentHighwayHost, isPaytrailCardMethodUrl, isPaytrailEntry, isPaytrailServed } from './paytrail.js';
@@ -86,9 +87,11 @@ import { isArmedTransbankUrl } from './transbank.js';
86
87
  import { isRedsysEntry, isRedsysHost, redsysEntryUrl } from './redsys.js';
87
88
  import { isArmedPrzelewy24Url, isPrzelewy24PayStep } from './przelewy24.js';
88
89
  import { isArmedBarionUrl, isBarionPayStep } from './barion.js';
90
+ import { isArmedPayplugUrl, isPayplugPayStep } from './payplug.js';
89
91
  import { isQuickpayCardLookup, isQuickpayEntry, quickpayEntryUrl } from './quickpay.js';
90
92
  import { isArmedComgateUrl } from './comgate.js';
91
93
  import { isArmedEximbayUrl, isEximbayEntry } from './eximbay.js';
94
+ import { isArmedOceanpaymentUrl, isOceanpaymentSendPay } from './oceanpayment.js';
92
95
  import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
93
96
  import { PAYPAL_CARD_FIELDS_REASON, PAYPAL_GUEST_CHECKOUT_REASON, isPaypalCardFieldsConfirm, isPaypalGraphql, paypalCardFieldsBody, paypalGraphqlCard, paypalGraphqlNamesCardOperation, } from './paypal-card-forms.js';
94
97
  import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, isSecureAcceptanceFramePost, } from './cybersource-secure-acceptance.js';
@@ -287,6 +290,38 @@ async function cardEndpointUnsupported(ctx, profile, reviewed) {
287
290
  await ctx.effects.abort();
288
291
  return { kind: 'abort' };
289
292
  }
293
+ // Nike's order after its card request (nike-order.ts, payment-core merchant-card-endpoint-nike-order.js), Felipe's amount
294
+ // rule C: the one request off a profile's endpoint the SDK holds. Once the adapters asked the cardholder to pay a card
295
+ // request through Nike's profile on this attachment, every write to Nike's API host is judged here. One reviewed order
296
+ // whose total and currency are the approved amount continues with its body unchanged, and only while the checkout still
297
+ // names that approval as under way (followUpMaySend); after it, only a byte-identical retry does. An order that fails the
298
+ // rule aborts, so Nike never authorizes the card, and the checkout ends failed with nothing charged
299
+ // (lifecycle.merchantOrderRefused); so does every order while no approved amount was read, and every later order under
300
+ // that approval. Another order after the one let go, or one the checkout no longer names as under way, aborts and leaves
301
+ // the checkout's state as it is. Anything else, every request of an attachment where no card went through Nike's
302
+ // profile included, defers. The adapters also run nikeOrderGate on a request paused at the browser target.
303
+ const nikeOrderPreClaim = (ctx) => nikeOrderGate(ctx);
304
+ /** The Nike order gate's decision, already performed, or undefined when the request is not its to judge. */
305
+ export function nikeOrderGate(ctx) {
306
+ const verdict = ctx.nikeOrders?.judge(ctx.request.url, ctx.request.method, () => ctx.request.body, (authorizationId) => followUpMaySend(ctx, { authorizationId })) ?? null;
307
+ if (!verdict)
308
+ return undefined;
309
+ return verdict.refusal === null ? nikeOrderReleased(ctx, verdict.authorizationId) : nikeOrderRefused(ctx, verdict.authorizationId, verdict.refusal);
310
+ }
311
+ // The refusals that stop one order and leave the checkout's state as it is.
312
+ const NIKE_ORDER_STOPS = new Set(['another_order', 'approval_not_current', 'earlier_order_refused']);
313
+ async function nikeOrderReleased(ctx, authorizationId) {
314
+ ctx.onEvent?.({ type: 'merchant_order_released', detail: { authorizationId, ...NIKE_ORDER_DETAIL } });
315
+ await ctx.effects.continueRequest().catch(() => { });
316
+ return { kind: 'continue' };
317
+ }
318
+ async function nikeOrderRefused(ctx, authorizationId, reason) {
319
+ if (!NIKE_ORDER_STOPS.has(reason))
320
+ ctx.lifecycle.merchantOrderRefused(`merchant_order_${reason}`);
321
+ ctx.onEvent?.({ type: 'merchant_order_refused', detail: { authorizationId, ...NIKE_ORDER_DETAIL, reason } });
322
+ await ctx.effects.abort();
323
+ return { kind: 'abort' };
324
+ }
290
325
  // Mercado Pago's native Checkout Pro shares one origin between its BIN-lookup
291
326
  // configuration and its card association. A request that is not for that origin
292
327
  // defers synchronously; a POST to it is handled asynchronously (a document read
@@ -1034,6 +1069,12 @@ async function pelecardBlock(ctx, reason) {
1034
1069
  // Everything else on ACI's hosts (the widget script, /bins, /v1/collect, GETs)
1035
1070
  // is not a POST to a checkout and defers. Synchronous for the card-number
1036
1071
  // request, so it pauses in the microtask it paused in.
1072
+ // A JSON split card the registry serves (aci.ts) takes the same path, keyed by
1073
+ // the URL its legs share: its card request defers to the arbiter unless that URL
1074
+ // already has one answered, or one running with no approval open
1075
+ // (split_card_reused), its code and expiry requests are held, a holder request
1076
+ // continues, and anything else is refused with the processor's code or
1077
+ // split_card_leg_unsupported. It has no /payment.
1037
1078
  const aciPreClaim = (ctx) => {
1038
1079
  const leg = ctx.aci?.classify(ctx.request.url, ctx.request.method, ctx.request.body);
1039
1080
  if (!leg)
@@ -1050,8 +1091,15 @@ const aciPreClaim = (ctx) => {
1050
1091
  return aciPayment(ctx);
1051
1092
  // The card-number request: the arbiter pauses it for the vault claim flow,
1052
1093
  // unless this checkout already has one running or answered (AciGate.begin).
1053
- if (ctx.aci.reused(leg))
1054
- return aciRefuse(ctx, ACI_CHECKOUT_REUSED);
1094
+ // While a JSON split card's approval is still open, its card request again is
1095
+ // the page's retry of the one it gave up on: the adapters bind that retry to
1096
+ // the open approval, or refuse it there as a second purchase, so the page's
1097
+ // retry gets the late approval's answer instead of a refusal here.
1098
+ if (ctx.aci.reused(leg)) {
1099
+ if ('group' in leg && ctx.awaitingApproval())
1100
+ return undefined;
1101
+ return aciRefuse(ctx, 'group' in leg ? SPLIT_CARD_REUSED : ACI_CHECKOUT_REUSED);
1102
+ }
1055
1103
  return undefined;
1056
1104
  };
1057
1105
  async function aciContinue(ctx) {
@@ -1059,16 +1107,17 @@ async function aciContinue(ctx) {
1059
1107
  return { kind: 'continue' };
1060
1108
  }
1061
1109
  async function aciHold(ctx, leg) {
1110
+ const prefix = 'group' in leg ? 'split_card' : 'aci';
1062
1111
  const answer = await ctx.aci.hold(leg);
1063
1112
  if (answer && !ctx.interceptionAborted() && !ctx.lifecycleAborted()) {
1064
1113
  try {
1065
1114
  await ctx.effects.answer(answer);
1066
- ctx.onEvent?.({ type: 'aci_leg_absorbed', detail: { url: redactUrl(ctx.request.url) } });
1115
+ ctx.onEvent?.({ type: `${prefix}_leg_absorbed`, detail: { url: redactUrl(ctx.request.url) } });
1067
1116
  return { kind: 'answer' };
1068
1117
  }
1069
1118
  catch { /* the page's request is gone; nothing was forwarded */ }
1070
1119
  }
1071
- ctx.onEvent?.({ type: 'blocked', detail: 'aci_leg_unanswered' });
1120
+ ctx.onEvent?.({ type: 'blocked', detail: `${prefix}_leg_unanswered` });
1072
1121
  await ctx.effects.abort();
1073
1122
  return { kind: 'abort' };
1074
1123
  }
@@ -1680,6 +1729,20 @@ export const barionPreClaim = (ctx) => {
1680
1729
  return undefined;
1681
1730
  return processorPageAbort(ctx);
1682
1731
  };
1732
+ // PayPlug's payment page (payplug.ts). Once this build's registry serves PayPlug's entry (a capability the API serves
1733
+ // only to Agentcard's own company), every request to PayPlug's page host pauses. The merchant's redirect to the page,
1734
+ // a GET of /pay/<payment id> or /pay/test/<payment id> exactly, is no step of this gate's: it defers, and the terminal
1735
+ // arbiter pauses it for the claim flow like any card request, so the cardholder's device pays it. Every step toward
1736
+ // paying on the page itself aborts as an unsupported checkout: every POST to the page's host, the card post among
1737
+ // them. So PayPlug never sees the card the agent typed. The page's own GETs, its scripts, styles, images and frames,
1738
+ // continue. With PayPlug not served, the gate defers, and every request continues as before. Synchronous until it must
1739
+ // abort, like every gate. Exported for the gate registry's tests.
1740
+ export const payplugPreClaim = (ctx) => {
1741
+ const { url, method } = ctx.request;
1742
+ if (!isPayplugPayStep(url, method) || !isArmedPayplugUrl(ctx.vault, url))
1743
+ return undefined;
1744
+ return processorPageAbort(ctx);
1745
+ };
1683
1746
  // NMI's hosted payment page (ekashu.ts). Once this build's registry serves NMI's entry (a
1684
1747
  // capability the API serves only to a company Agentcard turned NMI on for), every request to
1685
1748
  // NMI's two hosts pauses, and this gate judges the POSTs:
@@ -1844,6 +1907,21 @@ export const eximbayPreClaim = (ctx) => {
1844
1907
  return undefined;
1845
1908
  return processorPageAbort(ctx);
1846
1909
  };
1910
+ // Oceanpayment's hosted payment page (oceanpayment.ts). Once this build's registry serves Oceanpayment's card post (a
1911
+ // capability the API serves only to the company the gate turns Oceanpayment on for), every request to its two hosts
1912
+ // pauses, and this gate judges the ones that are not a GET:
1913
+ // - the page's card post, exactly as its form action writes it, defers, and the terminal arbiter pauses it for the
1914
+ // claim flow like any card request: the cardholder's device pays it;
1915
+ // - every other one (Cardinal's device-data call, which carries the typed number, a card post of another spelling, the
1916
+ // page's cancel) aborts as an unsupported checkout, so Oceanpayment never sees the card the agent typed.
1917
+ // A GET (the page, its scripts, its log) is not this gate's and continues. With Oceanpayment not served, the gate defers
1918
+ // and every request continues as before. Synchronous until it must abort, like every gate. Exported for its tests.
1919
+ export const oceanpaymentPreClaim = (ctx) => {
1920
+ const { url, method } = ctx.request;
1921
+ if (String(method).toUpperCase() === 'GET' || !isArmedOceanpaymentUrl(ctx.vault, url) || isOceanpaymentSendPay(url, method))
1922
+ return undefined;
1923
+ return processorPageAbort(ctx);
1924
+ };
1847
1925
  // PayPal's legacy Hosted Fields (paypal-hosted-fields.ts). Its tokenization frame posts the card the
1848
1926
  // agent typed to the Orders v2 confirm-payment-source on cors.api.paypal.com (or its sandbox), which
1849
1927
  // no Vault mode takes over, so every company's adapters hand each POST there to this gate:
@@ -2499,8 +2577,9 @@ async function unsupportedContinue(ctx) {
2499
2577
  * arbiter runs after all of them and is not part of this list. A gate appended
2500
2578
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
2501
2579
  * pauses each one itself. */
2502
- export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, tokenexIframePreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, sbpsPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, quickpayPreClaim, comgatePreClaim, przelewy24PreClaim, transbankPreClaim, eximbayPreClaim, barionPreClaim, paypalHostedFieldsPreClaim,
2503
- paypalCardFormsPreClaim, cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim, payrexxPreClaim, hipayPreClaim, paytrailPreClaim];
2580
+ export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, nikeOrderPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, tokenexIframePreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, sbpsPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, quickpayPreClaim, comgatePreClaim, przelewy24PreClaim, transbankPreClaim, eximbayPreClaim, barionPreClaim, payplugPreClaim, paypalHostedFieldsPreClaim,
2581
+ paypalCardFormsPreClaim, cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim, payrexxPreClaim, hipayPreClaim, paytrailPreClaim,
2582
+ oceanpaymentPreClaim];
2504
2583
  /** Run each gate in order; the first to return a decision wins. If none does,
2505
2584
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
2506
2585
  * Returns synchronously while every gate defers synchronously (an ordinary card
@@ -1193,10 +1193,6 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
1193
1193
  readonly psp: "vgs";
1194
1194
  readonly reason: "iframe_bound_tokenization";
1195
1195
  readonly action: "human_checkout";
1196
- }, {
1197
- readonly psp: "bluesnap";
1198
- readonly reason: "multi_request_tokenization_not_supported";
1199
- readonly action: "human_checkout";
1200
1196
  }, {
1201
1197
  readonly psp: "stripe";
1202
1198
  readonly reason: "unbound_token_to_payment_intent_continuation";
@@ -1840,11 +1840,6 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
1840
1840
  "reason": "iframe_bound_tokenization",
1841
1841
  "action": "human_checkout"
1842
1842
  },
1843
- {
1844
- "psp": "bluesnap",
1845
- "reason": "multi_request_tokenization_not_supported",
1846
- "action": "human_checkout"
1847
- },
1848
1843
  {
1849
1844
  "psp": "stripe",
1850
1845
  "reason": "unbound_token_to_payment_intent_continuation",
@@ -2139,6 +2139,52 @@
2139
2139
  "hostname_pattern": "^cdn\\.sandbox\\.[a-z0-9-]+\\.gr4vy\\.app$",
2140
2140
  "pathname_pattern": "^\\/secure-fields\\/(?:latest|[0-9]{1,3}(?:\\.[0-9]{1,3}){0,2})\\/secure-fields\\.js$"
2141
2141
  },
2142
+ {
2143
+ "id": "bambora-na-custom-checkout-fields",
2144
+ "psp": "bambora",
2145
+ "kinds": [
2146
+ "frame",
2147
+ "iframe",
2148
+ "document"
2149
+ ],
2150
+ "origin": "https://libs.na.bambora.com",
2151
+ "path_pattern": "/customcheckout/1.:minor.:patch{,-:build}/iframe.html",
2152
+ "sources": [
2153
+ {
2154
+ "kind": "primary_source",
2155
+ "reference": "https://docs.na.worldline-solutions.com/build-your-integration/checkout-form/custom-checkout/reference",
2156
+ "reviewed_on": "2026-09-11"
2157
+ },
2158
+ {
2159
+ "kind": "saved_observation",
2160
+ "reference": "catalog-psp-scan-2026-10-23:kent-ca-custom-checkout-1.0.2-card-fields",
2161
+ "reviewed_on": "2026-10-23"
2162
+ }
2163
+ ],
2164
+ "hostname_pattern": "^libs\\.na\\.bambora\\.com$",
2165
+ "pathname_pattern": "^\\/customcheckout\\/1\\.[0-9]{1,3}\\.[0-9]{1,3}(?:-[0-9]{1,4})?\\/iframe\\.html$"
2166
+ },
2167
+ {
2168
+ "id": "worldpay-wpg-hosted-payment-page",
2169
+ "psp": "worldpay",
2170
+ "kinds": [
2171
+ "document",
2172
+ "frame",
2173
+ "iframe"
2174
+ ],
2175
+ "origin": "https://payments.worldpay.com",
2176
+ "path_pattern": "/app/{hpp,hpp-iframe}/*",
2177
+ "sources": [
2178
+ {
2179
+ "kind": "saved_observation",
2180
+ "reference": "catalog-psp-scan-2026-10-23:razer-worldpay-hosted-payment-page",
2181
+ "reviewed_on": "2026-10-23"
2182
+ }
2183
+ ],
2184
+ "variant_unverified": "The implemented Worldpay adapter covers Access Checkout card sessions; Worldpay hosted payment pages under payments.worldpay.com/app/hpp take the card on Worldpay's own page and are not admitted.",
2185
+ "hostname_pattern": "^payments\\.worldpay\\.com$",
2186
+ "pathname_pattern": "^\\/app\\/hpp(?:-iframe)?\\/"
2187
+ },
2142
2188
  {
2143
2189
  "id": "paypal-hosted-fields-payments-sdk",
2144
2190
  "psp": "paypal",
@@ -6958,11 +7004,6 @@
6958
7004
  "reason": "iframe_bound_tokenization",
6959
7005
  "action": "human_checkout"
6960
7006
  },
6961
- {
6962
- "psp": "bluesnap",
6963
- "reason": "multi_request_tokenization_not_supported",
6964
- "action": "human_checkout"
6965
- },
6966
7007
  {
6967
7008
  "psp": "stripe",
6968
7009
  "reason": "unbound_token_to_payment_intent_continuation",
@@ -20,9 +20,9 @@ export interface CheckoutCollectionOptions {
20
20
  timeoutMs?: number;
21
21
  /** Maximum frame count. Default and maximum 64. */
22
22
  maxFrames?: number;
23
- /** Maximum document and asset observations. Default and maximum 2,048. */
23
+ /** Maximum document and asset observations. Default and maximum 2,048. An asset no reviewed rule can match is skipped and not counted. */
24
24
  maxSignals?: number;
25
- /** Maximum transient observation and returned snapshot size. Default and maximum 262,144 bytes; minimum 512 bytes. */
25
+ /** Maximum transient observation and returned snapshot size. Default and maximum 262,144 bytes; minimum 512 bytes. Skipped assets cost nothing. */
26
26
  maxSnapshotBytes?: number;
27
27
  }
28
28
  export type InspectCheckoutOptions = CheckoutPreflightOptions & {
@@ -1,10 +1,15 @@
1
- import { normalizeCheckoutSignals, assessCheckoutSupport, cardEntryAttributable, } from './preflight.js';
1
+ import { normalizeCheckoutSignals, assessCheckoutSupport, cardEntryAttributable, getCheckoutPreflightCatalog, CHECKOUT_PREFLIGHT_LIMITS, } from './preflight.js';
2
2
  const EVENTS = ['framenavigated', 'frameattached', 'framedetached'];
3
3
  const DEADLINE = Symbol('checkout inspection deadline');
4
4
  const DEFAULTS = { timeoutMs: 1_500, maxFrames: 64, maxSignals: 2_048, maxSnapshotBytes: 262_144 };
5
5
  /** Frames at one nesting depth are read together. A remote CDP connection pays per round trip, not per frame. */
6
6
  const CONCURRENCY = 8;
7
+ /** Script, iframe and form elements one frame's read examines, skipped ones included, so a page cannot keep the read walking. */
8
+ const ELEMENT_LIMIT = 16_384;
7
9
  const encoder = new TextEncoder();
10
+ let reviewedHosts;
11
+ /** Hostname patterns of the reviewed rules. A URL on any other host can never become evidence. */
12
+ const reviewedHostPatterns = () => reviewedHosts ??= [...new Set(getCheckoutPreflightCatalog().rules.map(rule => rule.hostname_pattern))];
8
13
  function bounded(value, fallback, maximum, minimum = 1) {
9
14
  if (value === undefined)
10
15
  return fallback;
@@ -185,7 +190,7 @@ export async function collectCheckoutSignals(page, options = {}) {
185
190
  }
186
191
  }
187
192
  visibility.set(frame, visible);
188
- const data = await beforeDeadline(() => frame.evaluate(({ remaining, byteBudget }) => {
193
+ const data = await beforeDeadline(() => frame.evaluate(({ signalRoom, byteBudget, hosts, elementLimit, urlLength, rawUrlLength }) => {
189
194
  const collected = [];
190
195
  let bytes = 0;
191
196
  let signalLimit = false;
@@ -203,9 +208,37 @@ export async function collectCheckoutSignals(page, options = {}) {
203
208
  const rect = element.getBoundingClientRect();
204
209
  return rect.width > 0 && rect.height > 0;
205
210
  };
211
+ // The normalizer drops a URL short enough to match without a trace when it is not https, carries credentials
212
+ // or a port, or sits on a host no reviewed rule names (preflight.ts). Such a URL can never become evidence,
213
+ // so the read skips it before it costs any signal or byte budget, and a store's own scripts (hundreds on a
214
+ // RequireJS checkout) no longer spend the budget before the read reaches the card frames. A URL kept here is
215
+ // still matched against the reviewed rules by the normalizer, so the page cannot add evidence this way. The
216
+ // patterns are tested case-insensitively: a hostname is lowercase, so this keeps every host a rule can match.
217
+ const reviewed = hosts.map(source => new RegExp(source, 'i'));
218
+ const named = new Map();
219
+ const silent = (url) => {
220
+ if (url.href.length > rawUrlLength || url.origin.length + url.pathname.length > urlLength)
221
+ return false;
222
+ if (url.protocol !== 'https:' || url.username || url.password || url.port)
223
+ return true;
224
+ let known = named.get(url.hostname);
225
+ if (known === undefined) {
226
+ known = reviewed.some(pattern => pattern.test(url.hostname));
227
+ named.set(url.hostname, known);
228
+ }
229
+ return !known;
230
+ };
231
+ const silentAddress = (address) => { try {
232
+ return silent(new URL(address));
233
+ }
234
+ catch {
235
+ return false;
236
+ } };
237
+ // The frame's own address takes a signal slot only when it is appended, which a silent one never is.
238
+ const remaining = Math.max(0, signalRoom - (silentAddress(document.URL) ? 0 : 1));
206
239
  const elements = document.querySelectorAll('script[src], iframe[src], form[action]');
207
240
  for (let index = 0; index < elements.length; index++) {
208
- if (index >= remaining) {
241
+ if (index >= elementLimit) {
209
242
  signalLimit = true;
210
243
  break;
211
244
  }
@@ -214,23 +247,29 @@ export async function collectCheckoutSignals(page, options = {}) {
214
247
  const source = element.getAttribute(kind === 'form' ? 'action' : 'src');
215
248
  if (!source)
216
249
  continue;
250
+ let url = null;
251
+ try {
252
+ url = new URL(source, document.baseURI);
253
+ }
254
+ catch { /* an unparsable location is skipped below unless it is oversized */ }
255
+ if (url && silent(url))
256
+ continue;
217
257
  if (source.length > byteBudget - bytes) {
218
258
  byteLimit = true;
219
259
  break;
220
260
  }
221
- let url;
222
- try {
223
- url = new URL(source, document.baseURI).href;
224
- }
225
- catch {
261
+ if (!url)
226
262
  continue;
263
+ if (collected.length >= remaining) {
264
+ signalLimit = true;
265
+ break;
227
266
  }
228
- bytes += url.length * 3 + 128;
267
+ bytes += url.href.length * 3 + 128;
229
268
  if (bytes > byteBudget) {
230
269
  byteLimit = true;
231
270
  break;
232
271
  }
233
- collected.push({ kind, url, visible: kind === 'script' ? null : shown(element) });
272
+ collected.push({ kind, url: url.href, visible: kind === 'script' ? null : shown(element) });
234
273
  }
235
274
  // Card-entry detection reads field metadata only: the autocomplete token or the
236
275
  // control's name and id. Values, labels and page text are never read.
@@ -248,11 +287,20 @@ export async function collectCheckoutSignals(page, options = {}) {
248
287
  if (cardField && shown(input))
249
288
  cardEntry = true;
250
289
  }
290
+ // The frame's own address follows the same rule, so an ad or widget frame's long address is not charged
291
+ // either: it neither trips the byte budget nor is blanked. It still comes back whole, since the navigation
292
+ // and card-entry checks below need it, and a silent address is at most rawUrlLength characters long.
251
293
  const documentUrl = document.URL;
252
- if (documentUrl.length * 3 > byteBudget)
294
+ const documentSilent = silentAddress(documentUrl);
295
+ const oversized = !documentSilent && documentUrl.length * 3 > byteBudget;
296
+ if (oversized)
253
297
  byteLimit = true;
254
- return { documentUrl: byteLimit && documentUrl.length * 3 > byteBudget ? '' : documentUrl, collected, signalLimit, byteLimit, cardEntry };
255
- }, { remaining: Math.max(0, limits.maxSignals - signals.length - 1), byteBudget: Math.max(0, limits.maxSnapshotBytes - transientBytes) }), deadline);
298
+ return { documentUrl: oversized ? '' : documentUrl, documentSilent, collected, signalLimit, byteLimit, cardEntry };
299
+ }, {
300
+ signalRoom: Math.max(0, limits.maxSignals - signals.length), byteBudget: Math.max(0, limits.maxSnapshotBytes - transientBytes),
301
+ hosts: reviewedHostPatterns(), elementLimit: ELEMENT_LIMIT,
302
+ urlLength: CHECKOUT_PREFLIGHT_LIMITS.url_length, rawUrlLength: CHECKOUT_PREFLIGHT_LIMITS.raw_url_length,
303
+ }), deadline);
256
304
  const releaseError = released ? await released : undefined;
257
305
  if (releaseError !== undefined)
258
306
  throw releaseError;
@@ -286,7 +334,8 @@ export async function collectCheckoutSignals(page, options = {}) {
286
334
  if (!outcome)
287
335
  continue;
288
336
  const { data, frame_id, parent_frame_id, nested, visible } = outcome;
289
- append({ kind: nested ? 'frame' : 'document', url: data.documentUrl, frame_id, parent_frame_id, visible });
337
+ if (!data.documentSilent)
338
+ append({ kind: nested ? 'frame' : 'document', url: data.documentUrl, frame_id, parent_frame_id, visible });
290
339
  for (const signal of data.collected) {
291
340
  if (!append({ ...signal, frame_id, parent_frame_id, visible: visible === false ? false : signal.kind === 'script' ? visible : visible === null ? null : signal.visible }))
292
341
  break;
@@ -466,6 +466,8 @@
466
466
  "mercado-pago-checkout-pro-card-form",
467
467
  "gravy-secure-fields-script",
468
468
  "gravy-secure-fields-script-sandbox",
469
+ "bambora-na-custom-checkout-fields",
470
+ "worldpay-wpg-hosted-payment-page",
469
471
  "paypal-hosted-fields-payments-sdk",
470
472
  "paypal-card-fields-v5",
471
473
  "paypal-card-fields-v6",
@@ -1102,6 +1104,8 @@
1102
1104
  "mercado-pago-checkout-pro-card-form",
1103
1105
  "gravy-secure-fields-script",
1104
1106
  "gravy-secure-fields-script-sandbox",
1107
+ "bambora-na-custom-checkout-fields",
1108
+ "worldpay-wpg-hosted-payment-page",
1105
1109
  "paypal-hosted-fields-payments-sdk",
1106
1110
  "paypal-card-fields-v5",
1107
1111
  "paypal-card-fields-v6",
@@ -1340,10 +1340,6 @@ export declare function getCheckoutPreflightCatalog(): {
1340
1340
  readonly psp: "vgs";
1341
1341
  readonly reason: "iframe_bound_tokenization";
1342
1342
  readonly action: "human_checkout";
1343
- }, {
1344
- readonly psp: "bluesnap";
1345
- readonly reason: "multi_request_tokenization_not_supported";
1346
- readonly action: "human_checkout";
1347
1343
  }, {
1348
1344
  readonly psp: "stripe";
1349
1345
  readonly reason: "unbound_token_to_payment_intent_continuation";
@@ -1,5 +1,5 @@
1
1
  // Generated from @agent-cards/payment-core. Do not edit.
2
- // source-sha256: 639ffa689e8dfcf093eeba7e34b86c570e53ab17970976ae462a41fd2120d753
2
+ // source-sha256: 761b59c7c043a43a8d462a840e204c1677c55c0022f37e6be4a007d025357af8
3
3
  import { CHECKOUT_PREFLIGHT_CAPABILITIES } from './preflight-capabilities.generated.js';
4
4
  /** Advisory page discovery. These rules never admit a payment destination. */
5
5
  export const CHECKOUT_PREFLIGHT_VERSION = '1';
@@ -69,6 +69,29 @@ RULES.push({ id: 'tranzila-terminal-hosted-page', psp: 'tranzila', kinds: ['docu
69
69
  { id: 'gravy-secure-fields-script', psp: 'gravy', kinds: ['script'], hostname: /^cdn\.[a-z0-9-]+\.gr4vy\.app$/, pathname: GRAVY_SECURE_FIELDS_SCRIPT, origin: 'https://cdn.:merchant.gr4vy.app', path_pattern: '/secure-fields/{latest,:version}/secure-fields.js',
70
70
  sources: GRAVY_SOURCES }, { id: 'gravy-secure-fields-script-sandbox', psp: 'gravy', kinds: ['script'], hostname: /^cdn\.sandbox\.[a-z0-9-]+\.gr4vy\.app$/, pathname: GRAVY_SECURE_FIELDS_SCRIPT, origin: 'https://cdn.sandbox.:merchant.gr4vy.app', path_pattern: '/secure-fields/{latest,:version}/secure-fields.js',
71
71
  sources: GRAVY_SOURCES });
72
+ // Card frames the catalog scan of 2026-10-23 read at stores' card forms (no card typed, nothing paid). Each
73
+ // rule names its processor from a frame on the processor's own host. A file on the store's own host that only
74
+ // carries the processor's name, such as the copy of Bambora's customcheckout.js that Kent serves itself, names
75
+ // nothing.
76
+ const catalogScan = (reference) => ({ kind: 'saved_observation', reference: `catalog-psp-scan-2026-10-23:${reference}`, reviewed_on: '2026-10-23' });
77
+ RULES.push(
78
+ // Bambora Custom Checkout draws each card field as an iframe from the release its library was built for:
79
+ // /customcheckout/1/customcheckout.js draws 1.0.5-39 (the reviewed rule above), and Kent's own copy draws
80
+ // 1.0.2. The release names the frame and nothing more. Payment-core's bambora entry matches the card request
81
+ // on its own (/scripts/tokenization/tokens), so a release whose frame sent the card anywhere else would send
82
+ // only the placeholder card the agent typed.
83
+ { id: 'bambora-na-custom-checkout-fields', psp: 'bambora', kinds: ['frame', 'iframe', 'document'], hostname: /^libs\.na\.bambora\.com$/,
84
+ pathname: /^\/customcheckout\/1\.[0-9]{1,3}\.[0-9]{1,3}(?:-[0-9]{1,4})?\/iframe\.html$/, origin: 'https://libs.na.bambora.com', path_pattern: '/customcheckout/1.:minor.:patch{,-:build}/iframe.html',
85
+ sources: [{ kind: 'primary_source', reference: 'https://docs.na.worldline-solutions.com/build-your-integration/checkout-form/custom-checkout/reference', reviewed_on: '2026-09-11' },
86
+ catalogScan('kent-ca-custom-checkout-1.0.2-card-fields')] },
87
+ // Worldpay's hosted payment pages on payments.worldpay.com: Razer's checkout opens
88
+ // /app/hpp-iframe/integration/wpg/corporate in an iframe, which lands on /app/hpp/<build>/payment/start, where
89
+ // the shopper types the card on Worldpay's own page. The SDK's worldpay entry takes over Access Checkout's card
90
+ // session only, so the page names worldpay and stays unknown, as the secure.worldpay.com/wcc pages do.
91
+ { id: 'worldpay-wpg-hosted-payment-page', psp: 'worldpay', kinds: ['document', 'frame', 'iframe'], hostname: /^payments\.worldpay\.com$/,
92
+ pathname: /^\/app\/hpp(?:-iframe)?\//, origin: 'https://payments.worldpay.com', path_pattern: '/app/{hpp,hpp-iframe}/*',
93
+ sources: [catalogScan('razer-worldpay-hosted-payment-page')],
94
+ variant_unverified: 'The implemented Worldpay adapter covers Access Checkout card sessions; Worldpay hosted payment pages under payments.worldpay.com/app/hpp take the card on Worldpay\'s own page and are not admitted.' });
72
95
  // PayPal's legacy Hosted Fields (its JS SDK's components=hosted-fields), reviewed 2026-10-07 from Planet Golf
73
96
  // UK's payment step (read live 2026-10-06, SDK 5.0.580) and the two frame files it loads. The SDK renders each
74
97
  // card field as braintree-web's 3.32.0-payments-sdk-dev hosted-fields-input-frame.min.html (sha256
@@ -37,8 +37,20 @@
37
37
  * approval after Pay. Its environments are its API hosts: api.openpay.<mx|co|pe> (production) and
38
38
  * sandbox-api.openpay.<mx|co|pe> (sandbox). The token URL names the merchant in its path, so no URL is
39
39
  * prepared in advance: the claim binds the request by its host's environment and its shape, as ACI's.
40
+ *
41
+ * vtex_payments: prepared only. Ollie's headless checkout aborts its card post to the store's VTEX vault 12 seconds after
42
+ * it starts it, too soon for an approval after Pay. Its one environment is production: VTEX has no test host. The card
43
+ * URL names the store's account in its host and VTEX's transaction in its path, neither known before Pay, so the claim
44
+ * binds the request by its shape, as Openpay's. Ollie sends the same post again on a non-OK answer or a network error,
45
+ * then once more to api.vtexvault.com: the first post spends the approval, and each later one is refused like any
46
+ * request on a spent preparation, so one approval is one card use.
47
+ *
48
+ * processout: prepared only. ProcessOut.js gives its card frame 20 seconds from the tokenize message before it calls
49
+ * the page's error callback, too soon for an approval after the shop's continue press. Its environments share one
50
+ * host, api.processout.com: the project id in the card URL's query names them, proj_ (production) and test-proj_
51
+ * (sandbox), so the claim binds the request by its project's environment and its shape, as Openpay's.
40
52
  */
41
- export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs' | 'wix_payments' | 'paygent' | 'hipay' | 'openpay_bbva';
53
+ export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs' | 'wix_payments' | 'paygent' | 'hipay' | 'openpay_bbva' | 'vtex_payments' | 'processout';
42
54
  export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
43
55
  /**
44
56
  * How the approved card reaches the processor: the device's own request (token), or
@@ -77,7 +89,8 @@ export declare function preparedApprovalAnswersRetry(psp: string): boolean;
77
89
  * live, testsecureacceptance is test. So are Paddle VGS's two card URLs: vault is live,
78
90
  * sandbox-vault is test. So are Paygent's two token hosts: token is live, sandbox is test. So are
79
91
  * HiPay's two vault hosts: secure2-vault is live, stage-secure2-vault is test. So are Openpay's API
80
- * hosts: api.openpay.<mx|co|pe> is live, sandbox-api.openpay.<mx|co|pe> is test.
92
+ * hosts: api.openpay.<mx|co|pe> is live, sandbox-api.openpay.<mx|co|pe> is test. ProcessOut's share
93
+ * one host, and the card URL's project id attests them: proj_ is live, test-proj_ is test.
81
94
  */
82
95
  export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
83
96
  /**
@@ -90,9 +103,9 @@ export declare function aciPreparationUrl(environment: PreparationEnvironment):
90
103
  /**
91
104
  * A processor whose card request names an id the merchant mints only at Pay time (ACI's checkout
92
105
  * id), or the merchant's own id (Openpay's token URL carries the merchant id in its path and the
93
- * country in its host), so no URL can be prepared in advance: whether the adapters can intercept it
94
- * is asked of the registry (VaultClient.recognizesProcessor), not of the prepared endpoint, which
95
- * only names the preparation.
106
+ * country in its host, ProcessOut's card URL the project id in its query), so no URL can be prepared
107
+ * in advance: whether the adapters can intercept it is asked of the registry
108
+ * (VaultClient.recognizesProcessor), not of the prepared endpoint, which only names the preparation.
96
109
  */
97
110
  export declare function preparationBindsByShape(psp: string): boolean;
98
111
  /**