@agent-cards/sdk 0.23.18 → 0.23.20
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 +30 -0
- package/PREFLIGHT.md +2 -0
- package/README.md +46 -7
- package/dist/aci.d.ts +0 -7
- package/dist/aci.js +0 -19
- 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 +974 -45
- package/dist/cdp.d.ts +29 -0
- package/dist/cdp.js +302 -18
- package/dist/client.d.ts +43 -5
- package/dist/client.js +237 -43
- package/dist/datatrans.d.ts +2 -2
- package/dist/datatrans.generated.js +16 -11
- package/dist/datatrans.js +2 -2
- package/dist/hipay.generated.d.ts +56 -0
- package/dist/hipay.generated.js +389 -0
- package/dist/hold.d.ts +180 -0
- package/dist/hold.js +264 -0
- package/dist/index.d.ts +3 -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-vgs.generated.d.ts +3 -2
- package/dist/paddle-vgs.generated.js +9 -5
- package/dist/pelecard.generated.d.ts +2 -1
- package/dist/pelecard.generated.js +14 -5
- package/dist/pre-claim.d.ts +14 -0
- package/dist/pre-claim.js +76 -3
- package/dist/prepared-processor.d.ts +6 -2
- package/dist/prepared-processor.js +18 -2
- package/dist/primer.generated.js +998 -69
- package/dist/quickpay.d.ts +44 -0
- package/dist/quickpay.js +65 -0
- package/dist/stripe-checkout.generated.js +320 -2
- package/dist/twoc2p.d.ts +54 -0
- package/dist/twoc2p.generated.d.ts +33 -0
- package/dist/twoc2p.generated.js +1145 -0
- package/dist/twoc2p.js +85 -0
- package/dist/voice.d.ts +16 -13
- package/dist/voice.js +13 -10
- package/package.json +3 -3
package/dist/cdp.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type VaultClient, type ExecutionMetadata } from './client.js';
|
|
2
2
|
import { type StripeCheckoutOptions } from './stripe-checkout.js';
|
|
3
|
+
import { type HoldOptions } from './hold.js';
|
|
3
4
|
import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard, type PausedCheckout } from './lifecycle.js';
|
|
4
5
|
/**
|
|
5
6
|
* The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
|
|
@@ -112,6 +113,34 @@ export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
|
|
|
112
113
|
pauseOnly?: boolean;
|
|
113
114
|
/** With `pauseOnly`: the paused request and the controller to resume it on. Required when `pauseOnly` is set. */
|
|
114
115
|
onPaused?: (paused: PausedCheckout) => void | Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Hold mode (CARD-REQUEST-HOLD.md): the first card request pauses and is HELD with nothing created, and
|
|
118
|
+
* `controller.hold` (CardRequestHold) answers it. `controller.release({ amount, currency, pageTotal })` creates the
|
|
119
|
+
* authorization only then, at the total the person confirmed, and only when the page shows it; a live client
|
|
120
|
+
* answers the page with the device's replay, a sandbox client never sends the request on. `controller.hold.cancel()`
|
|
121
|
+
* fails it. A request held past its processor's own wait is cancelled first. One request is held at a time; every
|
|
122
|
+
* other card request, and every one after a decision, fails at once. Besides the registry's processors, a hold
|
|
123
|
+
* catches Bolt's sealed tokenizer and, with `placeholderNumbers`, any request carrying the placeholder card (both
|
|
124
|
+
* cancel-only, except a sandbox placeholder post released with `continuePlaceholder`). Raw CDP only; not with
|
|
125
|
+
* `pauseOnly`. Service workers are bypassed in this mode unless `serviceWorkers: 'refuse'`.
|
|
126
|
+
*/
|
|
127
|
+
hold?: boolean | HoldOptions;
|
|
128
|
+
/**
|
|
129
|
+
* A site service worker in the merchant's browser context. `refuse` (default): the attachment refuses the context,
|
|
130
|
+
* since a worker can send a card request outside the frames' interception. `bypass` (default in hold mode): every
|
|
131
|
+
* armed frame sends its requests past service workers (Network.setBypassServiceWorker), so the worker is accepted.
|
|
132
|
+
*/
|
|
133
|
+
serviceWorkers?: 'refuse' | 'bypass';
|
|
134
|
+
/**
|
|
135
|
+
* A shared worker in the merchant's browser context (chewy.com runs three from blob: URLs). Its own requests pause
|
|
136
|
+
* nowhere: Fetch reaches no shared worker. `refuse` (default): the attachment refuses the context. `accept`
|
|
137
|
+
* (default in hold mode): the worker is accepted unwatched and a `shared_worker_unwatched` event names it; nothing
|
|
138
|
+
* it sends can be approved, and the browser only ever holds the placeholder card. Kernel answers an attach to such
|
|
139
|
+
* a worker with "Not allowed" and closes the connection, so `accept` never attaches. `observe`: the worker is
|
|
140
|
+
* attached and its Network reports read; a card request it sends ends the checkout and any hold, with nothing
|
|
141
|
+
* approved (a browser that allows the attach only).
|
|
142
|
+
*/
|
|
143
|
+
sharedWorkers?: 'refuse' | 'accept' | 'observe';
|
|
115
144
|
/** Explicit native hosted Stripe Checkout TEST integration; requires an Autopilot grant. */
|
|
116
145
|
stripeCheckout?: StripeCheckoutOptions;
|
|
117
146
|
/** Fallback when the browser cannot expose its top-level origin. Never an amount/payee authority. */
|
package/dist/cdp.js
CHANGED
|
@@ -8,15 +8,17 @@ 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
16
|
import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
|
|
17
17
|
import { paddleTokenexTokenContinuation } from './paddle-tokenex.js';
|
|
18
|
+
import { twoC2pContinuation, twoC2pRetire } from './twoc2p.js';
|
|
18
19
|
import { isArmedEkashuUrl } from './ekashu.js';
|
|
19
20
|
import { isArmedRedsysUrl } from './redsys.js';
|
|
21
|
+
import { isArmedQuickpayUrl } from './quickpay.js';
|
|
20
22
|
import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
|
|
21
23
|
import { EBANX_RESEND_PATTERNS, EbanxFollowUps, isEbanxResendUrl } from './ebanx.js';
|
|
22
24
|
import { PAYPAL_CARD_FORM_PATTERNS, isPaypalCardFormUrl } from './paypal-card-forms.js';
|
|
@@ -30,6 +32,7 @@ import { MerchantHandoffWatch } from './merchant-handoff.js';
|
|
|
30
32
|
import { MERCHANT_PAGE_WAIT_MS, MerchantTotalWatch } from './merchant-total-watch.js';
|
|
31
33
|
import { hostedFormSubmittedPage } from './hosted-form.js';
|
|
32
34
|
import { armSendWindow, failsAsNetworkError, preparedSendWindowMs, sendWindowProcessor } from './amadeus.js';
|
|
35
|
+
import { HoldAnsweredError, HoldEndedError, HoldGate, HoldRefusedError, matchHoldOnlyRule } from './hold.js';
|
|
33
36
|
import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
34
37
|
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, loadedCardMicroformFromUrls, refusedRequestFailure, } from './cybersource-flex.js';
|
|
35
38
|
import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointRouted, } from './card-endpoint.js';
|
|
@@ -99,13 +102,22 @@ function isExtensionWorker(type, url) {
|
|
|
99
102
|
* A different, explicitly identified context belongs to another checkout.
|
|
100
103
|
* An extension's own worker owns no merchant request and does not count.
|
|
101
104
|
*/
|
|
102
|
-
function assertContextTarget(target, merchantContext) {
|
|
105
|
+
function assertContextTarget(target, merchantContext, allow = {}) {
|
|
103
106
|
if (!targetRecord(target))
|
|
104
107
|
throw new CheckoutAttachmentError('unavailable');
|
|
105
108
|
if (contextId(target.browserContextId) && target.browserContextId !== merchantContext)
|
|
106
109
|
return;
|
|
107
110
|
if (isExtensionWorker(target.type, target.url))
|
|
108
111
|
return;
|
|
112
|
+
const knownContext = target.browserContextId === undefined || contextId(target.browserContextId);
|
|
113
|
+
// Every armed frame sends its requests past the site's service workers (Network.setBypassServiceWorker), so a
|
|
114
|
+
// worker in the merchant's context can no longer send a card request out of the interception's reach.
|
|
115
|
+
if (allow.serviceWorkers && target.type === 'service_worker' && knownContext)
|
|
116
|
+
return;
|
|
117
|
+
// A shared worker's own requests pause nowhere (Fetch does not reach them), so it is watched instead: its Network
|
|
118
|
+
// reports are read, and a card request seen there ends the checkout before anything is approved (sharedWorkers).
|
|
119
|
+
if (allow.sharedWorkers && target.type === 'shared_worker' && knownContext)
|
|
120
|
+
return;
|
|
109
121
|
if (!['page', 'iframe', 'worker', 'browser_ui'].includes(target.type)
|
|
110
122
|
|| (target.browserContextId !== undefined && !contextId(target.browserContextId))) {
|
|
111
123
|
throw new CheckoutAttachmentError('unavailable');
|
|
@@ -203,6 +215,9 @@ function isApprovalOutcome(err) {
|
|
|
203
215
|
function isTerminal(err) {
|
|
204
216
|
if (err instanceof CheckoutApiError)
|
|
205
217
|
return err.permanent;
|
|
218
|
+
// A hold's decision is final for the attachment: every later card request fails at once.
|
|
219
|
+
if (err instanceof HoldEndedError || err instanceof HoldAnsweredError)
|
|
220
|
+
return true;
|
|
206
221
|
// Refused at create for Adyen's test platform: the page's next request is refused the same way.
|
|
207
222
|
if (err instanceof AdyenTestPlatformRefusedError)
|
|
208
223
|
return err.stage === 'create';
|
|
@@ -350,6 +365,16 @@ function cseContinuation(opts, url, body, replay, approved, preparation) {
|
|
|
350
365
|
const paddleTokenex = replay.kind === undefined ? paddleTokenexTokenContinuation(url, body, replay.substitutions, replay.paddleTokenexFacts, approved) : null;
|
|
351
366
|
if (paddleTokenex)
|
|
352
367
|
return { body: paddleTokenex.body, shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
368
|
+
// 2C2P writes one card field under its pinned key, which the generic Adyen swap refuses as no JWE, so it is
|
|
369
|
+
// dispatched by the Payment URL to substituteTwoC2p (twoc2p.ts), only into the one Payment the approval was made for.
|
|
370
|
+
// A refusal there sent the field nowhere, so the approval is retired as merchant_never_retried, as a merchant-hosted
|
|
371
|
+
// one is (cseContinuationBody), and the API stops serving the field; a retirement the API does not confirm is
|
|
372
|
+
// reported as approval_retire_unconfirmed (twoc2p.ts twoC2pRetire).
|
|
373
|
+
const twoc2p = replay.kind === undefined
|
|
374
|
+
? twoC2pContinuation(url, body, replay.substitutions, approved, twoC2pRetire(opts.vault, replay.authorizationId, opts.onEvent))
|
|
375
|
+
: null;
|
|
376
|
+
if (twoc2p)
|
|
377
|
+
return { body: twoc2p.body, shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
353
378
|
return { body: cseContinuationBody(opts, body, replay, url, preparation), shijiCvv: null, shijiCvvKeySequenceSha256: null };
|
|
354
379
|
}
|
|
355
380
|
/**
|
|
@@ -1388,7 +1413,7 @@ function merchantAttempt(lifecycle, survives) {
|
|
|
1388
1413
|
// known non-payment, so the prepared checkout ends failed with that reason, and a fresh attachment can
|
|
1389
1414
|
// prepare again. Every other stop leaves the outcome unknown.
|
|
1390
1415
|
const error = () => reason === 'amadeus_send_window_closed' || reason === 'secure_acceptance_send_window_closed'
|
|
1391
|
-
|| reason === 'wix_payments_send_window_closed'
|
|
1416
|
+
|| reason === 'wix_payments_send_window_closed' || reason === 'hipay_send_window_closed'
|
|
1392
1417
|
? new CheckoutPreparationError(lifecycle.getState().preparationId ?? null, reason)
|
|
1393
1418
|
: new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, reason);
|
|
1394
1419
|
const attempt = {
|
|
@@ -1549,6 +1574,35 @@ function replayOf(result, timeoutMs) {
|
|
|
1549
1574
|
return { mode: 'token', authorizationId, status: response.status, headers: { ...response.headers }, body: response.body, amountVerified: null,
|
|
1550
1575
|
...(result.processor_context !== undefined ? { processorContext: result.processor_context } : {}), ...execution };
|
|
1551
1576
|
}
|
|
1577
|
+
/** A CDP Fetch url pattern (`*` any run of characters, `?` one, `\\` escapes) against a URL. */
|
|
1578
|
+
function urlPatternMatches(pattern, url) {
|
|
1579
|
+
let re = '';
|
|
1580
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1581
|
+
const c = pattern[i];
|
|
1582
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
1583
|
+
re += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
re += c === '*' ? '.*' : c === '?' ? '.' : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
return new RegExp(`^${re}$`).test(url);
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Wait for the hold's decision on a request; the request going away (its attempt stopped) ends the hold as lost,
|
|
1597
|
+
* which rejects the decision.
|
|
1598
|
+
*/
|
|
1599
|
+
function holdUntilDecided(decision, signal, lost) {
|
|
1600
|
+
if (signal.aborted)
|
|
1601
|
+
lost();
|
|
1602
|
+
else
|
|
1603
|
+
signal.addEventListener('abort', lost, { once: true });
|
|
1604
|
+
return decision.finally(() => signal.removeEventListener('abort', lost));
|
|
1605
|
+
}
|
|
1552
1606
|
/**
|
|
1553
1607
|
* Worldpay eProtect's registration, resumed by the business. The read carries the registration as the API
|
|
1554
1608
|
* stored it, bare JSON, and the paused request is the vendor's JSONP <script>, which runs its answer and reads
|
|
@@ -1688,6 +1742,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1688
1742
|
opts = safeOptions(opts);
|
|
1689
1743
|
if (opts.pauseOnly && typeof opts.onPaused !== 'function')
|
|
1690
1744
|
throw new Error('pauseOnly needs onPaused: the business receives the paused request there and answers it with controller.resume.');
|
|
1745
|
+
if (opts.pauseOnly && opts.hold)
|
|
1746
|
+
throw new Error('hold and pauseOnly are two different pauses: attach with one of them.');
|
|
1691
1747
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
1692
1748
|
const setupStop = new AbortController();
|
|
1693
1749
|
let attachmentReady = false;
|
|
@@ -1749,6 +1805,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1749
1805
|
// The page target's id, which is its main frame's id; read once the attachment is set up.
|
|
1750
1806
|
let pageTargetId;
|
|
1751
1807
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
1808
|
+
// Hold mode: the first card request is held with nothing created until controller.release or cancel (hold.ts).
|
|
1809
|
+
const holdGate = opts.hold ? new HoldGate(typeof opts.hold === 'object' ? opts.hold : {}, opts.vault?.sandbox === true, (type, detail) => opts.onEvent?.({ type, detail })) : null;
|
|
1810
|
+
if (holdGate) {
|
|
1811
|
+
lifecycle.setHold(holdGate);
|
|
1812
|
+
lifecycle.abort.signal.addEventListener('abort', () => { void holdGate.cancel('checkout_cancelled'); }, { once: true });
|
|
1813
|
+
}
|
|
1814
|
+
// 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.
|
|
1815
|
+
let holdOnlyNetworkId = null;
|
|
1816
|
+
const bypassServiceWorkers = (opts.serviceWorkers ?? (holdGate ? 'bypass' : 'refuse')) === 'bypass';
|
|
1817
|
+
const sharedWorkerMode = opts.sharedWorkers ?? (holdGate ? 'accept' : 'refuse');
|
|
1818
|
+
const observeSharedWorkers = sharedWorkerMode === 'observe';
|
|
1819
|
+
const targetAllowance = { serviceWorkers: bypassServiceWorkers, sharedWorkers: sharedWorkerMode !== 'refuse' };
|
|
1820
|
+
// Shared workers being watched: their target ids (claimed before the attach answers) and their sessions.
|
|
1821
|
+
const watchedWorkerTargets = new Set();
|
|
1822
|
+
const watchedWorkerSessions = new Set();
|
|
1823
|
+
// The watches still starting, by target id: a later report of the worker waits for its watch, and so does the attachment.
|
|
1824
|
+
const workerWatches = new Map();
|
|
1752
1825
|
const stripeCheckout = new StripeCheckoutGate(opts);
|
|
1753
1826
|
const mercadoCheckout = new MercadoCheckoutGate();
|
|
1754
1827
|
// CyberSource Flex card field iframes and their capture contexts, recorded from the CDP
|
|
@@ -2093,6 +2166,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2093
2166
|
...(recognizesGmo(opts.vault) ? GMO_TOKEN_V1_PATTERNS : []), ...juspayCheckout.urlPatterns(), ...cardEndpointPatterns(),
|
|
2094
2167
|
...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS, ...PAYPAL_CARD_FORM_PATTERNS,
|
|
2095
2168
|
...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS])];
|
|
2169
|
+
// The hold-only rules' patterns (Bolt's tokenizer; every request a placeholder card can ride, by resource type).
|
|
2170
|
+
const fetchPatterns = [...urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2171
|
+
...(holdGate ? holdGate.patterns().map((p) => ({ ...p, requestStage: 'Request' })) : [])];
|
|
2096
2172
|
const arm = async (sessionId, resume = false, worker = false) => {
|
|
2097
2173
|
const key = sessionId ?? '__root__';
|
|
2098
2174
|
if (armed.has(key))
|
|
@@ -2109,6 +2185,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2109
2185
|
};
|
|
2110
2186
|
await cdp.send('Network.enable', {}, sessionId);
|
|
2111
2187
|
assertActive();
|
|
2188
|
+
// Requests skip the site's service workers, so every one stays in this frame's Fetch interception. Required:
|
|
2189
|
+
// a frame that cannot bypass them is not armed.
|
|
2190
|
+
if (bypassServiceWorkers && !worker) {
|
|
2191
|
+
await cdp.send('Network.setBypassServiceWorker', { bypass: true }, sessionId);
|
|
2192
|
+
assertActive();
|
|
2193
|
+
}
|
|
2112
2194
|
// Worker Fetch does not exist in Chrome. Its requests pause on the owning
|
|
2113
2195
|
// frame, but its abort/finish events arrive on the worker Network session.
|
|
2114
2196
|
// Keep that session attached and paused until Network is listening, then
|
|
@@ -2116,9 +2198,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2116
2198
|
if (!worker) {
|
|
2117
2199
|
await cdp.send('Page.enable', {}, sessionId).catch(() => { });
|
|
2118
2200
|
assertActive();
|
|
2119
|
-
await cdp.send('Fetch.enable', {
|
|
2120
|
-
patterns: urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2121
|
-
}, sessionId);
|
|
2201
|
+
await cdp.send('Fetch.enable', { patterns: fetchPatterns }, sessionId);
|
|
2122
2202
|
assertActive();
|
|
2123
2203
|
}
|
|
2124
2204
|
// Descend into this target's own children (iframes inside iframes).
|
|
@@ -2147,6 +2227,56 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2147
2227
|
arming.delete(key);
|
|
2148
2228
|
}
|
|
2149
2229
|
};
|
|
2230
|
+
/**
|
|
2231
|
+
* Watch a shared worker of the merchant's context (sharedWorkers 'observe'): attach to it from the browser root,
|
|
2232
|
+
* read its Network reports, and release it. Its requests cannot be paused, so a card request seen there ends the
|
|
2233
|
+
* checkout (see the Network.requestWillBeSent handler). A worker that cannot be watched ends it too.
|
|
2234
|
+
*/
|
|
2235
|
+
const watchSharedWorker = async (target) => {
|
|
2236
|
+
if (sharedWorkerMode === 'refuse' || typeof target?.targetId !== 'string')
|
|
2237
|
+
return;
|
|
2238
|
+
// Chrome reports every existing target (Target.targetCreated) before it answers setDiscoverTargets, so that
|
|
2239
|
+
// report starts the watch: the attachment's own pass over the targets waits for it rather than passing it by.
|
|
2240
|
+
const starting = workerWatches.get(target.targetId);
|
|
2241
|
+
if (starting)
|
|
2242
|
+
return starting;
|
|
2243
|
+
if (watchedWorkerTargets.has(target.targetId))
|
|
2244
|
+
return;
|
|
2245
|
+
if (contextId(target.browserContextId) && target.browserContextId !== merchantContext)
|
|
2246
|
+
return;
|
|
2247
|
+
watchedWorkerTargets.add(target.targetId);
|
|
2248
|
+
if (!observeSharedWorkers) {
|
|
2249
|
+
opts.onEvent?.({ type: 'shared_worker_unwatched', detail: { scheme: String(target.url ?? '').split(':')[0] || null } });
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const watch = (async () => {
|
|
2253
|
+
const { sessionId: workerSession } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
|
|
2254
|
+
if (typeof workerSession !== 'string' || !workerSession)
|
|
2255
|
+
throw new CheckoutAttachmentError('unavailable');
|
|
2256
|
+
watchedWorkerSessions.add(workerSession);
|
|
2257
|
+
rejectedSessions.add(workerSession);
|
|
2258
|
+
await cdp.send('Network.enable', {}, workerSession);
|
|
2259
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, workerSession).catch(() => { });
|
|
2260
|
+
opts.onEvent?.({ type: 'shared_worker_watched', detail: { scheme: String(target.url ?? '').split(':')[0] || null } });
|
|
2261
|
+
})().catch((error) => failInterception(error instanceof CheckoutAttachmentError ? error : new CheckoutAttachmentError('unavailable')))
|
|
2262
|
+
.finally(() => workerWatches.delete(target.targetId));
|
|
2263
|
+
workerWatches.set(target.targetId, watch);
|
|
2264
|
+
return watch;
|
|
2265
|
+
};
|
|
2266
|
+
/** A card request a watched shared worker sent: it could not be held, so nothing may be approved from here on. */
|
|
2267
|
+
const sharedWorkerCardRequest = (request) => {
|
|
2268
|
+
const url = typeof request?.url === 'string' ? request.url : '';
|
|
2269
|
+
const method = typeof request?.method === 'string' ? request.method : 'GET';
|
|
2270
|
+
if (!url)
|
|
2271
|
+
return false;
|
|
2272
|
+
if (typeof opts.vault?.isCardRequest === 'function' && opts.vault.isCardRequest(url, method))
|
|
2273
|
+
return true;
|
|
2274
|
+
if (!holdGate)
|
|
2275
|
+
return false;
|
|
2276
|
+
const body = typeof request.postData === 'string' ? request.postData : request.hasPostData ? null : '';
|
|
2277
|
+
// A body too large to ride the report is read as a card request only by its address (Bolt's tokenizer).
|
|
2278
|
+
return !!matchHoldOnlyRule({ url, method, body: body ?? '' }, holdGate.rules) || (body === null && !!matchHoldOnlyRule({ url, method, body: '' }, holdGate.rules.filter((r) => !r.body)));
|
|
2279
|
+
};
|
|
2150
2280
|
const failInterception = (error) => {
|
|
2151
2281
|
if (setupFailureReported)
|
|
2152
2282
|
return;
|
|
@@ -2175,7 +2305,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2175
2305
|
}
|
|
2176
2306
|
if ((method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') && targetContextKnown) {
|
|
2177
2307
|
try {
|
|
2178
|
-
assertContextTarget(params?.targetInfo, merchantContext);
|
|
2308
|
+
assertContextTarget(params?.targetInfo, merchantContext, targetAllowance);
|
|
2309
|
+
if (params?.targetInfo?.type === 'shared_worker')
|
|
2310
|
+
void watchSharedWorker(params.targetInfo);
|
|
2179
2311
|
}
|
|
2180
2312
|
catch (error) {
|
|
2181
2313
|
failInterception(error);
|
|
@@ -2203,6 +2335,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2203
2335
|
|| (method === 'Target.detachedFromTarget' && params.sessionId === pageSessionId)) {
|
|
2204
2336
|
detachedSessions.add(pageSessionId);
|
|
2205
2337
|
setupStop.abort(new CheckoutAttachmentError('closed'));
|
|
2338
|
+
holdGate?.close();
|
|
2206
2339
|
preparationGate.invalidate('merchant_document_closed');
|
|
2207
2340
|
stripeCheckout.invalidate();
|
|
2208
2341
|
mercadoCheckout.invalidate();
|
|
@@ -2218,6 +2351,19 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2218
2351
|
if (handoffNetwork)
|
|
2219
2352
|
merchantHandoff.failed();
|
|
2220
2353
|
}
|
|
2354
|
+
// A watched shared worker sent a card request: it went out unpaused, so nothing is approved from here on.
|
|
2355
|
+
if (method === 'Network.requestWillBeSent' && sessionId && watchedWorkerSessions.has(sessionId) && sharedWorkerCardRequest(params?.request)) {
|
|
2356
|
+
let endpoint = '';
|
|
2357
|
+
try {
|
|
2358
|
+
const u = new URL(params.request.url);
|
|
2359
|
+
endpoint = `${u.origin}${u.pathname}`;
|
|
2360
|
+
}
|
|
2361
|
+
catch { /* unreadable */ }
|
|
2362
|
+
opts.onEvent?.({ type: 'card_request_unwatched', detail: { endpoint, from: 'shared_worker' } });
|
|
2363
|
+
void holdGate?.cancel('card_request_unwatched');
|
|
2364
|
+
failInterception(new CheckoutAttachmentError('unavailable'));
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2221
2367
|
// The transport is browser-scoped. A second tab's request or child target
|
|
2222
2368
|
// must never be authorized with this page's merchant, amount or preparation.
|
|
2223
2369
|
if (!sessionId || !ownedSessions.has(sessionId))
|
|
@@ -2325,6 +2471,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2325
2471
|
return;
|
|
2326
2472
|
}
|
|
2327
2473
|
if (method === 'Network.loadingFailed') {
|
|
2474
|
+
// The held request given up by the page (its own timeout) or failed by the hold itself: nothing was sent.
|
|
2475
|
+
if (holdGate && holdGate.state === 'held' && ((holdOnlyNetworkId && params.requestId === holdOnlyNetworkId)
|
|
2476
|
+
|| (activeRequest && activeRequest.networkId === params.requestId && activeRequest.sourceSessionId === sessionId)))
|
|
2477
|
+
holdGate.lost();
|
|
2328
2478
|
merchantTotal.log.failed(requestKey(sessionId, params.requestId));
|
|
2329
2479
|
const recordKey = cardEndpointRecord.keyFor(requestKey(sessionId, params.requestId), params.requestId);
|
|
2330
2480
|
if (recordKey)
|
|
@@ -2414,6 +2564,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2414
2564
|
}
|
|
2415
2565
|
if (method === 'Target.attachedToTarget') {
|
|
2416
2566
|
const child = params.sessionId;
|
|
2567
|
+
if (params?.targetInfo?.type === 'shared_worker' && watchedWorkerTargets.has(params.targetInfo.targetId)) {
|
|
2568
|
+
if (typeof child === 'string' && child) {
|
|
2569
|
+
watchedWorkerSessions.add(child);
|
|
2570
|
+
rejectedSessions.add(child);
|
|
2571
|
+
}
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2417
2574
|
if (rejectedSessions.has(child))
|
|
2418
2575
|
return;
|
|
2419
2576
|
// A browser-root transport can report another context through an owned
|
|
@@ -2437,6 +2594,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2437
2594
|
}
|
|
2438
2595
|
if (setupStop.signal.aborted)
|
|
2439
2596
|
return;
|
|
2597
|
+
// A service worker the page's frames bypass: released from its debugger wait and left alone, never owned.
|
|
2598
|
+
if (bypassServiceWorkers && params.targetInfo?.type === 'service_worker' && typeof child === 'string' && child
|
|
2599
|
+
&& child !== pageSessionId && !ownedSessions.has(child)) {
|
|
2600
|
+
rejectedSessions.add(child);
|
|
2601
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2440
2604
|
// CDP may omit the context ID for ordinary children; keep those attached.
|
|
2441
2605
|
try {
|
|
2442
2606
|
if (typeof child !== 'string' || !child || !['page', 'iframe', 'worker'].includes(params.targetInfo?.type)) {
|
|
@@ -2480,6 +2644,43 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2480
2644
|
// A paused document request has not committed either: it only starts the context's check.
|
|
2481
2645
|
if (resourceType === 'Document')
|
|
2482
2646
|
microforms.warm(withFragment(request));
|
|
2647
|
+
// Hold mode: a card request no recogniser pays (Bolt's sealed tokenizer, a body carrying the placeholder card) is
|
|
2648
|
+
// held too, and can only be cancelled (or, in sandbox, a placeholder post sent on unchanged). Never recorded.
|
|
2649
|
+
if (holdGate) {
|
|
2650
|
+
// A request any of the SDK's own rules arms (a recogniser, a reviewed card endpoint, a guard, Stripe Checkout,
|
|
2651
|
+
// Mercado, a merchant profile...) goes down the SDK's own path; only what none of them covers is hold-only.
|
|
2652
|
+
const recognised = (typeof opts.vault?.isCardRequest === 'function' && opts.vault.isCardRequest(request.url, request.method))
|
|
2653
|
+
|| urlPatterns.some((pattern) => urlPatternMatches(pattern, request.url));
|
|
2654
|
+
const holdRule = recognised ? null : matchHoldOnlyRule({ url: request.url, method: request.method, body: pausedBody(request) }, holdGate.rules);
|
|
2655
|
+
if (holdRule) {
|
|
2656
|
+
const failHeld = () => cdp.send('Fetch.failRequest', { requestId, errorReason: 'BlockedByClient' }, sessionId).catch(() => { });
|
|
2657
|
+
if (!attachmentReady || setupStop.signal.aborted || holdGate.refuse(holdRule.psp, request.url)) {
|
|
2658
|
+
await failHeld();
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
holdOnlyNetworkId = typeof networkId === 'string' && networkId ? networkId : null;
|
|
2662
|
+
let decision;
|
|
2663
|
+
try {
|
|
2664
|
+
decision = await holdGate.hold({ psp: holdRule.psp, url: request.url, kind: holdRule.kind, replayable: false, rule: holdRule });
|
|
2665
|
+
}
|
|
2666
|
+
catch {
|
|
2667
|
+
holdOnlyNetworkId = null;
|
|
2668
|
+
await failHeld();
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
finally { /* the decision is in */ }
|
|
2672
|
+
holdOnlyNetworkId = null;
|
|
2673
|
+
if (decision.kind === 'continue') {
|
|
2674
|
+
// Sandbox only (HoldGate.release): the post carries the documented placeholder test number and nothing else.
|
|
2675
|
+
const ok = await cdp.send('Fetch.continueRequest', { requestId }, sessionId).then(() => true, () => false);
|
|
2676
|
+
decision.settle(ok ? { outcome: 'continued' } : { outcome: 'failed', detail: 'continue_failed' });
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
await failHeld();
|
|
2680
|
+
decision.settle({ outcome: 'failed', detail: 'not_replayable' });
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2483
2684
|
// One pre-claim gate registry decides and performs the mechanics through
|
|
2484
2685
|
// these effects; the state accessors read the same closure variables the
|
|
2485
2686
|
// inline gates did. It resolves synchronously for an ordinary card request,
|
|
@@ -2670,6 +2871,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2670
2871
|
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
2671
2872
|
return;
|
|
2672
2873
|
}
|
|
2874
|
+
// Hold mode: one request held at a time, and none after a decision.
|
|
2875
|
+
if (holdGate && holdGate.refuse(recognizerNameOf(opts, request.url, request.method), request.url)) {
|
|
2876
|
+
if (preparation)
|
|
2877
|
+
preparationGate.retireUnboundClaim();
|
|
2878
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2673
2881
|
// Reserve BEFORE the first await (the authorize call below yields), so two
|
|
2674
2882
|
// requests can never both clear the check above and raise two prompts for
|
|
2675
2883
|
// one checkout.
|
|
@@ -2686,6 +2894,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2686
2894
|
let ebanxRemembered = false;
|
|
2687
2895
|
// The business's pause, so resume resolves once the page has its answer.
|
|
2688
2896
|
let businessPause = null;
|
|
2897
|
+
// Hold mode: the person's release, settled with what came of it.
|
|
2898
|
+
let holdRelease = null;
|
|
2689
2899
|
// A prepared Amadeus SDK add must be sent within its window (see amadeus.ts).
|
|
2690
2900
|
let disarmSendWindow = null;
|
|
2691
2901
|
try {
|
|
@@ -2776,15 +2986,45 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2776
2986
|
// The merchant's own record of the order, as of the pause, for a profile that reads one.
|
|
2777
2987
|
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url, pausedAt, attempt.signal) : null;
|
|
2778
2988
|
attempt.assertLive();
|
|
2989
|
+
// Hold mode: nothing is created until the person's total releases the request (hold.ts).
|
|
2990
|
+
if (holdGate) {
|
|
2991
|
+
if (holdGate.state !== 'watching') {
|
|
2992
|
+
// Another card request was held while this one was being read: this one is refused alone.
|
|
2993
|
+
holdGate.refuse(recognizerNameOf(opts, request.url, request.method), request.url);
|
|
2994
|
+
throw new HoldRefusedError();
|
|
2995
|
+
}
|
|
2996
|
+
const decision = await holdUntilDecided(holdGate.hold({ psp: recognizerNameOf(opts, request.url, request.method), url: request.url,
|
|
2997
|
+
kind: 'card_request', replayable: true }), attempt.signal, () => holdGate.lost());
|
|
2998
|
+
if (decision.kind !== 'release') {
|
|
2999
|
+
decision.settle({ outcome: 'failed', detail: 'not_releasable' });
|
|
3000
|
+
throw new HoldEndedError('not_releasable');
|
|
3001
|
+
}
|
|
3002
|
+
holdRelease = decision;
|
|
3003
|
+
attempt.assertLive();
|
|
3004
|
+
}
|
|
2779
3005
|
// Decision 176: pause only. The business creates the authorization from the paused request and answers
|
|
2780
3006
|
// through controller.resume; the replay it hands back goes down the same delivery path as a create's.
|
|
2781
3007
|
const pause = opts.pauseOnly ? pauseForBusiness(opts, lifecycle, attempt.signal, { url: request.url, method: request.method, headers: request.headers ?? {}, body }, pageOrigin) : null;
|
|
2782
3008
|
businessPause = pause;
|
|
2783
|
-
|
|
3009
|
+
// A sandbox hold never sends the request on: the approval is created at the released total and read, the
|
|
3010
|
+
// page's request fails the moment it exists, and the attempt ends with nothing charged.
|
|
3011
|
+
const holdSandbox = !!holdRelease && opts.vault.sandbox === true;
|
|
3012
|
+
const holdMerchantStop = holdSandbox ? new AbortController() : null;
|
|
3013
|
+
// The approval outlives the page's request, but not the checkout: cancelling the attachment while the approval is
|
|
3014
|
+
// open retires it at Agentcard (the client cancels an authorization whose merchant signal aborted).
|
|
3015
|
+
if (holdMerchantStop) {
|
|
3016
|
+
const retire = () => holdMerchantStop.abort();
|
|
3017
|
+
if (lifecycle.abort.signal.aborted)
|
|
3018
|
+
retire();
|
|
3019
|
+
else
|
|
3020
|
+
lifecycle.abort.signal.addEventListener('abort', retire, { once: true });
|
|
3021
|
+
}
|
|
3022
|
+
let holdRequestFailed = false;
|
|
3023
|
+
const authorization = pause ? null : opts.vault.authorize({
|
|
2784
3024
|
user: opts.user,
|
|
2785
3025
|
merchant: opts.merchant,
|
|
2786
|
-
amount: opts.amount,
|
|
2787
|
-
currency: opts.currency,
|
|
3026
|
+
amount: holdRelease ? holdRelease.amount : opts.amount,
|
|
3027
|
+
currency: holdRelease ? holdRelease.currency : opts.currency,
|
|
2788
3028
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
2789
3029
|
executionMode: opts.executionMode,
|
|
2790
3030
|
grantId: opts.grantId,
|
|
@@ -2796,11 +3036,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2796
3036
|
preparation,
|
|
2797
3037
|
timeoutMs: opts.timeoutMs,
|
|
2798
3038
|
signal: lifecycle.abort.signal,
|
|
2799
|
-
|
|
3039
|
+
// A sandbox hold's approval outlives the page's request on purpose: that request is failed once it exists.
|
|
3040
|
+
merchantSignal: holdMerchantStop?.signal ?? attempt.signal,
|
|
2800
3041
|
// What the merchant's own checkout said its total was, as this browser recorded it
|
|
2801
3042
|
// (read only for a reviewed profile priced by it): the page is the card request's.
|
|
2802
3043
|
merchantTotal: merchantTotal.capture(pausedAt, cardPage),
|
|
2803
|
-
onAuthorizationCreated: (id) =>
|
|
3044
|
+
onAuthorizationCreated: (id) => {
|
|
3045
|
+
lifecycle.approvalCreated(id);
|
|
3046
|
+
if (holdSandbox && !holdRequestFailed) {
|
|
3047
|
+
holdRequestFailed = true;
|
|
3048
|
+
// The page's request is let go of before it is failed, so its failure report does not end this attempt:
|
|
3049
|
+
// the approval it raised is read to its end, and nothing is ever delivered to the page.
|
|
3050
|
+
if (activeRequest?.attempt === attempt)
|
|
3051
|
+
activeRequest = null;
|
|
3052
|
+
opts.onEvent?.({ type: 'card_request_not_sent', detail: { authorizationId: id } });
|
|
3053
|
+
void cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
3054
|
+
}
|
|
3055
|
+
},
|
|
2804
3056
|
onApprovalUrl: (url) => { if (!preparation && !attempt.signal.aborted) {
|
|
2805
3057
|
lifecycle.approvalUrl(url);
|
|
2806
3058
|
return opts.onApprovalUrl?.(url);
|
|
@@ -2812,6 +3064,15 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2812
3064
|
...(flexPause ? flexPause.sidecar : {}),
|
|
2813
3065
|
...(payrailsSidecar ? { payrails: payrailsSidecar } : {}) },
|
|
2814
3066
|
});
|
|
3067
|
+
if (holdSandbox && authorization) {
|
|
3068
|
+
const outcome = await authorization.then((r) => ({ outcome: 'approved_not_charged', authorizationId: r.authorizationId }), (err) => ({ outcome: err instanceof ApprovalDeclinedError ? 'declined' : err instanceof ApprovalTimeoutError ? 'expired' : 'failed',
|
|
3069
|
+
authorizationId: lifecycle.getState().authorizationId, detail: failureSummary(err) }));
|
|
3070
|
+
if (!holdRequestFailed)
|
|
3071
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
3072
|
+
holdRelease.settle(outcome);
|
|
3073
|
+
throw new HoldAnsweredError(outcome);
|
|
3074
|
+
}
|
|
3075
|
+
const replay = pause ? eprotectResumedReplay(await pause.replay, request) : await authorization;
|
|
2815
3076
|
attempt.assertLive();
|
|
2816
3077
|
approvedId = replay.authorizationId;
|
|
2817
3078
|
if (lifecycle.isCancelled())
|
|
@@ -2992,8 +3253,18 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2992
3253
|
if (handedOver)
|
|
2993
3254
|
lifecycle.handedOff(replay);
|
|
2994
3255
|
businessPause?.delivered.resolve();
|
|
3256
|
+
holdRelease?.settle({ outcome: 'authorized', authorizationId: replay.authorizationId });
|
|
2995
3257
|
}
|
|
2996
3258
|
catch (err) {
|
|
3259
|
+
if (err instanceof HoldRefusedError) {
|
|
3260
|
+
// Refused alone: the request another rule holds, and the checkout's state, are left as they are.
|
|
3261
|
+
if (activeRequest?.attempt === attempt)
|
|
3262
|
+
activeRequest = null;
|
|
3263
|
+
await refusePaused(attempt.target?.requestId ?? requestId, attempt.target?.sessionId ?? sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, false, lifecycle));
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
holdRelease?.settle(err instanceof HoldAnsweredError ? err.outcome : { outcome: err instanceof ApprovalDeclinedError ? 'declined' : err instanceof ApprovalTimeoutError ? 'expired' : 'failed',
|
|
3267
|
+
authorizationId: approvedId ?? lifecycle.getState().authorizationId, detail: failureSummary(err) });
|
|
2997
3268
|
// Ignore the failure event caused by our own safe decline/error abort.
|
|
2998
3269
|
const dead = attempt.target ?? { requestId, sessionId };
|
|
2999
3270
|
// An EBANX token answer this handoff kept follow-ups for and did not finish handing over: nothing after it is
|
|
@@ -3074,24 +3345,29 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3074
3345
|
if (!Array.isArray(targetInfos))
|
|
3075
3346
|
throw new CheckoutAttachmentError('unavailable');
|
|
3076
3347
|
for (const target of targetInfos)
|
|
3077
|
-
assertContextTarget(target, merchantContext);
|
|
3348
|
+
assertContextTarget(target, merchantContext, targetAllowance);
|
|
3349
|
+
for (const target of targetInfos)
|
|
3350
|
+
if (target?.type === 'shared_worker')
|
|
3351
|
+
await watchSharedWorker(target);
|
|
3078
3352
|
await arm(pageSessionId);
|
|
3079
3353
|
// Existing OOPIFs report attachment before the parent's acknowledgement,
|
|
3080
3354
|
// 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
|
-
|
|
3355
|
+
// Pay while one of those already-running frames is still unprotected, or
|
|
3356
|
+
// while a shared worker reported during setup is not yet watched.
|
|
3357
|
+
while (arming.size || workerWatches.size)
|
|
3358
|
+
await Promise.all([...arming.values(), ...workerWatches.values()]);
|
|
3084
3359
|
assertActive();
|
|
3085
3360
|
}, setupTimeoutMs, setupStop.signal);
|
|
3086
3361
|
if (setupStop.signal.aborted)
|
|
3087
3362
|
throw attachmentFailure(setupStop.signal.reason);
|
|
3088
3363
|
attachmentReady = true;
|
|
3089
|
-
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: urlPatterns } });
|
|
3364
|
+
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: urlPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
|
|
3090
3365
|
}
|
|
3091
3366
|
catch (error) {
|
|
3092
3367
|
const failure = attachmentFailure(error);
|
|
3093
3368
|
setupStop.abort(failure);
|
|
3094
3369
|
terminal = failure;
|
|
3370
|
+
holdGate?.close();
|
|
3095
3371
|
if (!setupFailureReported) {
|
|
3096
3372
|
setupFailureReported = true;
|
|
3097
3373
|
lifecycle.cancel();
|
|
@@ -3116,6 +3392,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3116
3392
|
opts = safeOptions(opts);
|
|
3117
3393
|
if (opts.pauseOnly)
|
|
3118
3394
|
throw new Error('pauseOnly is a raw CDP mode (attachToCdp); attachToPlaywright creates the authorization itself.');
|
|
3395
|
+
if (opts.hold)
|
|
3396
|
+
throw new Error('hold is a raw CDP mode (attachToCdp): it needs the frames\' own interception.');
|
|
3119
3397
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
3120
3398
|
const setupStop = new AbortController();
|
|
3121
3399
|
let attachmentReady = false;
|
|
@@ -3471,6 +3749,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3471
3749
|
// Every request to a Redsys host once its entry is served (redsys.ts), as attachToCdp arms the
|
|
3472
3750
|
// hosts: the card page's post and Redsys's other card requests reach the Redsys gate and fail.
|
|
3473
3751
|
|| isArmedRedsysUrl(opts.vault, url.toString())
|
|
3752
|
+
// Every request to QuickPay's host once its entry is served (quickpay.ts), as attachToCdp arms the host:
|
|
3753
|
+
// the window's card post, its other card requests and its card lookup reach the QuickPay gate and fail.
|
|
3754
|
+
|| isArmedQuickpayUrl(opts.vault, url.toString())
|
|
3474
3755
|
// PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
|
|
3475
3756
|
|| isPaypalHostedFieldsConfirm(url.toString())
|
|
3476
3757
|
// PayPal's Card Fields confirm and GraphQL on www.paypal.com, for every org, as attachToCdp arms them (paypal-card-forms.ts).
|
|
@@ -3480,7 +3761,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3480
3761
|
// Paddle's checkout services, routed only for an org the API served Paddle VGS to (see attachToCdp).
|
|
3481
3762
|
|| (isPaddleCheckoutService(url.toString()) && recognizesPaddleVgs(opts.vault))
|
|
3482
3763
|
// EBANX's 3-D Secure calls and hosted confirm, for every org, as attachToCdp arms them (ebanx.ts).
|
|
3483
|
-
|| isEbanxResendUrl(url.toString())
|
|
3764
|
+
|| isEbanxResendUrl(url.toString())
|
|
3765
|
+
// HiPay's two vault hosts, routed only for an org the API served HiPay to, as attachToCdp arms both hosts from the
|
|
3766
|
+
// recognizer: the frame's /update reaches the HiPay gate and fails (pre-claim.ts routesHipayVault).
|
|
3767
|
+
|| routesHipayVault(opts.vault, url.toString()), async (route) => {
|
|
3484
3768
|
const request = route.request();
|
|
3485
3769
|
// When this request paused, and the page it came from: the merchant's total is read as of now.
|
|
3486
3770
|
const pausedAt = Date.now();
|