@agent-cards/sdk 0.23.20 → 0.23.21

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 (42) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/PREFLIGHT.md +2 -0
  3. package/README.md +143 -5
  4. package/dist/card-fields.generated.js +984 -57
  5. package/dist/cdp.js +146 -10
  6. package/dist/client.d.ts +104 -3
  7. package/dist/client.js +233 -23
  8. package/dist/comgate.d.ts +50 -0
  9. package/dist/comgate.js +80 -0
  10. package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
  11. package/dist/eprotect.generated.d.ts +1 -1
  12. package/dist/gmo.generated.js +7 -3
  13. package/dist/hipay.generated.d.ts +5 -5
  14. package/dist/hipay.generated.js +5 -2
  15. package/dist/hold.d.ts +1 -1
  16. package/dist/hold.js +10 -4
  17. package/dist/hosted-entry.d.ts +4 -0
  18. package/dist/hosted-entry.js +32 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/paddle-tokenex.generated.js +7 -3
  21. package/dist/payrails.generated.js +7 -3
  22. package/dist/pre-claim.d.ts +7 -1
  23. package/dist/pre-claim.js +127 -20
  24. package/dist/preflight-catalog.json +58 -0
  25. package/dist/preflight-schemas.json +4 -0
  26. package/dist/preflight.generated.js +26 -2
  27. package/dist/primer.generated.js +1027 -100
  28. package/dist/przelewy24.d.ts +47 -0
  29. package/dist/przelewy24.js +88 -0
  30. package/dist/registry.d.ts +6 -5
  31. package/dist/registry.js +27 -7
  32. package/dist/shiji-sts.generated.js +7 -3
  33. package/dist/stripe-checkout.generated.js +621 -12
  34. package/dist/tokenex-iframe.d.ts +93 -0
  35. package/dist/tokenex-iframe.generated.d.ts +81 -0
  36. package/dist/tokenex-iframe.generated.js +403 -0
  37. package/dist/tokenex-iframe.js +195 -0
  38. package/dist/transbank.d.ts +45 -0
  39. package/dist/transbank.js +73 -0
  40. package/dist/twoc2p.generated.js +7 -3
  41. package/examples/preflight/kernel-native/inventory.json +1 -1
  42. 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: 5773dac14c08b16588c9c36803bed4610e1be8f1cdcf6d38507cee1d35740dfc
3
+ // artifact-sha256: e8cb1ddfc6c8937f5126197b90d8261da8b07c9c68df1decb261052e8a2fabc1
4
4
  // src/hipay.js
5
5
  var HIPAY_PSP = "hipay";
6
6
  var HIPAY_CAPABILITY = "hipay_vault_token";
@@ -168,7 +168,10 @@ function hipayPinOriginUsable(origin) {
168
168
  const host = typeof origin === "string" ? PIN_ORIGIN.exec(origin) : null;
169
169
  return !!host && !HIPAY_DOMAIN.test(host[1]);
170
170
  }
171
- var HIPAY_MERCHANT_PINS = /* @__PURE__ */ Object.freeze([]);
171
+ var pin = (fields) => Object.freeze({ ...fields, origins: Object.freeze([...fields.origins]), usernames: Object.freeze([...fields.usernames]) });
172
+ var HIPAY_MERCHANT_PINS = /* @__PURE__ */ Object.freeze([
173
+ pin({ id: "happybulle", merchant: "HappyBulle", origins: ["https://www.happybulle.com"], usernames: ["94683309.secure-gateway.hipay-tpp.com"] })
174
+ ]);
172
175
  var usablePin = (entry) => !!entry && Array.isArray(entry.origins) && Array.isArray(entry.usernames) && entry.origins.length > 0 && entry.usernames.length > 0 && entry.origins.every(hipayPinOriginUsable) && entry.usernames.every((name) => hipayUsernameEnvironment(name) === "production");
173
176
  function hipayOriginPin(merchantOrigin, pins = HIPAY_MERCHANT_PINS) {
174
177
  if (typeof merchantOrigin !== "string" || !Array.isArray(pins) || !hipayPinOriginUsable(merchantOrigin))
package/dist/hold.d.ts CHANGED
@@ -55,7 +55,7 @@ export interface HoldOptions {
55
55
  export interface HeldCardRequest {
56
56
  /** The processor, by the recogniser's name (`braintree`), `bolt`, or `placeholder_card`; null when unnamed. */
57
57
  psp: string | null;
58
- /** Origin and path only. */
58
+ /** Origin and path only, with any credential a processor writes into the path masked (redactUrl). */
59
59
  endpoint: string;
60
60
  kind: 'tokenize' | 'store_card_post' | 'card_request';
61
61
  /** Whether a release can answer it: a recogniser the SDK pays, never Bolt or a store's own card post. */
package/dist/hold.js CHANGED
@@ -17,7 +17,9 @@
17
17
  // - Any request whose body carries the placeholder card number (`placeholderNumbers`): a store's own card post or
18
18
  // an unknown processor. The browser only ever holds the placeholder, so a body with its digits is a card request
19
19
  // wherever it goes. In sandbox it may be released unchanged (it carries the documented test number only).
20
- // The hold records the processor and the endpoint (origin and path), never a body or a header.
20
+ // The hold records the processor and the endpoint (origin and path, any path credential masked), never a body or a
21
+ // header.
22
+ import { redactUrl } from './client.js';
21
23
  /**
22
24
  * Bolt's embedded card frame (connect.bolt.com credit-card-input): a POST of `{ payload, nonce, public_key }` JSON,
23
25
  * the card boxed to Bolt's key, to `<base>/token` or `<base>/token/cvv`, with a 20 s timeout and retries.
@@ -87,14 +89,18 @@ export class HoldAnsweredError extends Error {
87
89
  this.name = 'HoldAnsweredError';
88
90
  }
89
91
  }
92
+ /**
93
+ * A held request's origin and path as redactUrl reads them, so a credential a processor writes into the path (GoPay's
94
+ * card form token, a PayMongo session) never reaches an event or onHeld.
95
+ */
90
96
  function endpointOf(url) {
91
97
  try {
92
- const u = new URL(url);
93
- return `${u.origin}${u.pathname}`;
98
+ new URL(url);
94
99
  }
95
100
  catch {
96
- return String(url).slice(0, 120);
101
+ return redactUrl(String(url)).slice(0, 120);
97
102
  }
103
+ return redactUrl(url);
98
104
  }
99
105
  function validTotal(t) {
100
106
  const v = t;
@@ -0,0 +1,4 @@
1
+ /** Whether this processor's hosted page is entered with a GET this build reads exactly. */
2
+ export declare function hasHostedGetEntry(psp: string): boolean;
3
+ /** Whether `url` is exactly the GET entry of this processor's hosted page (false for a processor with none). */
4
+ export declare function isHostedGetEntry(psp: string, url: string): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Hosted payment pages whose entry is a plain GET: the processors whose merchant sends the buyer's browser to the
3
+ * processor's own page with a link (a redirect, or a page or frame location set by script) rather than a form post.
4
+ *
5
+ * Such an entry carries no card. The cardholder's device takes it over (mode hosted_form): it makes the processor's
6
+ * own calls for the payment the link names, and the paused navigation gets this SDK's submitted_on_device notice.
7
+ * A GET recognizer otherwise reads as a card request carried in the URL (Worldpay eProtect's registration), which
8
+ * this build completes only for a grammar it checks, so a hosted page joins this table with the exact link
9
+ * payment-core's resolveRecognizerForUrl admits for it, and nothing else: a URL the table does not admit is never
10
+ * paused, as the API would refuse it, and the page's own request goes on unchanged.
11
+ *
12
+ * Each rule is dark on its own: a processor is paused only once the API serves its recognizer, which it does only
13
+ * for a build that declares the processor's capability (client.ts SUPPORTED_CAPABILITIES) and a company Agentcard
14
+ * turned the processor on for. A rule listed here for a processor this build does not declare pauses nothing.
15
+ */
16
+ import { PRZELEWY24_PSP, isPrzelewy24EntryUrl } from './przelewy24.js';
17
+ import { COMGATE_PSP, isComgateEntryUrl } from './comgate.js';
18
+ import { TRANSBANK_PSP, isTransbankEntryUrl } from './transbank.js';
19
+ const HOSTED_GET_ENTRIES = new Map([
20
+ [PRZELEWY24_PSP, isPrzelewy24EntryUrl],
21
+ [COMGATE_PSP, isComgateEntryUrl],
22
+ [TRANSBANK_PSP, isTransbankEntryUrl],
23
+ ]);
24
+ /** Whether this processor's hosted page is entered with a GET this build reads exactly. */
25
+ export function hasHostedGetEntry(psp) {
26
+ return HOSTED_GET_ENTRIES.has(psp);
27
+ }
28
+ /** Whether `url` is exactly the GET entry of this processor's hosted page (false for a processor with none). */
29
+ export function isHostedGetEntry(psp, url) {
30
+ const rule = HOSTED_GET_ENTRIES.get(psp);
31
+ return !!rule && typeof url === 'string' && rule(url) === true;
32
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { CheckoutAttachOptions } from './agentcard.js';
3
3
  export { CardEncryptedError, UnsupportedModeError, UnsupportedCapabilityError, PreparationRequiredError, AmountRequiredError, PageAmountRequiredError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, CheckoutAuthError, redactUrl, SUPPORTED_MODES, SUPPORTED_CAPABILITIES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, AdyenTestPlatformRefusedError, MerchantTotalError, PlaceholderCardError, } from './client.js';
4
4
  export type { GmoUnsupportedReason } from './gmo.js';
5
5
  export type { DatatransRefusalReason } from './datatrans.js';
6
+ export type { TokenexIframeRefusalReason } from './tokenex-iframe.js';
6
7
  export type { PelecardRefusalReason } from './pelecard.js';
7
8
  export type { PrimerUnsupportedReason, PrimerBlockedReason } from './primer.js';
8
9
  export { CONVERGE_REFUSAL_REASONS } from './elavon-converge.js';
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: c9116965f8802329701f18adf6519c9111857dfc4329e4f59d7666de150fb5a4
3
+ // artifact-sha256: 4289a22922e08251b579e9aadc396c8cff5d1a95b99ce35dd9a4618b43a33db5
4
4
  // src/sha256.js
5
5
  var K = /* @__PURE__ */ new Uint32Array([
6
6
  1116352408,
@@ -153,6 +153,9 @@ var typedArrayName = Object.getOwnPropertyDescriptor(TYPED_ARRAY, Symbol.toStrin
153
153
  function parseRsaPublicKey(key) {
154
154
  const spki = key && typeof key === "object" ? key.spki : void 0;
155
155
  const pin2 = key && typeof key === "object" ? key.sha256 : void 0;
156
+ const named = key && typeof key === "object" && Object.prototype.hasOwnProperty.call(key, "exponent") ? key.exponent : void 0;
157
+ if (named !== void 0 && named !== 17)
158
+ throw refuse("key");
156
159
  if (typeof spki !== "string" || spki.length > 1024 || spki.length % 4 !== 0 || !BASE64.test(spki))
157
160
  throw refuse("key");
158
161
  if (typeof pin2 !== "string" || !SHA256_HEX.test(pin2))
@@ -188,9 +191,10 @@ function parseRsaPublicKey(key) {
188
191
  if (!MODULUS_BYTES.includes(n.length) || !(n[0] & 128) || !(n[n.length - 1] & 1))
189
192
  throw refuse("key");
190
193
  const e = exponent.bytes;
191
- if (e.length !== 3 || e[0] !== 1 || e[1] !== 0 || e[2] !== 1)
194
+ const expected = named === 17 ? [17] : [1, 0, 1];
195
+ if (e.length !== expected.length || !expected.every((byte, i) => e[i] === byte))
192
196
  throw refuse("key");
193
- return Object.freeze({ n: toBigInt(n), e: 65537n, size: n.length });
197
+ return Object.freeze({ n: toBigInt(n), e: named === 17 ? 17n : 65537n, size: n.length });
194
198
  }
195
199
  function rsaesPkcs1v15Encrypt(key, message, random) {
196
200
  const { n, e, size } = parseRsaPublicKey(key);
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 38065a5bd834d5233979a0528fde4345378914e8e8b1a63e329bff0994572da5
3
+ // artifact-sha256: 056fa54de953be9af775505906b2fba30def2787a217a12fe2ca2a50b8614649
4
4
  // src/json-path.js
5
5
  function roundTrips(parsed, body) {
6
6
  try {
@@ -160,6 +160,9 @@ var typedArrayName = Object.getOwnPropertyDescriptor(TYPED_ARRAY, Symbol.toStrin
160
160
  function parseRsaPublicKey(key) {
161
161
  const spki = key && typeof key === "object" ? key.spki : void 0;
162
162
  const pin = key && typeof key === "object" ? key.sha256 : void 0;
163
+ const named = key && typeof key === "object" && Object.prototype.hasOwnProperty.call(key, "exponent") ? key.exponent : void 0;
164
+ if (named !== void 0 && named !== 17)
165
+ throw refuse("key");
163
166
  if (typeof spki !== "string" || spki.length > 1024 || spki.length % 4 !== 0 || !BASE64.test(spki))
164
167
  throw refuse("key");
165
168
  if (typeof pin !== "string" || !SHA256_HEX.test(pin))
@@ -195,9 +198,10 @@ function parseRsaPublicKey(key) {
195
198
  if (!MODULUS_BYTES.includes(n.length) || !(n[0] & 128) || !(n[n.length - 1] & 1))
196
199
  throw refuse("key");
197
200
  const e = exponent.bytes;
198
- if (e.length !== 3 || e[0] !== 1 || e[1] !== 0 || e[2] !== 1)
201
+ const expected = named === 17 ? [17] : [1, 0, 1];
202
+ if (e.length !== expected.length || !expected.every((byte, i) => e[i] === byte))
199
203
  throw refuse("key");
200
- return Object.freeze({ n: toBigInt(n), e: 65537n, size: n.length });
204
+ return Object.freeze({ n: toBigInt(n), e: named === 17 ? 17n : 65537n, size: n.length });
201
205
  }
202
206
  function readElement(der, at, tag) {
203
207
  if (at + 2 > der.length || der[at] !== tag)
@@ -2,6 +2,7 @@ import { StripeCheckoutGate } from './stripe-checkout.js';
2
2
  import { MercadoCheckoutGate } from './mercado-checkout.js';
3
3
  import { type JuspayCheckoutGate } from './juspay-gate.js';
4
4
  import { DatatransRepeats } from './datatrans.js';
5
+ import { TokenexIframeRepeats } from './tokenex-iframe.js';
5
6
  import { type AciGate } from './aci.js';
6
7
  import type { EprotectGate } from './eprotect.js';
7
8
  import { type ShijiStsCompanions } from './shiji-sts.js';
@@ -99,7 +100,7 @@ export interface PreClaimContext {
99
100
  verifoneProfileOf?(url: string): VerifoneProfile | null;
100
101
  cardOnFilePolicy?(url: string, method?: string): string | null;
101
102
  withoutCard?(url: string, body: string | null | undefined): 'continue' | 'refuse' | null;
102
- /** VaultClient.recognizesProcessor: whether the synced registry serves this processor. */
103
+ /** VaultClient.recognizesProcessor: whether the synced registry serves this processor (ACI, EBANX, the TokenEx iframe). */
103
104
  recognizesProcessor?(psp: string): boolean;
104
105
  /** VaultClient.sendEbanxFollowUp: a send an approved EBANX token request covers, from the cardholder's device. */
105
106
  sendEbanxFollowUp?(authorizationId: string, request: {
@@ -188,6 +189,8 @@ export interface PreClaimContext {
188
189
  juspay?: JuspayCheckoutGate;
189
190
  /** The attachment's approved Datatrans answers (the repeat rule, datatrans.ts). Absent: none are kept. */
190
191
  datatrans?: DatatransRepeats;
192
+ /** The attachment's approved TokenEx iframe answers (the repeat rule, tokenex-iframe.ts). Absent: none are kept. */
193
+ tokenexIframe?: TokenexIframeRepeats;
191
194
  /** The adapter's identity for the request's frame (CDP frameId, Playwright Frame); undefined when unknown. */
192
195
  frame?: unknown;
193
196
  /**
@@ -251,9 +254,12 @@ export declare const fiservPreClaim: PreClaimGate;
251
254
  export declare function isArmedCreditguardCall(vault: {
252
255
  recognizesProcessor?(psp: string): boolean;
253
256
  } | undefined, url: string): boolean;
257
+ export declare const przelewy24PreClaim: PreClaimGate;
254
258
  export declare const ekashuPreClaim: PreClaimGate;
259
+ export declare const transbankPreClaim: PreClaimGate;
255
260
  export declare const redsysPreClaim: PreClaimGate;
256
261
  export declare const quickpayPreClaim: PreClaimGate;
262
+ export declare const comgatePreClaim: PreClaimGate;
257
263
  export declare const paypalHostedFieldsPreClaim: PreClaimGate;
258
264
  export declare const paypalCardFormsPreClaim: PreClaimGate;
259
265
  export declare const cybersourceSecureAcceptancePreClaim: PreClaimGate;
package/dist/pre-claim.js CHANGED
@@ -7,7 +7,8 @@
7
7
  // the vault can never pay, CyberSource Flex Microform's field iframes (whose
8
8
  // capture context rides into the pause), Datatrans Secure Fields, whose
9
9
  // card URL also carries card posts the device could never fill and card posts
10
- // it already answered, and Pelecard's card submission URL, which also carries
10
+ // it already answered, the TokenEx hosted iframe v3, whose card path carries the
11
+ // same two, and Pelecard's card submission URL, which also carries
11
12
  // saved-token, wallet and keep-on-file bodies, Primer's
12
13
  // /payment-instruments, which also carries requests the vault can never pay,
13
14
  // Payrails, whose save needs the session key the page captured and whose
@@ -63,6 +64,7 @@ import { gmoRequestRefusal, isGmoTokenHost, isGmoTokenV1Request, recognizesGmo }
63
64
  import { JUSPAY_PSP } from './juspay-gate.js';
64
65
  import { datatransPauseDecision, isDatatransHost } from './datatrans.js';
65
66
  import { DATATRANS_INVALID_REQUEST } from './datatrans.generated.js';
67
+ import { INVALID_TOKENEX_IFRAME_REQUEST, TOKENEX_IFRAME_PSP, isTokenexIframePath, servesTokenexIframe, tokenexIframePauseDecision, } from './tokenex-iframe.js';
66
68
  import { isPelecardHost, pelecardRefusal } from './pelecard.js';
67
69
  import { ACI_CHECKOUT_REUSED } from './aci.js';
68
70
  import { ACI_EXPIRY_UNSTRIPPABLE } from './aci.generated.js';
@@ -79,8 +81,11 @@ import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoi
79
81
  import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isPayrailsPaymentUrl, isPayrailsStaticHost, payrailsKeyRefusal, payrailsPaymentRefusal, payrailsRequestKind, payrailsSaveRefusal, } from './payrails.js';
80
82
  import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
81
83
  import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
84
+ import { isArmedTransbankUrl } from './transbank.js';
82
85
  import { isRedsysEntry, isRedsysHost, redsysEntryUrl } from './redsys.js';
86
+ import { isArmedPrzelewy24Url, isPrzelewy24PayStep } from './przelewy24.js';
83
87
  import { isQuickpayCardLookup, isQuickpayEntry, quickpayEntryUrl } from './quickpay.js';
88
+ import { isArmedComgateUrl } from './comgate.js';
84
89
  import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
85
90
  import { PAYPAL_CARD_FIELDS_REASON, PAYPAL_GUEST_CHECKOUT_REASON, isPaypalCardFieldsConfirm, isPaypalGraphql, paypalCardFieldsBody, paypalGraphqlCard, paypalGraphqlNamesCardOperation, } from './paypal-card-forms.js';
86
91
  import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, isSecureAcceptanceFramePost, } from './cybersource-secure-acceptance.js';
@@ -817,10 +822,10 @@ async function juspayUnsupported(ctx, reason) {
817
822
  // card-on-file policy the API served with the recognizer (VaultClient.cardOnFilePolicy):
818
823
  // - a sound TOKENIZE card post defers, and the arbiter pauses it for approval, unless the same
819
824
  // frame's same form was already answered with an approved token (the repeat rule) and that
820
- // approval is still the one being handed off (datatransRepeatApproval): then that stored answer
825
+ // approval is still the one being handed off (handoffRepeatApproval): then that stored answer
821
826
  // is fulfilled here, with no claim, create or prompt. Once the answer reached the page, the
822
827
  // application's merchant resolver must first report the order still pending
823
- // (datatransRepeatAfterHandoff);
828
+ // (repeatAfterHandoff);
824
829
  // - a card post the API would refuse at create aborts with a `blocked` event that names the
825
830
  // API's own code, `invalid_datatrans_request` or `datatrans_card_on_file_refused` (the latter
826
831
  // only while the served policy refuses card on file), before anything is claimed, created or
@@ -848,25 +853,26 @@ const datatransPreClaim = (ctx) => {
848
853
  return undefined;
849
854
  if (decision !== 'card')
850
855
  return datatransBlock(ctx, decision);
851
- const approval = datatransRepeatApproval(ctx);
856
+ const approval = handoffRepeatApproval(ctx);
852
857
  const repeat = approval === null ? null : ctx.datatrans?.answerFor(ctx.frame, url, ctx.request.body, approval) ?? null;
853
858
  if (!repeat)
854
859
  return undefined;
855
- return ctx.lifecycle.getState().status === 'awaiting_merchant' ? datatransRepeatAfterHandoff(ctx, repeat) : datatransRepeat(ctx, repeat);
860
+ return ctx.lifecycle.getState().status === 'awaiting_merchant' ? repeatAfterHandoff(ctx, repeat, 'datatrans') : answerRepeat(ctx, repeat, 'datatrans');
856
861
  };
857
862
  /**
858
- * The approval whose stored Datatrans answer a repeat may still receive, or null. A stored token
863
+ * The approval whose stored Datatrans or TokenEx answer a repeat may still receive, or null. A stored token
859
864
  * answers a re-submit only while the approval it came from is still being handed off: the lifecycle
860
865
  * names it and reads awaiting_approval (its answer is being delivered) or awaiting_merchant (the
861
866
  * merchant has not confirmed yet), and nothing latched, tore down or cancelled the attachment. The
862
867
  * order completing or failing, an unknown outcome, a decline, a hold for the cardholder, a retry
863
868
  * after a merchant failure and a new authorization all end it, and the re-submit is then a new
864
- * question for the terminal arbiter, which holds or pauses it like any card post: TOKENIZE binds no
865
- * amount, so one approval never answers for a later purchase. Once the answer reached the page
866
- * (awaiting_merchant), the gate also reconciles before it answers (datatransRepeatAfterHandoff), and
867
- * the stored answer itself ends once its form goes quiet (DatatransRepeats, DATATRANS_REPEAT_IDLE_MS).
869
+ * question for the terminal arbiter, which holds or pauses it like any card post: neither card post
870
+ * binds an amount, so one approval never answers for a later purchase. Once the answer reached the
871
+ * page (awaiting_merchant), the gate also reconciles before it answers (repeatAfterHandoff). A stored
872
+ * Datatrans answer also ends once its form goes quiet (DatatransRepeats, DATATRANS_REPEAT_IDLE_MS), and
873
+ * a stored TokenEx answer once its iframe key expires (TokenexIframeRepeats).
868
874
  */
869
- function datatransRepeatApproval(ctx) {
875
+ function handoffRepeatApproval(ctx) {
870
876
  if (ctx.terminal() || ctx.interceptionAborted() || ctx.lifecycleAborted())
871
877
  return null;
872
878
  const { status, authorizationId } = ctx.lifecycle.getState();
@@ -883,36 +889,82 @@ function datatransRepeatApproval(ctx) {
883
889
  * arbiter, and so does every re-submit on an attachment with no resolver: a later purchase never
884
890
  * receives an earlier token without the cardholder, and the paused post never waits on a stalled read.
885
891
  */
886
- function datatransRepeatAfterHandoff(ctx, repeat) {
892
+ function repeatAfterHandoff(ctx, repeat, psp) {
887
893
  if (!ctx.lifecycle.hasMerchantResolver())
888
894
  return undefined;
889
- return datatransPendingRepeat(ctx, repeat);
895
+ return pendingRepeat(ctx, repeat, psp);
890
896
  }
891
- async function datatransPendingRepeat(ctx, repeat) {
897
+ async function pendingRepeat(ctx, repeat, psp) {
892
898
  if (!await ctx.lifecycle.merchantOrderPending())
893
899
  return undefined;
894
900
  // The resolver answered asynchronously: the checkout must still be handing off this approval.
895
- if (ctx.lifecycle.getState().status !== 'awaiting_merchant' || datatransRepeatApproval(ctx) !== repeat.authorizationId)
901
+ if (ctx.lifecycle.getState().status !== 'awaiting_merchant' || handoffRepeatApproval(ctx) !== repeat.authorizationId)
896
902
  return undefined;
897
- return datatransRepeat(ctx, repeat);
903
+ return answerRepeat(ctx, repeat, psp);
898
904
  }
899
905
  async function datatransBlock(ctx, reason) {
900
906
  ctx.onEvent?.({ type: 'blocked', detail: reason });
901
907
  await ctx.effects.abort();
902
908
  return { kind: 'abort' };
903
909
  }
904
- async function datatransRepeat(ctx, repeat) {
910
+ /** Fulfill a repeat with the stored answer (`repeat_answered`), or abort it when the browser cannot take it (`<psp>_repeat_unanswered`). */
911
+ async function answerRepeat(ctx, repeat, psp) {
905
912
  try {
906
913
  await ctx.effects.answer(repeat.response);
907
914
  }
908
915
  catch {
909
- ctx.onEvent?.({ type: 'blocked', detail: 'datatrans_repeat_unanswered' });
916
+ ctx.onEvent?.({ type: 'blocked', detail: `${psp}_repeat_unanswered` });
910
917
  await ctx.effects.abort();
911
918
  return { kind: 'abort' };
912
919
  }
913
- ctx.onEvent?.({ type: 'repeat_answered', detail: { psp: 'datatrans', authorizationId: repeat.authorizationId } });
920
+ ctx.onEvent?.({ type: 'repeat_answered', detail: { psp, authorizationId: repeat.authorizationId } });
914
921
  return { kind: 'answer' };
915
922
  }
923
+ // The TokenEx hosted iframe v3 (tokenex-iframe.ts), JetBlue's card form. TokenEx's card-number frame posts the
924
+ // whole card to its own document URL, and the same path takes posts the device could never fill (a cvvOnly frame,
925
+ // a query this lane has not read) and, after an approval, the frame's own re-sends. The recognizer matches only the
926
+ // exact card URL, so once the registry serves the TokenEx iframe (servesTokenexIframe), every POST on the frame's
927
+ // /iframe/v3 path is judged here, before the terminal arbiter would pause it, by payment-core's own pause decision:
928
+ // - the sound card post defers, and the arbiter pauses it for approval, unless the same frame already sent the
929
+ // same bytes and had them answered with an approved token, and that approval is still the one being handed off
930
+ // (handoffRepeatApproval): then that stored answer is fulfilled here, with no claim, create or prompt. Once the
931
+ // answer reached the page, the application's merchant resolver must first report the order still pending
932
+ // (repeatAfterHandoff). A post with another placeholder or CVV is a new question, as for Datatrans;
933
+ // - any other post on that path that carries a value in Data aborts with a `blocked` event that names the API's
934
+ // own code, invalid_tokenex_iframe_request, before anything is claimed, created or prompted. Like Datatrans's
935
+ // refusal it neither holds the attachment nor changes its state;
936
+ // - a post with nothing in Data carries no card and defers: on the exact URL it continues untouched through the
937
+ // recognizer's passWithoutCard, and anywhere else it continues as an unrecognized request.
938
+ // Nothing is decided without the registry: for an org the API did not serve the entry to, the gate defers and every
939
+ // TokenEx request continues as before. The frame's document GETs, BinLookup and Forter never reach it as a card post.
940
+ // Synchronous until an effect, so a card post pauses with no await. A check that throws blocks the request.
941
+ const tokenexIframePreClaim = (ctx) => {
942
+ const { url, method } = ctx.request;
943
+ if (method.toUpperCase() !== 'POST' || !isTokenexIframePath(url) || !servesTokenexIframe(ctx.vault))
944
+ return undefined;
945
+ let decision;
946
+ try {
947
+ decision = tokenexIframePauseDecision({ url, headers: ctx.request.headers, body: ctx.request.body });
948
+ }
949
+ catch {
950
+ decision = INVALID_TOKENEX_IFRAME_REQUEST;
951
+ }
952
+ if (decision === null)
953
+ return undefined;
954
+ if (decision !== 'card')
955
+ return tokenexIframeBlock(ctx, decision);
956
+ const approval = handoffRepeatApproval(ctx);
957
+ const repeat = approval === null ? null : ctx.tokenexIframe?.answerFor(ctx.frame, url, ctx.request.body, approval) ?? null;
958
+ if (!repeat)
959
+ return undefined;
960
+ return ctx.lifecycle.getState().status === 'awaiting_merchant'
961
+ ? repeatAfterHandoff(ctx, repeat, TOKENEX_IFRAME_PSP) : answerRepeat(ctx, repeat, TOKENEX_IFRAME_PSP);
962
+ };
963
+ async function tokenexIframeBlock(ctx, reason) {
964
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
965
+ await ctx.effects.abort();
966
+ return { kind: 'abort' };
967
+ }
916
968
  // Pelecard Iframe/Redirect 2.0 (pelecard.ts). The payment page's card URL, POST
917
969
  // /PaymentGW/Transaction on gateway21 or gateway20.pelecard.biz, also carries the wallet, Bit,
918
970
  // PIN pad and bank tabs with no card, and card-shaped bodies the device could never fill: a saved
@@ -1574,6 +1626,23 @@ async function shijiStsCompanion(ctx, body) {
1574
1626
  ctx.onEvent?.({ type: 'companion_continued', detail: { url: redactUrl(url), psp: 'shiji_sts', fields: ['cvv'] } });
1575
1627
  return { kind: 'continue' };
1576
1628
  }
1629
+ // Przelewy24's pay wall (przelewy24.ts). Once this build's registry serves Przelewy24's entry (a capability the
1630
+ // API serves only to a company Agentcard turned Przelewy24 on for), every request to the pay wall's two hosts
1631
+ // pauses. The merchant's entry, a GET of /trnRequest/<token> exactly, is no step of this gate's: it defers, and the
1632
+ // terminal arbiter pauses it for the claim flow like any card request, so the cardholder's device pays it. Every
1633
+ // step toward paying on the pay wall itself aborts as an unsupported checkout: every POST there (the card method's
1634
+ // post, the card form's post, a form posted to the entry's link) and the GET of the Saferpay resolver, which names
1635
+ // the typed card's type. So Przelewy24 never sees the card the agent typed. The agent's browser only reaches the
1636
+ // pay wall when the entry was not paused (an attachment made too late, say), and then only the cardholder's device
1637
+ // can pay. The pay wall's own pages, scripts and images are not this gate's and continue. With Przelewy24 not
1638
+ // served, the gate defers, and every request continues as before. Synchronous until it must abort, like every gate.
1639
+ // Exported for the gate registry's tests.
1640
+ export const przelewy24PreClaim = (ctx) => {
1641
+ const { url, method } = ctx.request;
1642
+ if (!isPrzelewy24PayStep(url, method) || !isArmedPrzelewy24Url(ctx.vault, url))
1643
+ return undefined;
1644
+ return processorPageAbort(ctx);
1645
+ };
1577
1646
  // NMI's hosted payment page (ekashu.ts). Once this build's registry serves NMI's entry (a
1578
1647
  // capability the API serves only to a company Agentcard turned NMI on for), every request to
1579
1648
  // NMI's two hosts pauses, and this gate judges the POSTs:
@@ -1607,6 +1676,27 @@ async function processorPageAbort(ctx) {
1607
1676
  await ctx.effects.abort();
1608
1677
  return { kind: 'abort' };
1609
1678
  }
1679
+ // Transbank's Webpay Plus (transbank.ts). Once this build's registry serves Webpay's entry (a capability the API
1680
+ // serves only to a company Agentcard turned Webpay on for), every request to Webpay's two hosts pauses, and this gate
1681
+ // judges them:
1682
+ // - the merchant's entry, the GET of initTransaction?token_ws=<token> exactly as the redirect writes it, is a GET
1683
+ // and not this gate's: the terminal arbiter pauses it for the claim flow like any card request, and the
1684
+ // cardholder's device pays it;
1685
+ // - every other request there but a GET (the token's one submit to init_transaction.cgi, Webpay's card lookup and
1686
+ // card check, the card post to bp_control.cgi, and an entry posted as a form) aborts as an unsupported checkout,
1687
+ // so the token is never spent in the agent's browser and Webpay never sees the card the agent typed. The agent's
1688
+ // browser only reaches Webpay's pages when the entry was not paused (an attachment made too late, an entry with
1689
+ // another parameter), and then only the cardholder's device can pay;
1690
+ // - another GET there (Webpay's scripts and images, or an entry written another way, whose page then submits the
1691
+ // token, which this gate aborts) continues.
1692
+ // With Webpay not served, the gate defers and every request continues as before. Synchronous until it must abort,
1693
+ // like every gate. Exported for the gate registry's tests.
1694
+ export const transbankPreClaim = (ctx) => {
1695
+ const { url, method } = ctx.request;
1696
+ if (String(method).toUpperCase() === 'GET' || !isArmedTransbankUrl(ctx.vault, url))
1697
+ return undefined;
1698
+ return processorPageAbort(ctx);
1699
+ };
1610
1700
  // Redsys's hosted payment page (redsys.ts). Once this build's registry serves Redsys's entry (a
1611
1701
  // capability the API serves only to a company Agentcard turned Redsys on for), every request to
1612
1702
  // Redsys's hosts pauses, and this gate judges the POSTs:
@@ -1655,6 +1745,23 @@ export const quickpayPreClaim = (ctx) => {
1655
1745
  return undefined;
1656
1746
  return processorPageAbort(ctx);
1657
1747
  };
1748
+ // ComGate's payment gateway (comgate.ts). Once this build's registry serves ComGate's entry (a capability the API serves
1749
+ // only to a company Agentcard turned ComGate on for), the gateway link pauses for the cardholder's device and every
1750
+ // request to ComGate's card hosts pauses too, and this gate judges those:
1751
+ // - every request but a GET (the gateway's card post and card check, the Web Checkout SDK's provider calls and card
1752
+ // check, the card frames' BIN lookup and their log) aborts as an unsupported checkout, so ComGate never sees the
1753
+ // card the agent typed. The agent's browser only reaches ComGate's card form when the link was not paused (an
1754
+ // attachment made too late, a link with another parameter, a shop on ComGate's Web Checkout SDK), and then only the
1755
+ // cardholder's device can pay;
1756
+ // - a GET there (the gateway's reads of the payment, the card frames' pages and scripts) is not this gate's and continues.
1757
+ // With ComGate not served, the gate defers and every request continues as before. Synchronous until it must abort, like
1758
+ // every gate. Exported for the gate registry's tests.
1759
+ export const comgatePreClaim = (ctx) => {
1760
+ const { url, method } = ctx.request;
1761
+ if (String(method).toUpperCase() === 'GET' || !isArmedComgateUrl(ctx.vault, url))
1762
+ return undefined;
1763
+ return processorPageAbort(ctx);
1764
+ };
1658
1765
  // PayPal's legacy Hosted Fields (paypal-hosted-fields.ts). Its tokenization frame posts the card the
1659
1766
  // agent typed to the Orders v2 confirm-payment-source on cors.api.paypal.com (or its sandbox), which
1660
1767
  // no Vault mode takes over, so every company's adapters hand each POST there to this gate:
@@ -2220,7 +2327,7 @@ async function unsupportedContinue(ctx) {
2220
2327
  * arbiter runs after all of them and is not part of this list. A gate appended
2221
2328
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
2222
2329
  * pauses each one itself. */
2223
- export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, quickpayPreClaim, paypalHostedFieldsPreClaim,
2330
+ 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, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, quickpayPreClaim, comgatePreClaim, przelewy24PreClaim, transbankPreClaim, paypalHostedFieldsPreClaim,
2224
2331
  paypalCardFormsPreClaim, cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim, hipayPreClaim];
2225
2332
  /** Run each gate in order; the first to return a decision wins. If none does,
2226
2333
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
@@ -2174,6 +2174,64 @@
2174
2174
  "hostname_pattern": "^assets\\.braintreegateway\\.com$",
2175
2175
  "pathname_pattern": "^\\/web\\/[0-9]+\\.[0-9]+\\.[0-9]+-payments-sdk(?:-[a-z]+)?\\/html\\/hosted-fields-(?:input-frame|payments-sdk-tokenization-frame)(?:\\.min)?\\.html$"
2176
2176
  },
2177
+ {
2178
+ "id": "paypal-card-fields-v5",
2179
+ "psp": "paypal",
2180
+ "kinds": [
2181
+ "frame",
2182
+ "iframe"
2183
+ ],
2184
+ "origin": "https://www.paypal.com",
2185
+ "path_pattern": "/smart/card-field",
2186
+ "sources": [
2187
+ {
2188
+ "kind": "primary_source",
2189
+ "reference": "https://www.paypal.com/sdk/js?components=card-fields",
2190
+ "reviewed_on": "2026-10-15"
2191
+ },
2192
+ {
2193
+ "kind": "primary_source",
2194
+ "reference": "https://developer.paypal.com/docs/checkout/advanced/integrate/",
2195
+ "reviewed_on": "2026-10-15"
2196
+ },
2197
+ {
2198
+ "kind": "saved_observation",
2199
+ "reference": "lake-antoine-park-2026-10-15:paypal-card-field-frames",
2200
+ "reviewed_on": "2026-10-15"
2201
+ }
2202
+ ],
2203
+ "hostname_pattern": "^www\\.paypal\\.com$",
2204
+ "pathname_pattern": "^\\/smart\\/card-field$"
2205
+ },
2206
+ {
2207
+ "id": "paypal-card-fields-v6",
2208
+ "psp": "paypal",
2209
+ "kinds": [
2210
+ "frame",
2211
+ "iframe"
2212
+ ],
2213
+ "origin": "https://www.paypal.com",
2214
+ "path_pattern": "/web-sdk/v6/card-fields-content",
2215
+ "sources": [
2216
+ {
2217
+ "kind": "primary_source",
2218
+ "reference": "https://www.paypal.com/sdk/js?components=card-fields",
2219
+ "reviewed_on": "2026-10-15"
2220
+ },
2221
+ {
2222
+ "kind": "primary_source",
2223
+ "reference": "https://developer.paypal.com/docs/checkout/advanced/integrate/",
2224
+ "reviewed_on": "2026-10-15"
2225
+ },
2226
+ {
2227
+ "kind": "saved_observation",
2228
+ "reference": "lake-antoine-park-2026-10-15:paypal-card-field-frames",
2229
+ "reviewed_on": "2026-10-15"
2230
+ }
2231
+ ],
2232
+ "hostname_pattern": "^www\\.paypal\\.com$",
2233
+ "pathname_pattern": "^\\/web-sdk\\/v6\\/card-fields-content$"
2234
+ },
2177
2235
  {
2178
2236
  "id": "cybersource-secure-acceptance-silent-order-post",
2179
2237
  "psp": "cybersource_secure_acceptance",
@@ -467,6 +467,8 @@
467
467
  "gravy-secure-fields-script",
468
468
  "gravy-secure-fields-script-sandbox",
469
469
  "paypal-hosted-fields-payments-sdk",
470
+ "paypal-card-fields-v5",
471
+ "paypal-card-fields-v6",
470
472
  "cybersource-secure-acceptance-silent-order-post",
471
473
  "fiserv-commercehub-payment-fields",
472
474
  "fiserv-commercehub-checkout-script",
@@ -1101,6 +1103,8 @@
1101
1103
  "gravy-secure-fields-script",
1102
1104
  "gravy-secure-fields-script-sandbox",
1103
1105
  "paypal-hosted-fields-payments-sdk",
1106
+ "paypal-card-fields-v5",
1107
+ "paypal-card-fields-v6",
1104
1108
  "cybersource-secure-acceptance-silent-order-post",
1105
1109
  "fiserv-commercehub-payment-fields",
1106
1110
  "fiserv-commercehub-checkout-script",
@@ -1,5 +1,5 @@
1
1
  // Generated from @agent-cards/payment-core. Do not edit.
2
- // source-sha256: f0596389845388a1c6150964e576c420abb0b44a805347e4ba4d0617f3b8ea2b
2
+ // source-sha256: 639ffa689e8dfcf093eeba7e34b86c570e53ab17970976ae462a41fd2120d753
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';
@@ -87,6 +87,20 @@ RULES.push({ id: 'paypal-hosted-fields-payments-sdk', psp: 'paypal', kinds: ['fr
87
87
  paypalHostedFieldsSource('https://www.paypal.com/sdk/js?components=hosted-fields'),
88
88
  { kind: 'saved_observation', reference: 'planet-golf-uk-2026-10-06:paypal-hosted-fields-payments-sdk-frames', reviewed_on: '2026-10-07' }],
89
89
  variant_unverified: 'PayPal legacy Hosted Fields frames send the card to the Orders v2 confirm-payment-source on cors.api.paypal.com; no Vault mode takes that request over.' });
90
+ // PayPal Card Fields (its JS SDK's components=card-fields, v5, and web-sdk v6), reviewed 2026-10-15 from
91
+ // WooCommerce PayPal Payments 4.1.3 checkouts (PayPal JS SDK 5.0.582, card-field client 1.270.0) and PayPal's
92
+ // public web-sdk v6 6.68.0 bundles, with no card typed. v5 renders each card field as a frame on
93
+ // www.paypal.com/smart/card-field (?type=number, expiry, cvv or name), and v6 as
94
+ // www.paypal.com/web-sdk/v6/card-fields-content. Each sends the card to the Orders v2 confirm-payment-source,
95
+ // which payment-core's paypal entry takes while it ships dark, so the frames name paypal and the result
96
+ // stays unknown until the lane is served to every client.
97
+ const paypalCardFieldsSource = (reference) => ({ kind: 'primary_source', reference, reviewed_on: '2026-10-15' });
98
+ const PAYPAL_CARD_FIELDS_SOURCES = [paypalCardFieldsSource('https://www.paypal.com/sdk/js?components=card-fields'),
99
+ paypalCardFieldsSource('https://developer.paypal.com/docs/checkout/advanced/integrate/'),
100
+ { kind: 'saved_observation', reference: 'lake-antoine-park-2026-10-15:paypal-card-field-frames', reviewed_on: '2026-10-15' }];
101
+ RULES.push({ id: 'paypal-card-fields-v5', psp: 'paypal', kinds: ['frame', 'iframe'], hostname: /^www\.paypal\.com$/, pathname: /^\/smart\/card-field$/,
102
+ origin: 'https://www.paypal.com', path_pattern: '/smart/card-field', sources: PAYPAL_CARD_FIELDS_SOURCES }, { id: 'paypal-card-fields-v6', psp: 'paypal', kinds: ['frame', 'iframe'], hostname: /^www\.paypal\.com$/, pathname: /^\/web-sdk\/v6\/card-fields-content$/,
103
+ origin: 'https://www.paypal.com', path_pattern: '/web-sdk/v6/card-fields-content', sources: PAYPAL_CARD_FIELDS_SOURCES });
90
104
  // CyberSource Secure Acceptance's Silent Order POST (the Secure Acceptance Checkout API), reviewed 2026-10-08 from
91
105
  // CyberSource's endpoint table and Arrow Electronics' public bundles (no card was sent). The merchant's own page
92
106
  // posts the card fields, unsigned, beside the fields its server signed, to a /silent/ path on CyberSource's Secure
@@ -308,7 +322,7 @@ RULES.push({ id: 'worldline-mycheckout-page', psp: 'worldline_mycheckout', kinds
308
322
  origin: 'https://payment.pay1.checkout.worldline-solutions.com', path_pattern: '/checkout/{payment-product-details/}:hosted_checkout', sources: worldlineSources }, { id: 'worldline-mycheckout-card-form', psp: 'worldline_mycheckout', kinds: ['form'], hostname: /^payment\.pay1\.checkout\.worldline-solutions\.com$/, pathname: /^\/checkout$/,
309
323
  origin: 'https://payment.pay1.checkout.worldline-solutions.com', path_pattern: '/checkout', sources: worldlineSources });
310
324
  // A processor that ships dark (its definition declares a capability) is left out of the bundled
311
- // catalog. Datatrans's discovery rules wait with it: they join RULES only once KNOWN_PSPS, the
325
+ // catalog. Datatrans's and the TokenEx iframe's discovery rules wait with it: they join RULES only once KNOWN_PSPS, the
312
326
  // bundled catalog's processors, lists its psp. Until then its assets are not evidence, so a page
313
327
  // that loads them, alone or beside another processor's SDK, reads exactly as it did before the
314
328
  // processor existed. (The dark GMO, CyberSource Flex, Juspay, Pelecard, ACI, CreditGuard,
@@ -326,6 +340,16 @@ for (const [suffix, hostname, origin] of [['', /^pay\.datatrans\.com$/, 'https:/
326
340
  sources: [datatransSource('https://pay.datatrans.com/upp/payment/js/secure-fields-2.0.0.js'), datatransSource('https://docs.pci-proxy.com/docs/secure-fields-quick-start')] }, { id: `datatrans-secure-fields-frame${suffix}`, psp: 'datatrans', kinds: ['document', 'frame', 'iframe'], hostname, pathname: /^\/upp\/payment\/SecureFields\/paymentField$/, origin, path_pattern: '/upp/payment/SecureFields/paymentField',
327
341
  sources: [datatransSource('https://pay.datatrans.com/upp/payment/SecureFields/field.js'), datatransSource('https://docs.datatrans.ch/docs/secure-fields')] });
328
342
  }
343
+ // TokenEx hosted iframe v3, read 2026-10-18 from JetBlue's checkout: the parent library
344
+ // (htp.tokenex.com/Iframe/Iframe-v3.min.js) and the iframe document it loads (/iframe/v3?<query>), on the
345
+ // live host and its test twin. Dark behind tokenex_iframe_tokenize, so it waits with Datatrans.
346
+ const tokenexSource = (reference) => ({ kind: 'primary_source', reference, reviewed_on: '2026-10-18' });
347
+ for (const [suffix, hostname, origin] of [['', /^htp\.tokenex\.com$/, 'https://htp.tokenex.com'],
348
+ ['-test', /^test-htp\.tokenex\.com$/, 'https://test-htp.tokenex.com']]) {
349
+ SERVED_ONLY_RULES.push({ id: `tokenex-iframe-v3-script${suffix}`, psp: 'tokenex_iframe', kinds: ['script'], hostname, pathname: /^\/Iframe\/Iframe-v3(?:\.min)?\.js$/, origin, path_pattern: '/Iframe/Iframe-v3{,.min}.js',
350
+ sources: [tokenexSource('https://htp.tokenex.com/Iframe/Iframe-v3.min.js')] }, { id: `tokenex-iframe-v3-frame${suffix}`, psp: 'tokenex_iframe', kinds: ['document', 'frame', 'iframe'], hostname, pathname: /^\/iframe\/v3$/, origin, path_pattern: '/iframe/v3',
351
+ sources: [tokenexSource('https://htp.tokenex.com/Iframe/v3.min.js?version=latest')] });
352
+ }
329
353
  RULES.push(...SERVED_ONLY_RULES.filter(rule => KNOWN_PSPS.includes(rule.psp)));
330
354
  // Keep sandbox evidence truthful instead of labelling it with a production origin.
331
355
  for (const [baseId, suffix, hostname, origin] of [