@agent-cards/sdk 0.23.28 → 0.24.1
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 +84 -0
- package/PREFLIGHT.md +7 -1
- package/README.md +197 -102
- package/dist/aci.d.ts +52 -22
- package/dist/aci.js +79 -15
- package/dist/adyen-merchant-hosted.generated.d.ts +2 -2
- package/dist/adyen-merchant-hosted.generated.js +586 -55
- 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/aurus.d.ts +33 -0
- package/dist/aurus.js +75 -0
- package/dist/builtin-registry.generated.js +1 -1
- 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 +2864 -249
- package/dist/cardlink-vpos.d.ts +43 -0
- package/dist/cardlink-vpos.js +90 -0
- package/dist/cdp.d.ts +3 -13
- package/dist/cdp.js +244 -175
- package/dist/checkout-com.generated.js +5 -1
- package/dist/client.d.ts +170 -22
- package/dist/client.js +281 -40
- package/dist/creditguard.generated.d.ts +4 -4
- package/dist/cybersource-flex.generated.d.ts +39 -1
- package/dist/cybersource-flex.generated.js +100 -11
- package/dist/datatrans.generated.js +22 -1
- package/dist/eghl.d.ts +40 -0
- package/dist/eghl.js +72 -0
- package/dist/elavon-converge.generated.js +5 -3
- package/dist/gmo.generated.js +7 -2
- package/dist/hold.d.ts +8 -0
- package/dist/hold.js +43 -2
- package/dist/hosted-entry.js +4 -0
- package/dist/index.d.ts +9 -15
- package/dist/index.js +6 -10
- 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 +111 -11
- package/dist/payplug.d.ts +41 -0
- package/dist/payplug.js +71 -0
- package/dist/payrails.generated.js +33 -8
- package/dist/pre-claim.d.ts +11 -0
- package/dist/pre-claim.js +130 -7
- package/dist/preflight-capabilities.generated.d.ts +0 -4
- package/dist/preflight-capabilities.generated.js +0 -5
- package/dist/preflight-catalog.json +200 -6
- package/dist/preflight-playwright.d.ts +2 -2
- package/dist/preflight-playwright.js +63 -14
- package/dist/preflight-schemas.json +18 -0
- package/dist/preflight.generated.d.ts +5 -4
- package/dist/preflight.generated.js +63 -3
- package/dist/preparation.d.ts +7 -0
- package/dist/preparation.js +21 -0
- package/dist/prepared-processor.d.ts +18 -5
- package/dist/prepared-processor.js +48 -8
- package/dist/primer.generated.js +2889 -268
- 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 +111 -11
- package/dist/stripe-checkout.generated.js +2761 -319
- package/dist/substitutions.generated.js +553 -26
- package/dist/tap-payments.generated.js +111 -11
- package/dist/twoc2p.generated.js +111 -11
- 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/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,18 @@ 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';
|
|
30
|
+
import { isArmedCardlinkVposUrl } from './cardlink-vpos.js';
|
|
31
|
+
import { isArmedEghlUrl } from './eghl.js';
|
|
29
32
|
import { isArmedQuickpayUrl } from './quickpay.js';
|
|
30
33
|
import { COMGATE_CARD_PATTERNS, isArmedComgateUrl, servesComgate } from './comgate.js';
|
|
31
34
|
import { isArmedEximbayUrl } from './eximbay.js';
|
|
35
|
+
import { isArmedOceanpaymentUrl } from './oceanpayment.js';
|
|
32
36
|
import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
|
|
33
37
|
import { EBANX_RESEND_PATTERNS, EbanxFollowUps, isEbanxResendUrl } from './ebanx.js';
|
|
34
38
|
import { PayrexxFollowUps } from './payrexx.js';
|
|
39
|
+
import { NIKE_ORDER_PATTERNS, NikeOrders, nikeOrderRouted } from './nike-order.js';
|
|
40
|
+
import { nikeOrderRequest as sharedNikeOrderRequest } from './merchant-card-endpoints.generated.js';
|
|
35
41
|
import { PAYPAL_CARD_FORM_PATTERNS, isPaypalCardFormUrl } from './paypal-card-forms.js';
|
|
36
42
|
import { CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, SecureAcceptanceFollowUpWatch, isCybersourceSecureAcceptanceUrl, secureAcceptanceFollowUp, secureAcceptanceFrameAnswer, secureAcceptanceReplyLast4, } from './cybersource-secure-acceptance.js';
|
|
37
43
|
import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
|
|
@@ -94,6 +100,18 @@ const HOSTED_FORM_REPEAT_REASON = 'already submitted on the cardholder\'s device
|
|
|
94
100
|
function targetRecord(value) {
|
|
95
101
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
96
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Per CDP connection, the attachment holding Nike's order at the browser target for each browser context: every one sees
|
|
105
|
+
* each pause there, and only the holder of the context that sent a request answers it (attachToCdp nikeOrderAtRoot). Each
|
|
106
|
+
* time an attachment asks through Nike's profile in a context, it takes the context over from whichever one held it.
|
|
107
|
+
*/
|
|
108
|
+
const nikeRootHolders = new WeakMap();
|
|
109
|
+
/**
|
|
110
|
+
* Per Playwright browser context, the attachment whose route judges Nike's order there: the last one whose route was on
|
|
111
|
+
* when it asked through Nike's profile (attachToPlaywright holdNikeOrdersInContext). Every other attachment's route
|
|
112
|
+
* hands the order to the next route.
|
|
113
|
+
*/
|
|
114
|
+
const nikeContextHolders = new WeakMap();
|
|
97
115
|
function contextId(value) {
|
|
98
116
|
return typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\u0000-\u0020\u007f]/.test(value);
|
|
99
117
|
}
|
|
@@ -130,6 +148,11 @@ function assertContextTarget(target, merchantContext, allow = {}) {
|
|
|
130
148
|
// reports are read, and a card request seen there ends the checkout before anything is approved (sharedWorkers).
|
|
131
149
|
if (allow.sharedWorkers && target.type === 'shared_worker' && knownContext)
|
|
132
150
|
return;
|
|
151
|
+
// A worklet (CDP type 'worklet': an audio, paint, animation or layout worklet) has no fetch, XHR, WebSocket or
|
|
152
|
+
// beacon in its global scope, so it can never send a card request; the module it loads is fetched by its owning
|
|
153
|
+
// frame, through that frame's interception. Shopify's checkout runs one in its web-pixel sandbox frames.
|
|
154
|
+
if (target.type === 'worklet' && knownContext)
|
|
155
|
+
return;
|
|
133
156
|
if (!['page', 'iframe', 'worker', 'browser_ui'].includes(target.type)
|
|
134
157
|
|| (target.browserContextId !== undefined && !contextId(target.browserContextId))) {
|
|
135
158
|
throw new CheckoutAttachmentError('unavailable');
|
|
@@ -1447,6 +1470,8 @@ function merchantAttempt(lifecycle, survives) {
|
|
|
1447
1470
|
// prepare again. Every other stop leaves the outcome unknown.
|
|
1448
1471
|
const error = () => reason === 'amadeus_send_window_closed' || reason === 'secure_acceptance_send_window_closed'
|
|
1449
1472
|
|| reason === 'wix_payments_send_window_closed' || reason === 'hipay_send_window_closed' || reason === 'openpay_send_window_closed'
|
|
1473
|
+
|| reason === 'vtex_payments_send_window_closed'
|
|
1474
|
+
|| reason === 'processout_send_window_closed'
|
|
1450
1475
|
? new CheckoutPreparationError(lifecycle.getState().preparationId ?? null, reason)
|
|
1451
1476
|
: new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, reason);
|
|
1452
1477
|
const attempt = {
|
|
@@ -1560,53 +1585,6 @@ async function retireUnusedApproval(opts, lifecycle, authorizationId) {
|
|
|
1560
1585
|
opts.onEvent?.({ type: 'failed', detail: 'PaymentOutcomeUnknownError' });
|
|
1561
1586
|
}
|
|
1562
1587
|
}
|
|
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
1588
|
/** A CDP Fetch url pattern (`*` any run of characters, `?` one, `\\` escapes) against a URL. */
|
|
1611
1589
|
function urlPatternMatches(pattern, url) {
|
|
1612
1590
|
let re = '';
|
|
@@ -1636,39 +1614,6 @@ function holdUntilDecided(decision, signal, lost) {
|
|
|
1636
1614
|
signal.addEventListener('abort', lost, { once: true });
|
|
1637
1615
|
return decision.finally(() => signal.removeEventListener('abort', lost));
|
|
1638
1616
|
}
|
|
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
1617
|
/** 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
1618
|
function recognizerNameOf(opts, url, method) {
|
|
1674
1619
|
const read = opts.vault.recognizerOf;
|
|
@@ -1681,61 +1626,6 @@ function recognizerNameOf(opts, url, method) {
|
|
|
1681
1626
|
return null;
|
|
1682
1627
|
}
|
|
1683
1628
|
}
|
|
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
1629
|
/**
|
|
1740
1630
|
* Arm the send window of a request paused under a preparation, when its processor sets one: a
|
|
1741
1631
|
* prepared Amadeus SDK add, a prepared Secure Acceptance token create and a prepared Wix tokenize
|
|
@@ -1789,10 +1679,6 @@ function watchSecureAcceptanceFollowUp(watch, preparation, replay) {
|
|
|
1789
1679
|
*/
|
|
1790
1680
|
export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
1791
1681
|
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
1682
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
1797
1683
|
const setupStop = new AbortController();
|
|
1798
1684
|
let attachmentReady = false;
|
|
@@ -1801,6 +1687,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1801
1687
|
const sessionTypes = new Map([[pageSessionId, 'page']]);
|
|
1802
1688
|
// Each window this attachment adopted (adoptOpenedWindow): its session, and its target's id, which is its main frame's.
|
|
1803
1689
|
const windowTargets = new Map();
|
|
1690
|
+
// The id of every target this attachment armed besides its page: each adopted window and each child it attached.
|
|
1691
|
+
const armedTargets = new Set();
|
|
1804
1692
|
const sessionParents = new Map();
|
|
1805
1693
|
// The frame in its parent session that a child target hangs from (TargetInfo.parentFrameId), so a
|
|
1806
1694
|
// frame lookup can climb from a cross-origin frame into the document that frames it.
|
|
@@ -1914,6 +1802,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1914
1802
|
const ebanxFollowUps = new EbanxFollowUps();
|
|
1915
1803
|
// The approved Payrexx card request on this attachment and the 3-D Secure sends claimed under it (payrexx.ts).
|
|
1916
1804
|
const payrexxFollowUps = new PayrexxFollowUps();
|
|
1805
|
+
// The approval Nike's order is held to once a card went through Nike's profile (nike-order.ts).
|
|
1806
|
+
const nikeOrders = new NikeOrders();
|
|
1917
1807
|
const aci = new AciGate(opts.vault);
|
|
1918
1808
|
const eprotect = new EprotectGate(opts.vault);
|
|
1919
1809
|
const sbps = new SbpsGate(opts.vault);
|
|
@@ -2246,8 +2136,15 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2246
2136
|
...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS, ...PAYPAL_CARD_FORM_PATTERNS,
|
|
2247
2137
|
...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS,
|
|
2248
2138
|
...(servesComgate(opts.vault) ? COMGATE_CARD_PATTERNS : [])])];
|
|
2139
|
+
// Nike's API host is armed in every frame this checkout arms, from the attach on: once a card went through Nike's
|
|
2140
|
+
// profile, Nike's order gate judges each order the page sends, a document loaded before the ask included, and anything
|
|
2141
|
+
// before that, or that is not the order, continues untouched (see nike-order.ts). Chrome applies the browser target's
|
|
2142
|
+
// interception (holdNikeOrdersAtRoot) only to documents loaded after it is armed, so it cannot hold these. The host
|
|
2143
|
+
// stays out of urlPatterns, which name what the SDK's own rules cover: in hold mode, a request to it that carries the
|
|
2144
|
+
// placeholder card is still held as hold-only.
|
|
2145
|
+
const armedPatterns = [...urlPatterns, ...NIKE_ORDER_PATTERNS];
|
|
2249
2146
|
// The hold-only rules' patterns (Bolt's tokenizer; every request a placeholder card can ride, by resource type).
|
|
2250
|
-
const fetchPatterns = [...
|
|
2147
|
+
const fetchPatterns = [...armedPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
|
|
2251
2148
|
...(holdGate ? holdGate.patterns().map((p) => ({ ...p, requestStage: 'Request' })) : [])];
|
|
2252
2149
|
const arm = async (sessionId, resume = false, worker = false) => {
|
|
2253
2150
|
const key = sessionId ?? '__root__';
|
|
@@ -2416,6 +2313,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2416
2313
|
sessionTypes.set(child, 'page');
|
|
2417
2314
|
sessionParents.set(child, pageSessionId);
|
|
2418
2315
|
windowTargets.set(child, info.targetId);
|
|
2316
|
+
armedTargets.add(info.targetId);
|
|
2419
2317
|
await arm(child);
|
|
2420
2318
|
opts.onEvent?.({ type: 'window_armed', detail: {} });
|
|
2421
2319
|
}
|
|
@@ -2423,6 +2321,107 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2423
2321
|
failInterception(error);
|
|
2424
2322
|
}
|
|
2425
2323
|
};
|
|
2324
|
+
/**
|
|
2325
|
+
* Nike's order is held at the browser target too, from the moment this attachment asks the cardholder to pay through
|
|
2326
|
+
* Nike's profile (nike-order.ts), for the routes this checkout's own frames do not cover (armedPatterns): Chrome applies
|
|
2327
|
+
* Fetch there only to a document, worker or shared worker that loads after it is armed, and pauses a request to Nike's
|
|
2328
|
+
* API host from every one of them in the browser, a tab this checkout did not open included. One that cannot be armed
|
|
2329
|
+
* refuses that payment before anything is created.
|
|
2330
|
+
*/
|
|
2331
|
+
let nikeAtRoot = null;
|
|
2332
|
+
// This attachment among those holding Nike's order on the same connection (nikeRootHolders), by its browser context.
|
|
2333
|
+
const nikeHolder = {};
|
|
2334
|
+
const holdNikeOrdersAtRoot = () => {
|
|
2335
|
+
// Every ask takes the context over, one for another attempt after a later attachment asked there included; the
|
|
2336
|
+
// browser target is armed once, and the context's other pages open by then at each ask.
|
|
2337
|
+
const holders = nikeRootHolders.get(cdp) ?? new Map();
|
|
2338
|
+
nikeRootHolders.set(cdp, holders.set(merchantContext, nikeHolder));
|
|
2339
|
+
return (nikeAtRoot ??= cdp.send('Fetch.enable', { patterns: NIKE_ORDER_PATTERNS.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })) }))
|
|
2340
|
+
.then(holdNikeOrdersInPages);
|
|
2341
|
+
};
|
|
2342
|
+
/**
|
|
2343
|
+
* Every other page of this browser context, and every frame in one, open when this attachment asks through Nike's
|
|
2344
|
+
* profile: the browser target's pause never reaches a document already loaded, so each gets its own interception for
|
|
2345
|
+
* Nike's API host (nikeOrderInPage), as Playwright's context route covers every page of its context. A page or frame
|
|
2346
|
+
* that cannot be armed refuses the payment, as the browser target does.
|
|
2347
|
+
*/
|
|
2348
|
+
const nikePageTargets = new Set();
|
|
2349
|
+
const nikePageSessions = new Set();
|
|
2350
|
+
const holdNikeOrdersInPages = async () => {
|
|
2351
|
+
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
2352
|
+
if (!Array.isArray(targetInfos))
|
|
2353
|
+
throw new Error('targets_unreadable');
|
|
2354
|
+
for (const info of targetInfos) {
|
|
2355
|
+
if (!targetRecord(info) || (info.type !== 'page' && info.type !== 'iframe') || info.browserContextId !== merchantContext
|
|
2356
|
+
|| typeof info.targetId !== 'string' || info.targetId === pageTargetId || armedTargets.has(info.targetId)
|
|
2357
|
+
|| nikePageTargets.has(info.targetId))
|
|
2358
|
+
continue;
|
|
2359
|
+
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: info.targetId, flatten: true });
|
|
2360
|
+
if (typeof sessionId !== 'string' || !sessionId)
|
|
2361
|
+
throw new Error('target_unattached');
|
|
2362
|
+
nikePageSessions.add(sessionId);
|
|
2363
|
+
await cdp.send('Fetch.enable', { patterns: NIKE_ORDER_PATTERNS.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })) }, sessionId);
|
|
2364
|
+
// Recorded once its interception is on, so the next attempt after one that failed here arms it again.
|
|
2365
|
+
nikePageTargets.add(info.targetId);
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
// Each target's browser context, from discovery on: a page or frame of another context, the default one included,
|
|
2369
|
+
// belongs to another checkout. A report that names no context never replaces one that did.
|
|
2370
|
+
const targetContexts = new Map();
|
|
2371
|
+
const noteTargetContext = (info) => {
|
|
2372
|
+
if (typeof info?.targetId === 'string' && (contextId(info.browserContextId) || !targetContexts.has(info.targetId))) {
|
|
2373
|
+
targetContexts.set(info.targetId, info.browserContextId);
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
/**
|
|
2377
|
+
* A request paused at the browser target, answered by the attachment holding Nike's order in the browser context that
|
|
2378
|
+
* sent it: its Nike order gate judges it, and anything else goes on untouched. Every attachment on the connection sees
|
|
2379
|
+
* each pause, so the others leave it alone, an earlier attachment a later one took the context over from included. A
|
|
2380
|
+
* request from a context nobody holds goes on untouched, from the first holder alone; one from a source the browser
|
|
2381
|
+
* never reported as a target (a shared worker) is the holder's while one context is held, and while several are, nobody
|
|
2382
|
+
* can tell whose it is, so the first holder refuses it when it is Nike's order and lets anything else go on.
|
|
2383
|
+
*/
|
|
2384
|
+
const nikeOrderAtRoot = async (requestId, request, frameId) => {
|
|
2385
|
+
// Root pauses this attachment did not arm are another attachment's to answer.
|
|
2386
|
+
if (!nikeAtRoot)
|
|
2387
|
+
return;
|
|
2388
|
+
const holders = nikeRootHolders.get(cdp) ?? new Map();
|
|
2389
|
+
// The page's own frame is this checkout's, whatever a later report named.
|
|
2390
|
+
const placed = typeof frameId === 'string' && (frameId === pageTargetId || targetContexts.has(frameId));
|
|
2391
|
+
const owner = placed ? holders.get(frameId === pageTargetId ? merchantContext : targetContexts.get(frameId))
|
|
2392
|
+
: holders.size === 1 ? [...holders.values()][0] : null;
|
|
2393
|
+
const fail = () => cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }).catch(() => { });
|
|
2394
|
+
if (owner !== nikeHolder) {
|
|
2395
|
+
// Only an order whose owner nobody can tell is refused: anything else nobody holds goes on.
|
|
2396
|
+
if ((owner === undefined || owner === null) && [...holders.values()][0] === nikeHolder) {
|
|
2397
|
+
await (owner === null && sharedNikeOrderRequest(request.url, request.method) === true ? fail()
|
|
2398
|
+
: cdp.send('Fetch.continueRequest', { requestId }).catch(() => { }));
|
|
2399
|
+
}
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
await judgeNikeOrder(requestId, request);
|
|
2403
|
+
};
|
|
2404
|
+
/**
|
|
2405
|
+
* A request paused in another page or frame of this context, armed for Nike's order at the ask (holdNikeOrdersInPages):
|
|
2406
|
+
* the attachment holding the context judges it, and any other lets it go on, to the holder, which armed every page and
|
|
2407
|
+
* frame of the context it does not own the same way when it asked.
|
|
2408
|
+
*/
|
|
2409
|
+
const nikeOrderInPage = async (sessionId, requestId, request) => {
|
|
2410
|
+
if (nikeRootHolders.get(cdp)?.get(merchantContext) === nikeHolder)
|
|
2411
|
+
await judgeNikeOrder(requestId, request, sessionId);
|
|
2412
|
+
else
|
|
2413
|
+
await cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
|
|
2414
|
+
};
|
|
2415
|
+
// Nike's order gate on a request paused at the browser target (no session) or in a page armed at the ask.
|
|
2416
|
+
const judgeNikeOrder = async (requestId, request, sessionId) => {
|
|
2417
|
+
const fail = () => cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
2418
|
+
const gate = {
|
|
2419
|
+
request: { url: request.url, method: request.method, headers: request.headers ?? {}, get body() { return pausedBody(request); } },
|
|
2420
|
+
nikeOrders, lifecycle, onEvent: opts.onEvent, lifecycleAborted: () => lifecycle.abort.signal.aborted, terminal: () => !!terminal,
|
|
2421
|
+
effects: { continueRequest: () => cdp.send('Fetch.continueRequest', { requestId }, sessionId), answer: fail, abort: fail },
|
|
2422
|
+
};
|
|
2423
|
+
await (nikeOrderGate(gate) ?? cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { }));
|
|
2424
|
+
};
|
|
2426
2425
|
cdp.on(async (method, params, sessionId) => {
|
|
2427
2426
|
// A frame that committed a new document drops the companion binding made for the getTokenId its
|
|
2428
2427
|
// old document sent, with the device's CVV ciphertext: the document that replaced it never
|
|
@@ -2435,6 +2434,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2435
2434
|
if ((method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') && targetContextKnown) {
|
|
2436
2435
|
// The page's own target reports each document it commits from discovery on, Page events or not.
|
|
2437
2436
|
notePageTarget(params?.targetInfo);
|
|
2437
|
+
noteTargetContext(params?.targetInfo);
|
|
2438
2438
|
try {
|
|
2439
2439
|
assertContextTarget(params?.targetInfo, merchantContext, targetAllowance);
|
|
2440
2440
|
if (params?.targetInfo?.type === 'shared_worker')
|
|
@@ -2455,6 +2455,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2455
2455
|
&& params?.frameId === pageTargetId && typeof params?.url === 'string') {
|
|
2456
2456
|
committedTopUrl = params.url.split('#')[0];
|
|
2457
2457
|
}
|
|
2458
|
+
// A history update that leaves the prepared page's origin, path and query as they were (a single-page
|
|
2459
|
+
// checkout's replaceState to its own URL, a hash change) is the same document, and keeps the preparation.
|
|
2460
|
+
if (method === 'Page.navigatedWithinDocument' && sessionId === pageSessionId && preparationFrameId && params?.frameId === preparationFrameId
|
|
2461
|
+
&& preparationGate.keepsDocument(params.url))
|
|
2462
|
+
return;
|
|
2458
2463
|
if (((method === 'Page.frameNavigated' && !params.frame?.parentId) || (method === 'Page.navigatedWithinDocument' && preparationFrameId && params.frameId === preparationFrameId)) && sessionId === pageSessionId) {
|
|
2459
2464
|
preparationGate.invalidate('merchant_document_changed');
|
|
2460
2465
|
mercadoCheckout.invalidate();
|
|
@@ -2506,6 +2511,16 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2506
2511
|
failInterception(new CheckoutAttachmentError('unavailable'));
|
|
2507
2512
|
return;
|
|
2508
2513
|
}
|
|
2514
|
+
// A request paused at the browser target, which this attachment arms only for Nike's order (nikeOrderAtRoot).
|
|
2515
|
+
if (method === 'Fetch.requestPaused' && sessionId === undefined) {
|
|
2516
|
+
await nikeOrderAtRoot(params.requestId, params.request, params.frameId);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
// A request paused in another page or frame of this context that this attachment armed for Nike's order at the ask.
|
|
2520
|
+
if (method === 'Fetch.requestPaused' && sessionId && nikePageSessions.has(sessionId)) {
|
|
2521
|
+
await nikeOrderInPage(sessionId, params.requestId, params.request);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2509
2524
|
// The transport is browser-scoped. A second tab's request or child target
|
|
2510
2525
|
// must never be authorized with this page's merchant, amount or preparation.
|
|
2511
2526
|
if (!sessionId || !ownedSessions.has(sessionId))
|
|
@@ -2754,6 +2769,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2754
2769
|
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2755
2770
|
return;
|
|
2756
2771
|
}
|
|
2772
|
+
// A worklet sends no request of its own (assertContextTarget): released from its debugger wait, never owned.
|
|
2773
|
+
if (params.targetInfo?.type === 'worklet' && typeof child === 'string' && child
|
|
2774
|
+
&& child !== pageSessionId && !ownedSessions.has(child)) {
|
|
2775
|
+
rejectedSessions.add(child);
|
|
2776
|
+
await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2757
2779
|
// CDP may omit the context ID for ordinary children; keep those attached.
|
|
2758
2780
|
try {
|
|
2759
2781
|
if (typeof child !== 'string' || !child || !['page', 'iframe', 'worker'].includes(params.targetInfo?.type)) {
|
|
@@ -2764,6 +2786,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2764
2786
|
throw new CheckoutAttachmentError('unavailable');
|
|
2765
2787
|
}
|
|
2766
2788
|
ownedSessions.add(child);
|
|
2789
|
+
if (typeof params.targetInfo.targetId === 'string')
|
|
2790
|
+
armedTargets.add(params.targetInfo.targetId);
|
|
2767
2791
|
sessionTypes.set(child, params.targetInfo.type);
|
|
2768
2792
|
sessionParents.set(child, sessionId);
|
|
2769
2793
|
if (typeof params.targetInfo.parentFrameId === 'string' && params.targetInfo.parentFrameId) {
|
|
@@ -2883,6 +2907,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2883
2907
|
tokenexIframe: tokenexIframeRepeats,
|
|
2884
2908
|
ebanx: ebanxFollowUps,
|
|
2885
2909
|
payrexx: payrexxFollowUps,
|
|
2910
|
+
nikeOrders,
|
|
2886
2911
|
frame: typeof frameId === 'string' && frameId ? frameId : undefined,
|
|
2887
2912
|
payrails,
|
|
2888
2913
|
shijiSts: { companions: shijiCompanions, page: () => cdpShijiPage(sessionId, frameId) },
|
|
@@ -3053,8 +3078,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3053
3078
|
// Whether this handoff kept an approval's follow-ups (ebanxFollowUps.remember, payrexxFollowUps.remember), which a
|
|
3054
3079
|
// failed handoff stops.
|
|
3055
3080
|
let followUpsRemembered = false;
|
|
3056
|
-
// The business's pause, so resume resolves once the page has its answer.
|
|
3057
|
-
let businessPause = null;
|
|
3058
3081
|
// Hold mode: the person's release, settled with what came of it.
|
|
3059
3082
|
let holdRelease = null;
|
|
3060
3083
|
// A prepared Amadeus SDK add must be sent within its window (see amadeus.ts).
|
|
@@ -3094,11 +3117,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3094
3117
|
activeRequest = { attempt, requestId, networkId, sessionId, frameId, request,
|
|
3095
3118
|
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined, flexPause?.shape, request.headers),
|
|
3096
3119
|
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.
|
|
3120
|
+
// An ACI or JSON split card's card request: the page's CVC and expiry requests for its
|
|
3121
|
+
// checkout are held until this approval ends (see AciGate). One whose checkout already has a
|
|
3122
|
+
// card request running or answered never begins; the pre-claim gate refuses it before this.
|
|
3100
3123
|
if (!aci.begin(request.url, request.method, body, preparation?.merchantOrigin)) {
|
|
3101
|
-
throw new CheckoutPreparationError(preparation?.id ?? null,
|
|
3124
|
+
throw new CheckoutPreparationError(preparation?.id ?? null, aci.reuseCode(request.url));
|
|
3102
3125
|
}
|
|
3103
3126
|
// A Worldpay eProtect registration: the vendor's give-up report for it ends this attempt as
|
|
3104
3127
|
// the page abandoning its request (see EprotectGate.gaveUp).
|
|
@@ -3148,8 +3171,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3148
3171
|
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url, pausedAt, attempt.signal) : null;
|
|
3149
3172
|
attempt.assertLive();
|
|
3150
3173
|
// 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 =
|
|
3174
|
+
// (a Billing /pay only; sent only with a Paddle TokenEx create).
|
|
3175
|
+
const paddleAnswers = await paddleCheckout.capture(pausedAt, request.url, attempt.signal);
|
|
3153
3176
|
attempt.assertLive();
|
|
3154
3177
|
// Hold mode: nothing is created until the person's total releases the request (hold.ts).
|
|
3155
3178
|
if (holdGate) {
|
|
@@ -3167,10 +3190,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3167
3190
|
holdRelease = decision;
|
|
3168
3191
|
attempt.assertLive();
|
|
3169
3192
|
}
|
|
3170
|
-
//
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3193
|
+
// From the create on, the device may store the card at the merchant: Nike's orders are held from here (nike-order.ts).
|
|
3194
|
+
if (cardEndpoint && nikeOrders.asking(cardEndpoint.id)) {
|
|
3195
|
+
await holdNikeOrdersAtRoot().catch(() => { throw new CardEndpointRefusedError(cardEndpoint.id, 'request_unsupported', "Nike's order cannot be held in this browser"); });
|
|
3196
|
+
attempt.assertLive();
|
|
3197
|
+
}
|
|
3174
3198
|
// A sandbox hold never sends the request on: the approval is created at the released total and read, the
|
|
3175
3199
|
// page's request fails the moment it exists, and the attempt ends with nothing charged.
|
|
3176
3200
|
const holdSandbox = !!holdRelease && opts.vault.sandbox === true;
|
|
@@ -3185,7 +3209,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3185
3209
|
lifecycle.abort.signal.addEventListener('abort', retire, { once: true });
|
|
3186
3210
|
}
|
|
3187
3211
|
let holdRequestFailed = false;
|
|
3188
|
-
const authorization =
|
|
3212
|
+
const authorization = opts.vault.authorize({
|
|
3189
3213
|
user: opts.user,
|
|
3190
3214
|
merchant: opts.merchant,
|
|
3191
3215
|
amount: holdRelease ? holdRelease.amount : opts.amount,
|
|
@@ -3198,6 +3222,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3198
3222
|
pageOrigin,
|
|
3199
3223
|
pageAmount,
|
|
3200
3224
|
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
3225
|
+
// When the request paused, which a VTEX card post's send-by counts from (the client sends it for VTEX alone).
|
|
3226
|
+
pausedAt,
|
|
3201
3227
|
preparation,
|
|
3202
3228
|
timeoutMs: opts.timeoutMs,
|
|
3203
3229
|
signal: lifecycle.abort.signal,
|
|
@@ -3231,7 +3257,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3231
3257
|
...(flexPause ? flexPause.sidecar : {}),
|
|
3232
3258
|
...(payrailsSidecar ? { payrails: payrailsSidecar } : {}) },
|
|
3233
3259
|
});
|
|
3234
|
-
if (holdSandbox
|
|
3260
|
+
if (holdSandbox) {
|
|
3235
3261
|
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
3262
|
authorizationId: lifecycle.getState().authorizationId, detail: failureSummary(err) }));
|
|
3237
3263
|
if (!holdRequestFailed)
|
|
@@ -3239,7 +3265,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3239
3265
|
holdRelease.settle(outcome);
|
|
3240
3266
|
throw new HoldAnsweredError(outcome);
|
|
3241
3267
|
}
|
|
3242
|
-
const replay =
|
|
3268
|
+
const replay = await authorization;
|
|
3243
3269
|
attempt.assertLive();
|
|
3244
3270
|
approvedId = replay.authorizationId;
|
|
3245
3271
|
if (lifecycle.isCancelled())
|
|
@@ -3272,6 +3298,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3272
3298
|
// False only for a card endpoint answer the device withheld (see deliverCardEndpointAnswer).
|
|
3273
3299
|
let handedOver = true;
|
|
3274
3300
|
if (replay.mode === 'card_endpoint') {
|
|
3301
|
+
// The approved amount Nike's order after the card is held to (nike-order.ts).
|
|
3302
|
+
nikeOrders.remember(replay);
|
|
3275
3303
|
handedOver = await deliverCardEndpointAnswer(opts, lifecycle, cardEndpoint, replay, { url: deliverRequest.url, headers: deliverRequest.headers }, {
|
|
3276
3304
|
readJar: async () => (await cdp.send('Network.getCookies', { urls: [deliverRequest.url] }, deliverOn))?.cookies,
|
|
3277
3305
|
writeCookie: async (cookie) => {
|
|
@@ -3430,7 +3458,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3430
3458
|
}
|
|
3431
3459
|
if (handedOver)
|
|
3432
3460
|
lifecycle.handedOff(replay);
|
|
3433
|
-
businessPause?.delivered.resolve();
|
|
3434
3461
|
holdRelease?.settle({ outcome: 'authorized', authorizationId: replay.authorizationId });
|
|
3435
3462
|
}
|
|
3436
3463
|
catch (err) {
|
|
@@ -3451,13 +3478,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3451
3478
|
ebanxFollowUps.stop();
|
|
3452
3479
|
payrexxFollowUps.stop();
|
|
3453
3480
|
}
|
|
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
3481
|
if (activeRequest?.attempt === attempt)
|
|
3462
3482
|
activeRequest = null;
|
|
3463
3483
|
// Vault clients may normalize a merchantSignal abort. Preserve the
|
|
@@ -3492,7 +3512,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3492
3512
|
// A Secure Acceptance frame post reads CyberSource's side too (secureAcceptanceFailedRefusal).
|
|
3493
3513
|
const sent = handoffStarted || approvedId !== null, failedFor = approvedId ?? lifecycle.getState().authorizationId;
|
|
3494
3514
|
await refusePaused(dead.requestId, dead.sessionId, request.url, cardEndpoint, secureAcceptance ? secureAcceptanceFailedRefusal(err, sent, failedFor) : failedNavigationRefusal(err, sent, failedFor), secureAcceptance);
|
|
3495
|
-
settlePause();
|
|
3496
3515
|
}
|
|
3497
3516
|
finally {
|
|
3498
3517
|
disarmSendWindow?.();
|
|
@@ -3532,8 +3551,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3532
3551
|
for (const target of targetInfos)
|
|
3533
3552
|
assertContextTarget(target, merchantContext, targetAllowance);
|
|
3534
3553
|
// 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)
|
|
3554
|
+
for (const target of targetInfos) {
|
|
3536
3555
|
notePageTarget(target);
|
|
3556
|
+
noteTargetContext(target);
|
|
3557
|
+
}
|
|
3537
3558
|
for (const target of targetInfos)
|
|
3538
3559
|
if (target?.type === 'shared_worker')
|
|
3539
3560
|
await watchSharedWorker(target);
|
|
@@ -3560,7 +3581,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3560
3581
|
// A window reported after that drain, before this line, is adopted the way a later window is.
|
|
3561
3582
|
for (const info of pendingWindows.splice(0))
|
|
3562
3583
|
void adoptOpenedWindow(info);
|
|
3563
|
-
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns:
|
|
3584
|
+
opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: armedPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
|
|
3564
3585
|
}
|
|
3565
3586
|
catch (error) {
|
|
3566
3587
|
const failure = attachmentFailure(error);
|
|
@@ -3589,8 +3610,6 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
3589
3610
|
*/
|
|
3590
3611
|
export async function attachToPlaywright(page, opts) {
|
|
3591
3612
|
opts = safeOptions(opts);
|
|
3592
|
-
if (opts.pauseOnly)
|
|
3593
|
-
throw new Error('pauseOnly is a raw CDP mode (attachToCdp); attachToPlaywright creates the authorization itself.');
|
|
3594
3613
|
if (opts.hold)
|
|
3595
3614
|
throw new Error('hold is a raw CDP mode (attachToCdp): it needs the frames\' own interception.');
|
|
3596
3615
|
const setupTimeoutMs = attachmentDeadline(opts.attachmentTimeoutMs);
|
|
@@ -3625,6 +3644,36 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3625
3644
|
const ebanxFollowUps = new EbanxFollowUps();
|
|
3626
3645
|
// The approved Payrexx card request on this attachment and the 3-D Secure sends claimed under it (payrexx.ts).
|
|
3627
3646
|
const payrexxFollowUps = new PayrexxFollowUps();
|
|
3647
|
+
// The approval Nike's order is held to once a card went through Nike's profile (nike-order.ts).
|
|
3648
|
+
const nikeOrders = new NikeOrders();
|
|
3649
|
+
/**
|
|
3650
|
+
* Nike's order is held for every page of the browser context, from the moment this attachment asks the cardholder to
|
|
3651
|
+
* pay through Nike's profile: a context route on Nike's API host, which the Nike order gate judges. Playwright routes
|
|
3652
|
+
* no shared or service worker's request. A context that cannot take the route refuses that payment before anything
|
|
3653
|
+
* is created. The context keeps the route after this checkout ends; only the route of the attachment holding the
|
|
3654
|
+
* context judges the order (nikeContextHolders), and a released order goes straight to the network, past the others.
|
|
3655
|
+
*/
|
|
3656
|
+
let nikeInContext = null;
|
|
3657
|
+
// This attachment, as the holder of Nike's order in its browser context (nikeContextHolders).
|
|
3658
|
+
const nikeHolder = {};
|
|
3659
|
+
const holdNikeOrdersInContext = () => Promise.resolve().then(async () => {
|
|
3660
|
+
const context = page.context();
|
|
3661
|
+
await (nikeInContext ??= Promise.resolve().then(() => context.route((url) => nikeOrderRouted(url.toString()), async (route) => {
|
|
3662
|
+
// Another attachment asked through Nike's profile here since this one did: its route judges the order.
|
|
3663
|
+
if (nikeContextHolders.get(context) !== nikeHolder)
|
|
3664
|
+
return route.fallback();
|
|
3665
|
+
const request = route.request();
|
|
3666
|
+
const fail = () => route.abort(refusalFailure(request.url()));
|
|
3667
|
+
const gate = {
|
|
3668
|
+
request: { url: request.url(), method: request.method(), get headers() { return request.headers(); }, get body() { return request.postData(); } },
|
|
3669
|
+
nikeOrders, lifecycle, onEvent: opts.onEvent, lifecycleAborted: () => lifecycle.abort.signal.aborted, terminal: () => !!terminal,
|
|
3670
|
+
effects: { continueRequest: () => route.continue(), answer: fail, abort: fail },
|
|
3671
|
+
};
|
|
3672
|
+
await (nikeOrderGate(gate) ?? route.fallback());
|
|
3673
|
+
})));
|
|
3674
|
+
// Every ask takes the context over once this attachment's route is on, one for another attempt included.
|
|
3675
|
+
nikeContextHolders.set(context, nikeHolder);
|
|
3676
|
+
});
|
|
3628
3677
|
const aci = new AciGate(opts.vault);
|
|
3629
3678
|
let navigationBodyReplaced;
|
|
3630
3679
|
const eprotect = new EprotectGate(opts.vault);
|
|
@@ -3984,6 +4033,15 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3984
4033
|
// Every request to Barion's gateway hosts once its entry is served (barion.ts), as attachToCdp arms the hosts:
|
|
3985
4034
|
// the gateway's email check, card posts and 3-D Secure post reach the Barion gate and fail.
|
|
3986
4035
|
|| isArmedBarionUrl(opts.vault, url.toString())
|
|
4036
|
+
// Every request to PayPlug's page host once its entry is served (payplug.ts): the page's card post reaches the
|
|
4037
|
+
// PayPlug gate and fails.
|
|
4038
|
+
|| isArmedPayplugUrl(opts.vault, url.toString())
|
|
4039
|
+
// Every request to the VPOS's hosts once its entry is served (cardlink-vpos.ts): the card page's post, which shares
|
|
4040
|
+
// the entry's URL, reaches the Cardlink gate and fails.
|
|
4041
|
+
|| isArmedCardlinkVposUrl(opts.vault, url.toString())
|
|
4042
|
+
// Every request to eGHL's hosts once its entry is served (eghl.ts): the card page's post, which shares the entry's
|
|
4043
|
+
// address, reaches the eGHL gate and fails.
|
|
4044
|
+
|| isArmedEghlUrl(opts.vault, url.toString())
|
|
3987
4045
|
// Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
|
|
3988
4046
|
// host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
|
|
3989
4047
|
|| isArmedEkashuUrl(opts.vault, url.toString())
|
|
@@ -4013,6 +4071,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4013
4071
|
// Every request to an Eximbay host once its entry is served there (eximbay.ts), as attachToCdp arms the host: the
|
|
4014
4072
|
// step, the window's lookups and its card post reach the Eximbay gate and fail.
|
|
4015
4073
|
|| isArmedEximbayUrl(opts.vault, url.toString())
|
|
4074
|
+
// Every request to an Oceanpayment host once its card post is served (oceanpayment.ts), as attachToCdp arms the
|
|
4075
|
+
// hosts: Cardinal's device-data call and any other post there reach the Oceanpayment gate and fail.
|
|
4076
|
+
|| isArmedOceanpaymentUrl(opts.vault, url.toString())
|
|
4016
4077
|
// PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
|
|
4017
4078
|
|| isPaypalHostedFieldsConfirm(url.toString())
|
|
4018
4079
|
// PayPal's Card Fields confirm and GraphQL on www.paypal.com, for every org, as attachToCdp arms them (paypal-card-forms.ts).
|
|
@@ -4121,6 +4182,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4121
4182
|
tokenexIframe: tokenexIframeRepeats,
|
|
4122
4183
|
ebanx: ebanxFollowUps,
|
|
4123
4184
|
payrexx: payrexxFollowUps,
|
|
4185
|
+
nikeOrders,
|
|
4124
4186
|
frame: frameOf(request),
|
|
4125
4187
|
payrails,
|
|
4126
4188
|
shijiSts: { companions: shijiCompanions, page: async () => (await playwrightShijiDocumentPage(request)) ?? `pw:none:${++playwrightShijiSeq}` },
|
|
@@ -4306,9 +4368,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4306
4368
|
activeRequest = { request, frames, attempt,
|
|
4307
4369
|
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined, flexPause?.shape, request.headers()),
|
|
4308
4370
|
preparedFrames: preparation ? [...frames] : undefined };
|
|
4309
|
-
// An ACI card
|
|
4371
|
+
// An ACI or JSON split card's card request: see the same step in attachToCdp.
|
|
4310
4372
|
if (!aci.begin(request.url(), request.method(), body, preparation?.merchantOrigin)) {
|
|
4311
|
-
throw new CheckoutPreparationError(preparation?.id ?? null,
|
|
4373
|
+
throw new CheckoutPreparationError(preparation?.id ?? null, aci.reuseCode(request.url()));
|
|
4312
4374
|
}
|
|
4313
4375
|
// A Worldpay eProtect registration: see the same step in attachToCdp.
|
|
4314
4376
|
eprotect.begin(request.url(), request.method(), () => attempt.stop());
|
|
@@ -4356,6 +4418,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4356
4418
|
// Paddle's own checkout answers as of the pause, once their texts are read (see attachToCdp).
|
|
4357
4419
|
const paddleAnswers = await paddleCheckout.capture(pausedAt, request.url(), attempt.signal);
|
|
4358
4420
|
assertRequestLive();
|
|
4421
|
+
if (cardEndpoint && nikeOrders.asking(cardEndpoint.id)) {
|
|
4422
|
+
await holdNikeOrdersInContext().catch(() => { throw new CardEndpointRefusedError(cardEndpoint.id, 'request_unsupported', "Nike's order cannot be held in this browser"); });
|
|
4423
|
+
assertRequestLive();
|
|
4424
|
+
}
|
|
4359
4425
|
const replay = await opts.vault.authorize({
|
|
4360
4426
|
user: opts.user,
|
|
4361
4427
|
merchant: opts.merchant,
|
|
@@ -4369,6 +4435,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4369
4435
|
pageOrigin,
|
|
4370
4436
|
pageAmount,
|
|
4371
4437
|
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
4438
|
+
// Same as the CDP path: when the request paused, for a VTEX card post's send-by.
|
|
4439
|
+
pausedAt,
|
|
4372
4440
|
preparation,
|
|
4373
4441
|
timeoutMs: opts.timeoutMs,
|
|
4374
4442
|
signal: lifecycle.abort.signal,
|
|
@@ -4415,6 +4483,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
4415
4483
|
// False only for a card endpoint answer the device withheld (see deliverCardEndpointAnswer).
|
|
4416
4484
|
let handedOver = true;
|
|
4417
4485
|
if (replay.mode === 'card_endpoint') {
|
|
4486
|
+
nikeOrders.remember(replay);
|
|
4418
4487
|
handedOver = await deliverCardEndpointAnswer(opts, lifecycle, cardEndpoint, replay, { url: deliverRequest.url(), headers: deliverRequest.headers() }, {
|
|
4419
4488
|
readJar: () => page.context().cookies(deliverRequest.url()),
|
|
4420
4489
|
writeCookie: (cookie) => page.context().addCookies([playwrightCookieParams(cookie)]),
|