@agent-cards/sdk 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/PREFLIGHT.md +4 -0
- package/README.md +153 -0
- package/dist/cdp.d.ts +2 -1
- package/dist/cdp.js +161 -31
- package/dist/client.d.ts +97 -1
- package/dist/client.js +172 -5
- package/dist/cybersource-flex.d.ts +183 -0
- package/dist/cybersource-flex.generated.d.ts +92 -0
- package/dist/cybersource-flex.generated.js +689 -0
- package/dist/cybersource-flex.js +275 -0
- package/dist/gmo.d.ts +63 -0
- package/dist/gmo.generated.d.ts +60 -0
- package/dist/gmo.generated.js +586 -0
- package/dist/gmo.js +119 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +8 -3
- package/dist/lifecycle.js +6 -2
- package/dist/pre-claim.d.ts +12 -1
- package/dist/pre-claim.js +135 -9
- package/dist/preflight-catalog.json +116 -0
- package/dist/preflight-schemas.json +8 -0
- package/dist/preflight.generated.js +15 -1
- package/dist/prepared-processor.d.ts +4 -2
- package/dist/prepared-processor.js +12 -2
- package/dist/registry.d.ts +17 -7
- package/dist/stripe-checkout.generated.js +37 -2
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
package/dist/cdp.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
|
|
2
|
-
import { AdyenTestPlatformRefusedError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedModeError, } from './client.js';
|
|
2
|
+
import { AdyenTestPlatformRefusedError, AmountRequiredError, ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, CheckoutCancelledError, MerchantTotalError, PaymentOutcomeUnknownError, PreparationRequiredError, ProcessorRefusedError, UnsupportedCapabilityError, UnsupportedModeError, } from './client.js';
|
|
3
3
|
import { PreparationGate } from './preparation.js';
|
|
4
4
|
import { StripeCheckoutGate } from './stripe-checkout.js';
|
|
5
5
|
import { MercadoCheckoutGate } from './mercado-checkout.js';
|
|
6
6
|
import { MERCADO_CHECKOUT_PATTERN } from './mercado-checkout.generated.js';
|
|
7
7
|
import { dispatchPreClaim, eventUrl } from './pre-claim.js';
|
|
8
|
+
import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
8
9
|
import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
|
|
9
10
|
import { cseBody } from './cse-body.js';
|
|
10
11
|
import { replayTemplatedUrl, reviewedMerchantProfileUrlPatterns } from './merchant-hosted.js';
|
|
@@ -12,6 +13,15 @@ import { MerchantHandoffWatch } from './merchant-handoff.js';
|
|
|
12
13
|
import { MERCHANT_PAGE_WAIT_MS, MerchantTotalWatch } from './merchant-total-watch.js';
|
|
13
14
|
import { hostedFormSubmittedPage } from './hosted-form.js';
|
|
14
15
|
import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
16
|
+
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, refusedRequestFailure } from './cybersource-flex.js';
|
|
17
|
+
/**
|
|
18
|
+
* Fetch.failRequest's errorReason for a paused request the CDP adapter will not let through: the
|
|
19
|
+
* CDP form of refusedRequestFailure ('Failed' for a CyberSource Flex request, so the Microform field
|
|
20
|
+
* reports it to the page; 'Aborted' for every other request, as before).
|
|
21
|
+
*/
|
|
22
|
+
function cdpFailure(url) {
|
|
23
|
+
return refusedRequestFailure(url) === 'failed' ? 'Failed' : 'Aborted';
|
|
24
|
+
}
|
|
15
25
|
// Every URL that leaves this module through onEvent is redacted to origin +
|
|
16
26
|
// path first. A paused PaymentIntent confirm can carry the client secret in
|
|
17
27
|
// its query string (Stripe.js puts it in the body; hand-rolled runtimes and
|
|
@@ -144,7 +154,8 @@ function isApprovalOutcome(err) {
|
|
|
144
154
|
*
|
|
145
155
|
* Terminal means "will answer identically next time no matter who does what":
|
|
146
156
|
* a misconfiguration or an unsupported PSP. Nothing a person does changes
|
|
147
|
-
* those, so asking again is pure waste.
|
|
157
|
+
* those, so asking again is pure waste. A GMO request with no `amount`
|
|
158
|
+
* (AmountRequiredError) is one: the attachment's options do not change.
|
|
148
159
|
*
|
|
149
160
|
* Other errors may be retryable unless the lifecycle holds the attachment
|
|
150
161
|
* for merchant reconciliation, as it does after a processor refusal.
|
|
@@ -161,7 +172,8 @@ function isTerminal(err) {
|
|
|
161
172
|
// Refused at create for Adyen's test platform: the page's next request is refused the same way.
|
|
162
173
|
if (err instanceof AdyenTestPlatformRefusedError)
|
|
163
174
|
return err.stage === 'create';
|
|
164
|
-
return err instanceof CardEncryptedError || err instanceof UnsupportedModeError || err instanceof PreparationRequiredError
|
|
175
|
+
return err instanceof CardEncryptedError || err instanceof UnsupportedModeError || err instanceof PreparationRequiredError
|
|
176
|
+
|| err instanceof AmountRequiredError || err instanceof UnsupportedCapabilityError;
|
|
165
177
|
}
|
|
166
178
|
/**
|
|
167
179
|
* cseBody, retiring a merchant-hosted approval it refuses. The refusal (a live body
|
|
@@ -734,12 +746,26 @@ function bodyShape(body) {
|
|
|
734
746
|
}
|
|
735
747
|
return { shape: `raw:${body}`, namesAmount: false };
|
|
736
748
|
}
|
|
737
|
-
/**
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
749
|
+
/**
|
|
750
|
+
* The identity of a request from its parts; see CheckoutRequestIdentity. A CyberSource Flex
|
|
751
|
+
* body is a fresh JWE every time the card field seals, and it names no amount, so its shape is
|
|
752
|
+
* the one the Flex gate read from the field iframe (cybersource-flex.ts: microform type, fields,
|
|
753
|
+
* the capture context's key id and the context itself), never the ciphertext.
|
|
754
|
+
*/
|
|
755
|
+
function requestIdentity(opts, url, method, body, documentUrl, pageAmount, flexShape) {
|
|
756
|
+
const shaped = flexShape !== undefined ? { shape: flexShape, namesAmount: false } : bodyShape(body);
|
|
757
|
+
// A GMO token is minted for the one shop its request names, and the body's shape leaves that value
|
|
758
|
+
// out, so the shop joins the identity: a retry that names another shop needs its own approval.
|
|
759
|
+
const shop = gmoRequestShop(url, body);
|
|
760
|
+
return { url, method, documentUrl, pageAmount, body: shop === undefined ? shaped.shape : `${shaped.shape};gmo_shop:${JSON.stringify(shop)}`,
|
|
741
761
|
amountEvidence: opts.amount != null || pageAmount !== undefined || shaped.namesAmount };
|
|
742
762
|
}
|
|
763
|
+
/** A CDP frame or request URL with its fragment, which CDP reports apart (urlFragment). */
|
|
764
|
+
function withFragment(value) {
|
|
765
|
+
if (typeof value?.url !== 'string')
|
|
766
|
+
return undefined;
|
|
767
|
+
return value.url + (typeof value.urlFragment === 'string' ? value.urlFragment : '');
|
|
768
|
+
}
|
|
743
769
|
/**
|
|
744
770
|
* May this request be answered from the approval the other one raised?
|
|
745
771
|
*
|
|
@@ -1115,6 +1141,39 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1115
1141
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
1116
1142
|
const stripeCheckout = new StripeCheckoutGate(opts);
|
|
1117
1143
|
const mercadoCheckout = new MercadoCheckoutGate();
|
|
1144
|
+
// CyberSource Flex card field iframes and their capture contexts, recorded from the CDP
|
|
1145
|
+
// events that report a frame's committed document with its fragment (cybersource-flex.ts).
|
|
1146
|
+
const microforms = new MicroformFrames();
|
|
1147
|
+
/**
|
|
1148
|
+
* One bounded read of a session's frame tree, for a Flex request whose frame is not on record:
|
|
1149
|
+
* records every frame it finds and resolves to the committed URL of `frameId`, or undefined
|
|
1150
|
+
* when that frame is not in the tree.
|
|
1151
|
+
*/
|
|
1152
|
+
const rereadMicroforms = async (sessionId, frameId) => {
|
|
1153
|
+
if (!sessionId)
|
|
1154
|
+
return undefined;
|
|
1155
|
+
let timer;
|
|
1156
|
+
try {
|
|
1157
|
+
const tree = await Promise.race([cdp.send('Page.getFrameTree', {}, sessionId),
|
|
1158
|
+
new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error('frame_tree_unavailable')), 1_000); })]);
|
|
1159
|
+
let issuing;
|
|
1160
|
+
const walk = (node, depth) => {
|
|
1161
|
+
if (!node?.frame || depth > 16)
|
|
1162
|
+
return;
|
|
1163
|
+
const url = withFragment(node.frame);
|
|
1164
|
+
microforms.observe(node.frame.id, url);
|
|
1165
|
+
if (node.frame.id === frameId)
|
|
1166
|
+
issuing = url;
|
|
1167
|
+
for (const child of Array.isArray(node.childFrames) ? node.childFrames : [])
|
|
1168
|
+
walk(child, depth + 1);
|
|
1169
|
+
};
|
|
1170
|
+
walk(tree?.frameTree, 0);
|
|
1171
|
+
return issuing;
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
clearTimeout(timer);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1118
1177
|
let preparationFrameId;
|
|
1119
1178
|
const readDocumentUrl = async () => {
|
|
1120
1179
|
const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
|
|
@@ -1293,8 +1352,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1293
1352
|
// that the vault names no profile for.
|
|
1294
1353
|
const merchantPatterns = [...reviewedMerchantProfileUrlPatterns(),
|
|
1295
1354
|
...(typeof opts.vault?.merchantProfileUrlPatterns === 'function' ? opts.vault.merchantProfileUrlPatterns() : [])];
|
|
1355
|
+
// GMO token v1's JSONP GET is armed only when the synced registry recognizes
|
|
1356
|
+
// GMO (an org the API served the dark entry to), so the gate can refuse it;
|
|
1357
|
+
// for every other org it is never intercepted, as before (see gmo.ts).
|
|
1296
1358
|
const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
|
|
1297
|
-
...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN, ...merchantPatterns
|
|
1359
|
+
...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN, ...merchantPatterns,
|
|
1360
|
+
...(recognizesGmo(opts.vault) ? GMO_TOKEN_V1_PATTERNS : [])])];
|
|
1298
1361
|
const arm = async (sessionId, resume = false, worker = false) => {
|
|
1299
1362
|
const key = sessionId ?? '__root__';
|
|
1300
1363
|
if (armed.has(key))
|
|
@@ -1421,6 +1484,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1421
1484
|
// Its Fetch events, children and commands remain permanently ignored.
|
|
1422
1485
|
if (sessionDetached(sessionId) && !matchesActive)
|
|
1423
1486
|
return;
|
|
1487
|
+
// A frame's document request carries its URL fragment (for an out-of-process card field
|
|
1488
|
+
// iframe, on the PARENT session before the child target exists), but a navigation can be
|
|
1489
|
+
// stopped before it commits: it only starts the capture context's check. The frame is
|
|
1490
|
+
// recorded once its document commits (Page.frameNavigated below).
|
|
1491
|
+
if (params.type === 'Document')
|
|
1492
|
+
microforms.warm(withFragment(params.request));
|
|
1424
1493
|
const key = requestKey(sessionId, params.requestId);
|
|
1425
1494
|
const record = { sessionId, networkId: params.requestId, request: params.request };
|
|
1426
1495
|
if (sessionDetached(sessionId) || retiredSources.has(key) || hasRetiredSummary(owner, params.requestId))
|
|
@@ -1446,6 +1515,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1446
1515
|
}
|
|
1447
1516
|
if (sessionDetached(sessionId))
|
|
1448
1517
|
return;
|
|
1518
|
+
if (method === 'Page.frameNavigated') {
|
|
1519
|
+
// A child frame committed a document: an out-of-process iframe reports its own on its
|
|
1520
|
+
// session, an in-process one on its parent's (the page's top frame is handled above).
|
|
1521
|
+
microforms.observe(params.frame?.id, withFragment(params.frame));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1449
1524
|
if (method === 'Network.loadingFinished') {
|
|
1450
1525
|
const key = requestKey(sessionId, params.requestId);
|
|
1451
1526
|
requestSources.delete(key);
|
|
@@ -1507,6 +1582,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1507
1582
|
return;
|
|
1508
1583
|
}
|
|
1509
1584
|
if (method === 'Target.detachedFromTarget' || method === 'Inspector.detached' || method === 'Page.frameDetached') {
|
|
1585
|
+
// A frame that moves to its own process ('swap') keeps its document; any other detach ends it.
|
|
1586
|
+
if (!(method === 'Page.frameDetached' && params.reason === 'swap')) {
|
|
1587
|
+
microforms.forget(method === 'Page.frameDetached' ? params.frameId : method === 'Target.detachedFromTarget' ? params.targetId : undefined);
|
|
1588
|
+
}
|
|
1510
1589
|
if (activeRequest && (method === 'Target.detachedFromTarget' ? activeRequest.sessionId === params.sessionId
|
|
1511
1590
|
: method === 'Inspector.detached' ? activeRequest.sessionId === sessionId
|
|
1512
1591
|
: activeRequest.sessionId === sessionId && activeRequest.frameId === params.frameId))
|
|
@@ -1579,6 +1658,10 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1579
1658
|
workerOwners.set(child, owner);
|
|
1580
1659
|
ownersWithWorkers.add(owner);
|
|
1581
1660
|
}
|
|
1661
|
+
// An iframe target's id is its frame id, and its url keeps the fragment: the one source
|
|
1662
|
+
// for a card field iframe that rendered before this attachment (empty on a fresh one).
|
|
1663
|
+
if (params.targetInfo.type === 'iframe')
|
|
1664
|
+
microforms.observe(params.targetInfo.targetId, params.targetInfo.url);
|
|
1582
1665
|
await arm(child, true, params.targetInfo.type === 'worker');
|
|
1583
1666
|
}
|
|
1584
1667
|
catch (error) {
|
|
@@ -1595,6 +1678,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1595
1678
|
const { requestId, request, resourceType, networkId, frameId } = params;
|
|
1596
1679
|
// When this request paused: the merchant's total is read as of this moment.
|
|
1597
1680
|
const pausedAt = Date.now();
|
|
1681
|
+
// A paused document request has not committed either: it only starts the context's check.
|
|
1682
|
+
if (resourceType === 'Document')
|
|
1683
|
+
microforms.warm(withFragment(request));
|
|
1598
1684
|
// One pre-claim gate registry decides and performs the mechanics through
|
|
1599
1685
|
// these effects; the state accessors read the same closure variables the
|
|
1600
1686
|
// inline gates did. It resolves synchronously for an ordinary card request,
|
|
@@ -1611,7 +1697,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1611
1697
|
responseHeaders: Object.entries(withCorsHeaders(response.headers, corsHeadersFor(request.url, request.headers)))
|
|
1612
1698
|
.map(([name, value]) => ({ name, value })),
|
|
1613
1699
|
body: Buffer.from(response.body).toString('base64') }, sessionId),
|
|
1614
|
-
abort: () => cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1700
|
+
abort: () => cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { }),
|
|
1615
1701
|
},
|
|
1616
1702
|
attachmentReady: () => attachmentReady,
|
|
1617
1703
|
interceptionAborted: () => setupStop.signal.aborted,
|
|
@@ -1623,14 +1709,18 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1623
1709
|
awaitingApproval: () => awaitingApproval,
|
|
1624
1710
|
stripeStep: null,
|
|
1625
1711
|
merchantHandoff: { retried: (profile) => merchantHandoff.retried(profile, (judge) => awaitRetryReport(sessionId, networkId, judge)) },
|
|
1712
|
+
// The card field iframe that sent a Flex request is the paused request's own frame.
|
|
1713
|
+
cybersourceFlex: { judge: (flexRequest) => microforms.judge(flexRequest, frameId, () => rereadMicroforms(sessionId, frameId)) },
|
|
1626
1714
|
});
|
|
1627
1715
|
const preClaim = preClaimResult instanceof Promise ? await preClaimResult : preClaimResult;
|
|
1628
1716
|
if (preClaim.kind !== 'pause')
|
|
1629
1717
|
return;
|
|
1630
1718
|
const stripeStep = preClaim.stripeStep;
|
|
1719
|
+
// The capture context and fields a CyberSource Flex create carries, and its retry shape.
|
|
1720
|
+
const flexPause = preClaim.cybersourceFlex;
|
|
1631
1721
|
if (!attachmentReady || arming.has(sessionId ?? '__root__') || setupStop.signal.aborted) {
|
|
1632
1722
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
1633
|
-
await cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1723
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
1634
1724
|
return;
|
|
1635
1725
|
}
|
|
1636
1726
|
let preparation;
|
|
@@ -1639,7 +1729,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1639
1729
|
}
|
|
1640
1730
|
catch (error) {
|
|
1641
1731
|
opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
|
|
1642
|
-
await cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1732
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
1643
1733
|
return;
|
|
1644
1734
|
}
|
|
1645
1735
|
// The page asking again for the purchase an outstanding approval is for:
|
|
@@ -1650,7 +1740,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1650
1740
|
if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()
|
|
1651
1741
|
&& typeof networkId === 'string' && networkId) {
|
|
1652
1742
|
const retryBody = pausedBody(request);
|
|
1653
|
-
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts));
|
|
1743
|
+
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape);
|
|
1654
1744
|
if (retry && activeRequest?.attempt.lost && sameCheckoutRequest(activeRequest.identity, retry)) {
|
|
1655
1745
|
if (preparation)
|
|
1656
1746
|
preparationGate.retireUnboundClaim();
|
|
@@ -1708,7 +1798,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1708
1798
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
1709
1799
|
if (preparation)
|
|
1710
1800
|
preparationGate.retireUnboundClaim();
|
|
1711
|
-
await cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1801
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
1712
1802
|
if (repeatedAuthorizationId)
|
|
1713
1803
|
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
1714
1804
|
return;
|
|
@@ -1742,7 +1832,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1742
1832
|
// about the page's configuration, so the next request is judged
|
|
1743
1833
|
// afresh (no latch, no cooldown).
|
|
1744
1834
|
opts.onEvent?.({ type: 'failed', detail: BODY_UNREADABLE_REASON });
|
|
1745
|
-
await cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1835
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
1746
1836
|
return;
|
|
1747
1837
|
}
|
|
1748
1838
|
// The body is needed to tell a re-post of the form the cardholder
|
|
@@ -1750,7 +1840,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1750
1840
|
// and before any API call: no second prompt, no round trip.
|
|
1751
1841
|
if (isRepeatOfSubmitted(lastSubmitted, request.url, body, repeatQuietMs)) {
|
|
1752
1842
|
opts.onEvent?.({ type: 'blocked', detail: HOSTED_FORM_REPEAT_REASON });
|
|
1753
|
-
await cdp.send('Fetch.failRequest', { requestId, errorReason:
|
|
1843
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
1754
1844
|
if (lastSubmitted)
|
|
1755
1845
|
void opts.vault.reportDuplicateGuard?.(lastSubmitted.authorizationId);
|
|
1756
1846
|
return;
|
|
@@ -1765,7 +1855,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1765
1855
|
}
|
|
1766
1856
|
attempt.bind({ requestId, sessionId, request });
|
|
1767
1857
|
activeRequest = { attempt, requestId, networkId, sessionId, frameId, request,
|
|
1768
|
-
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined) };
|
|
1858
|
+
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined, flexPause?.shape) };
|
|
1769
1859
|
await awaitRequestSource(activeRequest);
|
|
1770
1860
|
if (preparation)
|
|
1771
1861
|
await preparationGate.assertDocument();
|
|
@@ -1821,7 +1911,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1821
1911
|
} },
|
|
1822
1912
|
request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url, method: request.method, headers: request.headers, body,
|
|
1823
1913
|
...(mercadoCheckout.requiresDocument(request.url, request.method)
|
|
1824
|
-
? { mercado_checkout: mercadoCheckout.claimToken(request.url, request.method, await readDocumentUrl()) } : {})
|
|
1914
|
+
? { mercado_checkout: mercadoCheckout.claimToken(request.url, request.method, await readDocumentUrl()) } : {}),
|
|
1915
|
+
...(flexPause ? flexPause.sidecar : {}) },
|
|
1825
1916
|
});
|
|
1826
1917
|
attempt.assertLive();
|
|
1827
1918
|
approvedId = replay.authorizationId;
|
|
@@ -1956,7 +2047,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1956
2047
|
quietUntil = Date.now() + cooldownMs;
|
|
1957
2048
|
opts.onEvent?.({ type: 'failed', detail: failureSummary(err) });
|
|
1958
2049
|
}
|
|
1959
|
-
await cdp.send('Fetch.failRequest', { requestId: dead.requestId, errorReason:
|
|
2050
|
+
await cdp.send('Fetch.failRequest', { requestId: dead.requestId, errorReason: cdpFailure(request.url) }, dead.sessionId).catch(() => { });
|
|
1960
2051
|
settlePause();
|
|
1961
2052
|
}
|
|
1962
2053
|
finally {
|
|
@@ -2041,6 +2132,18 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2041
2132
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
2042
2133
|
const stripeCheckout = new StripeCheckoutGate(opts);
|
|
2043
2134
|
const mercadoCheckout = new MercadoCheckoutGate();
|
|
2135
|
+
// CyberSource Flex capture contexts, verified once each (cybersource-flex.ts). Playwright
|
|
2136
|
+
// keeps a frame's fragment in frame.url(), so the field iframes are read when a request pauses.
|
|
2137
|
+
const microformContexts = new CaptureContexts();
|
|
2138
|
+
const frameUrlOf = (frame) => {
|
|
2139
|
+
try {
|
|
2140
|
+
const url = frame?.url?.();
|
|
2141
|
+
return typeof url === 'string' ? url : undefined;
|
|
2142
|
+
}
|
|
2143
|
+
catch {
|
|
2144
|
+
return undefined;
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2044
2147
|
const readDocumentUrl = async () => {
|
|
2045
2148
|
if (page.isClosed?.() || typeof page.url !== 'function')
|
|
2046
2149
|
throw new Error('merchant_document_unavailable');
|
|
@@ -2162,6 +2265,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2162
2265
|
page.on?.('crash', () => { if (!attachmentReady)
|
|
2163
2266
|
setupStop.abort(new CheckoutAttachmentError('closed')); preparationGate.invalidate('merchant_document_closed'); stripeCheckout.invalidate(); mercadoCheckout.invalidate(); activeRequest?.attempt.stop(); handoffAnswer?.closed(); });
|
|
2164
2267
|
page.on?.('framenavigated', (frame) => {
|
|
2268
|
+
// A CyberSource Flex card field iframe loaded: start its capture-context check now, so its
|
|
2269
|
+
// card request pauses without waiting on it (cybersource-flex.ts).
|
|
2270
|
+
microformContexts.warm(frameUrlOf(frame));
|
|
2165
2271
|
if (frame === page.mainFrame?.()) {
|
|
2166
2272
|
if (documentRequest) {
|
|
2167
2273
|
documentRequest = null;
|
|
@@ -2204,7 +2310,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2204
2310
|
}
|
|
2205
2311
|
await page.route((url) => opts.vault.isCardRequest(url.toString()) || guards.matches(url.toString()) || stripeCheckout.matches(url.toString()) || mercadoCheckout.matches(url.toString())
|
|
2206
2312
|
// An armed merchant profile's endpoint or a sibling of it (merchant-hosted.ts), as attachToCdp arms it.
|
|
2207
|
-
|| (typeof opts.vault.merchantProfileOf === 'function' && opts.vault.merchantProfileOf(url.toString()) !== null)
|
|
2313
|
+
|| (typeof opts.vault.merchantProfileOf === 'function' && opts.vault.merchantProfileOf(url.toString()) !== null)
|
|
2314
|
+
// GMO token v1's JSONP GET, routed only for an org the API served GMO to (see attachToCdp).
|
|
2315
|
+
|| (isGmoTokenV1Request(url.toString()) && recognizesGmo(opts.vault)), async (route) => {
|
|
2208
2316
|
const request = route.request();
|
|
2209
2317
|
// When this request paused, and the page it came from: the merchant's total is read as of now.
|
|
2210
2318
|
const pausedAt = Date.now();
|
|
@@ -2226,7 +2334,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2226
2334
|
continueRequest: (postData) => postData !== undefined ? route.continue({ postData }) : route.fallback(),
|
|
2227
2335
|
answer: (response) => route.fulfill({ ...response,
|
|
2228
2336
|
headers: withCorsHeaders(response.headers, corsHeadersFor(request.url(), request.headers())) }),
|
|
2229
|
-
abort: () => route.abort(
|
|
2337
|
+
abort: () => route.abort(refusedRequestFailure(request.url())),
|
|
2230
2338
|
},
|
|
2231
2339
|
attachmentReady: () => attachmentReady,
|
|
2232
2340
|
interceptionAborted: () => setupStop.signal.aborted,
|
|
@@ -2239,14 +2347,34 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2239
2347
|
stripeStep: null,
|
|
2240
2348
|
// This route arrives after every answer the page read before sending it; only an unfollowed 3xx is left to settle.
|
|
2241
2349
|
merchantHandoff: { retried: (profile) => merchantHandoff.retried(profile, () => { handoffAnswer?.settle(); return false; }) },
|
|
2350
|
+
// The card field iframe that sent a Flex request, and every frame beside it on the page.
|
|
2351
|
+
cybersourceFlex: { judge: (flexRequest) => {
|
|
2352
|
+
let issuing;
|
|
2353
|
+
try {
|
|
2354
|
+
issuing = request.frame?.();
|
|
2355
|
+
}
|
|
2356
|
+
catch {
|
|
2357
|
+
issuing = undefined;
|
|
2358
|
+
}
|
|
2359
|
+
let frames = [];
|
|
2360
|
+
try {
|
|
2361
|
+
frames = page.frames?.() ?? [];
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
frames = [];
|
|
2365
|
+
}
|
|
2366
|
+
return judgeMicroformFrameUrls(flexRequest, frameUrlOf(issuing), frames.map(frameUrlOf), microformContexts);
|
|
2367
|
+
} },
|
|
2242
2368
|
});
|
|
2243
2369
|
const preClaim = preClaimResult instanceof Promise ? await preClaimResult : preClaimResult;
|
|
2244
2370
|
if (preClaim.kind !== 'pause')
|
|
2245
2371
|
return;
|
|
2246
2372
|
const stripeStep = preClaim.stripeStep;
|
|
2373
|
+
// The capture context and fields a CyberSource Flex create carries, and its retry shape.
|
|
2374
|
+
const flexPause = preClaim.cybersourceFlex;
|
|
2247
2375
|
if (!attachmentReady || setupStop.signal.aborted) {
|
|
2248
2376
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
2249
|
-
return route.abort(
|
|
2377
|
+
return route.abort(refusedRequestFailure(request.url()));
|
|
2250
2378
|
}
|
|
2251
2379
|
let preparation;
|
|
2252
2380
|
try {
|
|
@@ -2254,13 +2382,13 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2254
2382
|
}
|
|
2255
2383
|
catch (error) {
|
|
2256
2384
|
opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
|
|
2257
|
-
return route.abort(
|
|
2385
|
+
return route.abort(refusedRequestFailure(request.url()));
|
|
2258
2386
|
}
|
|
2259
2387
|
// The page asking again for the purchase an outstanding approval is
|
|
2260
2388
|
// for, after its own request timed out: bind it to that approval (see
|
|
2261
2389
|
// merchantAttempt and the same check in attachToCdp).
|
|
2262
2390
|
if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()) {
|
|
2263
|
-
const retry = requestIdentity(opts, request.url(), request.method(), request.postData() ?? '', documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts));
|
|
2391
|
+
const retry = requestIdentity(opts, request.url(), request.method(), request.postData() ?? '', documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape);
|
|
2264
2392
|
if (activeRequest?.attempt.lost && sameCheckoutRequest(activeRequest.identity, retry)) {
|
|
2265
2393
|
if (preparation)
|
|
2266
2394
|
preparationGate.retireUnboundClaim();
|
|
@@ -2297,9 +2425,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2297
2425
|
}
|
|
2298
2426
|
// Fail closed and stay quiet: no card may reach the PSP, but neither may
|
|
2299
2427
|
// the page's retry loop turn into a stream of doomed API calls. Every
|
|
2300
|
-
// abort in this adapter is 'aborted' (ERR_ABORTED),
|
|
2301
|
-
//
|
|
2302
|
-
//
|
|
2428
|
+
// abort in this adapter is 'aborted' (ERR_ABORTED), and a CyberSource
|
|
2429
|
+
// Flex request's is 'failed' (refusedRequestFailure), the same codes the
|
|
2430
|
+
// CDP adapter's Fetch.failRequest uses, so a refused request resolves
|
|
2431
|
+
// identically whichever adapter is attached.
|
|
2303
2432
|
if (terminal || lifecycle.isBlocked() || awaitingApproval || Date.now() < quietUntil) {
|
|
2304
2433
|
let repeatedAuthorizationId = null;
|
|
2305
2434
|
if (lastSubmitted) {
|
|
@@ -2316,7 +2445,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2316
2445
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
2317
2446
|
if (preparation)
|
|
2318
2447
|
preparationGate.retireUnboundClaim();
|
|
2319
|
-
const refused = await route.abort(
|
|
2448
|
+
const refused = await route.abort(refusedRequestFailure(request.url()));
|
|
2320
2449
|
if (repeatedAuthorizationId)
|
|
2321
2450
|
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
2322
2451
|
return refused;
|
|
@@ -2362,7 +2491,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2362
2491
|
const body = request.postData() ?? '';
|
|
2363
2492
|
if (isRepeatOfSubmitted(lastSubmitted, request.url(), body, repeatQuietMs)) {
|
|
2364
2493
|
opts.onEvent?.({ type: 'blocked', detail: HOSTED_FORM_REPEAT_REASON });
|
|
2365
|
-
const refused = await route.abort(
|
|
2494
|
+
const refused = await route.abort(refusedRequestFailure(request.url()));
|
|
2366
2495
|
if (lastSubmitted)
|
|
2367
2496
|
void opts.vault.reportDuplicateGuard?.(lastSubmitted.authorizationId);
|
|
2368
2497
|
return refused;
|
|
@@ -2373,7 +2502,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2373
2502
|
const payToInterceptMs = readPayToInterceptMs(opts.payClickedAt, approvalStartedAt);
|
|
2374
2503
|
attempt.bind({ route, request, frames });
|
|
2375
2504
|
activeRequest = { request, frames, attempt,
|
|
2376
|
-
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined) };
|
|
2505
|
+
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined, flexPause?.shape) };
|
|
2377
2506
|
if (preparation)
|
|
2378
2507
|
await preparationGate.assertDocument();
|
|
2379
2508
|
// One bounded read serves the merchant origin and the retry identity; see attachToCdp.
|
|
@@ -2415,7 +2544,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2415
2544
|
} },
|
|
2416
2545
|
request: stripeStep?.phase === 'final' ? stripeStep.request : { url: request.url(), method: request.method(), headers: request.headers(), body,
|
|
2417
2546
|
...(mercadoCheckout.requiresDocument(request.url(), request.method())
|
|
2418
|
-
? { mercado_checkout: mercadoCheckout.claimToken(request.url(), request.method(), await readDocumentUrl()) } : {})
|
|
2547
|
+
? { mercado_checkout: mercadoCheckout.claimToken(request.url(), request.method(), await readDocumentUrl()) } : {}),
|
|
2548
|
+
...(flexPause ? flexPause.sidecar : {}) },
|
|
2419
2549
|
});
|
|
2420
2550
|
assertRequestLive();
|
|
2421
2551
|
approvedId = replay.authorizationId;
|
|
@@ -2507,7 +2637,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2507
2637
|
quietUntil = Date.now() + cooldownMs;
|
|
2508
2638
|
opts.onEvent?.({ type: 'failed', detail: failureSummary(err) });
|
|
2509
2639
|
}
|
|
2510
|
-
await dead.abort(
|
|
2640
|
+
await dead.abort(refusedRequestFailure(request.url())).catch(() => { });
|
|
2511
2641
|
}
|
|
2512
2642
|
finally {
|
|
2513
2643
|
if (activeRequest?.attempt === attempt)
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { VoiceModule, type VoiceOptions } from './voice.js';
|
|
|
7
7
|
import { BuyModule } from './buy.js';
|
|
8
8
|
import { RequestRegistry } from './requests.js';
|
|
9
9
|
import type { ApiTransport } from './api-module.js';
|
|
10
|
+
import { type MicroformSidecar } from './cybersource-flex.js';
|
|
10
11
|
export interface PausedRequest {
|
|
11
12
|
url: string;
|
|
12
13
|
method: string;
|
|
@@ -14,6 +15,14 @@ export interface PausedRequest {
|
|
|
14
15
|
body: string;
|
|
15
16
|
/** Captured native Checkout Pro initialization, validated before use. */
|
|
16
17
|
mercado_checkout?: unknown;
|
|
18
|
+
/**
|
|
19
|
+
* CyberSource Flex only: the capture context the card field iframe was loaded with, byte for
|
|
20
|
+
* byte from its URL fragment. The cardholder's device seals the real card under it, so the API
|
|
21
|
+
* verifies it before anything exists. The adapters read it; refused on any other processor.
|
|
22
|
+
*/
|
|
23
|
+
capture_context?: string;
|
|
24
|
+
/** CyberSource Flex only: the Microform fields that loaded and the one that sent this request. */
|
|
25
|
+
microform?: MicroformSidecar;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* The modes this SDK can finish. Asked for on syncRegistry (the API serves
|
|
@@ -34,12 +43,22 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
|
|
|
34
43
|
/**
|
|
35
44
|
* Dark-launch capabilities this build can finish. Sent on syncRegistry as
|
|
36
45
|
* ?capabilities= so the API also serves the recognizers gated behind one of
|
|
37
|
-
* these
|
|
46
|
+
* these, and only to an org Agentcard enabled for the processor.
|
|
47
|
+
* `fiserv_card_capture`: Fiserv Commerce Hub's Secure Data Capture card
|
|
38
48
|
* capture, which this build pauses and pays only through a prepared checkout
|
|
39
49
|
* (pre-claim.ts, fiserv.ts). The API serves Fiserv's recognizer only to a build
|
|
40
50
|
* that lists it and only for a company Agentcard turned Fiserv on for, so an older
|
|
41
51
|
* SDK never pauses a capture it cannot finish and no SDK pauses one for a company
|
|
42
52
|
* Fiserv is off for; the API also refuses a Fiserv payment for every such company.
|
|
53
|
+
* `gmo_shared_pk`: GMO token v2's getToken on the shared-key path, which this
|
|
54
|
+
* build judges before it pauses (gmo.ts) and pays only for the amount the
|
|
55
|
+
* agent passes. A gated recognizer never sits in BUILTIN_REGISTRY, so it is
|
|
56
|
+
* paused only after a sync.
|
|
57
|
+
* `capture_context`: CyberSource Flex Microform, whose create carries the capture
|
|
58
|
+
* context this build reads from the card field iframe (cybersource-flex.ts); the API
|
|
59
|
+
* serves Flex's recognizer the same way, only for a company Agentcard turned it on for.
|
|
60
|
+
* A gated recognizer is never in BUILTIN_REGISTRY, so a build reaches it only through a
|
|
61
|
+
* sync that declares its capability.
|
|
43
62
|
*/
|
|
44
63
|
export declare const SUPPORTED_CAPABILITIES: readonly string[];
|
|
45
64
|
/**
|
|
@@ -273,6 +292,9 @@ export interface AuthorizeInput extends ExecutionMetadata {
|
|
|
273
292
|
* smallest unit is refused with nothing charged (an AmountMismatchError;
|
|
274
293
|
* `stage` says which check). Agentcard derives the display string; you
|
|
275
294
|
* never send one. Both or neither: one without the other is refused.
|
|
295
|
+
* Required when the processor's request names no amount at all (GMO): then
|
|
296
|
+
* this pair is what the cardholder approves, and a request without it
|
|
297
|
+
* throws AmountRequiredError before anything is created.
|
|
276
298
|
*/
|
|
277
299
|
amount?: number | string;
|
|
278
300
|
currency?: string;
|
|
@@ -337,6 +359,51 @@ export declare class UnsupportedModeError extends Error {
|
|
|
337
359
|
mode: string;
|
|
338
360
|
constructor(mode: string);
|
|
339
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* The recognizer is gated behind a dark-launch capability this SDK build does not
|
|
364
|
+
* declare (SUPPORTED_CAPABILITIES), so it cannot send what that processor's create
|
|
365
|
+
* needs. Thrown locally before any create or prompt, and terminal. A registry the
|
|
366
|
+
* API served never holds such an entry (it serves a gated one only to a build that
|
|
367
|
+
* declares it); a hand-built one can.
|
|
368
|
+
*/
|
|
369
|
+
export declare class UnsupportedCapabilityError extends Error {
|
|
370
|
+
capability: string;
|
|
371
|
+
constructor(capability: string);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* placeholderCard() found no stored card to describe (`card_not_found`), the stored card's
|
|
375
|
+
* network has no documented placeholder number (`brand_unsupported`), or the stored card
|
|
376
|
+
* carries no usable expiry (`expiry_unknown`). Nothing was created and nothing was charged.
|
|
377
|
+
*/
|
|
378
|
+
export declare class PlaceholderCardError extends Error {
|
|
379
|
+
reason: 'card_not_found' | 'brand_unsupported' | 'expiry_unknown';
|
|
380
|
+
constructor(reason: 'card_not_found' | 'brand_unsupported' | 'expiry_unknown');
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* What the agent types into a checkout so the page's own card facts match the stored card
|
|
384
|
+
* that pays: a documented test number of the same network, a placeholder security code of that
|
|
385
|
+
* network's length, and the stored card's real expiry. CyberSource Flex merchants post the brand
|
|
386
|
+
* the card field detected from the typed number and the expiry typed into their own fields beside
|
|
387
|
+
* the token (Sonos, Amtrak), so a mismatch there fails the merchant's payment.
|
|
388
|
+
*/
|
|
389
|
+
export interface PlaceholderCard {
|
|
390
|
+
/**
|
|
391
|
+
* The stored card these facts describe (GET /api/v2/vault_cards). Pass it as `cardId` to
|
|
392
|
+
* attachToCdp, attachToPlaywright or authorize(): the approval page then preselects this card.
|
|
393
|
+
* Without it the page preselects its own choice, which can be another card.
|
|
394
|
+
*/
|
|
395
|
+
readonly cardId: string;
|
|
396
|
+
/** Its network, as GET /api/v2/vault_cards names it. */
|
|
397
|
+
readonly brand: string;
|
|
398
|
+
/** A documented test card number of that network. Never a real card: the device supplies the real one. */
|
|
399
|
+
readonly number: string;
|
|
400
|
+
/** A placeholder security code of the length that network uses. */
|
|
401
|
+
readonly securityCode: string;
|
|
402
|
+
/** The stored card's expiry month, two digits ("04"). */
|
|
403
|
+
readonly expiryMonth: string;
|
|
404
|
+
/** The stored card's expiry year, four digits ("2031"). */
|
|
405
|
+
readonly expiryYear: string;
|
|
406
|
+
}
|
|
340
407
|
/**
|
|
341
408
|
* The recognizer says this processor is preparation-required, and no preparation
|
|
342
409
|
* was passed. Its card may be sent only after the cardholder approved a
|
|
@@ -349,6 +416,19 @@ export declare class PreparationRequiredError extends Error {
|
|
|
349
416
|
psp: string;
|
|
350
417
|
constructor(psp: string);
|
|
351
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* The paused request names no amount and none was passed. A GMO card token
|
|
421
|
+
* request carries no amount (the merchant's server charges the token later),
|
|
422
|
+
* so the `amount` and `currency` you pass are what the cardholder approves.
|
|
423
|
+
* Thrown locally before any create or prompt, and terminal: the same request
|
|
424
|
+
* without an amount fails the same way. The API answers 400 amount_required
|
|
425
|
+
* for the same case. A page total (`pageAmount`) does not count.
|
|
426
|
+
*/
|
|
427
|
+
export declare class AmountRequiredError extends Error {
|
|
428
|
+
psp: string;
|
|
429
|
+
readonly code: "amount_required";
|
|
430
|
+
constructor(psp: string);
|
|
431
|
+
}
|
|
352
432
|
export declare class ApprovalTimeoutError extends Error {
|
|
353
433
|
constructor(ms: number);
|
|
354
434
|
}
|
|
@@ -812,6 +892,22 @@ export declare class VaultClient {
|
|
|
812
892
|
* these patterns pause.
|
|
813
893
|
*/
|
|
814
894
|
cardUrlPatterns(): string[];
|
|
895
|
+
/**
|
|
896
|
+
* What to type into a checkout so the page's own card facts match the stored card that
|
|
897
|
+
* pays (see PlaceholderCard): read from GET /api/v2/vault_cards, which lists each stored card's
|
|
898
|
+
* network and expiry. `cardId` names the stored card; omitted, the user's most recently added
|
|
899
|
+
* card. Pin the returned `cardId` on the attachment or authorize(), so the approval page
|
|
900
|
+
* preselects that card: without a pin the page preselects its own choice (its recommended
|
|
901
|
+
* card, or the first card the cardholder's device can open), which can be another card. After
|
|
902
|
+
* prepare(), pass the prepared checkout's `cardId`, the card the cardholder already approved.
|
|
903
|
+
* The facts hold only while the cardholder pays with that card. Throws PlaceholderCardError
|
|
904
|
+
* when there is no such card, its network has no placeholder, or it carries no expiry.
|
|
905
|
+
*/
|
|
906
|
+
placeholderCard(input: {
|
|
907
|
+
user: string;
|
|
908
|
+
cardId?: string;
|
|
909
|
+
signal?: AbortSignal;
|
|
910
|
+
}): Promise<PlaceholderCard>;
|
|
815
911
|
/** Wait for real device approval before the caller starts native tokenization. */
|
|
816
912
|
prepareCheckout(input: PrepareCheckoutInput): Promise<PreparedCheckout>;
|
|
817
913
|
/** Cancel only an unconsumed preparation; a bound request is reconciled separately. */
|