@agent-cards/sdk 0.23.27 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/PREFLIGHT.md +7 -1
- package/README.md +137 -99
- package/dist/aci.d.ts +52 -22
- package/dist/aci.js +79 -15
- package/dist/adyen-merchant-hosted.generated.d.ts +1 -1
- package/dist/adyen-merchant-hosted.generated.js +459 -21
- package/dist/agent-amount.generated.js +1 -1
- package/dist/agentcard.d.ts +4 -5
- package/dist/agentcard.js +4 -5
- package/dist/amadeus.d.ts +9 -3
- package/dist/amadeus.js +33 -4
- package/dist/api-module.d.ts +1 -7
- package/dist/api-module.js +3 -5
- package/dist/{voice-retry.js → api-retry.js} +1 -1
- package/dist/api-types.d.ts +53 -0
- package/dist/{voice-types.js → api-types.js} +7 -2
- package/dist/approvals.d.ts +166 -0
- package/dist/approvals.js +213 -0
- package/dist/buy.d.ts +20 -101
- package/dist/buy.js +19 -74
- package/dist/capabilities.generated.js +1 -1
- package/dist/card-fields.generated.js +1398 -106
- package/dist/cdp.d.ts +3 -13
- package/dist/cdp.js +231 -175
- package/dist/client.d.ts +104 -22
- package/dist/client.js +215 -40
- package/dist/creditguard.generated.d.ts +4 -4
- package/dist/datatrans.generated.js +22 -1
- package/dist/elavon-converge.generated.js +5 -3
- package/dist/hold.d.ts +8 -0
- package/dist/hold.js +43 -2
- package/dist/hosted-entry.js +2 -0
- package/dist/index.d.ts +9 -15
- package/dist/index.js +6 -10
- package/dist/juspay-gate.js +2 -2
- package/dist/juspay.generated.d.ts +2 -2
- package/dist/juspay.generated.js +174 -4
- package/dist/lifecycle.d.ts +9 -61
- package/dist/lifecycle.js +24 -10
- package/dist/merchant-card-endpoints.generated.d.ts +6 -1
- package/dist/merchant-card-endpoints.generated.js +344 -19
- package/dist/merchant-hosted.d.ts +8 -1
- package/dist/nike-order.d.ts +51 -0
- package/dist/nike-order.js +81 -0
- package/dist/oceanpayment.d.ts +38 -0
- package/dist/oceanpayment.js +70 -0
- package/dist/paddle-tokenex.generated.js +105 -10
- package/dist/payplug.d.ts +41 -0
- package/dist/payplug.js +71 -0
- package/dist/payrails.generated.js +27 -7
- package/dist/payrexx.generated.js +176 -2
- package/dist/pre-claim.d.ts +9 -0
- package/dist/pre-claim.js +86 -7
- package/dist/preflight-capabilities.generated.d.ts +0 -4
- package/dist/preflight-capabilities.generated.js +0 -5
- package/dist/preflight-catalog.json +46 -5
- package/dist/preflight-playwright.d.ts +2 -2
- package/dist/preflight-playwright.js +63 -14
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.d.ts +0 -4
- package/dist/preflight.generated.js +24 -1
- package/dist/prepared-processor.d.ts +18 -5
- package/dist/prepared-processor.js +48 -8
- package/dist/primer.generated.js +1418 -120
- package/dist/processout.generated.d.ts +61 -0
- package/dist/processout.generated.js +577 -0
- package/dist/requests.js +1 -1
- package/dist/shiji-sts.generated.js +105 -10
- package/dist/stripe-checkout.generated.js +1405 -234
- package/dist/substitutions.generated.js +460 -26
- package/dist/tap-payments.generated.js +105 -10
- package/dist/twoc2p.generated.js +105 -10
- package/dist/url-masks/bluesnap.d.ts +2 -0
- package/dist/url-masks/bluesnap.js +15 -0
- package/dist/url-masks/oceanpayment.d.ts +2 -0
- package/dist/url-masks/oceanpayment.js +13 -0
- package/dist/url-masks/payplug.d.ts +2 -0
- package/dist/url-masks/payplug.js +13 -0
- package/dist/url-masks/vtex_payments.d.ts +2 -0
- package/dist/url-masks/vtex_payments.js +12 -0
- package/dist/url-masks.generated.d.ts +1 -1
- package/dist/url-masks.generated.js +6 -2
- package/dist/verifone.generated.js +42 -2
- package/dist/vtex-payments.generated.d.ts +127 -0
- package/dist/vtex-payments.generated.js +544 -0
- package/dist/webhooks.d.ts +2 -2
- package/dist/webhooks.js +2 -2
- package/dist/wspay.generated.d.ts +1 -1
- package/dist/wspay.generated.js +175 -3
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +6 -8
- package/dist/languages.d.ts +0 -23
- package/dist/languages.js +0 -76
- package/dist/voice-pkce.d.ts +0 -49
- package/dist/voice-pkce.js +0 -147
- package/dist/voice-stream.generated.d.ts +0 -118
- package/dist/voice-stream.generated.js +0 -170
- package/dist/voice-types.d.ts +0 -735
- package/dist/voice.d.ts +0 -373
- package/dist/voice.js +0 -815
- package/readme.test.mjs +0 -53
- /package/dist/{voice-retry.d.ts → api-retry.d.ts} +0 -0
package/dist/cdp.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
|
|
2
|
-
import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError,
|
|
2
|
+
import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, 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';
|
|
6
6
|
import { MercadoCheckoutGate } from './mercado-checkout.js';
|
|
7
7
|
import { MERCADO_CHECKOUT_PATTERN } from './mercado-checkout.generated.js';
|
|
8
8
|
import { JuspayCheckoutGate } from './juspay-gate.js';
|
|
9
|
-
import {
|
|
10
|
-
import { EprotectGate
|
|
11
|
-
import { SbpsGate
|
|
12
|
-
import { dispatchPreClaim, eventUrl, isArmedCreditguardCall, routesHipayVault } from './pre-claim.js';
|
|
9
|
+
import { AciGate, playwrightReplacesNavigationBody } from './aci.js';
|
|
10
|
+
import { EprotectGate } from './eprotect.js';
|
|
11
|
+
import { SbpsGate } from './sbps.js';
|
|
12
|
+
import { dispatchPreClaim, eventUrl, isArmedCreditguardCall, nikeOrderGate, routesHipayVault } from './pre-claim.js';
|
|
13
13
|
import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
14
14
|
import { isPaddleVgsCardsUrl } from './paddle-vgs.generated.js';
|
|
15
15
|
import { isToastPaymentMethodsUrl, readToastBody } from './toast.generated.js';
|
|
@@ -26,12 +26,16 @@ import { isArmedTransbankUrl, transbankMerchantReturn, transbankMerchantReturnAn
|
|
|
26
26
|
import { isArmedRedsysUrl } from './redsys.js';
|
|
27
27
|
import { isArmedPrzelewy24Url } from './przelewy24.js';
|
|
28
28
|
import { isArmedBarionUrl } from './barion.js';
|
|
29
|
+
import { isArmedPayplugUrl } from './payplug.js';
|
|
29
30
|
import { isArmedQuickpayUrl } from './quickpay.js';
|
|
30
31
|
import { COMGATE_CARD_PATTERNS, isArmedComgateUrl, servesComgate } from './comgate.js';
|
|
31
32
|
import { isArmedEximbayUrl } from './eximbay.js';
|
|
33
|
+
import { isArmedOceanpaymentUrl } from './oceanpayment.js';
|
|
32
34
|
import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
|
|
33
35
|
import { EBANX_RESEND_PATTERNS, EbanxFollowUps, isEbanxResendUrl } from './ebanx.js';
|
|
34
36
|
import { PayrexxFollowUps } from './payrexx.js';
|
|
37
|
+
import { NIKE_ORDER_PATTERNS, NikeOrders, nikeOrderRouted } from './nike-order.js';
|
|
38
|
+
import { nikeOrderRequest as sharedNikeOrderRequest } from './merchant-card-endpoints.generated.js';
|
|
35
39
|
import { PAYPAL_CARD_FORM_PATTERNS, isPaypalCardFormUrl } from './paypal-card-forms.js';
|
|
36
40
|
import { CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, SecureAcceptanceFollowUpWatch, isCybersourceSecureAcceptanceUrl, secureAcceptanceFollowUp, secureAcceptanceFrameAnswer, secureAcceptanceReplyLast4, } from './cybersource-secure-acceptance.js';
|
|
37
41
|
import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
|
|
@@ -94,6 +98,18 @@ const HOSTED_FORM_REPEAT_REASON = 'already submitted on the cardholder\'s device
|
|
|
94
98
|
function targetRecord(value) {
|
|
95
99
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
96
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Per CDP connection, the attachment holding Nike's order at the browser target for each browser context: every one sees
|
|
103
|
+
* each pause there, and only the holder of the context that sent a request answers it (attachToCdp nikeOrderAtRoot). Each
|
|
104
|
+
* time an attachment asks through Nike's profile in a context, it takes the context over from whichever one held it.
|
|
105
|
+
*/
|
|
106
|
+
const nikeRootHolders = new WeakMap();
|
|
107
|
+
/**
|
|
108
|
+
* Per Playwright browser context, the attachment whose route judges Nike's order there: the last one whose route was on
|
|
109
|
+
* when it asked through Nike's profile (attachToPlaywright holdNikeOrdersInContext). Every other attachment's route
|
|
110
|
+
* hands the order to the next route.
|
|
111
|
+
*/
|
|
112
|
+
const nikeContextHolders = new WeakMap();
|
|
97
113
|
function contextId(value) {
|
|
98
114
|
return typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\u0000-\u0020\u007f]/.test(value);
|
|
99
115
|
}
|
|
@@ -130,6 +146,11 @@ function assertContextTarget(target, merchantContext, allow = {}) {
|
|
|
130
146
|
// reports are read, and a card request seen there ends the checkout before anything is approved (sharedWorkers).
|
|
131
147
|
if (allow.sharedWorkers && target.type === 'shared_worker' && knownContext)
|
|
132
148
|
return;
|
|
149
|
+
// A worklet (CDP type 'worklet': an audio, paint, animation or layout worklet) has no fetch, XHR, WebSocket or
|
|
150
|
+
// beacon in its global scope, so it can never send a card request; the module it loads is fetched by its owning
|
|
151
|
+
// frame, through that frame's interception. Shopify's checkout runs one in its web-pixel sandbox frames.
|
|
152
|
+
if (target.type === 'worklet' && knownContext)
|
|
153
|
+
return;
|
|
133
154
|
if (!['page', 'iframe', 'worker', 'browser_ui'].includes(target.type)
|
|
134
155
|
|| (target.browserContextId !== undefined && !contextId(target.browserContextId))) {
|
|
135
156
|
throw new CheckoutAttachmentError('unavailable');
|
|
@@ -1447,6 +1468,8 @@ function merchantAttempt(lifecycle, survives) {
|
|
|
1447
1468
|
// prepare again. Every other stop leaves the outcome unknown.
|
|
1448
1469
|
const error = () => reason === 'amadeus_send_window_closed' || reason === 'secure_acceptance_send_window_closed'
|
|
1449
1470
|
|| reason === 'wix_payments_send_window_closed' || reason === 'hipay_send_window_closed' || reason === 'openpay_send_window_closed'
|
|
1471
|
+
|| reason === 'vtex_payments_send_window_closed'
|
|
1472
|
+
|| reason === 'processout_send_window_closed'
|
|
1450
1473
|
? new CheckoutPreparationError(lifecycle.getState().preparationId ?? null, reason)
|
|
1451
1474
|
: new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, reason);
|
|
1452
1475
|
const attempt = {
|
|
@@ -1560,53 +1583,6 @@ async function retireUnusedApproval(opts, lifecycle, authorizationId) {
|
|
|
1560
1583
|
opts.onEvent?.({ type: 'failed', detail: 'PaymentOutcomeUnknownError' });
|
|
1561
1584
|
}
|
|
1562
1585
|
}
|
|
1563
|
-
/**
|
|
1564
|
-
* The business's read, as a replay this transport can deliver. An approved `token` read carries
|
|
1565
|
-
* the processor's answer; `cse` the ciphertext; `hosted_form` the device's stamp. A terminal read is thrown as
|
|
1566
|
-
* the error the create path would have thrown for the same outcome, so the request is failed with nothing
|
|
1567
|
-
* charged and the page's next request is quieted as after any decline. A read that is not final, or an
|
|
1568
|
-
* approval without a usable answer, is refused here and leaves the request paused.
|
|
1569
|
-
*/
|
|
1570
|
-
function replayOf(result, timeoutMs) {
|
|
1571
|
-
const status = result.status;
|
|
1572
|
-
if (status === 'awaiting_approval')
|
|
1573
|
-
throw new Error('The purchase still awaits the person: resume takes a final read (voice.awaitResult), not an open one.');
|
|
1574
|
-
if (status === 'declined')
|
|
1575
|
-
throw new ApprovalDeclinedError(result.terminal_reason ?? 'declined');
|
|
1576
|
-
if (status === 'cancelled')
|
|
1577
|
-
throw new ApprovalDeclinedError(result.terminal_reason ?? 'cancelled');
|
|
1578
|
-
if (status === 'expired' || status === 'timeout')
|
|
1579
|
-
throw new ApprovalTimeoutError(timeoutMs);
|
|
1580
|
-
if (status !== 'approved' && status !== 'completed')
|
|
1581
|
-
throw new PaymentOutcomeUnknownError(result.id ?? null, 'authorization_status_unrecognized');
|
|
1582
|
-
const authorizationId = result.id;
|
|
1583
|
-
if (typeof authorizationId !== 'string' || !/^cauth_[A-Za-z0-9_-]{1,128}$/.test(authorizationId))
|
|
1584
|
-
throw new Error('resume needs the authorization\'s id on the read (voice.awaitResult carries it).');
|
|
1585
|
-
const execution = result.execution_mode === 'autopilot' || result.execution_mode === 'user_approval' ? { executionMode: result.execution_mode } : {};
|
|
1586
|
-
const mode = result.mode ?? (result.response ? 'token' : result.substitutions ? 'cse' : result.submitted_at ? 'hosted_form' : 'token');
|
|
1587
|
-
if (mode === 'hosted_form') {
|
|
1588
|
-
if (typeof result.submitted_at !== 'string' || !result.submitted_at)
|
|
1589
|
-
throw new Error('A hosted-form read resumes with submitted_at, the device\'s stamp.');
|
|
1590
|
-
return { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', authorizationId, submittedAt: result.submitted_at, ...execution };
|
|
1591
|
-
}
|
|
1592
|
-
if (mode === 'cse') {
|
|
1593
|
-
const sub = result.substitutions;
|
|
1594
|
-
if (!sub || sub.encoding !== 'json' || typeof sub.at !== 'string' || !sub.fields || typeof sub.fields !== 'object')
|
|
1595
|
-
throw new Error('A cse read resumes with substitutions, the ciphertext the device produced.');
|
|
1596
|
-
return { mode: 'cse', authorizationId, substitutions: { encoding: 'json', at: sub.at, fields: { ...sub.fields }, ...(sub.remove ? { remove: [...sub.remove] } : {}) }, ...execution };
|
|
1597
|
-
}
|
|
1598
|
-
if (mode !== 'token')
|
|
1599
|
-
throw new PaymentOutcomeUnknownError(authorizationId, 'approved_mode_unsupported');
|
|
1600
|
-
const response = result.response;
|
|
1601
|
-
if (!response || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || typeof response.body !== 'string'
|
|
1602
|
-
|| !response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
|
|
1603
|
-
throw new Error('An approved token read resumes with response, the processor\'s answer (voice.awaitResult carries it once the purchase finished).');
|
|
1604
|
-
}
|
|
1605
|
-
// The read's processor_context rides as it came: a Paddle VGS answer is handed to the page only with the
|
|
1606
|
-
// approved card's details its /pay names (paddle-vgs-pay.ts checks them before the hand-over).
|
|
1607
|
-
return { mode: 'token', authorizationId, status: response.status, headers: { ...response.headers }, body: response.body, amountVerified: null,
|
|
1608
|
-
...(result.processor_context !== undefined ? { processorContext: result.processor_context } : {}), ...execution };
|
|
1609
|
-
}
|
|
1610
1586
|
/** A CDP Fetch url pattern (`*` any run of characters, `?` one, `\\` escapes) against a URL. */
|
|
1611
1587
|
function urlPatternMatches(pattern, url) {
|
|
1612
1588
|
let re = '';
|
|
@@ -1636,39 +1612,6 @@ function holdUntilDecided(decision, signal, lost) {
|
|
|
1636
1612
|
signal.addEventListener('abort', lost, { once: true });
|
|
1637
1613
|
return decision.finally(() => signal.removeEventListener('abort', lost));
|
|
1638
1614
|
}
|
|
1639
|
-
/**
|
|
1640
|
-
* Worldpay eProtect's registration, resumed by the business. The read carries the registration as the API
|
|
1641
|
-
* stored it, bare JSON, and the paused request is the vendor's JSONP <script>, which runs its answer and reads
|
|
1642
|
-
* the registration only through the callback the paused URL named. So it gets the answer authorize() gives on
|
|
1643
|
-
* the create path (client.ts): the registration inside that callback, under Worldpay's script type, and only a
|
|
1644
|
-
* registration payment-core's judge accepts for that URL. A read in any other mode never answers it either. The
|
|
1645
|
-
* paused URL alone decides, whatever recognizer claimed it. Any other request's replay passes as it came, and a
|
|
1646
|
-
* create's replay never comes here, so no answer is wrapped twice.
|
|
1647
|
-
*/
|
|
1648
|
-
function eprotectResumedReplay(replay, request) {
|
|
1649
|
-
if (classifyEprotectRequest(request.url, request.method)?.kind !== 'registration')
|
|
1650
|
-
return replay;
|
|
1651
|
-
const answer = replay.mode === 'token' ? eprotectScriptAnswer(request.url, replay) : null;
|
|
1652
|
-
if (replay.mode !== 'token' || !answer)
|
|
1653
|
-
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'eprotect_registration_unconfirmed');
|
|
1654
|
-
return { ...replay, status: answer.status, headers: answer.headers, body: answer.body };
|
|
1655
|
-
}
|
|
1656
|
-
/**
|
|
1657
|
-
* SB Payment Service's token request, resumed by the business, on the same terms as eProtect's above. The read
|
|
1658
|
-
* carries the token as the API stored it, which the paused <script> cannot run, so it gets the answer authorize()
|
|
1659
|
-
* gives on the create path (client.ts): the token inside the callback the paused URL named, under a script type,
|
|
1660
|
-
* and only a token payment-core's judge accepts for that URL. A read in any other mode never answers it either.
|
|
1661
|
-
* The paused URL alone decides, whatever recognizer claimed it. Any other request's replay passes as it came, and
|
|
1662
|
-
* a create's replay never comes here, so no answer is wrapped twice.
|
|
1663
|
-
*/
|
|
1664
|
-
function sbpsResumedReplay(replay, request) {
|
|
1665
|
-
if (classifySbpsRequest(request.url, request.method)?.kind !== 'token_request')
|
|
1666
|
-
return replay;
|
|
1667
|
-
const answer = replay.mode === 'token' ? sbpsScriptAnswer(request.url, replay) : null;
|
|
1668
|
-
if (replay.mode !== 'token' || !answer)
|
|
1669
|
-
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'sbps_token_unconfirmed');
|
|
1670
|
-
return { ...replay, status: answer.status, headers: answer.headers, body: answer.body };
|
|
1671
|
-
}
|
|
1672
1615
|
/** The recogniser that claimed a paused request, by processor name, so a caller never re-matches the URL; null on a duck-typed vault without the read. */
|
|
1673
1616
|
function recognizerNameOf(opts, url, method) {
|
|
1674
1617
|
const read = opts.vault.recognizerOf;
|
|
@@ -1681,61 +1624,6 @@ function recognizerNameOf(opts, url, method) {
|
|
|
1681
1624
|
return null;
|
|
1682
1625
|
}
|
|
1683
1626
|
}
|
|
1684
|
-
function pauseForBusiness(opts, lifecycle, attemptSignal, request, pageOrigin) {
|
|
1685
|
-
const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
|
|
1686
|
-
let deliveredResolve = () => { }, deliveredReject = () => { };
|
|
1687
|
-
const delivered = new Promise((resolve, reject) => { deliveredResolve = resolve; deliveredReject = reject; });
|
|
1688
|
-
delivered.catch(() => { });
|
|
1689
|
-
const replay = new Promise((resolve, reject) => {
|
|
1690
|
-
let settled = false;
|
|
1691
|
-
const signals = [lifecycle.abort.signal, attemptSignal];
|
|
1692
|
-
let timer;
|
|
1693
|
-
const finish = (fn) => {
|
|
1694
|
-
if (settled)
|
|
1695
|
-
return;
|
|
1696
|
-
settled = true;
|
|
1697
|
-
clearTimeout(timer);
|
|
1698
|
-
for (const signal of signals)
|
|
1699
|
-
signal.removeEventListener('abort', onAbort);
|
|
1700
|
-
lifecycle.setResumeHandler(null);
|
|
1701
|
-
fn();
|
|
1702
|
-
};
|
|
1703
|
-
const onAbort = () => finish(() => reject(attemptSignal.aborted ? (attemptSignal.reason ?? new PaymentOutcomeUnknownError(null, 'merchant_request_aborted')) : new CheckoutCancelledError()));
|
|
1704
|
-
timer = setTimeout(() => finish(() => reject(new ApprovalTimeoutError(timeoutMs))), timeoutMs);
|
|
1705
|
-
timer.unref?.();
|
|
1706
|
-
for (const signal of signals)
|
|
1707
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
1708
|
-
if (signals.some((signal) => signal.aborted)) {
|
|
1709
|
-
onAbort();
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
lifecycle.setResumeHandler((result) => {
|
|
1713
|
-
if (typeof result.id === 'string' && result.id)
|
|
1714
|
-
lifecycle.approvalCreated(result.id);
|
|
1715
|
-
let outcome;
|
|
1716
|
-
try {
|
|
1717
|
-
outcome = replayOf(result, timeoutMs);
|
|
1718
|
-
}
|
|
1719
|
-
catch (err) {
|
|
1720
|
-
// A terminal outcome answers the request (failed, nothing charged); a read that is not final leaves it paused.
|
|
1721
|
-
if (err instanceof ApprovalDeclinedError || err instanceof ApprovalTimeoutError || err instanceof PaymentOutcomeUnknownError) {
|
|
1722
|
-
finish(() => reject(err));
|
|
1723
|
-
return delivered;
|
|
1724
|
-
}
|
|
1725
|
-
return Promise.reject(err);
|
|
1726
|
-
}
|
|
1727
|
-
finish(() => resolve(outcome));
|
|
1728
|
-
return delivered;
|
|
1729
|
-
});
|
|
1730
|
-
try {
|
|
1731
|
-
Promise.resolve(opts.onPaused({ request, recognizer: recognizerNameOf(opts, request.url, request.method), ...(pageOrigin ? { page_origin: pageOrigin } : {}), controller: lifecycle })).catch((err) => finish(() => reject(err)));
|
|
1732
|
-
}
|
|
1733
|
-
catch (err) {
|
|
1734
|
-
finish(() => reject(err));
|
|
1735
|
-
}
|
|
1736
|
-
});
|
|
1737
|
-
return { replay, delivered: { resolve: deliveredResolve, reject: deliveredReject } };
|
|
1738
|
-
}
|
|
1739
1627
|
/**
|
|
1740
1628
|
* Arm the send window of a request paused under a preparation, when its processor sets one: a
|
|
1741
1629
|
* prepared Amadeus SDK add, a prepared Secure Acceptance token create and a prepared Wix tokenize
|
|
@@ -1789,10 +1677,6 @@ function watchSecureAcceptanceFollowUp(watch, preparation, replay) {
|
|
|
1789
1677
|
*/
|
|
1790
1678
|
export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
1791
1679
|
opts = safeOptions(opts);
|
|
1792
|
-
if (opts.pauseOnly && typeof opts.onPaused !== 'function')
|
|
1793
|
-
throw new Error('pauseOnly needs onPaused: the business receives the paused request there and answers it with controller.resume.');
|
|
1794
|
-
if (opts.pauseOnly && opts.hold)
|
|
1795
|
-
throw new Error('hold and pauseOnly are two different pauses: attach with one of them.');
|
|
1796
1680
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
1797
1681
|
const setupStop = new AbortController();
|
|
1798
1682
|
let attachmentReady = false;
|
|
@@ -1801,6 +1685,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1801
1685
|
const sessionTypes = new Map([[pageSessionId, 'page']]);
|
|
1802
1686
|
// Each window this attachment adopted (adoptOpenedWindow): its session, and its target's id, which is its main frame's.
|
|
1803
1687
|
const windowTargets = new Map();
|
|
1688
|
+
// The id of every target this attachment armed besides its page: each adopted window and each child it attached.
|
|
1689
|
+
const armedTargets = new Set();
|
|
1804
1690
|
const sessionParents = new Map();
|
|
1805
1691
|
// The frame in its parent session that a child target hangs from (TargetInfo.parentFrameId), so a
|
|
1806
1692
|
// frame lookup can climb from a cross-origin frame into the document that frames it.
|
|
@@ -1914,6 +1800,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1914
1800
|
const ebanxFollowUps = new EbanxFollowUps();
|
|
1915
1801
|
// The approved Payrexx card request on this attachment and the 3-D Secure sends claimed under it (payrexx.ts).
|
|
1916
1802
|
const payrexxFollowUps = new PayrexxFollowUps();
|
|
1803
|
+
// The approval Nike's order is held to once a card went through Nike's profile (nike-order.ts).
|
|
1804
|
+
const nikeOrders = new NikeOrders();
|
|
1917
1805
|
const aci = new AciGate(opts.vault);
|
|
1918
1806
|
const eprotect = new EprotectGate(opts.vault);
|
|
1919
1807
|
const sbps = new SbpsGate(opts.vault);
|
|
@@ -2246,8 +2134,15 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2246
2134
|
...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS, ...PAYPAL_CARD_FORM_PATTERNS,
|
|
2247
2135
|
...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS,
|
|
2248
2136
|
...(servesComgate(opts.vault) ? COMGATE_CARD_PATTERNS : [])])];
|
|
2137
|
+
// Nike's API host is armed in every frame this checkout arms, from the attach on: once a card went through Nike's
|
|
2138
|
+
// profile, Nike's order gate judges each order the page sends, a document loaded before the ask included, and anything
|
|
2139
|
+
// before that, or that is not the order, continues untouched (see nike-order.ts). Chrome applies the browser target's
|
|
2140
|
+
// interception (holdNikeOrdersAtRoot) only to documents loaded after it is armed, so it cannot hold these. The host
|
|
2141
|
+
// stays out of urlPatterns, which name what the SDK's own rules cover: in hold mode, a request to it that carries the
|
|
2142
|
+
// placeholder card is still held as hold-only.
|
|
2143
|
+
const armedPatterns = [...urlPatterns, ...NIKE_ORDER_PATTERNS];
|
|
2249
2144
|
// The hold-only rules' patterns (Bolt's tokenizer; every request a placeholder card can ride, by resource type).
|
|
2250
|
-
const fetchPatterns = [...
|
|
2145
|
+
const fetchPatterns = [...armedPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2251
2146
|
...(holdGate ? holdGate.patterns().map((p) => ({ ...p, requestStage: 'Request' })) : [])];
|
|
2252
2147
|
const arm = async (sessionId, resume = false, worker = false) => {
|
|
2253
2148
|
const key = sessionId ?? '__root__';
|
|
@@ -2416,6 +2311,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2416
2311
|
sessionTypes.set(child, 'page');
|
|
2417
2312
|
sessionParents.set(child, pageSessionId);
|
|
2418
2313
|
windowTargets.set(child, info.targetId);
|
|
2314
|
+
armedTargets.add(info.targetId);
|
|
2419
2315
|
await arm(child);
|
|
2420
2316
|
opts.onEvent?.({ type: 'window_armed', detail: {} });
|
|
2421
2317
|
}
|
|
@@ -2423,6 +2319,107 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2423
2319
|
failInterception(error);
|
|
2424
2320
|
}
|
|
2425
2321
|
};
|
|
2322
|
+
/**
|
|
2323
|
+
* Nike's order is held at the browser target too, from the moment this attachment asks the cardholder to pay through
|
|
2324
|
+
* Nike's profile (nike-order.ts), for the routes this checkout's own frames do not cover (armedPatterns): Chrome applies
|
|
2325
|
+
* Fetch there only to a document, worker or shared worker that loads after it is armed, and pauses a request to Nike's
|
|
2326
|
+
* API host from every one of them in the browser, a tab this checkout did not open included. One that cannot be armed
|
|
2327
|
+
* refuses that payment before anything is created.
|
|
2328
|
+
*/
|
|
2329
|
+
let nikeAtRoot = null;
|
|
2330
|
+
// This attachment among those holding Nike's order on the same connection (nikeRootHolders), by its browser context.
|
|
2331
|
+
const nikeHolder = {};
|
|
2332
|
+
const holdNikeOrdersAtRoot = () => {
|
|
2333
|
+
// Every ask takes the context over, one for another attempt after a later attachment asked there included; the
|
|
2334
|
+
// browser target is armed once, and the context's other pages open by then at each ask.
|
|
2335
|
+
const holders = nikeRootHolders.get(cdp) ?? new Map();
|
|
2336
|
+
nikeRootHolders.set(cdp, holders.set(merchantContext, nikeHolder));
|
|
2337
|
+
return (nikeAtRoot ??= cdp.send('Fetch.enable', { patterns: NIKE_ORDER_PATTERNS.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })) }))
|
|
2338
|
+
.then(holdNikeOrdersInPages);
|
|
2339
|
+
};
|
|
2340
|
+
/**
|
|
2341
|
+
* Every other page of this browser context, and every frame in one, open when this attachment asks through Nike's
|
|
2342
|
+
* profile: the browser target's pause never reaches a document already loaded, so each gets its own interception for
|
|
2343
|
+
* Nike's API host (nikeOrderInPage), as Playwright's context route covers every page of its context. A page or frame
|
|
2344
|
+
* that cannot be armed refuses the payment, as the browser target does.
|
|
2345
|
+
*/
|
|
2346
|
+
const nikePageTargets = new Set();
|
|
2347
|
+
const nikePageSessions = new Set();
|
|
2348
|
+
const holdNikeOrdersInPages = async () => {
|
|
2349
|
+
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
2350
|
+
if (!Array.isArray(targetInfos))
|
|
2351
|
+
throw new Error('targets_unreadable');
|
|
2352
|
+
for (const info of targetInfos) {
|
|
2353
|
+
if (!targetRecord(info) || (info.type !== 'page' && info.type !== 'iframe') || info.browserContextId !== merchantContext
|
|
2354
|
+
|| typeof info.targetId !== 'string' || info.targetId === pageTargetId || armedTargets.has(info.targetId)
|
|
2355
|
+
|| nikePageTargets.has(info.targetId))
|
|
2356
|
+
continue;
|
|
2357
|
+
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: info.targetId, flatten: true });
|
|
2358
|
+
if (typeof sessionId !== 'string' || !sessionId)
|
|
2359
|
+
throw new Error('target_unattached');
|
|
2360
|
+
nikePageSessions.add(sessionId);
|
|
2361
|
+
await cdp.send('Fetch.enable', { patterns: NIKE_ORDER_PATTERNS.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })) }, sessionId);
|
|
2362
|
+
// Recorded once its interception is on, so the next attempt after one that failed here arms it again.
|
|
2363
|
+
nikePageTargets.add(info.targetId);
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
// Each target's browser context, from discovery on: a page or frame of another context, the default one included,
|
|
2367
|
+
// belongs to another checkout. A report that names no context never replaces one that did.
|
|
2368
|
+
const targetContexts = new Map();
|
|
2369
|
+
const noteTargetContext = (info) => {
|
|
2370
|
+
if (typeof info?.targetId === 'string' && (contextId(info.browserContextId) || !targetContexts.has(info.targetId))) {
|
|
2371
|
+
targetContexts.set(info.targetId, info.browserContextId);
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
/**
|
|
2375
|
+
* A request paused at the browser target, answered by the attachment holding Nike's order in the browser context that
|
|
2376
|
+
* sent it: its Nike order gate judges it, and anything else goes on untouched. Every attachment on the connection sees
|
|
2377
|
+
* each pause, so the others leave it alone, an earlier attachment a later one took the context over from included. A
|
|
2378
|
+
* request from a context nobody holds goes on untouched, from the first holder alone; one from a source the browser
|
|
2379
|
+
* never reported as a target (a shared worker) is the holder's while one context is held, and while several are, nobody
|
|
2380
|
+
* can tell whose it is, so the first holder refuses it when it is Nike's order and lets anything else go on.
|
|
2381
|
+
*/
|
|
2382
|
+
const nikeOrderAtRoot = async (requestId, request, frameId) => {
|
|
2383
|
+
// Root pauses this attachment did not arm are another attachment's to answer.
|
|
2384
|
+
if (!nikeAtRoot)
|
|
2385
|
+
return;
|
|
2386
|
+
const holders = nikeRootHolders.get(cdp) ?? new Map();
|
|
2387
|
+
// The page's own frame is this checkout's, whatever a later report named.
|
|
2388
|
+
const placed = typeof frameId === 'string' && (frameId === pageTargetId || targetContexts.has(frameId));
|
|
2389
|
+
const owner = placed ? holders.get(frameId === pageTargetId ? merchantContext : targetContexts.get(frameId))
|
|
2390
|
+
: holders.size === 1 ? [...holders.values()][0] : null;
|
|
2391
|
+
const fail = () => cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }).catch(() => { });
|
|
2392
|
+
if (owner !== nikeHolder) {
|
|
2393
|
+
// Only an order whose owner nobody can tell is refused: anything else nobody holds goes on.
|
|
2394
|
+
if ((owner === undefined || owner === null) && [...holders.values()][0] === nikeHolder) {
|
|
2395
|
+
await (owner === null && sharedNikeOrderRequest(request.url, request.method) === true ? fail()
|
|
2396
|
+
: cdp.send('Fetch.continueRequest', { requestId }).catch(() => { }));
|
|
2397
|
+
}
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
await judgeNikeOrder(requestId, request);
|
|
2401
|
+
};
|
|
2402
|
+
/**
|
|
2403
|
+
* A request paused in another page or frame of this context, armed for Nike's order at the ask (holdNikeOrdersInPages):
|
|
2404
|
+
* the attachment holding the context judges it, and any other lets it go on, to the holder, which armed every page and
|
|
2405
|
+
* frame of the context it does not own the same way when it asked.
|
|
2406
|
+
*/
|
|
2407
|
+
const nikeOrderInPage = async (sessionId, requestId, request) => {
|
|
2408
|
+
if (nikeRootHolders.get(cdp)?.get(merchantContext) === nikeHolder)
|
|
2409
|
+
await judgeNikeOrder(requestId, request, sessionId);
|
|
2410
|
+
else
|
|
2411
|
+
await cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
|
|
2412
|
+
};
|
|
2413
|
+
// Nike's order gate on a request paused at the browser target (no session) or in a page armed at the ask.
|
|
2414
|
+
const judgeNikeOrder = async (requestId, request, sessionId) => {
|
|
2415
|
+
const fail = () => cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
2416
|
+
const gate = {
|
|
2417
|
+
request: { url: request.url, method: request.method, headers: request.headers ?? {}, get body() { return pausedBody(request); } },
|
|
2418
|
+
nikeOrders, lifecycle, onEvent: opts.onEvent, lifecycleAborted: () => lifecycle.abort.signal.aborted, terminal: () => !!terminal,
|
|
2419
|
+
effects: { continueRequest: () => cdp.send('Fetch.continueRequest', { requestId }, sessionId), answer: fail, abort: fail },
|
|
2420
|
+
};
|
|
2421
|
+
await (nikeOrderGate(gate) ?? cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { }));
|
|
2422
|
+
};
|
|
2426
2423
|
cdp.on(async (method, params, sessionId) => {
|
|
2427
2424
|
// A frame that committed a new document drops the companion binding made for the getTokenId its
|
|
2428
2425
|
// old document sent, with the device's CVV ciphertext: the document that replaced it never
|
|
@@ -2435,6 +2432,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2435
2432
|
if ((method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') && targetContextKnown) {
|
|
2436
2433
|
// The page's own target reports each document it commits from discovery on, Page events or not.
|
|
2437
2434
|
notePageTarget(params?.targetInfo);
|
|
2435
|
+
noteTargetContext(params?.targetInfo);
|
|
2438
2436
|
try {
|
|
2439
2437
|
assertContextTarget(params?.targetInfo, merchantContext, targetAllowance);
|
|
2440
2438
|
if (params?.targetInfo?.type === 'shared_worker')
|
|
@@ -2506,6 +2504,16 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2506
2504
|
failInterception(new CheckoutAttachmentError('unavailable'));
|
|
2507
2505
|
return;
|
|
2508
2506
|
}
|
|
2507
|
+
// A request paused at the browser target, which this attachment arms only for Nike's order (nikeOrderAtRoot).
|
|
2508
|
+
if (method === 'Fetch.requestPaused' && sessionId === undefined) {
|
|
2509
|
+
await nikeOrderAtRoot(params.requestId, params.request, params.frameId);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
// A request paused in another page or frame of this context that this attachment armed for Nike's order at the ask.
|
|
2513
|
+
if (method === 'Fetch.requestPaused' && sessionId && nikePageSessions.has(sessionId)) {
|
|
2514
|
+
await nikeOrderInPage(sessionId, params.requestId, params.request);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2509
2517
|
// The transport is browser-scoped. A second tab's request or child target
|
|
2510
2518
|
// must never be authorized with this page's merchant, amount or preparation.
|
|
2511
2519
|
if (!sessionId || !ownedSessions.has(sessionId))
|
|
@@ -2754,6 +2762,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2754
2762
|
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2755
2763
|
return;
|
|
2756
2764
|
}
|
|
2765
|
+
// A worklet sends no request of its own (assertContextTarget): released from its debugger wait, never owned.
|
|
2766
|
+
if (params.targetInfo?.type === 'worklet' && typeof child === 'string' && child
|
|
2767
|
+
&& child !== pageSessionId && !ownedSessions.has(child)) {
|
|
2768
|
+
rejectedSessions.add(child);
|
|
2769
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2757
2772
|
// CDP may omit the context ID for ordinary children; keep those attached.
|
|
2758
2773
|
try {
|
|
2759
2774
|
if (typeof child !== 'string' || !child || !['page', 'iframe', 'worker'].includes(params.targetInfo?.type)) {
|
|
@@ -2764,6 +2779,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2764
2779
|
throw new CheckoutAttachmentError('unavailable');
|
|
2765
2780
|
}
|
|
2766
2781
|
ownedSessions.add(child);
|
|
2782
|
+
if (typeof params.targetInfo.targetId === 'string')
|
|
2783
|
+
armedTargets.add(params.targetInfo.targetId);
|
|
2767
2784
|
sessionTypes.set(child, params.targetInfo.type);
|
|
2768
2785
|
sessionParents.set(child, sessionId);
|
|
2769
2786
|
if (typeof params.targetInfo.parentFrameId === 'string' && params.targetInfo.parentFrameId) {
|
|
@@ -2883,6 +2900,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2883
2900
|
tokenexIframe: tokenexIframeRepeats,
|
|
2884
2901
|
ebanx: ebanxFollowUps,
|
|
2885
2902
|
payrexx: payrexxFollowUps,
|
|
2903
|
+
nikeOrders,
|
|
2886
2904
|
frame: typeof frameId === 'string' && frameId ? frameId : undefined,
|
|
2887
2905
|
payrails,
|
|
2888
2906
|
shijiSts: { companions: shijiCompanions, page: () => cdpShijiPage(sessionId, frameId) },
|
|
@@ -3053,8 +3071,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3053
3071
|
// Whether this handoff kept an approval's follow-ups (ebanxFollowUps.remember, payrexxFollowUps.remember), which a
|
|
3054
3072
|
// failed handoff stops.
|
|
3055
3073
|
let followUpsRemembered = false;
|
|
3056
|
-
// The business's pause, so resume resolves once the page has its answer.
|
|
3057
|
-
let businessPause = null;
|
|
3058
3074
|
// Hold mode: the person's release, settled with what came of it.
|
|
3059
3075
|
let holdRelease = null;
|
|
3060
3076
|
// A prepared Amadeus SDK add must be sent within its window (see amadeus.ts).
|
|
@@ -3094,11 +3110,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3094
3110
|
activeRequest = { attempt, requestId, networkId, sessionId, frameId, request,
|
|
3095
3111
|
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined, flexPause?.shape, request.headers),
|
|
3096
3112
|
preparedFrame: preparation ? { sessionId, frameId } : undefined };
|
|
3097
|
-
// An ACI card
|
|
3098
|
-
// until this approval ends (see AciGate). One whose checkout already has a
|
|
3099
|
-
// request running or answered never begins; the pre-claim gate refuses it before this.
|
|
3113
|
+
// An ACI or JSON split card's card request: the page's CVC and expiry requests for its
|
|
3114
|
+
// checkout are held until this approval ends (see AciGate). One whose checkout already has a
|
|
3115
|
+
// card request running or answered never begins; the pre-claim gate refuses it before this.
|
|
3100
3116
|
if (!aci.begin(request.url, request.method, body, preparation?.merchantOrigin)) {
|
|
3101
|
-
throw new CheckoutPreparationError(preparation?.id ?? null,
|
|
3117
|
+
throw new CheckoutPreparationError(preparation?.id ?? null, aci.reuseCode(request.url));
|
|
3102
3118
|
}
|
|
3103
3119
|
// A Worldpay eProtect registration: the vendor's give-up report for it ends this attempt as
|
|
3104
3120
|
// the page abandoning its request (see EprotectGate.gaveUp).
|
|
@@ -3148,8 +3164,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3148
3164
|
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url, pausedAt, attempt.signal) : null;
|
|
3149
3165
|
attempt.assertLive();
|
|
3150
3166
|
// Paddle's own checkout answers as of the pause, once the texts of those that finished before it are read
|
|
3151
|
-
// (a Billing /pay only; sent only with a Paddle TokenEx create
|
|
3152
|
-
const paddleAnswers =
|
|
3167
|
+
// (a Billing /pay only; sent only with a Paddle TokenEx create).
|
|
3168
|
+
const paddleAnswers = await paddleCheckout.capture(pausedAt, request.url, attempt.signal);
|
|
3153
3169
|
attempt.assertLive();
|
|
3154
3170
|
// Hold mode: nothing is created until the person's total releases the request (hold.ts).
|
|
3155
3171
|
if (holdGate) {
|
|
@@ -3167,10 +3183,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3167
3183
|
holdRelease = decision;
|
|
3168
3184
|
attempt.assertLive();
|
|
3169
3185
|
}
|
|
3170
|
-
//
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3186
|
+
// From the create on, the device may store the card at the merchant: Nike's orders are held from here (nike-order.ts).
|
|
3187
|
+
if (cardEndpoint && nikeOrders.asking(cardEndpoint.id)) {
|
|
3188
|
+
await holdNikeOrdersAtRoot().catch(() => { throw new CardEndpointRefusedError(cardEndpoint.id, 'request_unsupported', "Nike's order cannot be held in this browser"); });
|
|
3189
|
+
attempt.assertLive();
|
|
3190
|
+
}
|
|
3174
3191
|
// A sandbox hold never sends the request on: the approval is created at the released total and read, the
|
|
3175
3192
|
// page's request fails the moment it exists, and the attempt ends with nothing charged.
|
|
3176
3193
|
const holdSandbox = !!holdRelease && opts.vault.sandbox === true;
|
|
@@ -3185,7 +3202,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3185
3202
|
lifecycle.abort.signal.addEventListener('abort', retire, { once: true });
|
|
3186
3203
|
}
|
|
3187
3204
|
let holdRequestFailed = false;
|
|
3188
|
-
const authorization =
|
|
3205
|
+
const authorization = opts.vault.authorize({
|
|
3189
3206
|
user: opts.user,
|
|
3190
3207
|
merchant: opts.merchant,
|
|
3191
3208
|
amount: holdRelease ? holdRelease.amount : opts.amount,
|
|
@@ -3198,6 +3215,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3198
3215
|
pageOrigin,
|
|
3199
3216
|
pageAmount,
|
|
3200
3217
|
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
3218
|
+
// When the request paused, which a VTEX card post's send-by counts from (the client sends it for VTEX alone).
|
|
3219
|
+
pausedAt,
|
|
3201
3220
|
preparation,
|
|
3202
3221
|
timeoutMs: opts.timeoutMs,
|
|
3203
3222
|
signal: lifecycle.abort.signal,
|
|
@@ -3231,7 +3250,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3231
3250
|
...(flexPause ? flexPause.sidecar : {}),
|
|
3232
3251
|
...(payrailsSidecar ? { payrails: payrailsSidecar } : {}) },
|
|
3233
3252
|
});
|
|
3234
|
-
if (holdSandbox
|
|
3253
|
+
if (holdSandbox) {
|
|
3235
3254
|
const outcome = await authorization.then((r) => ({ outcome: 'approved_not_charged', authorizationId: r.authorizationId }), (err) => ({ outcome: err instanceof ApprovalDeclinedError ? 'declined' : err instanceof ApprovalTimeoutError ? 'expired' : 'failed',
|
|
3236
3255
|
authorizationId: lifecycle.getState().authorizationId, detail: failureSummary(err) }));
|
|
3237
3256
|
if (!holdRequestFailed)
|
|
@@ -3239,7 +3258,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3239
3258
|
holdRelease.settle(outcome);
|
|
3240
3259
|
throw new HoldAnsweredError(outcome);
|
|
3241
3260
|
}
|
|
3242
|
-
const replay =
|
|
3261
|
+
const replay = await authorization;
|
|
3243
3262
|
attempt.assertLive();
|
|
3244
3263
|
approvedId = replay.authorizationId;
|
|
3245
3264
|
if (lifecycle.isCancelled())
|
|
@@ -3272,6 +3291,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3272
3291
|
// False only for a card endpoint answer the device withheld (see deliverCardEndpointAnswer).
|
|
3273
3292
|
let handedOver = true;
|
|
3274
3293
|
if (replay.mode === 'card_endpoint') {
|
|
3294
|
+
// The approved amount Nike's order after the card is held to (nike-order.ts).
|
|
3295
|
+
nikeOrders.remember(replay);
|
|
3275
3296
|
handedOver = await deliverCardEndpointAnswer(opts, lifecycle, cardEndpoint, replay, { url: deliverRequest.url, headers: deliverRequest.headers }, {
|
|
3276
3297
|
readJar: async () => (await cdp.send('Network.getCookies', { urls: [deliverRequest.url] }, deliverOn))?.cookies,
|
|
3277
3298
|
writeCookie: async (cookie) => {
|
|
@@ -3430,7 +3451,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3430
3451
|
}
|
|
3431
3452
|
if (handedOver)
|
|
3432
3453
|
lifecycle.handedOff(replay);
|
|
3433
|
-
businessPause?.delivered.resolve();
|
|
3434
3454
|
holdRelease?.settle({ outcome: 'authorized', authorizationId: replay.authorizationId });
|
|
3435
3455
|
}
|
|
3436
3456
|
catch (err) {
|
|
@@ -3451,13 +3471,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3451
3471
|
ebanxFollowUps.stop();
|
|
3452
3472
|
payrexxFollowUps.stop();
|
|
3453
3473
|
}
|
|
3454
|
-
// The business's resume settles once the request is answered: a terminal read is an answer (failed, nothing charged), a lost handoff is not.
|
|
3455
|
-
const settlePause = () => { if (businessPause) {
|
|
3456
|
-
if (isApprovalOutcome(err))
|
|
3457
|
-
businessPause.delivered.resolve();
|
|
3458
|
-
else
|
|
3459
|
-
businessPause.delivered.reject(err);
|
|
3460
|
-
} };
|
|
3461
3474
|
if (activeRequest?.attempt === attempt)
|
|
3462
3475
|
activeRequest = null;
|
|
3463
3476
|
// Vault clients may normalize a merchantSignal abort. Preserve the
|
|
@@ -3492,7 +3505,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3492
3505
|
// A Secure Acceptance frame post reads CyberSource's side too (secureAcceptanceFailedRefusal).
|
|
3493
3506
|
const sent = handoffStarted || approvedId !== null, failedFor = approvedId ?? lifecycle.getState().authorizationId;
|
|
3494
3507
|
await refusePaused(dead.requestId, dead.sessionId, request.url, cardEndpoint, secureAcceptance ? secureAcceptanceFailedRefusal(err, sent, failedFor) : failedNavigationRefusal(err, sent, failedFor), secureAcceptance);
|
|
3495
|
-
settlePause();
|
|
3496
3508
|
}
|
|
3497
3509
|
finally {
|
|
3498
3510
|
disarmSendWindow?.();
|
|
@@ -3532,8 +3544,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3532
3544
|
for (const target of targetInfos)
|
|
3533
3545
|
assertContextTarget(target, merchantContext, targetAllowance);
|
|
3534
3546
|
// The page may have committed another document since its TargetInfo was read; the list names the one it holds.
|
|
3535
|
-
for (const target of targetInfos)
|
|
3547
|
+
for (const target of targetInfos) {
|
|
3536
3548
|
notePageTarget(target);
|
|
3549
|
+
noteTargetContext(target);
|
|
3550
|
+
}
|
|
3537
3551
|
for (const target of targetInfos)
|
|
3538
3552
|
if (target?.type === 'shared_worker')
|
|
3539
3553
|
await watchSharedWorker(target);
|
|
@@ -3560,7 +3574,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3560
3574
|
// A window reported after that drain, before this line, is adopted the way a later window is.
|
|
3561
3575
|
for (const info of pendingWindows.splice(0))
|
|
3562
3576
|
void adoptOpenedWindow(info);
|
|
3563
|
-
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns:
|
|
3577
|
+
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: armedPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
|
|
3564
3578
|
}
|
|
3565
3579
|
catch (error) {
|
|
3566
3580
|
const failure = attachmentFailure(error);
|
|
@@ -3589,8 +3603,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3589
3603
|
*/
|
|
3590
3604
|
export async function attachToPlaywright(page, opts) {
|
|
3591
3605
|
opts = safeOptions(opts);
|
|
3592
|
-
if (opts.pauseOnly)
|
|
3593
|
-
throw new Error('pauseOnly is a raw CDP mode (attachToCdp); attachToPlaywright creates the authorization itself.');
|
|
3594
3606
|
if (opts.hold)
|
|
3595
3607
|
throw new Error('hold is a raw CDP mode (attachToCdp): it needs the frames\' own interception.');
|
|
3596
3608
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
@@ -3625,6 +3637,36 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3625
3637
|
const ebanxFollowUps = new EbanxFollowUps();
|
|
3626
3638
|
// The approved Payrexx card request on this attachment and the 3-D Secure sends claimed under it (payrexx.ts).
|
|
3627
3639
|
const payrexxFollowUps = new PayrexxFollowUps();
|
|
3640
|
+
// The approval Nike's order is held to once a card went through Nike's profile (nike-order.ts).
|
|
3641
|
+
const nikeOrders = new NikeOrders();
|
|
3642
|
+
/**
|
|
3643
|
+
* Nike's order is held for every page of the browser context, from the moment this attachment asks the cardholder to
|
|
3644
|
+
* pay through Nike's profile: a context route on Nike's API host, which the Nike order gate judges. Playwright routes
|
|
3645
|
+
* no shared or service worker's request. A context that cannot take the route refuses that payment before anything
|
|
3646
|
+
* is created. The context keeps the route after this checkout ends; only the route of the attachment holding the
|
|
3647
|
+
* context judges the order (nikeContextHolders), and a released order goes straight to the network, past the others.
|
|
3648
|
+
*/
|
|
3649
|
+
let nikeInContext = null;
|
|
3650
|
+
// This attachment, as the holder of Nike's order in its browser context (nikeContextHolders).
|
|
3651
|
+
const nikeHolder = {};
|
|
3652
|
+
const holdNikeOrdersInContext = () => Promise.resolve().then(async () => {
|
|
3653
|
+
const context = page.context();
|
|
3654
|
+
await (nikeInContext ??= Promise.resolve().then(() => context.route((url) => nikeOrderRouted(url.toString()), async (route) => {
|
|
3655
|
+
// Another attachment asked through Nike's profile here since this one did: its route judges the order.
|
|
3656
|
+
if (nikeContextHolders.get(context) !== nikeHolder)
|
|
3657
|
+
return route.fallback();
|
|
3658
|
+
const request = route.request();
|
|
3659
|
+
const fail = () => route.abort(refusalFailure(request.url()));
|
|
3660
|
+
const gate = {
|
|
3661
|
+
request: { url: request.url(), method: request.method(), get headers() { return request.headers(); }, get body() { return request.postData(); } },
|
|
3662
|
+
nikeOrders, lifecycle, onEvent: opts.onEvent, lifecycleAborted: () => lifecycle.abort.signal.aborted, terminal: () => !!terminal,
|
|
3663
|
+
effects: { continueRequest: () => route.continue(), answer: fail, abort: fail },
|
|
3664
|
+
};
|
|
3665
|
+
await (nikeOrderGate(gate) ?? route.fallback());
|
|
3666
|
+
})));
|
|
3667
|
+
// Every ask takes the context over once this attachment's route is on, one for another attempt included.
|
|
3668
|
+
nikeContextHolders.set(context, nikeHolder);
|
|
3669
|
+
});
|
|
3628
3670
|
const aci = new AciGate(opts.vault);
|
|
3629
3671
|
let navigationBodyReplaced;
|
|
3630
3672
|
const eprotect = new EprotectGate(opts.vault);
|
|
@@ -3984,6 +4026,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3984
4026
|
// Every request to Barion's gateway hosts once its entry is served (barion.ts), as attachToCdp arms the hosts:
|
|
3985
4027
|
// the gateway's email check, card posts and 3-D Secure post reach the Barion gate and fail.
|
|
3986
4028
|
|| isArmedBarionUrl(opts.vault, url.toString())
|
|
4029
|
+
// Every request to PayPlug's page host once its entry is served (payplug.ts): the page's card post reaches the
|
|
4030
|
+
// PayPlug gate and fails.
|
|
4031
|
+
|| isArmedPayplugUrl(opts.vault, url.toString())
|
|
3987
4032
|
// Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
|
|
3988
4033
|
// host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
|
|
3989
4034
|
|| isArmedEkashuUrl(opts.vault, url.toString())
|
|
@@ -4013,6 +4058,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4013
4058
|
// Every request to an Eximbay host once its entry is served there (eximbay.ts), as attachToCdp arms the host: the
|
|
4014
4059
|
// step, the window's lookups and its card post reach the Eximbay gate and fail.
|
|
4015
4060
|
|| isArmedEximbayUrl(opts.vault, url.toString())
|
|
4061
|
+
// Every request to an Oceanpayment host once its card post is served (oceanpayment.ts), as attachToCdp arms the
|
|
4062
|
+
// hosts: Cardinal's device-data call and any other post there reach the Oceanpayment gate and fail.
|
|
4063
|
+
|| isArmedOceanpaymentUrl(opts.vault, url.toString())
|
|
4016
4064
|
// PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
|
|
4017
4065
|
|| isPaypalHostedFieldsConfirm(url.toString())
|
|
4018
4066
|
// PayPal's Card Fields confirm and GraphQL on www.paypal.com, for every org, as attachToCdp arms them (paypal-card-forms.ts).
|
|
@@ -4121,6 +4169,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4121
4169
|
tokenexIframe: tokenexIframeRepeats,
|
|
4122
4170
|
ebanx: ebanxFollowUps,
|
|
4123
4171
|
payrexx: payrexxFollowUps,
|
|
4172
|
+
nikeOrders,
|
|
4124
4173
|
frame: frameOf(request),
|
|
4125
4174
|
payrails,
|
|
4126
4175
|
shijiSts: { companions: shijiCompanions, page: async () => (await playwrightShijiDocumentPage(request)) ?? `pw:none:${++playwrightShijiSeq}` },
|
|
@@ -4306,9 +4355,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4306
4355
|
activeRequest = { request, frames, attempt,
|
|
4307
4356
|
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined, flexPause?.shape, request.headers()),
|
|
4308
4357
|
preparedFrames: preparation ? [...frames] : undefined };
|
|
4309
|
-
// An ACI card
|
|
4358
|
+
// An ACI or JSON split card's card request: see the same step in attachToCdp.
|
|
4310
4359
|
if (!aci.begin(request.url(), request.method(), body, preparation?.merchantOrigin)) {
|
|
4311
|
-
throw new CheckoutPreparationError(preparation?.id ?? null,
|
|
4360
|
+
throw new CheckoutPreparationError(preparation?.id ?? null, aci.reuseCode(request.url()));
|
|
4312
4361
|
}
|
|
4313
4362
|
// A Worldpay eProtect registration: see the same step in attachToCdp.
|
|
4314
4363
|
eprotect.begin(request.url(), request.method(), () => attempt.stop());
|
|
@@ -4356,6 +4405,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4356
4405
|
// Paddle's own checkout answers as of the pause, once their texts are read (see attachToCdp).
|
|
4357
4406
|
const paddleAnswers = await paddleCheckout.capture(pausedAt, request.url(), attempt.signal);
|
|
4358
4407
|
assertRequestLive();
|
|
4408
|
+
if (cardEndpoint && nikeOrders.asking(cardEndpoint.id)) {
|
|
4409
|
+
await holdNikeOrdersInContext().catch(() => { throw new CardEndpointRefusedError(cardEndpoint.id, 'request_unsupported', "Nike's order cannot be held in this browser"); });
|
|
4410
|
+
assertRequestLive();
|
|
4411
|
+
}
|
|
4359
4412
|
const replay = await opts.vault.authorize({
|
|
4360
4413
|
user: opts.user,
|
|
4361
4414
|
merchant: opts.merchant,
|
|
@@ -4369,6 +4422,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4369
4422
|
pageOrigin,
|
|
4370
4423
|
pageAmount,
|
|
4371
4424
|
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
4425
|
+
// Same as the CDP path: when the request paused, for a VTEX card post's send-by.
|
|
4426
|
+
pausedAt,
|
|
4372
4427
|
preparation,
|
|
4373
4428
|
timeoutMs: opts.timeoutMs,
|
|
4374
4429
|
signal: lifecycle.abort.signal,
|
|
@@ -4415,6 +4470,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4415
4470
|
// False only for a card endpoint answer the device withheld (see deliverCardEndpointAnswer).
|
|
4416
4471
|
let handedOver = true;
|
|
4417
4472
|
if (replay.mode === 'card_endpoint') {
|
|
4473
|
+
nikeOrders.remember(replay);
|
|
4418
4474
|
handedOver = await deliverCardEndpointAnswer(opts, lifecycle, cardEndpoint, replay, { url: deliverRequest.url(), headers: deliverRequest.headers() }, {
|
|
4419
4475
|
readJar: () => page.context().cookies(deliverRequest.url()),
|
|
4420
4476
|
writeCookie: (cookie) => page.context().addCookies([playwrightCookieParams(cookie)]),
|