@agent-cards/sdk 0.23.19 → 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.
- package/CHANGELOG.md +61 -2
- package/PREFLIGHT.md +4 -0
- package/README.md +175 -7
- package/dist/amadeus.d.ts +4 -2
- package/dist/amadeus.js +16 -3
- package/dist/buy.d.ts +84 -2
- package/dist/buy.js +16 -1
- package/dist/card-endpoint-facts.d.ts +2 -1
- package/dist/card-endpoint-facts.js +18 -9
- package/dist/card-endpoint.d.ts +20 -4
- package/dist/card-endpoint.js +38 -4
- package/dist/card-fields.generated.js +1444 -51
- package/dist/cdp.d.ts +29 -0
- package/dist/cdp.js +444 -24
- package/dist/client.d.ts +134 -7
- package/dist/client.js +350 -25
- package/dist/comgate.d.ts +50 -0
- package/dist/comgate.js +80 -0
- package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
- package/dist/eprotect.generated.d.ts +1 -1
- package/dist/gmo.generated.js +7 -3
- package/dist/hipay.generated.d.ts +56 -0
- package/dist/hipay.generated.js +392 -0
- package/dist/hold.d.ts +180 -0
- package/dist/hold.js +270 -0
- package/dist/hosted-entry.d.ts +4 -0
- package/dist/hosted-entry.js +32 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/lifecycle.d.ts +10 -0
- package/dist/lifecycle.js +21 -0
- package/dist/merchant-card-endpoints.generated.js +102 -11
- package/dist/paddle-tokenex.generated.js +7 -3
- package/dist/paddle-vgs.generated.d.ts +3 -2
- package/dist/paddle-vgs.generated.js +9 -5
- package/dist/payrails.generated.js +7 -3
- package/dist/pre-claim.d.ts +20 -0
- package/dist/pre-claim.js +202 -22
- package/dist/preflight-catalog.json +58 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +26 -2
- package/dist/prepared-processor.d.ts +6 -2
- package/dist/prepared-processor.js +18 -2
- package/dist/primer.generated.js +1487 -94
- package/dist/przelewy24.d.ts +47 -0
- package/dist/przelewy24.js +88 -0
- package/dist/quickpay.d.ts +44 -0
- package/dist/quickpay.js +65 -0
- package/dist/registry.d.ts +6 -5
- package/dist/registry.js +27 -7
- package/dist/shiji-sts.generated.js +7 -3
- package/dist/stripe-checkout.generated.js +744 -13
- package/dist/tokenex-iframe.d.ts +93 -0
- package/dist/tokenex-iframe.generated.d.ts +81 -0
- package/dist/tokenex-iframe.generated.js +403 -0
- package/dist/tokenex-iframe.js +195 -0
- package/dist/transbank.d.ts +45 -0
- package/dist/transbank.js +73 -0
- package/dist/twoc2p.d.ts +54 -0
- package/dist/twoc2p.generated.d.ts +33 -0
- package/dist/twoc2p.generated.js +1149 -0
- package/dist/twoc2p.js +85 -0
- package/dist/voice.d.ts +16 -13
- package/dist/voice.js +13 -10
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
package/dist/cdp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
|
|
2
|
-
import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, CheckoutPreparationError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, } from './client.js';
|
|
2
|
+
import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, CheckoutPreparationError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, redactUrl, } from './client.js';
|
|
3
3
|
import { PreparationGate } from './preparation.js';
|
|
4
4
|
import { preparedApprovalAnswersRetry } from './prepared-processor.js';
|
|
5
5
|
import { StripeCheckoutGate } from './stripe-checkout.js';
|
|
@@ -8,15 +8,21 @@ import { MERCADO_CHECKOUT_PATTERN } from './mercado-checkout.generated.js';
|
|
|
8
8
|
import { JuspayCheckoutGate } from './juspay-gate.js';
|
|
9
9
|
import { ACI_CHECKOUT_REUSED, AciGate, playwrightReplacesNavigationBody } from './aci.js';
|
|
10
10
|
import { EprotectGate, classifyEprotectRequest, eprotectScriptAnswer } from './eprotect.js';
|
|
11
|
-
import { dispatchPreClaim, eventUrl, isArmedCreditguardCall } from './pre-claim.js';
|
|
11
|
+
import { dispatchPreClaim, eventUrl, isArmedCreditguardCall, routesHipayVault } from './pre-claim.js';
|
|
12
12
|
import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
13
13
|
import { isPaddleVgsCardsUrl } from './paddle-vgs.generated.js';
|
|
14
14
|
import { PADDLE_VGS_PAY_PATTERNS, PaddleVgsPayGate, isPaddleCheckoutService, recognizesPaddleVgs } from './paddle-vgs-pay.js';
|
|
15
15
|
import { DatatransRepeats } from './datatrans.js';
|
|
16
|
+
import { TokenexIframeRepeats, isArmedTokenexIframeUrl } from './tokenex-iframe.js';
|
|
16
17
|
import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
|
|
17
18
|
import { paddleTokenexTokenContinuation } from './paddle-tokenex.js';
|
|
19
|
+
import { twoC2pContinuation, twoC2pRetire } from './twoc2p.js';
|
|
18
20
|
import { isArmedEkashuUrl } from './ekashu.js';
|
|
21
|
+
import { isArmedTransbankUrl } from './transbank.js';
|
|
19
22
|
import { isArmedRedsysUrl } from './redsys.js';
|
|
23
|
+
import { isArmedPrzelewy24Url } from './przelewy24.js';
|
|
24
|
+
import { isArmedQuickpayUrl } from './quickpay.js';
|
|
25
|
+
import { COMGATE_CARD_PATTERNS, isArmedComgateUrl, servesComgate } from './comgate.js';
|
|
20
26
|
import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
|
|
21
27
|
import { EBANX_RESEND_PATTERNS, EbanxFollowUps, isEbanxResendUrl } from './ebanx.js';
|
|
22
28
|
import { PAYPAL_CARD_FORM_PATTERNS, isPaypalCardFormUrl } from './paypal-card-forms.js';
|
|
@@ -30,6 +36,7 @@ import { MerchantHandoffWatch } from './merchant-handoff.js';
|
|
|
30
36
|
import { MERCHANT_PAGE_WAIT_MS, MerchantTotalWatch } from './merchant-total-watch.js';
|
|
31
37
|
import { hostedFormSubmittedPage } from './hosted-form.js';
|
|
32
38
|
import { armSendWindow, failsAsNetworkError, preparedSendWindowMs, sendWindowProcessor } from './amadeus.js';
|
|
39
|
+
import { HoldAnsweredError, HoldEndedError, HoldGate, HoldRefusedError, matchHoldOnlyRule } from './hold.js';
|
|
33
40
|
import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
34
41
|
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, loadedCardMicroformFromUrls, refusedRequestFailure, } from './cybersource-flex.js';
|
|
35
42
|
import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointRouted, } from './card-endpoint.js';
|
|
@@ -99,13 +106,22 @@ function isExtensionWorker(type, url) {
|
|
|
99
106
|
* A different, explicitly identified context belongs to another checkout.
|
|
100
107
|
* An extension's own worker owns no merchant request and does not count.
|
|
101
108
|
*/
|
|
102
|
-
function assertContextTarget(target, merchantContext) {
|
|
109
|
+
function assertContextTarget(target, merchantContext, allow = {}) {
|
|
103
110
|
if (!targetRecord(target))
|
|
104
111
|
throw new CheckoutAttachmentError('unavailable');
|
|
105
112
|
if (contextId(target.browserContextId) && target.browserContextId !== merchantContext)
|
|
106
113
|
return;
|
|
107
114
|
if (isExtensionWorker(target.type, target.url))
|
|
108
115
|
return;
|
|
116
|
+
const knownContext = target.browserContextId === undefined || contextId(target.browserContextId);
|
|
117
|
+
// Every armed frame sends its requests past the site's service workers (Network.setBypassServiceWorker), so a
|
|
118
|
+
// worker in the merchant's context can no longer send a card request out of the interception's reach.
|
|
119
|
+
if (allow.serviceWorkers && target.type === 'service_worker' && knownContext)
|
|
120
|
+
return;
|
|
121
|
+
// A shared worker's own requests pause nowhere (Fetch does not reach them), so it is watched instead: its Network
|
|
122
|
+
// reports are read, and a card request seen there ends the checkout before anything is approved (sharedWorkers).
|
|
123
|
+
if (allow.sharedWorkers && target.type === 'shared_worker' && knownContext)
|
|
124
|
+
return;
|
|
109
125
|
if (!['page', 'iframe', 'worker', 'browser_ui'].includes(target.type)
|
|
110
126
|
|| (target.browserContextId !== undefined && !contextId(target.browserContextId))) {
|
|
111
127
|
throw new CheckoutAttachmentError('unavailable');
|
|
@@ -203,6 +219,9 @@ function isApprovalOutcome(err) {
|
|
|
203
219
|
function isTerminal(err) {
|
|
204
220
|
if (err instanceof CheckoutApiError)
|
|
205
221
|
return err.permanent;
|
|
222
|
+
// A hold's decision is final for the attachment: every later card request fails at once.
|
|
223
|
+
if (err instanceof HoldEndedError || err instanceof HoldAnsweredError)
|
|
224
|
+
return true;
|
|
206
225
|
// Refused at create for Adyen's test platform: the page's next request is refused the same way.
|
|
207
226
|
if (err instanceof AdyenTestPlatformRefusedError)
|
|
208
227
|
return err.stage === 'create';
|
|
@@ -350,6 +369,16 @@ function cseContinuation(opts, url, body, replay, approved, preparation) {
|
|
|
350
369
|
const paddleTokenex = replay.kind === undefined ? paddleTokenexTokenContinuation(url, body, replay.substitutions, replay.paddleTokenexFacts, approved) : null;
|
|
351
370
|
if (paddleTokenex)
|
|
352
371
|
return { body: paddleTokenex.body, shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
372
|
+
// 2C2P writes one card field under its pinned key, which the generic Adyen swap refuses as no JWE, so it is
|
|
373
|
+
// dispatched by the Payment URL to substituteTwoC2p (twoc2p.ts), only into the one Payment the approval was made for.
|
|
374
|
+
// A refusal there sent the field nowhere, so the approval is retired as merchant_never_retried, as a merchant-hosted
|
|
375
|
+
// one is (cseContinuationBody), and the API stops serving the field; a retirement the API does not confirm is
|
|
376
|
+
// reported as approval_retire_unconfirmed (twoc2p.ts twoC2pRetire).
|
|
377
|
+
const twoc2p = replay.kind === undefined
|
|
378
|
+
? twoC2pContinuation(url, body, replay.substitutions, approved, twoC2pRetire(opts.vault, replay.authorizationId, opts.onEvent))
|
|
379
|
+
: null;
|
|
380
|
+
if (twoc2p)
|
|
381
|
+
return { body: twoc2p.body, shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
353
382
|
return { body: cseContinuationBody(opts, body, replay, url, preparation), shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
354
383
|
}
|
|
355
384
|
/**
|
|
@@ -1388,7 +1417,7 @@ function merchantAttempt(lifecycle, survives) {
|
|
|
1388
1417
|
// known non-payment, so the prepared checkout ends failed with that reason, and a fresh attachment can
|
|
1389
1418
|
// prepare again. Every other stop leaves the outcome unknown.
|
|
1390
1419
|
const error = () => reason === 'amadeus_send_window_closed' || reason === 'secure_acceptance_send_window_closed'
|
|
1391
|
-
|| reason === 'wix_payments_send_window_closed'
|
|
1420
|
+
|| reason === 'wix_payments_send_window_closed' || reason === 'hipay_send_window_closed'
|
|
1392
1421
|
? new CheckoutPreparationError(lifecycle.getState().preparationId ?? null, reason)
|
|
1393
1422
|
: new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, reason);
|
|
1394
1423
|
const attempt = {
|
|
@@ -1549,6 +1578,35 @@ function replayOf(result, timeoutMs) {
|
|
|
1549
1578
|
return { mode: 'token', authorizationId, status: response.status, headers: { ...response.headers }, body: response.body, amountVerified: null,
|
|
1550
1579
|
...(result.processor_context !== undefined ? { processorContext: result.processor_context } : {}), ...execution };
|
|
1551
1580
|
}
|
|
1581
|
+
/** A CDP Fetch url pattern (`*` any run of characters, `?` one, `\\` escapes) against a URL. */
|
|
1582
|
+
function urlPatternMatches(pattern, url) {
|
|
1583
|
+
let re = '';
|
|
1584
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1585
|
+
const c = pattern[i];
|
|
1586
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
1587
|
+
re += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
re += c === '*' ? '.*' : c === '?' ? '.' : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1591
|
+
}
|
|
1592
|
+
try {
|
|
1593
|
+
return new RegExp(`^${re}$`).test(url);
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Wait for the hold's decision on a request; the request going away (its attempt stopped) ends the hold as lost,
|
|
1601
|
+
* which rejects the decision.
|
|
1602
|
+
*/
|
|
1603
|
+
function holdUntilDecided(decision, signal, lost) {
|
|
1604
|
+
if (signal.aborted)
|
|
1605
|
+
lost();
|
|
1606
|
+
else
|
|
1607
|
+
signal.addEventListener('abort', lost, { once: true });
|
|
1608
|
+
return decision.finally(() => signal.removeEventListener('abort', lost));
|
|
1609
|
+
}
|
|
1552
1610
|
/**
|
|
1553
1611
|
* Worldpay eProtect's registration, resumed by the business. The read carries the registration as the API
|
|
1554
1612
|
* stored it, bare JSON, and the paused request is the vendor's JSONP <script>, which runs its answer and reads
|
|
@@ -1688,12 +1746,16 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1688
1746
|
opts = safeOptions(opts);
|
|
1689
1747
|
if (opts.pauseOnly && typeof opts.onPaused !== 'function')
|
|
1690
1748
|
throw new Error('pauseOnly needs onPaused: the business receives the paused request there and answers it with controller.resume.');
|
|
1749
|
+
if (opts.pauseOnly && opts.hold)
|
|
1750
|
+
throw new Error('hold and pauseOnly are two different pauses: attach with one of them.');
|
|
1691
1751
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
1692
1752
|
const setupStop = new AbortController();
|
|
1693
1753
|
let attachmentReady = false;
|
|
1694
1754
|
let setupFailureReported = false;
|
|
1695
1755
|
const ownedSessions = new Set([pageSessionId]);
|
|
1696
1756
|
const sessionTypes = new Map([[pageSessionId, 'page']]);
|
|
1757
|
+
// Each window this attachment adopted (adoptOpenedWindow): its session, and its target's id, which is its main frame's.
|
|
1758
|
+
const windowTargets = new Map();
|
|
1697
1759
|
const sessionParents = new Map();
|
|
1698
1760
|
// The frame in its parent session that a child target hangs from (TargetInfo.parentFrameId), so a
|
|
1699
1761
|
// frame lookup can climb from a cross-origin frame into the document that frames it.
|
|
@@ -1749,6 +1811,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1749
1811
|
// The page target's id, which is its main frame's id; read once the attachment is set up.
|
|
1750
1812
|
let pageTargetId;
|
|
1751
1813
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
1814
|
+
// Hold mode: the first card request is held with nothing created until controller.release or cancel (hold.ts).
|
|
1815
|
+
const holdGate = opts.hold ? new HoldGate(typeof opts.hold === 'object' ? opts.hold : {}, opts.vault?.sandbox === true, (type, detail) => opts.onEvent?.({ type, detail })) : null;
|
|
1816
|
+
if (holdGate) {
|
|
1817
|
+
lifecycle.setHold(holdGate);
|
|
1818
|
+
lifecycle.abort.signal.addEventListener('abort', () => { void holdGate.cancel('checkout_cancelled'); }, { once: true });
|
|
1819
|
+
}
|
|
1820
|
+
// The hold-only request (Bolt's tokenizer, a placeholder post) held now, by its Network id, so the page giving it up ends the hold.
|
|
1821
|
+
let holdOnlyNetworkId = null;
|
|
1822
|
+
const bypassServiceWorkers = (opts.serviceWorkers ?? (holdGate ? 'bypass' : 'refuse')) === 'bypass';
|
|
1823
|
+
const sharedWorkerMode = opts.sharedWorkers ?? (holdGate ? 'accept' : 'refuse');
|
|
1824
|
+
const observeSharedWorkers = sharedWorkerMode === 'observe';
|
|
1825
|
+
const targetAllowance = { serviceWorkers: bypassServiceWorkers, sharedWorkers: sharedWorkerMode !== 'refuse' };
|
|
1826
|
+
// Shared workers being watched: their target ids (claimed before the attach answers) and their sessions.
|
|
1827
|
+
const watchedWorkerTargets = new Set();
|
|
1828
|
+
const watchedWorkerSessions = new Set();
|
|
1829
|
+
// The watches still starting, by target id: a later report of the worker waits for its watch, and so does the attachment.
|
|
1830
|
+
const workerWatches = new Map();
|
|
1752
1831
|
const stripeCheckout = new StripeCheckoutGate(opts);
|
|
1753
1832
|
const mercadoCheckout = new MercadoCheckoutGate();
|
|
1754
1833
|
// CyberSource Flex card field iframes and their capture contexts, recorded from the CDP
|
|
@@ -1801,6 +1880,20 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1801
1880
|
preparationFrameId = tree.frameTree.frame.id;
|
|
1802
1881
|
return tree.frameTree.frame.url;
|
|
1803
1882
|
};
|
|
1883
|
+
// The URL the page's main frame last committed: its target's URL when the attachment is set up, then kept from
|
|
1884
|
+
// Page.frameNavigated and Page.navigatedWithinDocument, and from the page target's own reports (notePageTarget),
|
|
1885
|
+
// which name a document the page commits before this attachment has enabled its Page events. Chromium leaves
|
|
1886
|
+
// Page.getFrameTree unanswered on the page's session while that session's own top-level navigation is paused (a
|
|
1887
|
+
// hosted page entry the page sends this window to, by script or through the merchant's redirect), so
|
|
1888
|
+
// readDocumentUrl would wait out its bound and the create would name no checkout page. For that one paused request
|
|
1889
|
+
// the page is still the document it last committed.
|
|
1890
|
+
let committedTopUrl;
|
|
1891
|
+
/** A TargetInfo of the page's own target names the URL the page last committed: the target list, and each report. */
|
|
1892
|
+
const notePageTarget = (info) => {
|
|
1893
|
+
if (pageTargetId !== undefined && info?.targetId === pageTargetId && info?.type === 'page' && typeof info.url === 'string' && info.url) {
|
|
1894
|
+
committedTopUrl = info.url.split('#')[0];
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1804
1897
|
// prepare({ tokenizeBeforePay: true }) sends the card Microform whose field documents are committed
|
|
1805
1898
|
// now. A field's commit can come before this attachment enabled its session, so no event reports
|
|
1806
1899
|
// it; each iframe session's own frame tree, and the page's, is read once first (as rereadMicroforms
|
|
@@ -1835,7 +1928,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1835
1928
|
const frame = find(tree?.frameTree, 0);
|
|
1836
1929
|
if (!frame)
|
|
1837
1930
|
return null;
|
|
1838
|
-
return { navigation: frame.parentId || sessionId !== pageSessionId ? 'frame' : 'top',
|
|
1931
|
+
return { navigation: frame.parentId || (sessionId !== pageSessionId && !(sessionId && windowTargets.has(sessionId))) ? 'frame' : 'top',
|
|
1932
|
+
url: typeof frame.url === 'string' ? frame.url : undefined };
|
|
1839
1933
|
};
|
|
1840
1934
|
/**
|
|
1841
1935
|
* Which frame a paused document request navigates, as documentFrame reads it, without the frame tree where
|
|
@@ -1851,6 +1945,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1851
1945
|
return 'frame';
|
|
1852
1946
|
if (sessionId === pageSessionId && pageTargetId !== undefined && frameId === pageTargetId)
|
|
1853
1947
|
return 'top';
|
|
1948
|
+
// An adopted window's main frame is that window's own top-level document, and its frame tree is as unanswered.
|
|
1949
|
+
if (sessionId && windowTargets.get(sessionId) === frameId)
|
|
1950
|
+
return 'top';
|
|
1854
1951
|
return (await documentFrame(sessionId, frameId))?.navigation ?? null;
|
|
1855
1952
|
};
|
|
1856
1953
|
/**
|
|
@@ -2044,6 +2141,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2044
2141
|
let lastSubmitted = null;
|
|
2045
2142
|
// The approved Datatrans answers, per frameId: the repeat rule (datatrans.ts).
|
|
2046
2143
|
const datatransRepeats = new DatatransRepeats();
|
|
2144
|
+
// The approved TokenEx iframe answers, per frameId (the card-number frame is an out-of-process iframe whose own
|
|
2145
|
+
// session pauses its post; its frameId names it): the repeat rule (tokenex-iframe.ts).
|
|
2146
|
+
const tokenexIframeRepeats = new TokenexIframeRepeats();
|
|
2047
2147
|
// The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
|
|
2048
2148
|
const worldlineProducts = new WorldlineMyCheckoutProducts();
|
|
2049
2149
|
// The Shiji STS getCvvId companion of the getTokenId this attachment last continued (shiji-sts.ts).
|
|
@@ -2088,11 +2188,17 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2088
2188
|
// EBANX's 3-D Secure calls and hosted confirm, which send the typed card again
|
|
2089
2189
|
// after its token: the cardholder's device sends one the approved token request
|
|
2090
2190
|
// covers, and any other is refused (see ebanx.ts).
|
|
2191
|
+
// ComGate's card hosts are armed only for an org the API served ComGate to, so the
|
|
2192
|
+
// gate can fail every card post there while the gateway link pauses (see comgate.ts).
|
|
2091
2193
|
const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
|
|
2092
2194
|
...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN, ...merchantPatterns,
|
|
2093
2195
|
...(recognizesGmo(opts.vault) ? GMO_TOKEN_V1_PATTERNS : []), ...juspayCheckout.urlPatterns(), ...cardEndpointPatterns(),
|
|
2094
2196
|
...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS, ...PAYPAL_CARD_FORM_PATTERNS,
|
|
2095
|
-
...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS
|
|
2197
|
+
...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS,
|
|
2198
|
+
...(servesComgate(opts.vault) ? COMGATE_CARD_PATTERNS : [])])];
|
|
2199
|
+
// The hold-only rules' patterns (Bolt's tokenizer; every request a placeholder card can ride, by resource type).
|
|
2200
|
+
const fetchPatterns = [...urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2201
|
+
...(holdGate ? holdGate.patterns().map((p) => ({ ...p, requestStage: 'Request' })) : [])];
|
|
2096
2202
|
const arm = async (sessionId, resume = false, worker = false) => {
|
|
2097
2203
|
const key = sessionId ?? '__root__';
|
|
2098
2204
|
if (armed.has(key))
|
|
@@ -2109,6 +2215,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2109
2215
|
};
|
|
2110
2216
|
await cdp.send('Network.enable', {}, sessionId);
|
|
2111
2217
|
assertActive();
|
|
2218
|
+
// Requests skip the site's service workers, so every one stays in this frame's Fetch interception. Required:
|
|
2219
|
+
// a frame that cannot bypass them is not armed.
|
|
2220
|
+
if (bypassServiceWorkers && !worker) {
|
|
2221
|
+
await cdp.send('Network.setBypassServiceWorker', { bypass: true }, sessionId);
|
|
2222
|
+
assertActive();
|
|
2223
|
+
}
|
|
2112
2224
|
// Worker Fetch does not exist in Chrome. Its requests pause on the owning
|
|
2113
2225
|
// frame, but its abort/finish events arrive on the worker Network session.
|
|
2114
2226
|
// Keep that session attached and paused until Network is listening, then
|
|
@@ -2116,9 +2228,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2116
2228
|
if (!worker) {
|
|
2117
2229
|
await cdp.send('Page.enable', {}, sessionId).catch(() => { });
|
|
2118
2230
|
assertActive();
|
|
2119
|
-
await cdp.send('Fetch.enable', {
|
|
2120
|
-
patterns: urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2121
|
-
}, sessionId);
|
|
2231
|
+
await cdp.send('Fetch.enable', { patterns: fetchPatterns }, sessionId);
|
|
2122
2232
|
assertActive();
|
|
2123
2233
|
}
|
|
2124
2234
|
// Descend into this target's own children (iframes inside iframes).
|
|
@@ -2147,6 +2257,56 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2147
2257
|
arming.delete(key);
|
|
2148
2258
|
}
|
|
2149
2259
|
};
|
|
2260
|
+
/**
|
|
2261
|
+
* Watch a shared worker of the merchant's context (sharedWorkers 'observe'): attach to it from the browser root,
|
|
2262
|
+
* read its Network reports, and release it. Its requests cannot be paused, so a card request seen there ends the
|
|
2263
|
+
* checkout (see the Network.requestWillBeSent handler). A worker that cannot be watched ends it too.
|
|
2264
|
+
*/
|
|
2265
|
+
const watchSharedWorker = async (target) => {
|
|
2266
|
+
if (sharedWorkerMode === 'refuse' || typeof target?.targetId !== 'string')
|
|
2267
|
+
return;
|
|
2268
|
+
// Chrome reports every existing target (Target.targetCreated) before it answers setDiscoverTargets, so that
|
|
2269
|
+
// report starts the watch: the attachment's own pass over the targets waits for it rather than passing it by.
|
|
2270
|
+
const starting = workerWatches.get(target.targetId);
|
|
2271
|
+
if (starting)
|
|
2272
|
+
return starting;
|
|
2273
|
+
if (watchedWorkerTargets.has(target.targetId))
|
|
2274
|
+
return;
|
|
2275
|
+
if (contextId(target.browserContextId) && target.browserContextId !== merchantContext)
|
|
2276
|
+
return;
|
|
2277
|
+
watchedWorkerTargets.add(target.targetId);
|
|
2278
|
+
if (!observeSharedWorkers) {
|
|
2279
|
+
opts.onEvent?.({ type: 'shared_worker_unwatched', detail: { scheme: String(target.url ?? '').split(':')[0] || null } });
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const watch = (async () => {
|
|
2283
|
+
const { sessionId: workerSession } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
|
|
2284
|
+
if (typeof workerSession !== 'string' || !workerSession)
|
|
2285
|
+
throw new CheckoutAttachmentError('unavailable');
|
|
2286
|
+
watchedWorkerSessions.add(workerSession);
|
|
2287
|
+
rejectedSessions.add(workerSession);
|
|
2288
|
+
await cdp.send('Network.enable', {}, workerSession);
|
|
2289
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, workerSession).catch(() => { });
|
|
2290
|
+
opts.onEvent?.({ type: 'shared_worker_watched', detail: { scheme: String(target.url ?? '').split(':')[0] || null } });
|
|
2291
|
+
})().catch((error) => failInterception(error instanceof CheckoutAttachmentError ? error : new CheckoutAttachmentError('unavailable')))
|
|
2292
|
+
.finally(() => workerWatches.delete(target.targetId));
|
|
2293
|
+
workerWatches.set(target.targetId, watch);
|
|
2294
|
+
return watch;
|
|
2295
|
+
};
|
|
2296
|
+
/** A card request a watched shared worker sent: it could not be held, so nothing may be approved from here on. */
|
|
2297
|
+
const sharedWorkerCardRequest = (request) => {
|
|
2298
|
+
const url = typeof request?.url === 'string' ? request.url : '';
|
|
2299
|
+
const method = typeof request?.method === 'string' ? request.method : 'GET';
|
|
2300
|
+
if (!url)
|
|
2301
|
+
return false;
|
|
2302
|
+
if (typeof opts.vault?.isCardRequest === 'function' && opts.vault.isCardRequest(url, method))
|
|
2303
|
+
return true;
|
|
2304
|
+
if (!holdGate)
|
|
2305
|
+
return false;
|
|
2306
|
+
const body = typeof request.postData === 'string' ? request.postData : request.hasPostData ? null : '';
|
|
2307
|
+
// A body too large to ride the report is read as a card request only by its address (Bolt's tokenizer).
|
|
2308
|
+
return !!matchHoldOnlyRule({ url, method, body: body ?? '' }, holdGate.rules) || (body === null && !!matchHoldOnlyRule({ url, method, body: '' }, holdGate.rules.filter((r) => !r.body)));
|
|
2309
|
+
};
|
|
2150
2310
|
const failInterception = (error) => {
|
|
2151
2311
|
if (setupFailureReported)
|
|
2152
2312
|
return;
|
|
@@ -2164,6 +2324,55 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2164
2324
|
lifecycle.failed(new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, 'browser_interception_unavailable'));
|
|
2165
2325
|
opts.onEvent?.({ type: 'failed', detail: 'browser_interception_unavailable' });
|
|
2166
2326
|
};
|
|
2327
|
+
/**
|
|
2328
|
+
* A window the merchant's page opened (window.open, or a form or link aimed at a new window), or one such a window
|
|
2329
|
+
* opened in turn, in the merchant's own browser context: it belongs to this checkout, so it is attached and armed
|
|
2330
|
+
* as the page is, and its requests reach the same gates. Chromium reports the window (Target.targetCreated, with its
|
|
2331
|
+
* openerId) as it starts, and the page session's auto-attach never covers a new page target. The attach races the
|
|
2332
|
+
* window's first request: a window that opens on the merchant's page and is sent on to a hosted page by script or
|
|
2333
|
+
* by the merchant's redirect is armed before that entry; a window opened straight onto a hosted page can load it
|
|
2334
|
+
* first, as every window did before. A window reported while this attachment still arms the page waits in
|
|
2335
|
+
* pendingWindows and is adopted once the page is armed, inside the setup deadline and before the attachment reports
|
|
2336
|
+
* ready, its opener judged then, so a window that window opened is adopted after it. A window that is gone before it
|
|
2337
|
+
* could be attached was never armed, as before; one attached but not armed fails the attachment, as a frame does.
|
|
2338
|
+
*/
|
|
2339
|
+
const openedWindows = new Set();
|
|
2340
|
+
const pendingWindows = [];
|
|
2341
|
+
const adoptOpenedWindow = async (info, duringSetup = false) => {
|
|
2342
|
+
if (!info || info.type !== 'page' || typeof info.targetId !== 'string' || !info.targetId || info.targetId === pageTargetId
|
|
2343
|
+
|| openedWindows.has(info.targetId) || info.browserContextId !== merchantContext)
|
|
2344
|
+
return;
|
|
2345
|
+
if (setupStop.signal.aborted)
|
|
2346
|
+
return;
|
|
2347
|
+
if (!attachmentReady && !duringSetup) {
|
|
2348
|
+
pendingWindows.push(info);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (typeof info.openerId !== 'string' || !(info.openerId === pageTargetId || openedWindows.has(info.openerId)))
|
|
2352
|
+
return;
|
|
2353
|
+
openedWindows.add(info.targetId);
|
|
2354
|
+
let child;
|
|
2355
|
+
try {
|
|
2356
|
+
child = (await cdp.send('Target.attachToTarget', { targetId: info.targetId, flatten: true }))?.sessionId;
|
|
2357
|
+
}
|
|
2358
|
+
catch {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
try {
|
|
2362
|
+
if (typeof child !== 'string' || !child || ownedSessions.has(child) || detachedSessions.has(child) || rejectedSessions.has(child)) {
|
|
2363
|
+
throw new CheckoutAttachmentError('unavailable');
|
|
2364
|
+
}
|
|
2365
|
+
ownedSessions.add(child);
|
|
2366
|
+
sessionTypes.set(child, 'page');
|
|
2367
|
+
sessionParents.set(child, pageSessionId);
|
|
2368
|
+
windowTargets.set(child, info.targetId);
|
|
2369
|
+
await arm(child);
|
|
2370
|
+
opts.onEvent?.({ type: 'window_armed', detail: {} });
|
|
2371
|
+
}
|
|
2372
|
+
catch (error) {
|
|
2373
|
+
failInterception(error);
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2167
2376
|
cdp.on(async (method, params, sessionId) => {
|
|
2168
2377
|
// A frame that committed a new document drops the companion binding made for the getTokenId its
|
|
2169
2378
|
// old document sent, with the device's CVV ciphertext: the document that replaced it never
|
|
@@ -2174,14 +2383,28 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2174
2383
|
shijiCompanions.releasePage((page) => page === replaced);
|
|
2175
2384
|
}
|
|
2176
2385
|
if ((method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') && targetContextKnown) {
|
|
2386
|
+
// The page's own target reports each document it commits from discovery on, Page events or not.
|
|
2387
|
+
notePageTarget(params?.targetInfo);
|
|
2177
2388
|
try {
|
|
2178
|
-
assertContextTarget(params?.targetInfo, merchantContext);
|
|
2389
|
+
assertContextTarget(params?.targetInfo, merchantContext, targetAllowance);
|
|
2390
|
+
if (params?.targetInfo?.type === 'shared_worker')
|
|
2391
|
+
void watchSharedWorker(params.targetInfo);
|
|
2179
2392
|
}
|
|
2180
2393
|
catch (error) {
|
|
2181
2394
|
failInterception(error);
|
|
2182
2395
|
}
|
|
2396
|
+
if (method === 'Target.targetCreated')
|
|
2397
|
+
await adoptOpenedWindow(params?.targetInfo);
|
|
2183
2398
|
return;
|
|
2184
2399
|
}
|
|
2400
|
+
// The page's main frame committed a document, or moved within it: the URL a held top-level navigation leaves.
|
|
2401
|
+
if (sessionId === pageSessionId && method === 'Page.frameNavigated' && !params.frame?.parentId && typeof params.frame?.url === 'string') {
|
|
2402
|
+
committedTopUrl = params.frame.url;
|
|
2403
|
+
}
|
|
2404
|
+
if (sessionId === pageSessionId && method === 'Page.navigatedWithinDocument' && pageTargetId !== undefined
|
|
2405
|
+
&& params?.frameId === pageTargetId && typeof params?.url === 'string') {
|
|
2406
|
+
committedTopUrl = params.url.split('#')[0];
|
|
2407
|
+
}
|
|
2185
2408
|
if (((method === 'Page.frameNavigated' && !params.frame?.parentId) || (method === 'Page.navigatedWithinDocument' && preparationFrameId && params.frameId === preparationFrameId)) && sessionId === pageSessionId) {
|
|
2186
2409
|
preparationGate.invalidate('merchant_document_changed');
|
|
2187
2410
|
mercadoCheckout.invalidate();
|
|
@@ -2203,6 +2426,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2203
2426
|
|| (method === 'Target.detachedFromTarget' && params.sessionId === pageSessionId)) {
|
|
2204
2427
|
detachedSessions.add(pageSessionId);
|
|
2205
2428
|
setupStop.abort(new CheckoutAttachmentError('closed'));
|
|
2429
|
+
holdGate?.close();
|
|
2206
2430
|
preparationGate.invalidate('merchant_document_closed');
|
|
2207
2431
|
stripeCheckout.invalidate();
|
|
2208
2432
|
mercadoCheckout.invalidate();
|
|
@@ -2218,6 +2442,20 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2218
2442
|
if (handoffNetwork)
|
|
2219
2443
|
merchantHandoff.failed();
|
|
2220
2444
|
}
|
|
2445
|
+
// A watched shared worker sent a card request: it went out unpaused, so nothing is approved from here on.
|
|
2446
|
+
if (method === 'Network.requestWillBeSent' && sessionId && watchedWorkerSessions.has(sessionId) && sharedWorkerCardRequest(params?.request)) {
|
|
2447
|
+
// Origin and path as redactUrl reads them, so a credential a processor writes into the path never shows.
|
|
2448
|
+
let endpoint = '';
|
|
2449
|
+
try {
|
|
2450
|
+
new URL(params.request.url);
|
|
2451
|
+
endpoint = redactUrl(params.request.url);
|
|
2452
|
+
}
|
|
2453
|
+
catch { /* unreadable */ }
|
|
2454
|
+
opts.onEvent?.({ type: 'card_request_unwatched', detail: { endpoint, from: 'shared_worker' } });
|
|
2455
|
+
void holdGate?.cancel('card_request_unwatched');
|
|
2456
|
+
failInterception(new CheckoutAttachmentError('unavailable'));
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2221
2459
|
// The transport is browser-scoped. A second tab's request or child target
|
|
2222
2460
|
// must never be authorized with this page's merchant, amount or preparation.
|
|
2223
2461
|
if (!sessionId || !ownedSessions.has(sessionId))
|
|
@@ -2325,6 +2563,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2325
2563
|
return;
|
|
2326
2564
|
}
|
|
2327
2565
|
if (method === 'Network.loadingFailed') {
|
|
2566
|
+
// The held request given up by the page (its own timeout) or failed by the hold itself: nothing was sent.
|
|
2567
|
+
if (holdGate && holdGate.state === 'held' && ((holdOnlyNetworkId && params.requestId === holdOnlyNetworkId)
|
|
2568
|
+
|| (activeRequest && activeRequest.networkId === params.requestId && activeRequest.sourceSessionId === sessionId)))
|
|
2569
|
+
holdGate.lost();
|
|
2328
2570
|
merchantTotal.log.failed(requestKey(sessionId, params.requestId));
|
|
2329
2571
|
const recordKey = cardEndpointRecord.keyFor(requestKey(sessionId, params.requestId), params.requestId);
|
|
2330
2572
|
if (recordKey)
|
|
@@ -2414,6 +2656,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2414
2656
|
}
|
|
2415
2657
|
if (method === 'Target.attachedToTarget') {
|
|
2416
2658
|
const child = params.sessionId;
|
|
2659
|
+
if (params?.targetInfo?.type === 'shared_worker' && watchedWorkerTargets.has(params.targetInfo.targetId)) {
|
|
2660
|
+
if (typeof child === 'string' && child) {
|
|
2661
|
+
watchedWorkerSessions.add(child);
|
|
2662
|
+
rejectedSessions.add(child);
|
|
2663
|
+
}
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2417
2666
|
if (rejectedSessions.has(child))
|
|
2418
2667
|
return;
|
|
2419
2668
|
// A browser-root transport can report another context through an owned
|
|
@@ -2437,6 +2686,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2437
2686
|
}
|
|
2438
2687
|
if (setupStop.signal.aborted)
|
|
2439
2688
|
return;
|
|
2689
|
+
// A service worker the page's frames bypass: released from its debugger wait and left alone, never owned.
|
|
2690
|
+
if (bypassServiceWorkers && params.targetInfo?.type === 'service_worker' && typeof child === 'string' && child
|
|
2691
|
+
&& child !== pageSessionId && !ownedSessions.has(child)) {
|
|
2692
|
+
rejectedSessions.add(child);
|
|
2693
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2440
2696
|
// CDP may omit the context ID for ordinary children; keep those attached.
|
|
2441
2697
|
try {
|
|
2442
2698
|
if (typeof child !== 'string' || !child || !['page', 'iframe', 'worker'].includes(params.targetInfo?.type)) {
|
|
@@ -2477,9 +2733,50 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2477
2733
|
const { requestId, request, resourceType, networkId, frameId } = params;
|
|
2478
2734
|
// When this request paused: the merchant's total is read as of this moment.
|
|
2479
2735
|
const pausedAt = Date.now();
|
|
2736
|
+
// The page's own top-level navigation, paused: its frame tree cannot be read until this request is decided, so its
|
|
2737
|
+
// document is the one it last committed (committedTopUrl). Every other request reads the frame tree as before.
|
|
2738
|
+
const readPausedDocument = resourceType === 'Document' && sessionId === pageSessionId && pageTargetId !== undefined
|
|
2739
|
+
&& frameId === pageTargetId && committedTopUrl !== undefined ? async () => committedTopUrl : readDocumentUrl;
|
|
2480
2740
|
// A paused document request has not committed either: it only starts the context's check.
|
|
2481
2741
|
if (resourceType === 'Document')
|
|
2482
2742
|
microforms.warm(withFragment(request));
|
|
2743
|
+
// Hold mode: a card request no recogniser pays (Bolt's sealed tokenizer, a body carrying the placeholder card) is
|
|
2744
|
+
// held too, and can only be cancelled (or, in sandbox, a placeholder post sent on unchanged). Never recorded.
|
|
2745
|
+
if (holdGate) {
|
|
2746
|
+
// A request any of the SDK's own rules arms (a recogniser, a reviewed card endpoint, a guard, Stripe Checkout,
|
|
2747
|
+
// Mercado, a merchant profile...) goes down the SDK's own path; only what none of them covers is hold-only.
|
|
2748
|
+
const recognised = (typeof opts.vault?.isCardRequest === 'function' && opts.vault.isCardRequest(request.url, request.method))
|
|
2749
|
+
|| urlPatterns.some((pattern) => urlPatternMatches(pattern, request.url));
|
|
2750
|
+
const holdRule = recognised ? null : matchHoldOnlyRule({ url: request.url, method: request.method, body: pausedBody(request) }, holdGate.rules);
|
|
2751
|
+
if (holdRule) {
|
|
2752
|
+
const failHeld = () => cdp.send('Fetch.failRequest', { requestId, errorReason: 'BlockedByClient' }, sessionId).catch(() => { });
|
|
2753
|
+
if (!attachmentReady || setupStop.signal.aborted || holdGate.refuse(holdRule.psp, request.url)) {
|
|
2754
|
+
await failHeld();
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
holdOnlyNetworkId = typeof networkId === 'string' && networkId ? networkId : null;
|
|
2758
|
+
let decision;
|
|
2759
|
+
try {
|
|
2760
|
+
decision = await holdGate.hold({ psp: holdRule.psp, url: request.url, kind: holdRule.kind, replayable: false, rule: holdRule });
|
|
2761
|
+
}
|
|
2762
|
+
catch {
|
|
2763
|
+
holdOnlyNetworkId = null;
|
|
2764
|
+
await failHeld();
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
finally { /* the decision is in */ }
|
|
2768
|
+
holdOnlyNetworkId = null;
|
|
2769
|
+
if (decision.kind === 'continue') {
|
|
2770
|
+
// Sandbox only (HoldGate.release): the post carries the documented placeholder test number and nothing else.
|
|
2771
|
+
const ok = await cdp.send('Fetch.continueRequest', { requestId }, sessionId).then(() => true, () => false);
|
|
2772
|
+
decision.settle(ok ? { outcome: 'continued' } : { outcome: 'failed', detail: 'continue_failed' });
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
await failHeld();
|
|
2776
|
+
decision.settle({ outcome: 'failed', detail: 'not_replayable' });
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2483
2780
|
// One pre-claim gate registry decides and performs the mechanics through
|
|
2484
2781
|
// these effects; the state accessors read the same closure variables the
|
|
2485
2782
|
// inline gates did. It resolves synchronously for an ordinary card request,
|
|
@@ -2522,6 +2819,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2522
2819
|
cybersourceFlex: { judge: (flexRequest) => microforms.judge(flexRequest, frameId, () => rereadMicroforms(sessionId, frameId)) },
|
|
2523
2820
|
juspay: juspayCheckout,
|
|
2524
2821
|
datatrans: datatransRepeats,
|
|
2822
|
+
tokenexIframe: tokenexIframeRepeats,
|
|
2525
2823
|
ebanx: ebanxFollowUps,
|
|
2526
2824
|
frame: typeof frameId === 'string' && frameId ? frameId : undefined,
|
|
2527
2825
|
payrails,
|
|
@@ -2593,7 +2891,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2593
2891
|
if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()
|
|
2594
2892
|
&& typeof networkId === 'string' && networkId) {
|
|
2595
2893
|
const retryBody = pausedBody(request);
|
|
2596
|
-
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(
|
|
2894
|
+
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readPausedDocument, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape, request.headers);
|
|
2597
2895
|
// A prepared approval is answered only in the card frame it was bound in: the same request from
|
|
2598
2896
|
// another frame (a replacement for a removed one, or a second card frame) is refused below.
|
|
2599
2897
|
if (retry && activeRequest?.attempt.lost && sameCheckoutRequest(activeRequest.identity, retry)
|
|
@@ -2670,6 +2968,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2670
2968
|
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
2671
2969
|
return;
|
|
2672
2970
|
}
|
|
2971
|
+
// Hold mode: one request held at a time, and none after a decision.
|
|
2972
|
+
if (holdGate && holdGate.refuse(recognizerNameOf(opts, request.url, request.method), request.url)) {
|
|
2973
|
+
if (preparation)
|
|
2974
|
+
preparationGate.retireUnboundClaim();
|
|
2975
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2673
2978
|
// Reserve BEFORE the first await (the authorize call below yields), so two
|
|
2674
2979
|
// requests can never both clear the check above and raise two prompts for
|
|
2675
2980
|
// one checkout.
|
|
@@ -2686,6 +2991,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2686
2991
|
let ebanxRemembered = false;
|
|
2687
2992
|
// The business's pause, so resume resolves once the page has its answer.
|
|
2688
2993
|
let businessPause = null;
|
|
2994
|
+
// Hold mode: the person's release, settled with what came of it.
|
|
2995
|
+
let holdRelease = null;
|
|
2689
2996
|
// A prepared Amadeus SDK add must be sent within its window (see amadeus.ts).
|
|
2690
2997
|
let disarmSendWindow = null;
|
|
2691
2998
|
try {
|
|
@@ -2738,7 +3045,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2738
3045
|
await preparationGate.assertDocument();
|
|
2739
3046
|
// One bounded read of the document serves both the merchant origin and
|
|
2740
3047
|
// the identity a retry has to repeat (see sameCheckoutRequest).
|
|
2741
|
-
const documentUrl = await documentUrlOf(
|
|
3048
|
+
const documentUrl = await documentUrlOf(readPausedDocument, attempt.signal);
|
|
2742
3049
|
const pageOrigin = pageOriginFrom(documentUrl);
|
|
2743
3050
|
// The agent's amount is a card endpoint's only authority: no page total is read.
|
|
2744
3051
|
const pageAmount = cardEndpoint ? undefined : await pageAmountOf(opts);
|
|
@@ -2776,15 +3083,45 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2776
3083
|
// The merchant's own record of the order, as of the pause, for a profile that reads one.
|
|
2777
3084
|
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url, pausedAt, attempt.signal) : null;
|
|
2778
3085
|
attempt.assertLive();
|
|
3086
|
+
// Hold mode: nothing is created until the person's total releases the request (hold.ts).
|
|
3087
|
+
if (holdGate) {
|
|
3088
|
+
if (holdGate.state !== 'watching') {
|
|
3089
|
+
// Another card request was held while this one was being read: this one is refused alone.
|
|
3090
|
+
holdGate.refuse(recognizerNameOf(opts, request.url, request.method), request.url);
|
|
3091
|
+
throw new HoldRefusedError();
|
|
3092
|
+
}
|
|
3093
|
+
const decision = await holdUntilDecided(holdGate.hold({ psp: recognizerNameOf(opts, request.url, request.method), url: request.url,
|
|
3094
|
+
kind: 'card_request', replayable: true }), attempt.signal, () => holdGate.lost());
|
|
3095
|
+
if (decision.kind !== 'release') {
|
|
3096
|
+
decision.settle({ outcome: 'failed', detail: 'not_releasable' });
|
|
3097
|
+
throw new HoldEndedError('not_releasable');
|
|
3098
|
+
}
|
|
3099
|
+
holdRelease = decision;
|
|
3100
|
+
attempt.assertLive();
|
|
3101
|
+
}
|
|
2779
3102
|
// Decision 176: pause only. The business creates the authorization from the paused request and answers
|
|
2780
3103
|
// through controller.resume; the replay it hands back goes down the same delivery path as a create's.
|
|
2781
3104
|
const pause = opts.pauseOnly ? pauseForBusiness(opts, lifecycle, attempt.signal, { url: request.url, method: request.method, headers: request.headers ?? {}, body }, pageOrigin) : null;
|
|
2782
3105
|
businessPause = pause;
|
|
2783
|
-
|
|
3106
|
+
// A sandbox hold never sends the request on: the approval is created at the released total and read, the
|
|
3107
|
+
// page's request fails the moment it exists, and the attempt ends with nothing charged.
|
|
3108
|
+
const holdSandbox = !!holdRelease && opts.vault.sandbox === true;
|
|
3109
|
+
const holdMerchantStop = holdSandbox ? new AbortController() : null;
|
|
3110
|
+
// The approval outlives the page's request, but not the checkout: cancelling the attachment while the approval is
|
|
3111
|
+
// open retires it at Agentcard (the client cancels an authorization whose merchant signal aborted).
|
|
3112
|
+
if (holdMerchantStop) {
|
|
3113
|
+
const retire = () => holdMerchantStop.abort();
|
|
3114
|
+
if (lifecycle.abort.signal.aborted)
|
|
3115
|
+
retire();
|
|
3116
|
+
else
|
|
3117
|
+
lifecycle.abort.signal.addEventListener('abort', retire, { once: true });
|
|
3118
|
+
}
|
|
3119
|
+
let holdRequestFailed = false;
|
|
3120
|
+
const authorization = pause ? null : opts.vault.authorize({
|
|
2784
3121
|
user: opts.user,
|
|
2785
3122
|
merchant: opts.merchant,
|
|
2786
|
-
amount: opts.amount,
|
|
2787
|
-
currency: opts.currency,
|
|
3123
|
+
amount: holdRelease ? holdRelease.amount : opts.amount,
|
|
3124
|
+
currency: holdRelease ? holdRelease.currency : opts.currency,
|
|
2788
3125
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
2789
3126
|
executionMode: opts.executionMode,
|
|
2790
3127
|
grantId: opts.grantId,
|
|
@@ -2796,11 +3133,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2796
3133
|
preparation,
|
|
2797
3134
|
timeoutMs: opts.timeoutMs,
|
|
2798
3135
|
signal: lifecycle.abort.signal,
|
|
2799
|
-
|
|
3136
|
+
// A sandbox hold's approval outlives the page's request on purpose: that request is failed once it exists.
|
|
3137
|
+
merchantSignal: holdMerchantStop?.signal ?? attempt.signal,
|
|
2800
3138
|
// What the merchant's own checkout said its total was, as this browser recorded it
|
|
2801
3139
|
// (read only for a reviewed profile priced by it): the page is the card request's.
|
|
2802
3140
|
merchantTotal: merchantTotal.capture(pausedAt, cardPage),
|
|
2803
|
-
onAuthorizationCreated: (id) =>
|
|
3141
|
+
onAuthorizationCreated: (id) => {
|
|
3142
|
+
lifecycle.approvalCreated(id);
|
|
3143
|
+
if (holdSandbox && !holdRequestFailed) {
|
|
3144
|
+
holdRequestFailed = true;
|
|
3145
|
+
// The page's request is let go of before it is failed, so its failure report does not end this attempt:
|
|
3146
|
+
// the approval it raised is read to its end, and nothing is ever delivered to the page.
|
|
3147
|
+
if (activeRequest?.attempt === attempt)
|
|
3148
|
+
activeRequest = null;
|
|
3149
|
+
opts.onEvent?.({ type: 'card_request_not_sent', detail: { authorizationId: id } });
|
|
3150
|
+
void cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
2804
3153
|
onApprovalUrl: (url) => { if (!preparation && !attempt.signal.aborted) {
|
|
2805
3154
|
lifecycle.approvalUrl(url);
|
|
2806
3155
|
return opts.onApprovalUrl?.(url);
|
|
@@ -2812,6 +3161,15 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2812
3161
|
...(flexPause ? flexPause.sidecar : {}),
|
|
2813
3162
|
...(payrailsSidecar ? { payrails: payrailsSidecar } : {}) },
|
|
2814
3163
|
});
|
|
3164
|
+
if (holdSandbox && authorization) {
|
|
3165
|
+
const outcome = await authorization.then((r) => ({ outcome: 'approved_not_charged', authorizationId: r.authorizationId }), (err) => ({ outcome: err instanceof ApprovalDeclinedError ? 'declined' : err instanceof ApprovalTimeoutError ? 'expired' : 'failed',
|
|
3166
|
+
authorizationId: lifecycle.getState().authorizationId, detail: failureSummary(err) }));
|
|
3167
|
+
if (!holdRequestFailed)
|
|
3168
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
3169
|
+
holdRelease.settle(outcome);
|
|
3170
|
+
throw new HoldAnsweredError(outcome);
|
|
3171
|
+
}
|
|
3172
|
+
const replay = pause ? eprotectResumedReplay(await pause.replay, request) : await authorization;
|
|
2815
3173
|
attempt.assertLive();
|
|
2816
3174
|
approvedId = replay.authorizationId;
|
|
2817
3175
|
if (lifecycle.isCancelled())
|
|
@@ -2979,6 +3337,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2979
3337
|
}, deliverOn);
|
|
2980
3338
|
// The frame that received it: the paused request's, or its retry's once rebound.
|
|
2981
3339
|
datatransRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
|
|
3340
|
+
tokenexIframeRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
|
|
2982
3341
|
attempt.assertLive();
|
|
2983
3342
|
// An ACI card-number request answered: its checkout's held CVC and expiry requests get the
|
|
2984
3343
|
// same answer, and its /payment submission may continue (see AciGate).
|
|
@@ -2992,8 +3351,18 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2992
3351
|
if (handedOver)
|
|
2993
3352
|
lifecycle.handedOff(replay);
|
|
2994
3353
|
businessPause?.delivered.resolve();
|
|
3354
|
+
holdRelease?.settle({ outcome: 'authorized', authorizationId: replay.authorizationId });
|
|
2995
3355
|
}
|
|
2996
3356
|
catch (err) {
|
|
3357
|
+
if (err instanceof HoldRefusedError) {
|
|
3358
|
+
// Refused alone: the request another rule holds, and the checkout's state, are left as they are.
|
|
3359
|
+
if (activeRequest?.attempt === attempt)
|
|
3360
|
+
activeRequest = null;
|
|
3361
|
+
await refusePaused(attempt.target?.requestId ?? requestId, attempt.target?.sessionId ?? sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, false, lifecycle));
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
holdRelease?.settle(err instanceof HoldAnsweredError ? err.outcome : { outcome: err instanceof ApprovalDeclinedError ? 'declined' : err instanceof ApprovalTimeoutError ? 'expired' : 'failed',
|
|
3365
|
+
authorizationId: approvedId ?? lifecycle.getState().authorizationId, detail: failureSummary(err) });
|
|
2997
3366
|
// Ignore the failure event caused by our own safe decline/error abort.
|
|
2998
3367
|
const dead = attempt.target ?? { requestId, sessionId };
|
|
2999
3368
|
// An EBANX token answer this handoff kept follow-ups for and did not finish handing over: nothing after it is
|
|
@@ -3067,6 +3436,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3067
3436
|
targetContextKnown = true;
|
|
3068
3437
|
// A page target's id is its main frame's id: a navigation of that frame is the page's own (pausedNavigation).
|
|
3069
3438
|
pageTargetId = typeof targetInfo.targetId === 'string' && targetInfo.targetId ? targetInfo.targetId : undefined;
|
|
3439
|
+
// The page's URL as its target reports it: the document a held top-level navigation leaves until the page commits
|
|
3440
|
+
// another one (the Page.frameNavigated branch above, and notePageTarget for a commit before the Page events are on).
|
|
3441
|
+
// No frame tree read: a navigation the page starts while this attachment arms would hold it.
|
|
3442
|
+
if (committedTopUrl === undefined && typeof targetInfo.url === 'string' && targetInfo.url)
|
|
3443
|
+
committedTopUrl = targetInfo.url.split('#')[0];
|
|
3070
3444
|
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
3071
3445
|
assertActive();
|
|
3072
3446
|
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
@@ -3074,24 +3448,43 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3074
3448
|
if (!Array.isArray(targetInfos))
|
|
3075
3449
|
throw new CheckoutAttachmentError('unavailable');
|
|
3076
3450
|
for (const target of targetInfos)
|
|
3077
|
-
assertContextTarget(target, merchantContext);
|
|
3451
|
+
assertContextTarget(target, merchantContext, targetAllowance);
|
|
3452
|
+
// The page may have committed another document since its TargetInfo was read; the list names the one it holds.
|
|
3453
|
+
for (const target of targetInfos)
|
|
3454
|
+
notePageTarget(target);
|
|
3455
|
+
for (const target of targetInfos)
|
|
3456
|
+
if (target?.type === 'shared_worker')
|
|
3457
|
+
await watchSharedWorker(target);
|
|
3078
3458
|
await arm(pageSessionId);
|
|
3079
3459
|
// Existing OOPIFs report attachment before the parent's acknowledgement,
|
|
3080
3460
|
// but their Fetch setup finishes later. Do not invite the caller to press
|
|
3081
|
-
// Pay while one of those already-running frames is still unprotected
|
|
3082
|
-
while
|
|
3083
|
-
|
|
3461
|
+
// Pay while one of those already-running frames is still unprotected, or
|
|
3462
|
+
// while a shared worker reported during setup is not yet watched.
|
|
3463
|
+
// A window the page opened while this attachment armed it is attached and armed here too, in the order Chromium
|
|
3464
|
+
// reported it (adoptOpenedWindow), and the frames and workers such a window brings are waited for as the page's
|
|
3465
|
+
// own are, all under the setup deadline. One that cannot be armed fails the attachment.
|
|
3466
|
+
while (arming.size || workerWatches.size || pendingWindows.length) {
|
|
3467
|
+
if (arming.size || workerWatches.size)
|
|
3468
|
+
await Promise.all([...arming.values(), ...workerWatches.values()]);
|
|
3469
|
+
else
|
|
3470
|
+
await adoptOpenedWindow(pendingWindows.shift(), true);
|
|
3471
|
+
assertActive();
|
|
3472
|
+
}
|
|
3084
3473
|
assertActive();
|
|
3085
3474
|
}, setupTimeoutMs, setupStop.signal);
|
|
3086
3475
|
if (setupStop.signal.aborted)
|
|
3087
3476
|
throw attachmentFailure(setupStop.signal.reason);
|
|
3088
3477
|
attachmentReady = true;
|
|
3089
|
-
|
|
3478
|
+
// A window reported after that drain, before this line, is adopted the way a later window is.
|
|
3479
|
+
for (const info of pendingWindows.splice(0))
|
|
3480
|
+
void adoptOpenedWindow(info);
|
|
3481
|
+
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: urlPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
|
|
3090
3482
|
}
|
|
3091
3483
|
catch (error) {
|
|
3092
3484
|
const failure = attachmentFailure(error);
|
|
3093
3485
|
setupStop.abort(failure);
|
|
3094
3486
|
terminal = failure;
|
|
3487
|
+
holdGate?.close();
|
|
3095
3488
|
if (!setupFailureReported) {
|
|
3096
3489
|
setupFailureReported = true;
|
|
3097
3490
|
lifecycle.cancel();
|
|
@@ -3116,6 +3509,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3116
3509
|
opts = safeOptions(opts);
|
|
3117
3510
|
if (opts.pauseOnly)
|
|
3118
3511
|
throw new Error('pauseOnly is a raw CDP mode (attachToCdp); attachToPlaywright creates the authorization itself.');
|
|
3512
|
+
if (opts.hold)
|
|
3513
|
+
throw new Error('hold is a raw CDP mode (attachToCdp): it needs the frames\' own interception.');
|
|
3119
3514
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
3120
3515
|
const setupStop = new AbortController();
|
|
3121
3516
|
let attachmentReady = false;
|
|
@@ -3422,6 +3817,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3422
3817
|
let lastSubmitted = null;
|
|
3423
3818
|
// The approved Datatrans answers, per Frame: the repeat rule (datatrans.ts).
|
|
3424
3819
|
const datatransRepeats = new DatatransRepeats();
|
|
3820
|
+
// The approved TokenEx iframe answers, per Frame: the repeat rule (tokenex-iframe.ts).
|
|
3821
|
+
const tokenexIframeRepeats = new TokenexIframeRepeats();
|
|
3425
3822
|
// The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
|
|
3426
3823
|
const worldlineProducts = new WorldlineMyCheckoutProducts();
|
|
3427
3824
|
const frameOf = (request) => { try {
|
|
@@ -3442,8 +3839,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3442
3839
|
// The matcher sees only the URL. Worldpay eProtect's registration is a GET, so its family and
|
|
3443
3840
|
// the vendor's give-up reports are routed by the eProtect gate (never its scripts).
|
|
3444
3841
|
// Gravy's session fields are a PUT, so a URL a PUT recognizer claims is routed too; the
|
|
3445
|
-
// pre-claim gates then pause only the PUT and continue the URL's other methods.
|
|
3842
|
+
// pre-claim gates then pause only the PUT and continue the URL's other methods. A hosted page
|
|
3843
|
+
// entered with a plain GET (hosted-entry.ts, ComGate's gateway link) is routed by its exact link.
|
|
3446
3844
|
(url) => opts.vault.isCardRequest(url.toString()) || opts.vault.isCardRequest(url.toString(), 'PUT')
|
|
3845
|
+
|| opts.vault.isCardRequest(url.toString(), 'GET')
|
|
3447
3846
|
|| guards.matches(url.toString()) || stripeCheckout.matches(url.toString()) || mercadoCheckout.matches(url.toString())
|
|
3448
3847
|
|| eprotect.matches(url.toString())
|
|
3449
3848
|
// An armed merchant profile's endpoint or a sibling of it (merchant-hosted.ts), as attachToCdp arms it.
|
|
@@ -3461,9 +3860,16 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3461
3860
|
|| aci.matches(url.toString())
|
|
3462
3861
|
// Every Payrails host, routed only for an org the API served Payrails to (see attachToCdp).
|
|
3463
3862
|
|| (payrails !== null && isPayrailsHost(url.toString()))
|
|
3863
|
+
// Every request to Przelewy24's pay wall hosts once its entry is served (przelewy24.ts), as attachToCdp arms
|
|
3864
|
+
// the hosts: the card method's post, the card form's post and the Saferpay resolver reach the Przelewy24
|
|
3865
|
+
// gate and fail.
|
|
3866
|
+
|| isArmedPrzelewy24Url(opts.vault, url.toString())
|
|
3464
3867
|
// Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
|
|
3465
3868
|
// host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
|
|
3466
3869
|
|| isArmedEkashuUrl(opts.vault, url.toString())
|
|
3870
|
+
// Every request to a Webpay host once its entry is served (transbank.ts), as attachToCdp arms the hosts: the
|
|
3871
|
+
// token's submit and Webpay's card steps reach the Webpay gate and fail, and the entry pauses.
|
|
3872
|
+
|| isArmedTransbankUrl(opts.vault, url.toString())
|
|
3467
3873
|
// A CreditGuard page's own calls beside its submit (its card lookup and its 3-D Secure calls), for an
|
|
3468
3874
|
// org the API served CreditGuard to, as attachToCdp arms its hosts: the CreditGuard gate answers them
|
|
3469
3875
|
// under a ready preparation and lets them go on with none (pre-claim.ts isArmedCreditguardCall).
|
|
@@ -3471,6 +3877,15 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3471
3877
|
// Every request to a Redsys host once its entry is served (redsys.ts), as attachToCdp arms the
|
|
3472
3878
|
// hosts: the card page's post and Redsys's other card requests reach the Redsys gate and fail.
|
|
3473
3879
|
|| isArmedRedsysUrl(opts.vault, url.toString())
|
|
3880
|
+
// Every request to QuickPay's host once its entry is served (quickpay.ts), as attachToCdp arms the host:
|
|
3881
|
+
// the window's card post, its other card requests and its card lookup reach the QuickPay gate and fail.
|
|
3882
|
+
|| isArmedQuickpayUrl(opts.vault, url.toString())
|
|
3883
|
+
// Every request to one of ComGate's card hosts once its entry is served (comgate.ts), as attachToCdp arms them:
|
|
3884
|
+
// the gateway's card post and every other card call there reach the ComGate gate and fail.
|
|
3885
|
+
|| isArmedComgateUrl(opts.vault, url.toString())
|
|
3886
|
+
// Every request on the TokenEx iframe's /iframe/v3 path once its entry is served (tokenex-iframe.ts), as attachToCdp
|
|
3887
|
+
// arms its hosts: a card post the recognizer's exact URL does not match reaches the TokenEx gate and fails.
|
|
3888
|
+
|| isArmedTokenexIframeUrl(opts.vault, url.toString())
|
|
3474
3889
|
// PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
|
|
3475
3890
|
|| isPaypalHostedFieldsConfirm(url.toString())
|
|
3476
3891
|
// PayPal's Card Fields confirm and GraphQL on www.paypal.com, for every org, as attachToCdp arms them (paypal-card-forms.ts).
|
|
@@ -3480,7 +3895,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3480
3895
|
// Paddle's checkout services, routed only for an org the API served Paddle VGS to (see attachToCdp).
|
|
3481
3896
|
|| (isPaddleCheckoutService(url.toString()) && recognizesPaddleVgs(opts.vault))
|
|
3482
3897
|
// EBANX's 3-D Secure calls and hosted confirm, for every org, as attachToCdp arms them (ebanx.ts).
|
|
3483
|
-
|| isEbanxResendUrl(url.toString())
|
|
3898
|
+
|| isEbanxResendUrl(url.toString())
|
|
3899
|
+
// HiPay's two vault hosts, routed only for an org the API served HiPay to, as attachToCdp arms both hosts from the
|
|
3900
|
+
// recognizer: the frame's /update reaches the HiPay gate and fails (pre-claim.ts routesHipayVault).
|
|
3901
|
+
|| routesHipayVault(opts.vault, url.toString()), async (route) => {
|
|
3484
3902
|
const request = route.request();
|
|
3485
3903
|
// When this request paused, and the page it came from: the merchant's total is read as of now.
|
|
3486
3904
|
const pausedAt = Date.now();
|
|
@@ -3573,6 +3991,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3573
3991
|
} },
|
|
3574
3992
|
juspay: juspayCheckout,
|
|
3575
3993
|
datatrans: datatransRepeats,
|
|
3994
|
+
tokenexIframe: tokenexIframeRepeats,
|
|
3576
3995
|
ebanx: ebanxFollowUps,
|
|
3577
3996
|
frame: frameOf(request),
|
|
3578
3997
|
payrails,
|
|
@@ -3959,6 +4378,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3959
4378
|
}
|
|
3960
4379
|
await deliverTo.fulfill({ status: replay.status, headers: withCorsHeaders(replay.headers, cors.headers), body: replay.body });
|
|
3961
4380
|
datatransRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
|
|
4381
|
+
tokenexIframeRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
|
|
3962
4382
|
assertRequestLive();
|
|
3963
4383
|
// See the same step in attachToCdp.
|
|
3964
4384
|
aci.answered(deliverRequest.url(), { status: replay.status, headers: replay.headers, body: replay.body }, replay.authorizationId);
|