@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/PREFLIGHT.md +7 -1
  3. package/README.md +197 -102
  4. package/dist/aci.d.ts +52 -22
  5. package/dist/aci.js +79 -15
  6. package/dist/adyen-merchant-hosted.generated.d.ts +2 -2
  7. package/dist/adyen-merchant-hosted.generated.js +586 -55
  8. package/dist/agent-amount.generated.js +1 -1
  9. package/dist/agentcard.d.ts +4 -5
  10. package/dist/agentcard.js +4 -5
  11. package/dist/amadeus.d.ts +9 -3
  12. package/dist/amadeus.js +33 -4
  13. package/dist/api-module.d.ts +1 -7
  14. package/dist/api-module.js +3 -5
  15. package/dist/{voice-retry.js → api-retry.js} +1 -1
  16. package/dist/api-types.d.ts +53 -0
  17. package/dist/{voice-types.js → api-types.js} +7 -2
  18. package/dist/approvals.d.ts +166 -0
  19. package/dist/approvals.js +213 -0
  20. package/dist/aurus.d.ts +33 -0
  21. package/dist/aurus.js +75 -0
  22. package/dist/builtin-registry.generated.js +1 -1
  23. package/dist/buy.d.ts +20 -101
  24. package/dist/buy.js +19 -74
  25. package/dist/capabilities.generated.js +1 -1
  26. package/dist/card-fields.generated.js +2864 -249
  27. package/dist/cardlink-vpos.d.ts +43 -0
  28. package/dist/cardlink-vpos.js +90 -0
  29. package/dist/cdp.d.ts +3 -13
  30. package/dist/cdp.js +244 -175
  31. package/dist/checkout-com.generated.js +5 -1
  32. package/dist/client.d.ts +170 -22
  33. package/dist/client.js +281 -40
  34. package/dist/creditguard.generated.d.ts +4 -4
  35. package/dist/cybersource-flex.generated.d.ts +39 -1
  36. package/dist/cybersource-flex.generated.js +100 -11
  37. package/dist/datatrans.generated.js +22 -1
  38. package/dist/eghl.d.ts +40 -0
  39. package/dist/eghl.js +72 -0
  40. package/dist/elavon-converge.generated.js +5 -3
  41. package/dist/gmo.generated.js +7 -2
  42. package/dist/hold.d.ts +8 -0
  43. package/dist/hold.js +43 -2
  44. package/dist/hosted-entry.js +4 -0
  45. package/dist/index.d.ts +9 -15
  46. package/dist/index.js +6 -10
  47. package/dist/lifecycle.d.ts +9 -61
  48. package/dist/lifecycle.js +24 -10
  49. package/dist/merchant-card-endpoints.generated.d.ts +6 -1
  50. package/dist/merchant-card-endpoints.generated.js +344 -19
  51. package/dist/merchant-hosted.d.ts +8 -1
  52. package/dist/nike-order.d.ts +51 -0
  53. package/dist/nike-order.js +81 -0
  54. package/dist/oceanpayment.d.ts +38 -0
  55. package/dist/oceanpayment.js +70 -0
  56. package/dist/paddle-tokenex.generated.js +111 -11
  57. package/dist/payplug.d.ts +41 -0
  58. package/dist/payplug.js +71 -0
  59. package/dist/payrails.generated.js +33 -8
  60. package/dist/pre-claim.d.ts +11 -0
  61. package/dist/pre-claim.js +130 -7
  62. package/dist/preflight-capabilities.generated.d.ts +0 -4
  63. package/dist/preflight-capabilities.generated.js +0 -5
  64. package/dist/preflight-catalog.json +200 -6
  65. package/dist/preflight-playwright.d.ts +2 -2
  66. package/dist/preflight-playwright.js +63 -14
  67. package/dist/preflight-schemas.json +18 -0
  68. package/dist/preflight.generated.d.ts +5 -4
  69. package/dist/preflight.generated.js +63 -3
  70. package/dist/preparation.d.ts +7 -0
  71. package/dist/preparation.js +21 -0
  72. package/dist/prepared-processor.d.ts +18 -5
  73. package/dist/prepared-processor.js +48 -8
  74. package/dist/primer.generated.js +2889 -268
  75. package/dist/processout.generated.d.ts +61 -0
  76. package/dist/processout.generated.js +577 -0
  77. package/dist/requests.js +1 -1
  78. package/dist/shiji-sts.generated.js +111 -11
  79. package/dist/stripe-checkout.generated.js +2761 -319
  80. package/dist/substitutions.generated.js +553 -26
  81. package/dist/tap-payments.generated.js +111 -11
  82. package/dist/twoc2p.generated.js +111 -11
  83. package/dist/url-masks/bluesnap.d.ts +2 -0
  84. package/dist/url-masks/bluesnap.js +15 -0
  85. package/dist/url-masks/oceanpayment.d.ts +2 -0
  86. package/dist/url-masks/oceanpayment.js +13 -0
  87. package/dist/url-masks/payplug.d.ts +2 -0
  88. package/dist/url-masks/payplug.js +13 -0
  89. package/dist/url-masks/vtex_payments.d.ts +2 -0
  90. package/dist/url-masks/vtex_payments.js +12 -0
  91. package/dist/url-masks.generated.d.ts +1 -1
  92. package/dist/url-masks.generated.js +6 -2
  93. package/dist/verifone.generated.js +42 -2
  94. package/dist/vtex-payments.generated.d.ts +127 -0
  95. package/dist/vtex-payments.generated.js +544 -0
  96. package/dist/webhooks.d.ts +2 -2
  97. package/dist/webhooks.js +2 -2
  98. package/examples/preflight/kernel-native/inventory.json +1 -1
  99. package/package.json +6 -8
  100. package/dist/languages.d.ts +0 -23
  101. package/dist/languages.js +0 -76
  102. package/dist/voice-pkce.d.ts +0 -49
  103. package/dist/voice-pkce.js +0 -147
  104. package/dist/voice-stream.generated.d.ts +0 -118
  105. package/dist/voice-stream.generated.js +0 -170
  106. package/dist/voice-types.d.ts +0 -735
  107. package/dist/voice.d.ts +0 -373
  108. package/dist/voice.js +0 -815
  109. package/readme.test.mjs +0 -53
  110. /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, CheckoutCancelledError, CheckoutPreparationError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, redactUrl, } from './client.js';
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 { ACI_CHECKOUT_REUSED, AciGate, playwrightReplacesNavigationBody } from './aci.js';
10
- import { EprotectGate, classifyEprotectRequest, eprotectScriptAnswer } from './eprotect.js';
11
- import { SbpsGate, classifySbpsRequest, sbpsScriptAnswer } from './sbps.js';
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 = [...urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
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-number request: the page's CVC and expiry requests for its checkout are held
3098
- // until this approval ends (see AciGate). One whose checkout already has a card-number
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, ACI_CHECKOUT_REUSED);
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, which a pause-only business makes itself).
3152
- const paddleAnswers = opts.pauseOnly ? undefined : await paddleCheckout.capture(pausedAt, request.url, attempt.signal);
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
- // Decision 176: pause only. The business creates the authorization from the paused request and answers
3171
- // through controller.resume; the replay it hands back goes down the same delivery path as a create's.
3172
- const pause = opts.pauseOnly ? pauseForBusiness(opts, lifecycle, attempt.signal, { url: request.url, method: request.method, headers: request.headers ?? {}, body }, pageOrigin) : null;
3173
- businessPause = pause;
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 = pause ? null : opts.vault.authorize({
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 && authorization) {
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 = pause ? sbpsResumedReplay(eprotectResumedReplay(await pause.replay, request), request) : await authorization;
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: urlPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
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-number request: see the same step in attachToCdp.
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, ACI_CHECKOUT_REUSED);
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)]),