@agent-cards/sdk 0.23.20 → 0.23.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/PREFLIGHT.md +2 -0
  3. package/README.md +143 -5
  4. package/dist/card-fields.generated.js +984 -57
  5. package/dist/cdp.js +146 -10
  6. package/dist/client.d.ts +104 -3
  7. package/dist/client.js +233 -23
  8. package/dist/comgate.d.ts +50 -0
  9. package/dist/comgate.js +80 -0
  10. package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
  11. package/dist/eprotect.generated.d.ts +1 -1
  12. package/dist/gmo.generated.js +7 -3
  13. package/dist/hipay.generated.d.ts +5 -5
  14. package/dist/hipay.generated.js +5 -2
  15. package/dist/hold.d.ts +1 -1
  16. package/dist/hold.js +10 -4
  17. package/dist/hosted-entry.d.ts +4 -0
  18. package/dist/hosted-entry.js +32 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/paddle-tokenex.generated.js +7 -3
  21. package/dist/payrails.generated.js +7 -3
  22. package/dist/pre-claim.d.ts +7 -1
  23. package/dist/pre-claim.js +127 -20
  24. package/dist/preflight-catalog.json +58 -0
  25. package/dist/preflight-schemas.json +4 -0
  26. package/dist/preflight.generated.js +26 -2
  27. package/dist/primer.generated.js +1027 -100
  28. package/dist/przelewy24.d.ts +47 -0
  29. package/dist/przelewy24.js +88 -0
  30. package/dist/registry.d.ts +6 -5
  31. package/dist/registry.js +27 -7
  32. package/dist/shiji-sts.generated.js +7 -3
  33. package/dist/stripe-checkout.generated.js +621 -12
  34. package/dist/tokenex-iframe.d.ts +93 -0
  35. package/dist/tokenex-iframe.generated.d.ts +81 -0
  36. package/dist/tokenex-iframe.generated.js +403 -0
  37. package/dist/tokenex-iframe.js +195 -0
  38. package/dist/transbank.d.ts +45 -0
  39. package/dist/transbank.js +73 -0
  40. package/dist/twoc2p.generated.js +7 -3
  41. package/examples/preflight/kernel-native/inventory.json +1 -1
  42. package/package.json +3 -3
package/dist/cdp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
2
- import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, CheckoutPreparationError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, } from './client.js';
2
+ import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, CheckoutPreparationError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, redactUrl, } from './client.js';
3
3
  import { PreparationGate } from './preparation.js';
4
4
  import { preparedApprovalAnswersRetry } from './prepared-processor.js';
5
5
  import { StripeCheckoutGate } from './stripe-checkout.js';
@@ -13,12 +13,16 @@ import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesG
13
13
  import { isPaddleVgsCardsUrl } from './paddle-vgs.generated.js';
14
14
  import { PADDLE_VGS_PAY_PATTERNS, PaddleVgsPayGate, isPaddleCheckoutService, recognizesPaddleVgs } from './paddle-vgs-pay.js';
15
15
  import { DatatransRepeats } from './datatrans.js';
16
+ import { TokenexIframeRepeats, isArmedTokenexIframeUrl } from './tokenex-iframe.js';
16
17
  import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
17
18
  import { paddleTokenexTokenContinuation } from './paddle-tokenex.js';
18
19
  import { twoC2pContinuation, twoC2pRetire } from './twoc2p.js';
19
20
  import { isArmedEkashuUrl } from './ekashu.js';
21
+ import { isArmedTransbankUrl } from './transbank.js';
20
22
  import { isArmedRedsysUrl } from './redsys.js';
23
+ import { isArmedPrzelewy24Url } from './przelewy24.js';
21
24
  import { isArmedQuickpayUrl } from './quickpay.js';
25
+ import { COMGATE_CARD_PATTERNS, isArmedComgateUrl, servesComgate } from './comgate.js';
22
26
  import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
23
27
  import { EBANX_RESEND_PATTERNS, EbanxFollowUps, isEbanxResendUrl } from './ebanx.js';
24
28
  import { PAYPAL_CARD_FORM_PATTERNS, isPaypalCardFormUrl } from './paypal-card-forms.js';
@@ -1750,6 +1754,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1750
1754
  let setupFailureReported = false;
1751
1755
  const ownedSessions = new Set([pageSessionId]);
1752
1756
  const sessionTypes = new Map([[pageSessionId, 'page']]);
1757
+ // Each window this attachment adopted (adoptOpenedWindow): its session, and its target's id, which is its main frame's.
1758
+ const windowTargets = new Map();
1753
1759
  const sessionParents = new Map();
1754
1760
  // The frame in its parent session that a child target hangs from (TargetInfo.parentFrameId), so a
1755
1761
  // frame lookup can climb from a cross-origin frame into the document that frames it.
@@ -1874,6 +1880,20 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1874
1880
  preparationFrameId = tree.frameTree.frame.id;
1875
1881
  return tree.frameTree.frame.url;
1876
1882
  };
1883
+ // The URL the page's main frame last committed: its target's URL when the attachment is set up, then kept from
1884
+ // Page.frameNavigated and Page.navigatedWithinDocument, and from the page target's own reports (notePageTarget),
1885
+ // which name a document the page commits before this attachment has enabled its Page events. Chromium leaves
1886
+ // Page.getFrameTree unanswered on the page's session while that session's own top-level navigation is paused (a
1887
+ // hosted page entry the page sends this window to, by script or through the merchant's redirect), so
1888
+ // readDocumentUrl would wait out its bound and the create would name no checkout page. For that one paused request
1889
+ // the page is still the document it last committed.
1890
+ let committedTopUrl;
1891
+ /** A TargetInfo of the page's own target names the URL the page last committed: the target list, and each report. */
1892
+ const notePageTarget = (info) => {
1893
+ if (pageTargetId !== undefined && info?.targetId === pageTargetId && info?.type === 'page' && typeof info.url === 'string' && info.url) {
1894
+ committedTopUrl = info.url.split('#')[0];
1895
+ }
1896
+ };
1877
1897
  // prepare({ tokenizeBeforePay: true }) sends the card Microform whose field documents are committed
1878
1898
  // now. A field's commit can come before this attachment enabled its session, so no event reports
1879
1899
  // it; each iframe session's own frame tree, and the page's, is read once first (as rereadMicroforms
@@ -1908,7 +1928,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1908
1928
  const frame = find(tree?.frameTree, 0);
1909
1929
  if (!frame)
1910
1930
  return null;
1911
- return { navigation: frame.parentId || sessionId !== pageSessionId ? 'frame' : 'top', url: typeof frame.url === 'string' ? frame.url : undefined };
1931
+ return { navigation: frame.parentId || (sessionId !== pageSessionId && !(sessionId && windowTargets.has(sessionId))) ? 'frame' : 'top',
1932
+ url: typeof frame.url === 'string' ? frame.url : undefined };
1912
1933
  };
1913
1934
  /**
1914
1935
  * Which frame a paused document request navigates, as documentFrame reads it, without the frame tree where
@@ -1924,6 +1945,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1924
1945
  return 'frame';
1925
1946
  if (sessionId === pageSessionId && pageTargetId !== undefined && frameId === pageTargetId)
1926
1947
  return 'top';
1948
+ // An adopted window's main frame is that window's own top-level document, and its frame tree is as unanswered.
1949
+ if (sessionId && windowTargets.get(sessionId) === frameId)
1950
+ return 'top';
1927
1951
  return (await documentFrame(sessionId, frameId))?.navigation ?? null;
1928
1952
  };
1929
1953
  /**
@@ -2117,6 +2141,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2117
2141
  let lastSubmitted = null;
2118
2142
  // The approved Datatrans answers, per frameId: the repeat rule (datatrans.ts).
2119
2143
  const datatransRepeats = new DatatransRepeats();
2144
+ // The approved TokenEx iframe answers, per frameId (the card-number frame is an out-of-process iframe whose own
2145
+ // session pauses its post; its frameId names it): the repeat rule (tokenex-iframe.ts).
2146
+ const tokenexIframeRepeats = new TokenexIframeRepeats();
2120
2147
  // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
2121
2148
  const worldlineProducts = new WorldlineMyCheckoutProducts();
2122
2149
  // The Shiji STS getCvvId companion of the getTokenId this attachment last continued (shiji-sts.ts).
@@ -2161,11 +2188,14 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2161
2188
  // EBANX's 3-D Secure calls and hosted confirm, which send the typed card again
2162
2189
  // after its token: the cardholder's device sends one the approved token request
2163
2190
  // covers, and any other is refused (see ebanx.ts).
2191
+ // ComGate's card hosts are armed only for an org the API served ComGate to, so the
2192
+ // gate can fail every card post there while the gateway link pauses (see comgate.ts).
2164
2193
  const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
2165
2194
  ...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN, ...merchantPatterns,
2166
2195
  ...(recognizesGmo(opts.vault) ? GMO_TOKEN_V1_PATTERNS : []), ...juspayCheckout.urlPatterns(), ...cardEndpointPatterns(),
2167
2196
  ...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS, ...PAYPAL_CARD_FORM_PATTERNS,
2168
- ...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS])];
2197
+ ...CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, ...(recognizesPaddleVgs(opts.vault) ? PADDLE_VGS_PAY_PATTERNS : []), ...EBANX_RESEND_PATTERNS,
2198
+ ...(servesComgate(opts.vault) ? COMGATE_CARD_PATTERNS : [])])];
2169
2199
  // The hold-only rules' patterns (Bolt's tokenizer; every request a placeholder card can ride, by resource type).
2170
2200
  const fetchPatterns = [...urlPatterns.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
2171
2201
  ...(holdGate ? holdGate.patterns().map((p) => ({ ...p, requestStage: 'Request' })) : [])];
@@ -2294,6 +2324,55 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2294
2324
  lifecycle.failed(new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, 'browser_interception_unavailable'));
2295
2325
  opts.onEvent?.({ type: 'failed', detail: 'browser_interception_unavailable' });
2296
2326
  };
2327
+ /**
2328
+ * A window the merchant's page opened (window.open, or a form or link aimed at a new window), or one such a window
2329
+ * opened in turn, in the merchant's own browser context: it belongs to this checkout, so it is attached and armed
2330
+ * as the page is, and its requests reach the same gates. Chromium reports the window (Target.targetCreated, with its
2331
+ * openerId) as it starts, and the page session's auto-attach never covers a new page target. The attach races the
2332
+ * window's first request: a window that opens on the merchant's page and is sent on to a hosted page by script or
2333
+ * by the merchant's redirect is armed before that entry; a window opened straight onto a hosted page can load it
2334
+ * first, as every window did before. A window reported while this attachment still arms the page waits in
2335
+ * pendingWindows and is adopted once the page is armed, inside the setup deadline and before the attachment reports
2336
+ * ready, its opener judged then, so a window that window opened is adopted after it. A window that is gone before it
2337
+ * could be attached was never armed, as before; one attached but not armed fails the attachment, as a frame does.
2338
+ */
2339
+ const openedWindows = new Set();
2340
+ const pendingWindows = [];
2341
+ const adoptOpenedWindow = async (info, duringSetup = false) => {
2342
+ if (!info || info.type !== 'page' || typeof info.targetId !== 'string' || !info.targetId || info.targetId === pageTargetId
2343
+ || openedWindows.has(info.targetId) || info.browserContextId !== merchantContext)
2344
+ return;
2345
+ if (setupStop.signal.aborted)
2346
+ return;
2347
+ if (!attachmentReady && !duringSetup) {
2348
+ pendingWindows.push(info);
2349
+ return;
2350
+ }
2351
+ if (typeof info.openerId !== 'string' || !(info.openerId === pageTargetId || openedWindows.has(info.openerId)))
2352
+ return;
2353
+ openedWindows.add(info.targetId);
2354
+ let child;
2355
+ try {
2356
+ child = (await cdp.send('Target.attachToTarget', { targetId: info.targetId, flatten: true }))?.sessionId;
2357
+ }
2358
+ catch {
2359
+ return;
2360
+ }
2361
+ try {
2362
+ if (typeof child !== 'string' || !child || ownedSessions.has(child) || detachedSessions.has(child) || rejectedSessions.has(child)) {
2363
+ throw new CheckoutAttachmentError('unavailable');
2364
+ }
2365
+ ownedSessions.add(child);
2366
+ sessionTypes.set(child, 'page');
2367
+ sessionParents.set(child, pageSessionId);
2368
+ windowTargets.set(child, info.targetId);
2369
+ await arm(child);
2370
+ opts.onEvent?.({ type: 'window_armed', detail: {} });
2371
+ }
2372
+ catch (error) {
2373
+ failInterception(error);
2374
+ }
2375
+ };
2297
2376
  cdp.on(async (method, params, sessionId) => {
2298
2377
  // A frame that committed a new document drops the companion binding made for the getTokenId its
2299
2378
  // old document sent, with the device's CVV ciphertext: the document that replaced it never
@@ -2304,6 +2383,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2304
2383
  shijiCompanions.releasePage((page) => page === replaced);
2305
2384
  }
2306
2385
  if ((method === 'Target.targetCreated' || method === 'Target.targetInfoChanged') && targetContextKnown) {
2386
+ // The page's own target reports each document it commits from discovery on, Page events or not.
2387
+ notePageTarget(params?.targetInfo);
2307
2388
  try {
2308
2389
  assertContextTarget(params?.targetInfo, merchantContext, targetAllowance);
2309
2390
  if (params?.targetInfo?.type === 'shared_worker')
@@ -2312,8 +2393,18 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2312
2393
  catch (error) {
2313
2394
  failInterception(error);
2314
2395
  }
2396
+ if (method === 'Target.targetCreated')
2397
+ await adoptOpenedWindow(params?.targetInfo);
2315
2398
  return;
2316
2399
  }
2400
+ // The page's main frame committed a document, or moved within it: the URL a held top-level navigation leaves.
2401
+ if (sessionId === pageSessionId && method === 'Page.frameNavigated' && !params.frame?.parentId && typeof params.frame?.url === 'string') {
2402
+ committedTopUrl = params.frame.url;
2403
+ }
2404
+ if (sessionId === pageSessionId && method === 'Page.navigatedWithinDocument' && pageTargetId !== undefined
2405
+ && params?.frameId === pageTargetId && typeof params?.url === 'string') {
2406
+ committedTopUrl = params.url.split('#')[0];
2407
+ }
2317
2408
  if (((method === 'Page.frameNavigated' && !params.frame?.parentId) || (method === 'Page.navigatedWithinDocument' && preparationFrameId && params.frameId === preparationFrameId)) && sessionId === pageSessionId) {
2318
2409
  preparationGate.invalidate('merchant_document_changed');
2319
2410
  mercadoCheckout.invalidate();
@@ -2353,10 +2444,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2353
2444
  }
2354
2445
  // A watched shared worker sent a card request: it went out unpaused, so nothing is approved from here on.
2355
2446
  if (method === 'Network.requestWillBeSent' && sessionId && watchedWorkerSessions.has(sessionId) && sharedWorkerCardRequest(params?.request)) {
2447
+ // Origin and path as redactUrl reads them, so a credential a processor writes into the path never shows.
2356
2448
  let endpoint = '';
2357
2449
  try {
2358
- const u = new URL(params.request.url);
2359
- endpoint = `${u.origin}${u.pathname}`;
2450
+ new URL(params.request.url);
2451
+ endpoint = redactUrl(params.request.url);
2360
2452
  }
2361
2453
  catch { /* unreadable */ }
2362
2454
  opts.onEvent?.({ type: 'card_request_unwatched', detail: { endpoint, from: 'shared_worker' } });
@@ -2641,6 +2733,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2641
2733
  const { requestId, request, resourceType, networkId, frameId } = params;
2642
2734
  // When this request paused: the merchant's total is read as of this moment.
2643
2735
  const pausedAt = Date.now();
2736
+ // The page's own top-level navigation, paused: its frame tree cannot be read until this request is decided, so its
2737
+ // document is the one it last committed (committedTopUrl). Every other request reads the frame tree as before.
2738
+ const readPausedDocument = resourceType === 'Document' && sessionId === pageSessionId && pageTargetId !== undefined
2739
+ && frameId === pageTargetId && committedTopUrl !== undefined ? async () => committedTopUrl : readDocumentUrl;
2644
2740
  // A paused document request has not committed either: it only starts the context's check.
2645
2741
  if (resourceType === 'Document')
2646
2742
  microforms.warm(withFragment(request));
@@ -2723,6 +2819,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2723
2819
  cybersourceFlex: { judge: (flexRequest) => microforms.judge(flexRequest, frameId, () => rereadMicroforms(sessionId, frameId)) },
2724
2820
  juspay: juspayCheckout,
2725
2821
  datatrans: datatransRepeats,
2822
+ tokenexIframe: tokenexIframeRepeats,
2726
2823
  ebanx: ebanxFollowUps,
2727
2824
  frame: typeof frameId === 'string' && frameId ? frameId : undefined,
2728
2825
  payrails,
@@ -2794,7 +2891,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2794
2891
  if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()
2795
2892
  && typeof networkId === 'string' && networkId) {
2796
2893
  const retryBody = pausedBody(request);
2797
- const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape, request.headers);
2894
+ const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readPausedDocument, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape, request.headers);
2798
2895
  // A prepared approval is answered only in the card frame it was bound in: the same request from
2799
2896
  // another frame (a replacement for a removed one, or a second card frame) is refused below.
2800
2897
  if (retry && activeRequest?.attempt.lost && sameCheckoutRequest(activeRequest.identity, retry)
@@ -2948,7 +3045,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2948
3045
  await preparationGate.assertDocument();
2949
3046
  // One bounded read of the document serves both the merchant origin and
2950
3047
  // the identity a retry has to repeat (see sameCheckoutRequest).
2951
- const documentUrl = await documentUrlOf(readDocumentUrl, attempt.signal);
3048
+ const documentUrl = await documentUrlOf(readPausedDocument, attempt.signal);
2952
3049
  const pageOrigin = pageOriginFrom(documentUrl);
2953
3050
  // The agent's amount is a card endpoint's only authority: no page total is read.
2954
3051
  const pageAmount = cardEndpoint ? undefined : await pageAmountOf(opts);
@@ -3240,6 +3337,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
3240
3337
  }, deliverOn);
3241
3338
  // The frame that received it: the paused request's, or its retry's once rebound.
3242
3339
  datatransRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
3340
+ tokenexIframeRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
3243
3341
  attempt.assertLive();
3244
3342
  // An ACI card-number request answered: its checkout's held CVC and expiry requests get the
3245
3343
  // same answer, and its /payment submission may continue (see AciGate).
@@ -3338,6 +3436,11 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
3338
3436
  targetContextKnown = true;
3339
3437
  // A page target's id is its main frame's id: a navigation of that frame is the page's own (pausedNavigation).
3340
3438
  pageTargetId = typeof targetInfo.targetId === 'string' && targetInfo.targetId ? targetInfo.targetId : undefined;
3439
+ // The page's URL as its target reports it: the document a held top-level navigation leaves until the page commits
3440
+ // another one (the Page.frameNavigated branch above, and notePageTarget for a commit before the Page events are on).
3441
+ // No frame tree read: a navigation the page starts while this attachment arms would hold it.
3442
+ if (committedTopUrl === undefined && typeof targetInfo.url === 'string' && targetInfo.url)
3443
+ committedTopUrl = targetInfo.url.split('#')[0];
3341
3444
  await cdp.send('Target.setDiscoverTargets', { discover: true });
3342
3445
  assertActive();
3343
3446
  const { targetInfos } = await cdp.send('Target.getTargets');
@@ -3346,6 +3449,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
3346
3449
  throw new CheckoutAttachmentError('unavailable');
3347
3450
  for (const target of targetInfos)
3348
3451
  assertContextTarget(target, merchantContext, targetAllowance);
3452
+ // The page may have committed another document since its TargetInfo was read; the list names the one it holds.
3453
+ for (const target of targetInfos)
3454
+ notePageTarget(target);
3349
3455
  for (const target of targetInfos)
3350
3456
  if (target?.type === 'shared_worker')
3351
3457
  await watchSharedWorker(target);
@@ -3354,13 +3460,24 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
3354
3460
  // but their Fetch setup finishes later. Do not invite the caller to press
3355
3461
  // Pay while one of those already-running frames is still unprotected, or
3356
3462
  // while a shared worker reported during setup is not yet watched.
3357
- while (arming.size || workerWatches.size)
3358
- await Promise.all([...arming.values(), ...workerWatches.values()]);
3463
+ // A window the page opened while this attachment armed it is attached and armed here too, in the order Chromium
3464
+ // reported it (adoptOpenedWindow), and the frames and workers such a window brings are waited for as the page's
3465
+ // own are, all under the setup deadline. One that cannot be armed fails the attachment.
3466
+ while (arming.size || workerWatches.size || pendingWindows.length) {
3467
+ if (arming.size || workerWatches.size)
3468
+ await Promise.all([...arming.values(), ...workerWatches.values()]);
3469
+ else
3470
+ await adoptOpenedWindow(pendingWindows.shift(), true);
3471
+ assertActive();
3472
+ }
3359
3473
  assertActive();
3360
3474
  }, setupTimeoutMs, setupStop.signal);
3361
3475
  if (setupStop.signal.aborted)
3362
3476
  throw attachmentFailure(setupStop.signal.reason);
3363
3477
  attachmentReady = true;
3478
+ // A window reported after that drain, before this line, is adopted the way a later window is.
3479
+ for (const info of pendingWindows.splice(0))
3480
+ void adoptOpenedWindow(info);
3364
3481
  opts.onEvent?.({ type: 'fetch_armed', detail: { patterns: urlPatterns, ...(holdGate ? { hold: true, holdRules: holdGate.rules.map((r) => r.psp) } : {}), ...(bypassServiceWorkers ? { serviceWorkers: 'bypass' } : {}) } });
3365
3482
  }
3366
3483
  catch (error) {
@@ -3700,6 +3817,8 @@ export async function attachToPlaywright(page, opts) {
3700
3817
  let lastSubmitted = null;
3701
3818
  // The approved Datatrans answers, per Frame: the repeat rule (datatrans.ts).
3702
3819
  const datatransRepeats = new DatatransRepeats();
3820
+ // The approved TokenEx iframe answers, per Frame: the repeat rule (tokenex-iframe.ts).
3821
+ const tokenexIframeRepeats = new TokenexIframeRepeats();
3703
3822
  // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
3704
3823
  const worldlineProducts = new WorldlineMyCheckoutProducts();
3705
3824
  const frameOf = (request) => { try {
@@ -3720,8 +3839,10 @@ export async function attachToPlaywright(page, opts) {
3720
3839
  // The matcher sees only the URL. Worldpay eProtect's registration is a GET, so its family and
3721
3840
  // the vendor's give-up reports are routed by the eProtect gate (never its scripts).
3722
3841
  // Gravy's session fields are a PUT, so a URL a PUT recognizer claims is routed too; the
3723
- // pre-claim gates then pause only the PUT and continue the URL's other methods.
3842
+ // pre-claim gates then pause only the PUT and continue the URL's other methods. A hosted page
3843
+ // entered with a plain GET (hosted-entry.ts, ComGate's gateway link) is routed by its exact link.
3724
3844
  (url) => opts.vault.isCardRequest(url.toString()) || opts.vault.isCardRequest(url.toString(), 'PUT')
3845
+ || opts.vault.isCardRequest(url.toString(), 'GET')
3725
3846
  || guards.matches(url.toString()) || stripeCheckout.matches(url.toString()) || mercadoCheckout.matches(url.toString())
3726
3847
  || eprotect.matches(url.toString())
3727
3848
  // An armed merchant profile's endpoint or a sibling of it (merchant-hosted.ts), as attachToCdp arms it.
@@ -3739,9 +3860,16 @@ export async function attachToPlaywright(page, opts) {
3739
3860
  || aci.matches(url.toString())
3740
3861
  // Every Payrails host, routed only for an org the API served Payrails to (see attachToCdp).
3741
3862
  || (payrails !== null && isPayrailsHost(url.toString()))
3863
+ // Every request to Przelewy24's pay wall hosts once its entry is served (przelewy24.ts), as attachToCdp arms
3864
+ // the hosts: the card method's post, the card form's post and the Saferpay resolver reach the Przelewy24
3865
+ // gate and fail.
3866
+ || isArmedPrzelewy24Url(opts.vault, url.toString())
3742
3867
  // Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
3743
3868
  // host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
3744
3869
  || isArmedEkashuUrl(opts.vault, url.toString())
3870
+ // Every request to a Webpay host once its entry is served (transbank.ts), as attachToCdp arms the hosts: the
3871
+ // token's submit and Webpay's card steps reach the Webpay gate and fail, and the entry pauses.
3872
+ || isArmedTransbankUrl(opts.vault, url.toString())
3745
3873
  // A CreditGuard page's own calls beside its submit (its card lookup and its 3-D Secure calls), for an
3746
3874
  // org the API served CreditGuard to, as attachToCdp arms its hosts: the CreditGuard gate answers them
3747
3875
  // under a ready preparation and lets them go on with none (pre-claim.ts isArmedCreditguardCall).
@@ -3752,6 +3880,12 @@ export async function attachToPlaywright(page, opts) {
3752
3880
  // Every request to QuickPay's host once its entry is served (quickpay.ts), as attachToCdp arms the host:
3753
3881
  // the window's card post, its other card requests and its card lookup reach the QuickPay gate and fail.
3754
3882
  || isArmedQuickpayUrl(opts.vault, url.toString())
3883
+ // Every request to one of ComGate's card hosts once its entry is served (comgate.ts), as attachToCdp arms them:
3884
+ // the gateway's card post and every other card call there reach the ComGate gate and fail.
3885
+ || isArmedComgateUrl(opts.vault, url.toString())
3886
+ // Every request on the TokenEx iframe's /iframe/v3 path once its entry is served (tokenex-iframe.ts), as attachToCdp
3887
+ // arms its hosts: a card post the recognizer's exact URL does not match reaches the TokenEx gate and fails.
3888
+ || isArmedTokenexIframeUrl(opts.vault, url.toString())
3755
3889
  // PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
3756
3890
  || isPaypalHostedFieldsConfirm(url.toString())
3757
3891
  // PayPal's Card Fields confirm and GraphQL on www.paypal.com, for every org, as attachToCdp arms them (paypal-card-forms.ts).
@@ -3857,6 +3991,7 @@ export async function attachToPlaywright(page, opts) {
3857
3991
  } },
3858
3992
  juspay: juspayCheckout,
3859
3993
  datatrans: datatransRepeats,
3994
+ tokenexIframe: tokenexIframeRepeats,
3860
3995
  ebanx: ebanxFollowUps,
3861
3996
  frame: frameOf(request),
3862
3997
  payrails,
@@ -4243,6 +4378,7 @@ export async function attachToPlaywright(page, opts) {
4243
4378
  }
4244
4379
  await deliverTo.fulfill({ status: replay.status, headers: withCorsHeaders(replay.headers, cors.headers), body: replay.body });
4245
4380
  datatransRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
4381
+ tokenexIframeRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
4246
4382
  assertRequestLive();
4247
4383
  // See the same step in attachToCdp.
4248
4384
  aci.answered(deliverRequest.url(), { status: replay.status, headers: replay.headers, body: replay.body }, replay.authorizationId);
package/dist/client.d.ts CHANGED
@@ -242,6 +242,13 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
242
242
  * checkout relay. Served on the same terms: only to a build that lists it, for a company
243
243
  * Agentcard turned Payfast on for.
244
244
  *
245
+ * `przelewy24_hosted_payment_page`: Przelewy24's pay wall, whose card-free link
246
+ * (secure.przelewy24.pl/trnRequest/<token>, a plain GET the merchant's redirect or script sends
247
+ * the buyer to) this build pauses for the cardholder's device by its exact link (hosted-entry.ts),
248
+ * and whose card method post, card form post and Saferpay resolver this build never lets the
249
+ * agent's browser send (pre-claim.ts, przelewy24.ts). Served on the same terms as NMI's and
250
+ * Redsys's: only to a build that lists it, for a company Agentcard turned Przelewy24 on for.
251
+ *
245
252
  * `wix_payments_tokenize`: the card request Wix Cashier's card frame sends to Wix's
246
253
  * tokenizer (cashier-pci.wixapps.net), only through a prepared checkout: Wix's payment
247
254
  * widget fails the payment 30 seconds after it asks the frame for a token. The
@@ -258,6 +265,15 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
258
265
  * the checkout relay. Served on the same terms: only to a build that lists it, for a company
259
266
  * Agentcard turned PayMongo on for.
260
267
  *
268
+ * `gopay_card_form`: the card post GoPay's card frame sends to its own URL
269
+ * (secure.gopay.com/gp-card-comm/h/<token>) inside the GoPay gateway a shop opens. This build pauses
270
+ * only the frame's own card post and answers it with GoPay's answer as the API recorded it, the
271
+ * empty JSON object the frame reads; the gateway then reads the payment's end from GoPay and runs
272
+ * 3-D Secure in the agent's browser when the bank asks for it. The cardholder's device sends the card
273
+ * through the checkout relay. Served only to a build that lists it, and only for Agentcard's proof
274
+ * company until a reviewed change binds GoPay's payee and amount to the approval: the post names
275
+ * neither. The API takes it only at a shop Agentcard reviewed.
276
+ *
261
277
  * `hipay_vault_token`: the token create HiPay's hosted fields send to HiPay's Secure Vault
262
278
  * (secure2-vault.hipay-tpp.com, stage-secure2-vault for a test row), only through a prepared
263
279
  * checkout: the merchant's page gives the frame 20 seconds after Pay. The cardholder's device
@@ -279,6 +295,84 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
279
295
  * Secure step and card lookup this build never lets the agent's browser send (pre-claim.ts,
280
296
  * quickpay.ts). Served on the same terms as NMI's and Redsys's: only to a build that lists it, for a
281
297
  * company Agentcard turned QuickPay on for.
298
+ *
299
+ * `revolut_card_field`: the card payment Revolut Business's card field or card popup sends from
300
+ * its frame to /api/public/orders/<order token>/payments on merchant.revolut.com, on a merchant's
301
+ * own page or Revolut's hosted checkout. This build pauses only the frame's card payment and
302
+ * answers it with Revolut's answer as the API recorded it: the payment Revolut created, which the
303
+ * frame then follows, through any 3-D Secure step, in the agent's browser. A payment by Apple Pay,
304
+ * Google Pay or Revolut Pay on the same URL carries no card and continues untouched. Revolut's
305
+ * host answers the Vault's origin, so the cardholder's device sends the card itself, with no relay.
306
+ * Served on the same terms: only to a build that lists it, for a company Agentcard turned Revolut
307
+ * on for.
308
+ *
309
+ * `pay_nl_card_token`: the card token request a shop's card form sends to Pay. (pay.nl),
310
+ * Weeztix's sandboxed frame posting its FormData to payment.pay.nl/v1/Payment/getCardToken/json.
311
+ * This build pauses only the form's own multipart body at that exact URL; the cardholder's
312
+ * device sends the card through the checkout relay, and the frame gets the token answer
313
+ * payment-core's judge rebuilt, with the CORS answer for the frame's own origin, null. The
314
+ * request names no amount, so the agent's pair, above zero, is the one the cardholder approves
315
+ * (PAY_NL_PSP below). Served on the same terms: only to a build that lists it, for a company
316
+ * Agentcard turned Pay. on for.
317
+ *
318
+ * `paypal_card_fields`: PayPal Card Fields' confirm, the POST its v5 frame sends to
319
+ * www.paypal.com and its v6 frame to api-m.paypal.com. This build pauses it only in
320
+ * the frames' own shape, refuses one that asks PayPal to keep the card or forces
321
+ * 3-D Secure in the browser with a `blocked` event, and answers the frame with the
322
+ * order's id and status as the API recorded them. The device sends a v5 confirm
323
+ * through the checkout relay and a v6 one directly. Served on the same terms: only
324
+ * to a build that lists it, for a company Agentcard turned PayPal Card Fields on
325
+ * for. Every other company keeps the checkout guard's refusal (paypal-card-forms.ts).
326
+ *
327
+ * `comgate_hosted_payment_page`: ComGate's payment gateway, whose card-free gateway link
328
+ * (payN.comgate.cz/init?id=<transaction id>, a plain GET) this build pauses for the
329
+ * cardholder's device by its exact link (hosted-entry.ts), in the page, in a frame of it or
330
+ * in a window it opened, and whose card hosts this build never lets the agent's browser post
331
+ * to (pre-claim.ts, comgate.ts). Served on the same terms as NMI's and Redsys's: only to a
332
+ * build that lists it, for a company Agentcard turned ComGate on for.
333
+ *
334
+ * `makecommerce_card_frame`: the card post MakeCommerce's card frame sends to its own card endpoint
335
+ * (cc.maksekeskus.ee/card-payment) under the shop's publishable key. This build pauses only the
336
+ * frame's own one-time card payment and answers it with MakeCommerce's answer as the API recorded
337
+ * it: the URL of MakeCommerce's 3-D Secure page, which the frame then opens in the agent's browser.
338
+ * The cardholder's device sends the card through the checkout relay. Served on the same terms: only
339
+ * to a build that lists it, for a company Agentcard turned MakeCommerce on for.
340
+ *
341
+ * `poynt_collect_nonce`: GoDaddy Payments' card request, the POST Poynt Collect's
342
+ * card frame sends to a store's generate-nonce on services.poynt.net. This build
343
+ * pauses it only as the frame writes it, refuses a body that asks Poynt to keep the
344
+ * card in the browser with a `blocked` event, and answers the frame with the nonce as
345
+ * the API recorded it. The device sends the card through the checkout relay. Served
346
+ * on the same terms: only to a build that lists it, for a company Agentcard turned
347
+ * GoDaddy Payments on for.
348
+ *
349
+ * `tokenex_iframe_tokenize`: the card post TokenEx's hosted iframe v3 sends from its card-number frame
350
+ * to its own document URL on htp.tokenex.com (JetBlue's card form). This build pauses only the PCI frame's
351
+ * own card post with its CVV frame, refuses in the browser every other post on that path that carries a
352
+ * card (tokenex-iframe.ts), answers it with TokenEx's token as the API recorded it, rebuilt from
353
+ * payment-core's allow-list, and answers the same frame's byte-identical repeat from that answer while the
354
+ * approval is being handed off. Served on the same terms: only to a build that lists it, for a company
355
+ * Agentcard turned the TokenEx iframe on for.
356
+ *
357
+ * `transbank_webpay_hosted_payment_page`: Transbank's Webpay Plus, whose card-free entry
358
+ * (webpay3g.transbank.cl/webpayserver/initTransaction?token_ws=<token>, the merchant's
359
+ * redirect, a plain GET) this build pauses for the cardholder's device by its exact link
360
+ * (hosted-entry.ts) in the page or a frame of it, and with attachToCdp also in a window the
361
+ * page opened, and whose hosts this build never lets the agent's browser post to (pre-claim.ts,
362
+ * transbank.ts), so the token is never spent there. attachToPlaywright covers the page it is
363
+ * attached to and that page's frames only, and pauses only a direct or scripted navigation to
364
+ * the entry: its page.route sees no HTTP redirect hop, so a merchant's redirect needs attachToCdp.
365
+ * Served on the same terms as NMI's and Redsys's: only to a build that lists it, for a company
366
+ * Agentcard turned Webpay on for.
367
+ *
368
+ * `sumup_checkout_process`: the card request SumUp's Payment Widget sends from its hidden form frame
369
+ * on gateway.sumup.com, a PUT to https://api.sumup.com/v0.2/checkouts/<checkout id>. This build pauses
370
+ * that PUT as it pauses Gravy's, so only the CDP and Playwright adapters see it (the form frame is
371
+ * cross-origin to the page), and answers the frame with SumUp's own answer as the API recorded it: the
372
+ * paid checkout, or the 3-D Secure step the widget then runs in the agent's browser. The cardholder's
373
+ * device sends the card itself, and the API holds the payment to a merchant Agentcard reviewed, at the
374
+ * amount SumUp holds for the checkout. Served on the same terms: only to a build that lists it, for a
375
+ * company Agentcard turned SumUp on for.
282
376
  */
283
377
  export declare const SUPPORTED_CAPABILITIES: readonly string[];
284
378
  /**
@@ -838,7 +932,10 @@ export interface PlaceholderCard {
838
932
  /**
839
933
  * The placeholder number for a network (lowercase, as GET /api/v2/vault_cards names it) on a
840
934
  * processor's page: the processor's own number for that network when `overrides` names one, else
841
- * the network's default, else undefined for a network with no placeholder.
935
+ * the network's default, else undefined for a network with no placeholder. On a TokenEx iframe
936
+ * (`tokenex_iframe`) the number must also be one TokenEx's frame reads as that network, since the frame
937
+ * names the brand to the page from the typed digits and the cardholder's device refuses a mismatch: the
938
+ * frame's table takes Visa, Mastercard and American Express, so every other network gets undefined.
842
939
  */
843
940
  export declare function placeholderNumber(brand: string, psp?: string, overrides?: Readonly<Record<string, Readonly<Record<string, string>>>>): string | undefined;
844
941
  /**
@@ -861,7 +958,9 @@ export declare class PreparationRequiredError extends Error {
861
958
  * when the merchant's server created the payment), so the `amount` and
862
959
  * `currency` you pass are what the cardholder approves, and for WSPay the
863
960
  * figure the API compares with WSPay's answer when that answer names one (a
864
- * 3-D Secure step names none). Thrown locally before any create or
961
+ * 3-D Secure step names none). A Revolut card payment names the order's
962
+ * amount but not its currency, so it takes the same pair, above zero.
963
+ * Thrown locally before any create or
865
964
  * prompt, and terminal: the same request without an amount fails the same
866
965
  * way. The API answers 400 amount_required for the same case. A page total
867
966
  * (`pageAmount`) does not count.
@@ -1482,7 +1581,9 @@ export declare class VaultClient {
1482
1581
  * The facts hold only while the cardholder pays with that card. `psp` names the processor
1483
1582
  * whose card form the agent types into (the recognizer's name, `worldline_mycheckout`): a
1484
1583
  * processor that needs its own number for the card's network gets it, and every other gets the
1485
- * network's default. Throws PlaceholderCardError when there is no such card, its network has no
1584
+ * network's default. On a TokenEx iframe (`tokenex_iframe`, JetBlue) the number is one TokenEx's frame
1585
+ * reads as the card's own network, Visa, Mastercard or American Express, and any other network has no
1586
+ * placeholder there. Throws PlaceholderCardError when there is no such card, its network has no
1486
1587
  * placeholder, or it carries no expiry.
1487
1588
  */
1488
1589
  placeholderCard(input: {