@agent-cards/sdk 0.23.20 → 0.23.22

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 (64) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/PREFLIGHT.md +6 -0
  3. package/README.md +169 -5
  4. package/dist/amadeus-checkout.generated.d.ts +6 -1
  5. package/dist/amadeus-checkout.generated.js +26 -2
  6. package/dist/card-fields.generated.js +2341 -127
  7. package/dist/cdp.js +199 -28
  8. package/dist/client.d.ts +177 -9
  9. package/dist/client.js +387 -37
  10. package/dist/comgate.d.ts +50 -0
  11. package/dist/comgate.js +80 -0
  12. package/dist/creditguard.generated.js +4 -2
  13. package/dist/cse-body.js +11 -0
  14. package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
  15. package/dist/ebanx.d.ts +1 -1
  16. package/dist/ebanx.js +1 -1
  17. package/dist/eprotect.generated.d.ts +1 -1
  18. package/dist/eximbay.d.ts +46 -0
  19. package/dist/eximbay.js +69 -0
  20. package/dist/gmo.generated.js +7 -3
  21. package/dist/hipay.generated.d.ts +5 -5
  22. package/dist/hipay.generated.js +5 -2
  23. package/dist/hold.d.ts +1 -1
  24. package/dist/hold.js +10 -4
  25. package/dist/hosted-entry.d.ts +4 -0
  26. package/dist/hosted-entry.js +32 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/lifecycle.d.ts +9 -4
  29. package/dist/lifecycle.js +16 -4
  30. package/dist/paddle-tokenex.generated.js +7 -3
  31. package/dist/paygent.generated.d.ts +60 -0
  32. package/dist/paygent.generated.js +425 -0
  33. package/dist/payrails.generated.js +7 -3
  34. package/dist/payrexx.d.ts +91 -0
  35. package/dist/payrexx.generated.d.ts +91 -0
  36. package/dist/payrexx.generated.js +536 -0
  37. package/dist/payrexx.js +141 -0
  38. package/dist/pre-claim.d.ts +30 -1
  39. package/dist/pre-claim.js +257 -39
  40. package/dist/preflight-catalog.json +58 -0
  41. package/dist/preflight-schemas.json +4 -0
  42. package/dist/preflight.generated.js +26 -2
  43. package/dist/prepared-processor.d.ts +7 -2
  44. package/dist/prepared-processor.js +16 -2
  45. package/dist/primer.generated.js +2469 -255
  46. package/dist/przelewy24.d.ts +47 -0
  47. package/dist/przelewy24.js +88 -0
  48. package/dist/registry.d.ts +6 -5
  49. package/dist/registry.js +27 -7
  50. package/dist/shiji-sts.generated.js +7 -3
  51. package/dist/stripe-checkout.generated.js +896 -19
  52. package/dist/toast-seal.generated.d.ts +37 -0
  53. package/dist/toast-seal.generated.js +625 -0
  54. package/dist/toast.generated.d.ts +41 -0
  55. package/dist/toast.generated.js +113 -0
  56. package/dist/tokenex-iframe.d.ts +93 -0
  57. package/dist/tokenex-iframe.generated.d.ts +81 -0
  58. package/dist/tokenex-iframe.generated.js +403 -0
  59. package/dist/tokenex-iframe.js +195 -0
  60. package/dist/transbank.d.ts +45 -0
  61. package/dist/transbank.js +73 -0
  62. package/dist/twoc2p.generated.js +7 -3
  63. package/examples/preflight/kernel-native/inventory.json +1 -1
  64. package/package.json +3 -3
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ComGate's payment gateway (pay1 to pay4.comgate.cz, where most Czech and Slovak e-shops send the buyer), as this
3
+ * SDK reads it.
4
+ *
5
+ * The merchant's server creates the payment at ComGate and sends the buyer's browser to the gateway link ComGate
6
+ * answered with, https://payN.comgate.cz/init?id=<transaction id>, by a redirect or by setting the page's or a
7
+ * frame's location. That GET carries no card and nothing but the payment's id, and it is the one request a build
8
+ * that declares ComGate's capability pauses for ComGate (hosted-entry.ts): the cardholder's device then makes the
9
+ * gateway's own calls for that payment over the end-to-end TLS relay (payment-core processors/comgate.js). Only the
10
+ * link exactly as ComGate writes it is the entry; any other parameter would be handed on to the gateway's own
11
+ * route, which these rules never see.
12
+ *
13
+ * COMGATE_GATEWAY_HOSTS is payment-core's COMGATE_GATEWAY_HOSTS, and isComgateEntryUrl is payment-core's rule of
14
+ * the same name; hosted-entry.test.mjs pins both to payment-core on every case payment-core's own tests list.
15
+ */
16
+ export declare const COMGATE_PSP = "comgate";
17
+ /** The dark-launch capability the API serves ComGate's recognizer behind (payment-core definitions.js). */
18
+ export declare const COMGATE_CAPABILITY = "comgate_hosted_payment_page";
19
+ /** The four gateway hosts ComGate's create answer names, each a host of its own entry. */
20
+ export declare const COMGATE_GATEWAY_HOSTS: readonly string[];
21
+ /**
22
+ * The hosts that take a card, or part of one, for ComGate: the gateway's API (payments.comgate.cz behind pay1 and pay2,
23
+ * payments-gcp.comgate.cz behind pay3 and pay4), where the gateway posts the card (/client/instructions/card-process,
24
+ * check-card) and ComGate's Web Checkout SDK posts its own (/checkout/provider/*, /checkout/card/check-card), and the
25
+ * card frames' hosts (sec.comgate.cz, sec-gcp.comgate.cz), which post the card's first digits to their /bin-checker.
26
+ * The card is already encrypted in ComGate's frame by then, so no body rule could see it: once ComGate is served, this
27
+ * build fails every request to these hosts but a GET, so the card the agent typed never reaches ComGate.
28
+ */
29
+ export declare const COMGATE_CARD_HOSTS: readonly string[];
30
+ /** The Fetch.enable globs attachToCdp arms for the card hosts, only while ComGate is served (cdp.ts). */
31
+ export declare const COMGATE_CARD_PATTERNS: readonly string[];
32
+ /** Whether this URL is on one of ComGate's card hosts (https, no credentials, any port, a trailing dot or not). */
33
+ export declare function isComgateCardHost(url: string): boolean;
34
+ /** Whether this build's registry serves ComGate's entry: a company Agentcard turned ComGate on for, on a build that declares it. */
35
+ export declare function servesComgate(vault: {
36
+ isCardRequest(url: string, method?: string): boolean;
37
+ } | undefined): boolean;
38
+ /**
39
+ * Whether an adapter must hand this request to the pre-claim gates: it is on one of ComGate's card hosts and this
40
+ * build's registry serves ComGate. attachToPlaywright's route predicate asks this, as attachToCdp arms the hosts.
41
+ */
42
+ export declare function isArmedComgateUrl(vault: {
43
+ isCardRequest(url: string, method?: string): boolean;
44
+ } | undefined, url: string): boolean;
45
+ /**
46
+ * Whether a URL is ComGate's gateway link exactly as ComGate's create answer writes it: https, one of the four
47
+ * gateway hosts in lowercase, the default port, no credentials or fragment, the path /init, and a query of the one
48
+ * `id` parameter holding a transaction id. A string that parses to another spelling is not it.
49
+ */
50
+ export declare function isComgateEntryUrl(raw: unknown): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ComGate's payment gateway (pay1 to pay4.comgate.cz, where most Czech and Slovak e-shops send the buyer), as this
3
+ * SDK reads it.
4
+ *
5
+ * The merchant's server creates the payment at ComGate and sends the buyer's browser to the gateway link ComGate
6
+ * answered with, https://payN.comgate.cz/init?id=<transaction id>, by a redirect or by setting the page's or a
7
+ * frame's location. That GET carries no card and nothing but the payment's id, and it is the one request a build
8
+ * that declares ComGate's capability pauses for ComGate (hosted-entry.ts): the cardholder's device then makes the
9
+ * gateway's own calls for that payment over the end-to-end TLS relay (payment-core processors/comgate.js). Only the
10
+ * link exactly as ComGate writes it is the entry; any other parameter would be handed on to the gateway's own
11
+ * route, which these rules never see.
12
+ *
13
+ * COMGATE_GATEWAY_HOSTS is payment-core's COMGATE_GATEWAY_HOSTS, and isComgateEntryUrl is payment-core's rule of
14
+ * the same name; hosted-entry.test.mjs pins both to payment-core on every case payment-core's own tests list.
15
+ */
16
+ export const COMGATE_PSP = 'comgate';
17
+ /** The dark-launch capability the API serves ComGate's recognizer behind (payment-core definitions.js). */
18
+ export const COMGATE_CAPABILITY = 'comgate_hosted_payment_page';
19
+ /** The four gateway hosts ComGate's create answer names, each a host of its own entry. */
20
+ export const COMGATE_GATEWAY_HOSTS = Object.freeze(['pay1.comgate.cz', 'pay2.comgate.cz', 'pay3.comgate.cz', 'pay4.comgate.cz']);
21
+ const COMGATE_ENTRY_PATH = '/init';
22
+ // The id the gateway's router takes: three groups of four letters or digits.
23
+ const TRANS_ID = /^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$/;
24
+ /**
25
+ * The hosts that take a card, or part of one, for ComGate: the gateway's API (payments.comgate.cz behind pay1 and pay2,
26
+ * payments-gcp.comgate.cz behind pay3 and pay4), where the gateway posts the card (/client/instructions/card-process,
27
+ * check-card) and ComGate's Web Checkout SDK posts its own (/checkout/provider/*, /checkout/card/check-card), and the
28
+ * card frames' hosts (sec.comgate.cz, sec-gcp.comgate.cz), which post the card's first digits to their /bin-checker.
29
+ * The card is already encrypted in ComGate's frame by then, so no body rule could see it: once ComGate is served, this
30
+ * build fails every request to these hosts but a GET, so the card the agent typed never reaches ComGate.
31
+ */
32
+ export const COMGATE_CARD_HOSTS = Object.freeze(['payments.comgate.cz', 'payments-gcp.comgate.cz', 'sec.comgate.cz', 'sec-gcp.comgate.cz']);
33
+ /** The Fetch.enable globs attachToCdp arms for the card hosts, only while ComGate is served (cdp.ts). */
34
+ export const COMGATE_CARD_PATTERNS = Object.freeze(COMGATE_CARD_HOSTS.map((host) => `*${host}*`));
35
+ // A gateway link, as ComGate writes one: the registry reads ComGate as served when it takes this as a GET entry.
36
+ const COMGATE_SAMPLE_ENTRY = 'https://pay1.comgate.cz/init?id=AAAA-BBBB-CCCC';
37
+ /** Whether this URL is on one of ComGate's card hosts (https, no credentials, any port, a trailing dot or not). */
38
+ export function isComgateCardHost(url) {
39
+ let parsed;
40
+ try {
41
+ parsed = new URL(url);
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ return parsed.protocol === 'https:' && !parsed.username && !parsed.password
47
+ && COMGATE_CARD_HOSTS.includes(parsed.hostname.toLowerCase().replace(/\.$/, ''));
48
+ }
49
+ /** Whether this build's registry serves ComGate's entry: a company Agentcard turned ComGate on for, on a build that declares it. */
50
+ export function servesComgate(vault) {
51
+ return typeof vault?.isCardRequest === 'function' && vault.isCardRequest(COMGATE_SAMPLE_ENTRY, 'GET');
52
+ }
53
+ /**
54
+ * Whether an adapter must hand this request to the pre-claim gates: it is on one of ComGate's card hosts and this
55
+ * build's registry serves ComGate. attachToPlaywright's route predicate asks this, as attachToCdp arms the hosts.
56
+ */
57
+ export function isArmedComgateUrl(vault, url) {
58
+ return isComgateCardHost(url) && servesComgate(vault);
59
+ }
60
+ /**
61
+ * Whether a URL is ComGate's gateway link exactly as ComGate's create answer writes it: https, one of the four
62
+ * gateway hosts in lowercase, the default port, no credentials or fragment, the path /init, and a query of the one
63
+ * `id` parameter holding a transaction id. A string that parses to another spelling is not it.
64
+ */
65
+ export function isComgateEntryUrl(raw) {
66
+ if (typeof raw !== 'string' || raw.length > 256)
67
+ return false;
68
+ let url;
69
+ try {
70
+ url = new URL(raw);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ if (url.href !== raw || url.protocol !== 'https:' || url.port || url.username || url.password || url.hash)
76
+ return false;
77
+ if (!COMGATE_GATEWAY_HOSTS.includes(url.hostname) || url.pathname !== COMGATE_ENTRY_PATH)
78
+ return false;
79
+ return url.search.startsWith('?id=') && TRANS_ID.test(url.search.slice(4));
80
+ }
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 0110d9fe2e862aad2c1191c5927ebb98dcc4be9905554ed46e18719a0bad05fb
3
+ // artifact-sha256: 24cac2580feefe7d273b89de8e5b84e744e561fd258252f98008801750370460
4
4
  // src/creditguard.js
5
5
  var CREDITGUARD_PPS_HOSTS = Object.freeze(["pps.creditguard.co.il", "pps2.creditguard.co.il", "ppsuat.creditguard.co.il"]);
6
6
  var CREDITGUARD_SANDBOX_HOST = "ppsuat.creditguard.co.il";
@@ -707,6 +707,7 @@ var THREE_DS_INIT_KEYS = Object.freeze([
707
707
  "cvv",
708
708
  "isGooglePayTxn",
709
709
  "isPayboxTxn",
710
+ "recaptchaValue",
710
711
  "useDcc",
711
712
  "acsWindowSize",
712
713
  "browserColorDepth",
@@ -837,6 +838,7 @@ function creditguardDeviceThreeDSInitRequest(processUrl, card, options = {}) {
837
838
  cvv: parts.cvc,
838
839
  isGooglePayTxn: false,
839
840
  isPayboxTxn: false,
841
+ recaptchaValue: "",
840
842
  useDcc: false,
841
843
  acsWindowSize: "4",
842
844
  browserColorDepth: browser.colorDepth,
@@ -887,7 +889,7 @@ function deviceStepBodyRefusal(step, txId, body) {
887
889
  return "dcc";
888
890
  if (value.txId !== txId)
889
891
  return "txid_mismatch";
890
- if (value.acsWindowSize !== "4" || value.browserHeader !== BROWSER_HEADER || value.browserJavascriptEnabled !== true)
892
+ if (value.recaptchaValue !== "" || value.acsWindowSize !== "4" || value.browserHeader !== BROWSER_HEADER || value.browserJavascriptEnabled !== true)
891
893
  return "malformed_body";
892
894
  try {
893
895
  deviceBrowser({
package/dist/cse-body.js CHANGED
@@ -4,6 +4,8 @@ import { substituteEncryptedFields } from './substitute.js';
4
4
  import { substituteMerchantHostedBody } from './merchant-hosted.js';
5
5
  import { fiservCapturePinId, isFiservCaptureHost, substituteFiservEnvelope } from './fiserv.js';
6
6
  import { substituteVerifoneBody } from './verifone.js';
7
+ import { isToastHost } from './toast.generated.js';
8
+ import { substituteToastCardData } from './toast-seal.generated.js';
7
9
  /**
8
10
  * The cse continuation: the paused body with the vault's ciphertext in place
9
11
  * of the dummy blobs. The adapters send it as postData ALONE. Chromium
@@ -44,6 +46,15 @@ export function cseBody(body, replay, url, preparation) {
44
46
  }
45
47
  if (isFiservCaptureHost(url))
46
48
  throw new Error('Refusing to substitute: a Fiserv capture continues only through a prepared Fiserv approval.');
49
+ // A request on Toast's payments host takes only Toast's own swap: the device's sealed card written over
50
+ // the paused card.cardData and no other byte, refused unless the paused body is still the frame's guest
51
+ // card request and the card is sealed under the key payment-core pins (toast-seal.js substituteToastCardData).
52
+ if (isToastHost(url)) {
53
+ if (replay.kind !== undefined || replay.substitutions.at !== 'card' || (replay.substitutions.remove ?? []).length > 0) {
54
+ throw new Error('Refusing to substitute: the paused request is not the Toast card request the cardholder approved.');
55
+ }
56
+ return substituteToastCardData(url, body, replay.substitutions.fields);
57
+ }
47
58
  // A merchant-hosted approval is written into the LIVE paused body, which must
48
59
  // still be the body the API checked at create (merchant-hosted.ts).
49
60
  if (replay.kind === 'merchant_hosted')
@@ -38,7 +38,7 @@ declare function isPreparedSecureAcceptanceRequest(url: any, method: any, body:
38
38
  declare function secureAcceptanceMerchantEntry(profileId: any, merchantOrigin: any, entries?: any): any;
39
39
  declare function secureAcceptanceMerchantRefusal(environment: any, profileId: any, merchantOrigin: any, accessKey: any, entries?: any): "unsupported_environment" | "invalid_profile_id" | "secure_acceptance_merchant_unreviewed" | "secure_acceptance_merchant_pending" | "secure_acceptance_access_key_unreviewed" | "secure_acceptance_merchant_observe" | null;
40
40
  declare function secureAcceptanceCardType(number: any): "001" | "002" | "003" | "004" | null;
41
- declare function secureAcceptanceCardRefusal(environment: any, card: any): "unsupported_environment" | "sandbox_host_requires_documented_test_card" | "invalid_card_number" | "live_host_refuses_documented_test_card" | "card_network_unsupported" | "invalid_expiry" | "invalid_cvn" | null;
41
+ declare function secureAcceptanceCardRefusal(environment: any, card: any): "invalid_card_number" | "unsupported_environment" | "sandbox_host_requires_documented_test_card" | "live_host_refuses_documented_test_card" | "card_network_unsupported" | "invalid_expiry" | "invalid_cvn" | null;
42
42
  declare function isSecureAcceptanceDeviceRequest(templateBody: any, deviceBody: any, environment: any): boolean;
43
43
  declare function secureAcceptanceDeviceRequest(request: any, card: any, options: any): {
44
44
  url: any;
package/dist/ebanx.d.ts CHANGED
@@ -75,7 +75,7 @@ export declare class EbanxFollowUps {
75
75
  stop(): void;
76
76
  /**
77
77
  * The approval the remembered token request went under, or null when none is remembered. The gate sends nothing
78
- * unless the checkout's own state names this same approval (pre-claim.ts ebanxMaySend).
78
+ * unless the checkout's own state names this same approval (pre-claim.ts followUpMaySend).
79
79
  */
80
80
  get authorizationId(): string | null;
81
81
  /**
package/dist/ebanx.js CHANGED
@@ -108,7 +108,7 @@ export class EbanxFollowUps {
108
108
  stop() { this.stopped = true; }
109
109
  /**
110
110
  * The approval the remembered token request went under, or null when none is remembered. The gate sends nothing
111
- * unless the checkout's own state names this same approval (pre-claim.ts ebanxMaySend).
111
+ * unless the checkout's own state names this same approval (pre-claim.ts followUpMaySend).
112
112
  */
113
113
  get authorizationId() { return this.parent?.authorizationId ?? null; }
114
114
  /**
@@ -81,7 +81,7 @@ declare function eprotectMerchantPin(paypageId: any, merchantOrigin: any, pins?:
81
81
  declare function eprotectMerchantRefusal(environment: any, paypageId: any, merchantOrigin: any, pins?: any): "invalid_paypage_id" | "unsupported_environment" | "eprotect_merchant_unreviewed" | null;
82
82
  declare function defaultRandom(bytes: any): void;
83
83
  declare function eprotectEncrypt(text: any, key: any, random?: typeof defaultRandom): string;
84
- declare function eprotectCardRefusal(environment: any, cardNumber: any): "unsupported_environment" | "sandbox_host_requires_documented_test_card" | "invalid_card_number" | "live_host_refuses_documented_test_card" | null;
84
+ declare function eprotectCardRefusal(environment: any, cardNumber: any): "invalid_card_number" | "unsupported_environment" | "sandbox_host_requires_documented_test_card" | "live_host_refuses_documented_test_card" | null;
85
85
  declare function eprotectDeviceRequest(templateUrl: any, card: any, sessionKey: any, random?: typeof defaultRandom): {
86
86
  url: string;
87
87
  method: string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Eximbay's payment window (secureapi.eximbay.com, the Korean gateway that takes foreign cards for Korean merchants),
3
+ * as this SDK reads it.
4
+ *
5
+ * The merchant's page posts a signed entry form, top-level or into a window it opens, to
6
+ * https://secureapi.eximbay.com/Gateway/BasicProcessor.krp (Eximbay's test host for a test merchant), and Eximbay
7
+ * answers with a page that posts itself, with the session's paramkey, into its window. That entry carries no card,
8
+ * and it is the one request this SDK pauses: the cardholder's device repeats it over the end-to-end TLS relay and
9
+ * fills the card into the window Eximbay returns to it (payment-core processors/eximbay.js). Everything else the
10
+ * agent's browser posts to Eximbay follows the entry and leads to a card: the step that opens the window
11
+ * (/Gateway/BasicProcessor/230/NewGlobalStep1.jsp), the window's amount query and card lookup, which carries the
12
+ * card's first eight digits (controll/checkApprvJson.do, controll/checkCardBin.do), its card post
13
+ * (controll/checkApprv.jsp) and its other posts. This SDK aborts every one of them in a page it watches, so a
14
+ * placeholder card never reaches Eximbay from one. attachToCdp watches a window the merchant's page opens once it has
15
+ * armed that window. Eximbay's popup integration opens the window and posts the entry into it at once, so that entry
16
+ * can leave before the window is armed; the window's next post, the step, is then aborted like any other, and the
17
+ * checkout reads unsupported. attachToPlaywright routes the page alone, never a window it opens.
18
+ *
19
+ * EXIMBAY_HOSTS is payment-core's EXIMBAY_HOSTS, and isEximbayEntry takes the URLs payment-core's isEximbayEntryUrl
20
+ * takes; eximbay.test.mjs pins both to payment-core.
21
+ */
22
+ export declare const EXIMBAY_PSP = "eximbay";
23
+ /** The dark-launch capability the API serves Eximbay's recognizer behind (payment-core definitions.js). */
24
+ export declare const EXIMBAY_CAPABILITY = "eximbay_hosted_payment_page";
25
+ /** The live host and Eximbay's test host, which serves the same window for a test merchant. */
26
+ export declare const EXIMBAY_HOSTS: readonly string[];
27
+ /** Whether this URL is on one of Eximbay's two hosts. */
28
+ export declare function isEximbayHost(url: string): boolean;
29
+ /**
30
+ * The only request on Eximbay's hosts that is the merchant's entry: a POST to the entry path, written exactly as a
31
+ * browser posts a form to it, with no port, query or fragment (payment-core isEximbayEntryUrl takes those two strings
32
+ * only). The entry the SDK pauses; every other POST there is aborted.
33
+ */
34
+ export declare function isEximbayEntry(url: string, method: string): boolean;
35
+ /** The entry URL on the same Eximbay host, which the registry recognizes when this build is armed for Eximbay, or null. */
36
+ export declare function eximbayEntryUrl(url: string): string | null;
37
+ /**
38
+ * Whether an adapter must hand this request to the pre-claim gates: it is on an Eximbay host and this build's registry
39
+ * serves Eximbay's entry there. The step, the window's lookups and its card post are not the entry, so the registry's
40
+ * card-request test does not name them; the CDP adapter still pauses them through the recognizer's host pattern, and
41
+ * attachToPlaywright's route predicate asks this, so both adapters show the gates every request to Eximbay once
42
+ * Eximbay is armed.
43
+ */
44
+ export declare function isArmedEximbayUrl(vault: {
45
+ isCardRequest(url: string, method?: string): boolean;
46
+ }, url: string): boolean;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Eximbay's payment window (secureapi.eximbay.com, the Korean gateway that takes foreign cards for Korean merchants),
3
+ * as this SDK reads it.
4
+ *
5
+ * The merchant's page posts a signed entry form, top-level or into a window it opens, to
6
+ * https://secureapi.eximbay.com/Gateway/BasicProcessor.krp (Eximbay's test host for a test merchant), and Eximbay
7
+ * answers with a page that posts itself, with the session's paramkey, into its window. That entry carries no card,
8
+ * and it is the one request this SDK pauses: the cardholder's device repeats it over the end-to-end TLS relay and
9
+ * fills the card into the window Eximbay returns to it (payment-core processors/eximbay.js). Everything else the
10
+ * agent's browser posts to Eximbay follows the entry and leads to a card: the step that opens the window
11
+ * (/Gateway/BasicProcessor/230/NewGlobalStep1.jsp), the window's amount query and card lookup, which carries the
12
+ * card's first eight digits (controll/checkApprvJson.do, controll/checkCardBin.do), its card post
13
+ * (controll/checkApprv.jsp) and its other posts. This SDK aborts every one of them in a page it watches, so a
14
+ * placeholder card never reaches Eximbay from one. attachToCdp watches a window the merchant's page opens once it has
15
+ * armed that window. Eximbay's popup integration opens the window and posts the entry into it at once, so that entry
16
+ * can leave before the window is armed; the window's next post, the step, is then aborted like any other, and the
17
+ * checkout reads unsupported. attachToPlaywright routes the page alone, never a window it opens.
18
+ *
19
+ * EXIMBAY_HOSTS is payment-core's EXIMBAY_HOSTS, and isEximbayEntry takes the URLs payment-core's isEximbayEntryUrl
20
+ * takes; eximbay.test.mjs pins both to payment-core.
21
+ */
22
+ export const EXIMBAY_PSP = 'eximbay';
23
+ /** The dark-launch capability the API serves Eximbay's recognizer behind (payment-core definitions.js). */
24
+ export const EXIMBAY_CAPABILITY = 'eximbay_hosted_payment_page';
25
+ /** The live host and Eximbay's test host, which serves the same window for a test merchant. */
26
+ export const EXIMBAY_HOSTS = Object.freeze(['secureapi.eximbay.com', 'secureapi.test.eximbay.com']);
27
+ const ENTRY_PATH = '/Gateway/BasicProcessor.krp';
28
+ /** The Eximbay host a URL is on (https, no credentials, any port, any letter case, a trailing dot or not), or null. */
29
+ function eximbayHost(raw) {
30
+ let url;
31
+ try {
32
+ url = new URL(raw);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ if (url.protocol !== 'https:' || url.username || url.password)
38
+ return null;
39
+ const host = url.hostname.toLowerCase().replace(/\.$/, '');
40
+ return EXIMBAY_HOSTS.includes(host) ? host : null;
41
+ }
42
+ /** Whether this URL is on one of Eximbay's two hosts. */
43
+ export function isEximbayHost(url) {
44
+ return eximbayHost(url) !== null;
45
+ }
46
+ /**
47
+ * The only request on Eximbay's hosts that is the merchant's entry: a POST to the entry path, written exactly as a
48
+ * browser posts a form to it, with no port, query or fragment (payment-core isEximbayEntryUrl takes those two strings
49
+ * only). The entry the SDK pauses; every other POST there is aborted.
50
+ */
51
+ export function isEximbayEntry(url, method) {
52
+ return String(method).toUpperCase() === 'POST' && EXIMBAY_HOSTS.some((host) => url === `https://${host}${ENTRY_PATH}`);
53
+ }
54
+ /** The entry URL on the same Eximbay host, which the registry recognizes when this build is armed for Eximbay, or null. */
55
+ export function eximbayEntryUrl(url) {
56
+ const host = eximbayHost(url);
57
+ return host === null ? null : `https://${host}${ENTRY_PATH}`;
58
+ }
59
+ /**
60
+ * Whether an adapter must hand this request to the pre-claim gates: it is on an Eximbay host and this build's registry
61
+ * serves Eximbay's entry there. The step, the window's lookups and its card post are not the entry, so the registry's
62
+ * card-request test does not name them; the CDP adapter still pauses them through the recognizer's host pattern, and
63
+ * attachToPlaywright's route predicate asks this, so both adapters show the gates every request to Eximbay once
64
+ * Eximbay is armed.
65
+ */
66
+ export function isArmedEximbayUrl(vault, url) {
67
+ const entry = eximbayEntryUrl(url);
68
+ return entry !== null && vault.isCardRequest(entry, 'POST');
69
+ }
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 8149714daf5515a2fd3ec90d5aa0c7eb58a66d5ce79c3e3f0ba81c29d2946dc3
3
+ // artifact-sha256: a65199bcbf0b67546130fb4f2c12ba72c8f51a2ff5b4e3054e2bfcc29b06f95d
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 pin = 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 pin !== "string" || !SHA256_HEX.test(pin))
@@ -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);
@@ -25,11 +25,11 @@ declare function hipayUsername(headers: any): string | null;
25
25
  declare function hipayRequestRefusal(request: any): any;
26
26
  declare function isPreparedHipayRequest(url: any, method: any, body: any, environment: any, headers: any): boolean;
27
27
  declare function hipayPinOriginUsable(origin: any): boolean;
28
- declare var HIPAY_MERCHANT_PINS: readonly never[];
29
- declare function hipayOriginPin(merchantOrigin: any, pins?: readonly never[]): null;
30
- declare function hipayPreparationRefusal(environment: any, merchantOrigin: any, pins?: readonly never[]): "unsupported_environment" | "hipay_merchant_unreviewed" | null;
31
- declare function hipayMerchantPin(merchantOrigin: any, username: any, pins?: readonly never[]): null;
32
- declare function hipayMerchantRefusal(environment: any, merchantOrigin: any, username: any, pins?: readonly never[]): "hipay_headers_refused" | "unsupported_environment" | "hipay_merchant_unreviewed" | null;
28
+ declare var HIPAY_MERCHANT_PINS: readonly any[];
29
+ declare function hipayOriginPin(merchantOrigin: any, pins?: readonly any[]): any;
30
+ declare function hipayPreparationRefusal(environment: any, merchantOrigin: any, pins?: readonly any[]): "unsupported_environment" | "hipay_merchant_unreviewed" | null;
31
+ declare function hipayMerchantPin(merchantOrigin: any, username: any, pins?: readonly any[]): any;
32
+ declare function hipayMerchantRefusal(environment: any, merchantOrigin: any, username: any, pins?: readonly any[]): "hipay_headers_refused" | "unsupported_environment" | "hipay_merchant_unreviewed" | null;
33
33
  declare function hipayRefusalMessage(code: any): any;
34
34
  declare function injectHipayCard(body: any, card: any): {
35
35
  body: string;
@@ -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';
@@ -159,6 +159,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
159
159
  private stripeContinuationChecks;
160
160
  /** The handoff in progress is WSPay's 3-D Secure step; see handedOff. */
161
161
  private wspayChallenge;
162
+ private heldBeforeHandoff;
162
163
  private stripeContinuedReason;
163
164
  private approvedPaymentMethod;
164
165
  private ownedShopOrderId;
@@ -191,7 +192,8 @@ export declare class CheckoutLifecycle implements CheckoutController {
191
192
  preparationFailed(error: CheckoutPreparationError): void;
192
193
  isBlocked(): boolean;
193
194
  /**
194
- * An EBANX send left for the cardholder's device under this attachment's approval (pre-claim.ts ebanxFollowUp). Until
195
+ * An EBANX or Payrexx send left for the cardholder's device under this attachment's approval (pre-claim.ts ebanxFollowUp,
196
+ * payrexxFollowUp). Until
195
197
  * the returned function runs, once the send's answer reached the page or the send failed, reconcile() and
196
198
  * retryAfterMerchantFailure() throw, as they do while a card request is active: a confirm may still pay. The returned
197
199
  * function settles the send once, however often it runs.
@@ -200,7 +202,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
200
202
  /** Whether a reconcile() is still waiting on the application's merchant resolver; pre-claim.ts claims no EBANX send then. */
201
203
  isReconciling(): boolean;
202
204
  /**
203
- * Whether an EBANX send this attachment claimed is still out (followUpStarted). Both adapters refuse a new card
205
+ * Whether an EBANX or Payrexx send this attachment claimed is still out (followUpStarted). Both adapters refuse a new card
204
206
  * request meanwhile, as they do while an approval is outstanding: a confirm that may pay is never hidden behind
205
207
  * another payment's state.
206
208
  */
@@ -284,10 +286,13 @@ export declare class CheckoutLifecycle implements CheckoutController {
284
286
  * paddle-vgs-pay.ts reasons), or the typed card that EBANX's 3-D Secure calls or
285
287
  * its hosted page's confirm send again after the token, when no approved token
286
288
  * request on this attachment covers that send or the cardholder's device did not
287
- * send it (unsupported_ebanx_card_resend, ebanx.ts): once an approval exists the
289
+ * send it (unsupported_ebanx_card_resend, ebanx.ts), or the typed card Payrexx's card
290
+ * frame sends again for a 3-D Secure step when no approved card request on this
291
+ * attachment covers that send or the cardholder's device did not send it
292
+ * (unsupported_payrexx_card_resend, payrexx.ts): once an approval exists the
288
293
  * state keeps it, and only the hold changes.
289
294
  */
290
- unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields' | PaypalCardFormReason | 'unsupported_cybersource_secure_acceptance' | PaddleVgsPayRefusal | 'unsupported_ebanx_card_resend'): void;
295
+ unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields' | PaypalCardFormReason | 'unsupported_cybersource_secure_acceptance' | PaddleVgsPayRefusal | 'unsupported_ebanx_card_resend' | 'unsupported_payrexx_card_resend'): void;
291
296
  /**
292
297
  * A card request that only a prepared checkout pays (an Adyen merchant
293
298
  * profile's, or a Fiserv card capture) arrived with no preparation on this
package/dist/lifecycle.js CHANGED
@@ -57,6 +57,9 @@ export class CheckoutLifecycle {
57
57
  stripeContinuationChecks = 0;
58
58
  /** The handoff in progress is WSPay's 3-D Secure step; see handedOff. */
59
59
  wspayChallenge = false;
60
+ // Whether the attachment held when the handoff in progress began (prepareHandoff): a hold raised after that, while
61
+ // the answer was still landing, survives handedOff.
62
+ heldBeforeHandoff = false;
60
63
  // What the continuation that went through confirmed: a payment, or a card save.
61
64
  stripeContinuedReason = 'stripe_payment_continued';
62
65
  // The payment method a Stripe /v1/payment_methods approval returned: see approvedStripePaymentMethod.
@@ -120,7 +123,8 @@ export class CheckoutLifecycle {
120
123
  }
121
124
  isBlocked() { return this.held || this.cancelled; }
122
125
  /**
123
- * An EBANX send left for the cardholder's device under this attachment's approval (pre-claim.ts ebanxFollowUp). Until
126
+ * An EBANX or Payrexx send left for the cardholder's device under this attachment's approval (pre-claim.ts ebanxFollowUp,
127
+ * payrexxFollowUp). Until
124
128
  * the returned function runs, once the send's answer reached the page or the send failed, reconcile() and
125
129
  * retryAfterMerchantFailure() throw, as they do while a card request is active: a confirm may still pay. The returned
126
130
  * function settles the send once, however often it runs.
@@ -136,7 +140,7 @@ export class CheckoutLifecycle {
136
140
  /** Whether a reconcile() is still waiting on the application's merchant resolver; pre-claim.ts claims no EBANX send then. */
137
141
  isReconciling() { return this.reconciliation !== null; }
138
142
  /**
139
- * Whether an EBANX send this attachment claimed is still out (followUpStarted). Both adapters refuse a new card
143
+ * Whether an EBANX or Payrexx send this attachment claimed is still out (followUpStarted). Both adapters refuse a new card
140
144
  * request meanwhile, as they do while an approval is outstanding: a confirm that may pay is never hidden behind
141
145
  * another payment's state.
142
146
  */
@@ -283,7 +287,10 @@ export class CheckoutLifecycle {
283
287
  * paddle-vgs-pay.ts reasons), or the typed card that EBANX's 3-D Secure calls or
284
288
  * its hosted page's confirm send again after the token, when no approved token
285
289
  * request on this attachment covers that send or the cardholder's device did not
286
- * send it (unsupported_ebanx_card_resend, ebanx.ts): once an approval exists the
290
+ * send it (unsupported_ebanx_card_resend, ebanx.ts), or the typed card Payrexx's card
291
+ * frame sends again for a 3-D Secure step when no approved card request on this
292
+ * attachment covers that send or the cardholder's device did not send it
293
+ * (unsupported_payrexx_card_resend, payrexx.ts): once an approval exists the
287
294
  * state keeps it, and only the hold changes.
288
295
  */
289
296
  unsupported(reason = 'unrecognized_payment_endpoint') {
@@ -305,6 +312,7 @@ export class CheckoutLifecycle {
305
312
  this.set({ ...this.state, status: 'failed', reason: 'preparation_required' });
306
313
  }
307
314
  prepareHandoff(replay, requestUrl) {
315
+ this.heldBeforeHandoff = this.held;
308
316
  this.wspayChallenge = isWspayChallenge(requestUrl, replay);
309
317
  this.handoffs.push(requestUrl);
310
318
  if (replay.mode === 'token' && replay.executionMode === 'autopilot' && replay.shopOrderId) {
@@ -422,10 +430,14 @@ export class CheckoutLifecycle {
422
430
  return;
423
431
  }
424
432
  const mismatch = (!replay.mode || replay.mode === 'token') && replay.amountVerified === false;
433
+ // A hold raised while the answer was still landing stands: the page may send its first follow-up the moment the
434
+ // answer lands, and an EBANX or Payrexx send that fails then stops the approval's follow-ups for good
435
+ // (pre-claim.ts), so a new card request must not reopen the checkout under an approval that cannot finish.
436
+ const heldDuringHandoff = this.held && !this.heldBeforeHandoff;
425
437
  // A card endpoint answer means the merchant already holds the card: its page's own
426
438
  // retry, or a step-up that sends the card again, is refused until the application
427
439
  // reconciles the order, exactly as for a hosted form the device submitted.
428
- this.held = !!this.options.requireMerchantResult || replay.mode === 'hosted_form' || replay.mode === 'card_endpoint' || mismatch || this.unboundStripeToken;
440
+ this.held = heldDuringHandoff || !!this.options.requireMerchantResult || replay.mode === 'hosted_form' || replay.mode === 'card_endpoint' || mismatch || this.unboundStripeToken;
429
441
  if (mismatch)
430
442
  this.stripeTokenPrepared = false;
431
443
  this.set({ status: 'awaiting_merchant', authorizationId: replay.authorizationId, mode: replay.mode ?? 'token',