@agent-cards/sdk 0.23.6 → 0.23.7
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 +7 -1
- package/PREFLIGHT.md +1 -1
- package/README.md +146 -4
- package/dist/amadeus.d.ts +16 -4
- package/dist/amadeus.js +51 -16
- package/dist/cdp.js +183 -26
- package/dist/client.d.ts +22 -0
- package/dist/client.js +165 -13
- package/dist/creditguard.generated.d.ts +1 -1
- package/dist/cybersource-secure-acceptance.d.ts +142 -34
- package/dist/cybersource-secure-acceptance.generated.d.ts +66 -0
- package/dist/cybersource-secure-acceptance.generated.js +955 -0
- package/dist/cybersource-secure-acceptance.js +244 -3
- package/dist/index.d.ts +1 -0
- package/dist/juspay.generated.d.ts +1 -1
- package/dist/pre-claim.d.ts +3 -1
- package/dist/pre-claim.js +85 -6
- package/dist/preparation.js +15 -2
- package/dist/prepared-processor.d.ts +19 -5
- package/dist/prepared-processor.js +70 -3
- package/dist/registry.js +14 -1
- package/dist/stripe-checkout.generated.js +35 -1
- package/package.json +1 -1
package/dist/cdp.js
CHANGED
|
@@ -13,7 +13,7 @@ import { DatatransRepeats } from './datatrans.js';
|
|
|
13
13
|
import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
|
|
14
14
|
import { isArmedEkashuUrl } from './ekashu.js';
|
|
15
15
|
import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
|
|
16
|
-
import { CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, isCybersourceSecureAcceptanceUrl } from './cybersource-secure-acceptance.js';
|
|
16
|
+
import { CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, SecureAcceptanceFollowUpWatch, isCybersourceSecureAcceptanceUrl, secureAcceptanceFollowUp, secureAcceptanceFrameAnswer, secureAcceptanceReplyLast4, } from './cybersource-secure-acceptance.js';
|
|
17
17
|
import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
|
|
18
18
|
import { cseBody } from './cse-body.js';
|
|
19
19
|
import { STRIPE_SETUP_INTENT_CONFIRM, setupConfirmPayingWith } from './stripe-setup.js';
|
|
@@ -22,7 +22,7 @@ import { verifoneTemplatedUrl } from './verifone.js';
|
|
|
22
22
|
import { MerchantHandoffWatch } from './merchant-handoff.js';
|
|
23
23
|
import { MERCHANT_PAGE_WAIT_MS, MerchantTotalWatch } from './merchant-total-watch.js';
|
|
24
24
|
import { hostedFormSubmittedPage } from './hosted-form.js';
|
|
25
|
-
import {
|
|
25
|
+
import { armSendWindow, failsAsNetworkError, preparedSendWindowMs, sendWindowProcessor } from './amadeus.js';
|
|
26
26
|
import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
27
27
|
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, loadedCardMicroformFromUrls, refusedRequestFailure, } from './cybersource-flex.js';
|
|
28
28
|
import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointRouted, } from './card-endpoint.js';
|
|
@@ -678,6 +678,43 @@ function playwrightCookieParams(cookie) {
|
|
|
678
678
|
function stoppedNavigationPage(profile, outcome, reason, authorizationId) {
|
|
679
679
|
return cardEndpointStoppedPage({ outcome, reason, merchant: profile.merchant, authorizationId });
|
|
680
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Who a paused request's stopped page names, or null when the request is not a form navigation the
|
|
683
|
+
* adapters answer with one: a card endpoint profile whose page submits its form as a navigation names
|
|
684
|
+
* its merchant, and a Secure Acceptance token create the lane held (a frame navigation, pre-claim.ts)
|
|
685
|
+
* names CyberSource. Any other request is failed instead.
|
|
686
|
+
*/
|
|
687
|
+
function stoppedPageRecipient(profile, secureAcceptance) {
|
|
688
|
+
if (secureAcceptance)
|
|
689
|
+
return CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT;
|
|
690
|
+
return profile && profile.navigation !== null ? profile.merchant : null;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* How a held Secure Acceptance frame post whose approval failed is answered. The card reached
|
|
694
|
+
* CyberSource once the device approved or the hand-off began, and CyberSource also answered a row the
|
|
695
|
+
* API declined as processor_refused, or one whose reply the SDK would not hand the frame (a reply that
|
|
696
|
+
* failed the rule, or one the API no longer serves): each reads `sent_answer_not_shown`. A reply that
|
|
697
|
+
* never came within the window reads `stopped_before_send` only when the API confirmed it retired the
|
|
698
|
+
* row before any processor request, and `sent_answer_not_shown` otherwise, since the device may have
|
|
699
|
+
* sent the card. Everything else reads as for a card endpoint (failedNavigationRefusal).
|
|
700
|
+
*/
|
|
701
|
+
function secureAcceptanceFailedRefusal(error, sent, authorizationId) {
|
|
702
|
+
if (sent)
|
|
703
|
+
return { outcome: 'sent_answer_not_shown', reason: 'outcome_unknown', authorizationId };
|
|
704
|
+
if (error instanceof ProcessorRefusedError)
|
|
705
|
+
return { outcome: 'sent_answer_not_shown', reason: 'answer_withheld', authorizationId: error.authorizationId ?? authorizationId };
|
|
706
|
+
if (error instanceof PaymentOutcomeUnknownError) {
|
|
707
|
+
const id = error.authorizationId ?? authorizationId;
|
|
708
|
+
if (error.reason === 'secure_acceptance_reply_unconfirmed' || error.reason === 'response_expired') {
|
|
709
|
+
return { outcome: 'sent_answer_not_shown', reason: 'answer_withheld', authorizationId: id };
|
|
710
|
+
}
|
|
711
|
+
if (error.reason === 'secure_acceptance_reply_late') {
|
|
712
|
+
return error.processorRequestStarted === false ? { outcome: 'stopped_before_send', reason: 'not_approved', authorizationId: id }
|
|
713
|
+
: { outcome: 'sent_answer_not_shown', reason: 'outcome_unknown', authorizationId: id };
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return failedNavigationRefusal(error, false, authorizationId);
|
|
717
|
+
}
|
|
681
718
|
/** The API refused the create because this checkout already has a payment waiting or sent (409 merchant_checkout_in_use). */
|
|
682
719
|
function checkoutInUse(error) {
|
|
683
720
|
return error instanceof CheckoutApiError && error.code === 'merchant_checkout_in_use';
|
|
@@ -697,6 +734,18 @@ function heldNavigationRefusal(terminal, awaitingApproval, lifecycle) {
|
|
|
697
734
|
const outcome = refusedNavigationOutcome({ awaitingApproval, status: state.status });
|
|
698
735
|
return { outcome, reason: 'blocked', authorizationId: outcome === 'payment_in_progress' ? state.authorizationId : null };
|
|
699
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* How a held Secure Acceptance frame post that no ready preparation binds is answered (the claim
|
|
739
|
+
* refused it: none was ready, it names another profile id, it carries no Origin): nothing reached the
|
|
740
|
+
* API for it and its card never left, so the page says so, unless another payment for this checkout is
|
|
741
|
+
* waiting or may exist (refusedNavigationOutcome), which the page then names.
|
|
742
|
+
*/
|
|
743
|
+
function unboundNavigationRefusal(awaitingApproval, lifecycle) {
|
|
744
|
+
const state = lifecycle.getState();
|
|
745
|
+
const outcome = refusedNavigationOutcome({ awaitingApproval, status: state.status });
|
|
746
|
+
return outcome === 'payment_in_progress' ? { outcome, reason: 'blocked', authorizationId: state.authorizationId }
|
|
747
|
+
: { outcome, reason: 'not_approved', authorizationId: null };
|
|
748
|
+
}
|
|
700
749
|
/**
|
|
701
750
|
* How a card form navigation whose approval failed is answered, for `authorizationId`. Once
|
|
702
751
|
* the device approved or the hand-off began (`sent`), the card reached the merchant. Before
|
|
@@ -1286,10 +1335,11 @@ function merchantAttempt(lifecycle, survives) {
|
|
|
1286
1335
|
let rebound = false;
|
|
1287
1336
|
let delivering = false;
|
|
1288
1337
|
let bound = null;
|
|
1289
|
-
//
|
|
1290
|
-
// never can (amadeus.ts): a known non-payment, so the
|
|
1291
|
-
// reason, and a fresh attachment can prepare again. Every
|
|
1292
|
-
|
|
1338
|
+
// A send window (an Amadeus SDK add's, a Secure Acceptance token create's) closes only once the API
|
|
1339
|
+
// confirms the card never left the device and never can (amadeus.ts): a known non-payment, so the
|
|
1340
|
+
// prepared checkout ends failed with that reason, and a fresh attachment can prepare again. Every
|
|
1341
|
+
// other stop leaves the outcome unknown.
|
|
1342
|
+
const error = () => reason === 'amadeus_send_window_closed' || reason === 'secure_acceptance_send_window_closed'
|
|
1293
1343
|
? new CheckoutPreparationError(lifecycle.getState().preparationId ?? null, reason)
|
|
1294
1344
|
: new PaymentOutcomeUnknownError(lifecycle.getState().authorizationId, reason);
|
|
1295
1345
|
const attempt = {
|
|
@@ -1515,13 +1565,15 @@ function pauseForBusiness(opts, lifecycle, attemptSignal, request, pageOrigin) {
|
|
|
1515
1565
|
return { replay, delivered: { resolve: deliveredResolve, reject: deliveredReject } };
|
|
1516
1566
|
}
|
|
1517
1567
|
/**
|
|
1518
|
-
* Arm the send window of a request paused under a preparation, when its processor sets one:
|
|
1519
|
-
* prepared Amadeus SDK add
|
|
1520
|
-
*
|
|
1568
|
+
* Arm the send window of a request paused under a preparation, when its processor sets one: a
|
|
1569
|
+
* prepared Amadeus SDK add and a prepared Secure Acceptance token create do (amadeus.ts
|
|
1570
|
+
* preparedSendWindowMs). Returns the disarm function, or null with no window. lifecycle.begin()
|
|
1571
|
+
* cleared the authorization ID, so the one the window reads is this attempt's.
|
|
1521
1572
|
*/
|
|
1522
1573
|
function armWindowFor(opts, lifecycle, attempt, preparation, url, delivering) {
|
|
1523
|
-
const ms = preparation ?
|
|
1524
|
-
|
|
1574
|
+
const ms = preparation ? preparedSendWindowMs(preparation.psp, url) : null;
|
|
1575
|
+
const processor = preparation ? sendWindowProcessor(preparation.psp) : null;
|
|
1576
|
+
if (ms === null || processor === null || !preparation)
|
|
1525
1577
|
return null;
|
|
1526
1578
|
return armSendWindow(ms, attempt, {
|
|
1527
1579
|
authorizationId: () => lifecycle.getState().authorizationId,
|
|
@@ -1529,7 +1581,19 @@ function armWindowFor(opts, lifecycle, attempt, preparation, url, delivering) {
|
|
|
1529
1581
|
cancel: typeof opts.vault?.cancelAuthorization === 'function' ? (id) => opts.vault.cancelAuthorization(id) : undefined,
|
|
1530
1582
|
retirePreparation: typeof opts.vault?.retirePreparation === 'function' ? () => opts.vault.retirePreparation(preparation.id) : undefined,
|
|
1531
1583
|
onEvent: opts.onEvent,
|
|
1532
|
-
});
|
|
1584
|
+
}, processor);
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Begin the follow-up watch after a Secure Acceptance reply was delivered to the frame, when the
|
|
1588
|
+
* preparation's merchant has a reviewed entry that names its next request (secureAcceptanceFollowUp):
|
|
1589
|
+
* none on the test host. Read-only; see SecureAcceptanceFollowUpWatch.
|
|
1590
|
+
*/
|
|
1591
|
+
function watchSecureAcceptanceFollowUp(watch, preparation, replay) {
|
|
1592
|
+
if (!preparation)
|
|
1593
|
+
return;
|
|
1594
|
+
const followUp = secureAcceptanceFollowUp(preparation);
|
|
1595
|
+
if (followUp)
|
|
1596
|
+
watch.begin(followUp, { authorizationId: replay.authorizationId, last4: secureAcceptanceReplyLast4(replay.body) });
|
|
1533
1597
|
}
|
|
1534
1598
|
/**
|
|
1535
1599
|
* Take over card tokenization for a page.
|
|
@@ -1613,6 +1677,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1613
1677
|
};
|
|
1614
1678
|
let merchantContext;
|
|
1615
1679
|
let targetContextKnown = false;
|
|
1680
|
+
// The page target's id, which is its main frame's id; read once the attachment is set up.
|
|
1681
|
+
let pageTargetId;
|
|
1616
1682
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
1617
1683
|
const stripeCheckout = new StripeCheckoutGate(opts);
|
|
1618
1684
|
const mercadoCheckout = new MercadoCheckoutGate();
|
|
@@ -1653,6 +1719,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1653
1719
|
const juspayCheckout = new JuspayCheckoutGate(opts.vault);
|
|
1654
1720
|
const aci = new AciGate(opts.vault);
|
|
1655
1721
|
const eprotect = new EprotectGate(opts.vault);
|
|
1722
|
+
// The merchant's follow-up request after a delivered Secure Acceptance reply (cybersource-secure-acceptance.ts).
|
|
1723
|
+
const secureAcceptanceFollowUpWatch = new SecureAcceptanceFollowUpWatch(opts.onEvent);
|
|
1724
|
+
lifecycle.abort.signal.addEventListener('abort', () => secureAcceptanceFollowUpWatch.end(), { once: true });
|
|
1656
1725
|
let preparationFrameId;
|
|
1657
1726
|
const readDocumentUrl = async () => {
|
|
1658
1727
|
const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
|
|
@@ -1697,15 +1766,34 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1697
1766
|
return null;
|
|
1698
1767
|
return { navigation: frame.parentId || sessionId !== pageSessionId ? 'frame' : 'top', url: typeof frame.url === 'string' ? frame.url : undefined };
|
|
1699
1768
|
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Which frame a paused document request navigates, as documentFrame reads it, without the frame tree where
|
|
1771
|
+
* the answer is already known: a navigation paused on an iframe target's session is a frame's, and one of
|
|
1772
|
+
* the page's main frame (whose id is the page target's id) is the page's own. Chromium does not answer
|
|
1773
|
+
* Page.getFrameTree on a session while that session's own top-level navigation is paused, so the read
|
|
1774
|
+
* would wait for a decision only this answer can make; any other frame's is read from the tree.
|
|
1775
|
+
*/
|
|
1776
|
+
const pausedNavigation = async (sessionId, frameId) => {
|
|
1777
|
+
if (typeof frameId !== 'string' || !frameId)
|
|
1778
|
+
return null;
|
|
1779
|
+
if (sessionId && sessionId !== pageSessionId && sessionTypes.get(sessionId) === 'iframe')
|
|
1780
|
+
return 'frame';
|
|
1781
|
+
if (sessionId === pageSessionId && pageTargetId !== undefined && frameId === pageTargetId)
|
|
1782
|
+
return 'top';
|
|
1783
|
+
return (await documentFrame(sessionId, frameId))?.navigation ?? null;
|
|
1784
|
+
};
|
|
1700
1785
|
/**
|
|
1701
1786
|
* Refuse a paused request after the pre-claim gates: a card form navigation gets the
|
|
1702
1787
|
* stopped page (card-endpoint-page.ts) that `refusal` describes, since an aborted page
|
|
1703
1788
|
* load renders nothing and invites another click on Pay; any other request fails as
|
|
1704
|
-
* cdpFailure says (a network error for CyberSource Flex and Amadeus, else an abort).
|
|
1789
|
+
* cdpFailure says (a network error for CyberSource Flex and Amadeus, else an abort). A
|
|
1790
|
+
* Secure Acceptance token create the lane held is a frame navigation, and its page names
|
|
1791
|
+
* CyberSource (stoppedPageRecipient).
|
|
1705
1792
|
*/
|
|
1706
|
-
const refusePaused = async (requestId, sessionId, url, profile, refusal) => {
|
|
1707
|
-
|
|
1708
|
-
|
|
1793
|
+
const refusePaused = async (requestId, sessionId, url, profile, refusal, secureAcceptance = false) => {
|
|
1794
|
+
const merchant = stoppedPageRecipient(profile, secureAcceptance);
|
|
1795
|
+
if (merchant !== null) {
|
|
1796
|
+
const page = cardEndpointStoppedPage({ outcome: refusal.outcome, reason: refusal.reason, merchant, authorizationId: refusal.authorizationId });
|
|
1709
1797
|
await cdp.send('Fetch.fulfillRequest', { requestId, responseCode: page.status, responseHeaders: headerEntries(page.headers),
|
|
1710
1798
|
body: Buffer.from(page.body).toString('base64') }, sessionId).catch(() => { });
|
|
1711
1799
|
return;
|
|
@@ -2071,6 +2159,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2071
2159
|
// Read-only: the page this request came from (its session and document loader), and
|
|
2072
2160
|
// the request itself when a priced merchant profile's rules read it.
|
|
2073
2161
|
if (!sessionDetached(sessionId) && params.request) {
|
|
2162
|
+
// The merchant's follow-up after a delivered Secure Acceptance reply ends its watch (read-only).
|
|
2163
|
+
secureAcceptanceFollowUpWatch.saw(params.request.method, params.request.url);
|
|
2074
2164
|
merchantTotal.sent(key, { method: params.request.method, url: params.request.url,
|
|
2075
2165
|
...(typeof params.request.postData === 'string' ? { requestBody: params.request.postData } : {}),
|
|
2076
2166
|
...(typeof params.loaderId === 'string' && params.loaderId ? { page: `${owner}:${params.loaderId}` } : {}) });
|
|
@@ -2317,7 +2407,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2317
2407
|
replacesNavigationBody: async () => true,
|
|
2318
2408
|
readDocumentUrl, lifecycle, preparationGate, onEvent: opts.onEvent, failureSummary,
|
|
2319
2409
|
// Read only for a card endpoint whose page submits its form as a navigation.
|
|
2320
|
-
navigation: async () => (resourceType === 'Document' ? (
|
|
2410
|
+
navigation: async () => (resourceType === 'Document' ? pausedNavigation(sessionId, frameId) : null),
|
|
2321
2411
|
effects: {
|
|
2322
2412
|
continueRequest: (postData) => cdp.send('Fetch.continueRequest', { requestId, ...(postData !== undefined ? { postData: Buffer.from(postData).toString('base64') } : {}) }, sessionId),
|
|
2323
2413
|
answer: (response) => cdp.send('Fetch.fulfillRequest', { requestId, responseCode: response.status,
|
|
@@ -2360,9 +2450,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2360
2450
|
// A Payrails card save the gate bound to a captured session: its key rides on the create.
|
|
2361
2451
|
const payrailsSidecar = preClaim.payrails;
|
|
2362
2452
|
const cardNavigation = preClaim.navigation;
|
|
2453
|
+
// A Secure Acceptance token create the lane held, a navigation of the merchant's hidden frame: every
|
|
2454
|
+
// refusal answers it with the stopped page naming CyberSource, and its approval with CyberSource's page.
|
|
2455
|
+
const secureAcceptance = preClaim.secureAcceptance === true;
|
|
2363
2456
|
if (!attachmentReady || arming.has(sessionId ?? '__root__') || setupStop.signal.aborted) {
|
|
2364
2457
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
2365
|
-
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2458
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle), secureAcceptance);
|
|
2366
2459
|
return;
|
|
2367
2460
|
}
|
|
2368
2461
|
// A request the catalog marks as routine without a card (a confirmation
|
|
@@ -2389,7 +2482,12 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2389
2482
|
}
|
|
2390
2483
|
catch (error) {
|
|
2391
2484
|
opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
|
|
2392
|
-
|
|
2485
|
+
// A Secure Acceptance frame post no ready preparation binds (none ready, another profile, no Origin)
|
|
2486
|
+
// ends on the stopped page: nothing reached the API, and the card never left.
|
|
2487
|
+
if (secureAcceptance)
|
|
2488
|
+
await refusePaused(requestId, sessionId, request.url, null, unboundNavigationRefusal(awaitingApproval, lifecycle), true);
|
|
2489
|
+
else
|
|
2490
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(request.url) }, sessionId).catch(() => { });
|
|
2393
2491
|
return;
|
|
2394
2492
|
}
|
|
2395
2493
|
// The page asking again for the purchase an outstanding approval is for:
|
|
@@ -2450,7 +2548,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2450
2548
|
if (preparation)
|
|
2451
2549
|
preparationGate.retireUnboundClaim();
|
|
2452
2550
|
// A card form navigation never reads that nothing was sent while a payment for this checkout may exist.
|
|
2453
|
-
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2551
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle), secureAcceptance);
|
|
2454
2552
|
if (repeatedAuthorizationId)
|
|
2455
2553
|
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
2456
2554
|
return;
|
|
@@ -2717,6 +2815,27 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2717
2815
|
attempt.assertLive();
|
|
2718
2816
|
opts.onEvent?.({ type: 'authorized', detail: cseAuthorizedDetail(replay, deliverRequest.url, continuation.shijiCvv) });
|
|
2719
2817
|
}
|
|
2818
|
+
else if (secureAcceptance) {
|
|
2819
|
+
// CyberSource's own reply page for the paused frame navigation, as the API stored it and authorize()
|
|
2820
|
+
// checked it, checked once more here so a replay from any path meets the same rule
|
|
2821
|
+
// (secureAcceptanceFrameAnswer): status 200, its one content type, and no CORS answer, which a
|
|
2822
|
+
// navigation never reads. Chrome commits it as CyberSource's own document in the merchant's hidden
|
|
2823
|
+
// frame, and its auto-post takes CyberSource's signed reply to the merchant's receipt page in this
|
|
2824
|
+
// browser's own session, as it would with no Agentcard in between.
|
|
2825
|
+
const answer = replay.mode === 'device_encrypt' ? null : secureAcceptanceFrameAnswer({ url: request.url, body }, replay);
|
|
2826
|
+
if (!answer)
|
|
2827
|
+
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'secure_acceptance_reply_unconfirmed');
|
|
2828
|
+
await cdp.send('Fetch.fulfillRequest', {
|
|
2829
|
+
requestId: deliverTo,
|
|
2830
|
+
responseCode: answer.status,
|
|
2831
|
+
responseHeaders: headerEntries(answer.headers),
|
|
2832
|
+
body: Buffer.from(answer.body).toString('base64'),
|
|
2833
|
+
}, deliverOn);
|
|
2834
|
+
attempt.assertLive();
|
|
2835
|
+
opts.onEvent?.({ type: 'authorized', detail: { mode: 'token', authorizationId: replay.authorizationId,
|
|
2836
|
+
amountVerified: replay.mode === 'device_encrypt' ? null : replay.amountVerified ?? null, cors: 'none' } });
|
|
2837
|
+
watchSecureAcceptanceFollowUp(secureAcceptanceFollowUpWatch, preparation, replay);
|
|
2838
|
+
}
|
|
2720
2839
|
else {
|
|
2721
2840
|
// The processor's answer, handed to the page as if the processor had
|
|
2722
2841
|
// sent it. Cross-origin, the browser then runs its CORS check on it:
|
|
@@ -2790,7 +2909,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2790
2909
|
}
|
|
2791
2910
|
// A card form navigation ends on the stopped page: its card reached the merchant once the
|
|
2792
2911
|
// device approved (the merchant answered it) or the hand-off began; see failedNavigationRefusal.
|
|
2793
|
-
|
|
2912
|
+
// A Secure Acceptance frame post reads CyberSource's side too (secureAcceptanceFailedRefusal).
|
|
2913
|
+
const sent = handoffStarted || approvedId !== null, failedFor = approvedId ?? lifecycle.getState().authorizationId;
|
|
2914
|
+
await refusePaused(dead.requestId, dead.sessionId, request.url, cardEndpoint, secureAcceptance ? secureAcceptanceFailedRefusal(err, sent, failedFor) : failedNavigationRefusal(err, sent, failedFor), secureAcceptance);
|
|
2794
2915
|
settlePause();
|
|
2795
2916
|
}
|
|
2796
2917
|
finally {
|
|
@@ -2815,6 +2936,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2815
2936
|
}
|
|
2816
2937
|
merchantContext = targetInfo.browserContextId;
|
|
2817
2938
|
targetContextKnown = true;
|
|
2939
|
+
// A page target's id is its main frame's id: a navigation of that frame is the page's own (pausedNavigation).
|
|
2940
|
+
pageTargetId = typeof targetInfo.targetId === 'string' && targetInfo.targetId ? targetInfo.targetId : undefined;
|
|
2818
2941
|
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
2819
2942
|
assertActive();
|
|
2820
2943
|
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
@@ -2895,6 +3018,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2895
3018
|
const aci = new AciGate(opts.vault);
|
|
2896
3019
|
let navigationBodyReplaced;
|
|
2897
3020
|
const eprotect = new EprotectGate(opts.vault);
|
|
3021
|
+
// The merchant's follow-up request after a delivered Secure Acceptance reply (cybersource-secure-acceptance.ts).
|
|
3022
|
+
const secureAcceptanceFollowUpWatch = new SecureAcceptanceFollowUpWatch(opts.onEvent);
|
|
3023
|
+
lifecycle.abort.signal.addEventListener('abort', () => secureAcceptanceFollowUpWatch.end(), { once: true });
|
|
2898
3024
|
const readDocumentUrl = async () => {
|
|
2899
3025
|
if (page.isClosed?.() || typeof page.url !== 'function')
|
|
2900
3026
|
throw new Error('merchant_document_unavailable');
|
|
@@ -2966,6 +3092,13 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2966
3092
|
// A new document is on its way: the main frame's next navigation commits it.
|
|
2967
3093
|
if (mainDocument(request))
|
|
2968
3094
|
documentRequest = request;
|
|
3095
|
+
// The merchant's follow-up after a delivered Secure Acceptance reply ends its watch (read-only).
|
|
3096
|
+
if (secureAcceptanceFollowUpWatch.active) {
|
|
3097
|
+
try {
|
|
3098
|
+
secureAcceptanceFollowUpWatch.saw(request.method(), request.url());
|
|
3099
|
+
}
|
|
3100
|
+
catch { /* a request the page already dropped */ }
|
|
3101
|
+
}
|
|
2969
3102
|
try {
|
|
2970
3103
|
const body = typeof request.postData === 'function' ? request.postData() : null;
|
|
2971
3104
|
merchantTotal.sent(keyOf(request), { method: request.method(), url: request.url(),
|
|
@@ -3303,11 +3436,16 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3303
3436
|
// A Payrails card save the gate bound to a captured session: its key rides on the create.
|
|
3304
3437
|
const payrailsSidecar = preClaim.payrails;
|
|
3305
3438
|
const cardNavigation = preClaim.navigation;
|
|
3439
|
+
// A Secure Acceptance token create the lane held, a navigation of the merchant's hidden frame (see attachToCdp).
|
|
3440
|
+
const secureAcceptance = preClaim.secureAcceptance === true;
|
|
3306
3441
|
// A card form navigation is refused with the stopped page, anything else as refusalFailure says (see attachToCdp
|
|
3307
3442
|
// refusePaused).
|
|
3308
|
-
const refuseRoute = (target, refusal) =>
|
|
3309
|
-
|
|
3310
|
-
|
|
3443
|
+
const refuseRoute = (target, refusal) => {
|
|
3444
|
+
const merchant = stoppedPageRecipient(cardEndpoint, secureAcceptance);
|
|
3445
|
+
return merchant !== null
|
|
3446
|
+
? target.fulfill(cardEndpointStoppedPage({ outcome: refusal.outcome, reason: refusal.reason, merchant, authorizationId: refusal.authorizationId }))
|
|
3447
|
+
: target.abort(refusalFailure(request.url()));
|
|
3448
|
+
};
|
|
3311
3449
|
if (!attachmentReady || setupStop.signal.aborted) {
|
|
3312
3450
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
3313
3451
|
return refuseRoute(route, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
@@ -3325,6 +3463,9 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3325
3463
|
}
|
|
3326
3464
|
catch (error) {
|
|
3327
3465
|
opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
|
|
3466
|
+
// As in attachToCdp: a Secure Acceptance frame post no ready preparation binds ends on the stopped page.
|
|
3467
|
+
if (secureAcceptance)
|
|
3468
|
+
return refuseRoute(route, unboundNavigationRefusal(awaitingApproval, lifecycle));
|
|
3328
3469
|
return route.abort(refusalFailure(request.url()));
|
|
3329
3470
|
}
|
|
3330
3471
|
// The page asking again for the purchase an outstanding approval is
|
|
@@ -3614,6 +3755,20 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3614
3755
|
assertRequestLive();
|
|
3615
3756
|
opts.onEvent?.({ type: 'authorized', detail: cseAuthorizedDetail(replay, deliverRequest.url(), continuation.shijiCvv) });
|
|
3616
3757
|
}
|
|
3758
|
+
else if (secureAcceptance) {
|
|
3759
|
+
// Same as the CDP path: CyberSource's own page, status 200 and its one content type, checked once
|
|
3760
|
+
// more, for the paused frame navigation. The SDK writes no CORS answer; Playwright appends its own to
|
|
3761
|
+
// any cross-origin fulfill whose request names an Origin (microsoft/playwright#12929), which a
|
|
3762
|
+
// navigation never reads.
|
|
3763
|
+
const answer = replay.mode === 'device_encrypt' ? null : secureAcceptanceFrameAnswer({ url: request.url(), body }, replay);
|
|
3764
|
+
if (!answer)
|
|
3765
|
+
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'secure_acceptance_reply_unconfirmed');
|
|
3766
|
+
await deliverTo.fulfill({ status: answer.status, headers: answer.headers, body: answer.body });
|
|
3767
|
+
assertRequestLive();
|
|
3768
|
+
opts.onEvent?.({ type: 'authorized', detail: { mode: 'token', authorizationId: replay.authorizationId,
|
|
3769
|
+
amountVerified: replay.mode === 'device_encrypt' ? null : replay.amountVerified ?? null, cors: 'none' } });
|
|
3770
|
+
watchSecureAcceptanceFollowUp(secureAcceptanceFollowUpWatch, preparation, replay);
|
|
3771
|
+
}
|
|
3617
3772
|
else {
|
|
3618
3773
|
// Playwright adds these itself when a cross-origin fulfill carries
|
|
3619
3774
|
// none; written here anyway (replacing a stale value) so a
|
|
@@ -3658,8 +3813,10 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3658
3813
|
reportCardEndpointRefusal(opts, cardEndpoint, err);
|
|
3659
3814
|
opts.onEvent?.({ type: 'failed', detail: failureSummary(err) });
|
|
3660
3815
|
}
|
|
3661
|
-
// As in attachToCdp: a card form navigation ends on the stopped page (failedNavigationRefusal)
|
|
3662
|
-
|
|
3816
|
+
// As in attachToCdp: a card form navigation ends on the stopped page (failedNavigationRefusal), and a
|
|
3817
|
+
// Secure Acceptance frame post reads CyberSource's side too (secureAcceptanceFailedRefusal).
|
|
3818
|
+
const sent = handoffStarted || approvedId !== null, failedFor = approvedId ?? lifecycle.getState().authorizationId;
|
|
3819
|
+
await Promise.resolve().then(() => refuseRoute(dead, secureAcceptance ? secureAcceptanceFailedRefusal(err, sent, failedFor) : failedNavigationRefusal(err, sent, failedFor))).catch(() => { });
|
|
3663
3820
|
}
|
|
3664
3821
|
finally {
|
|
3665
3822
|
disarmSendWindow?.();
|
package/dist/client.d.ts
CHANGED
|
@@ -176,6 +176,13 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
|
|
|
176
176
|
* `ekashu_hosted_payment_page`: NMI's hosted payment page, whose card-free entry this
|
|
177
177
|
* build pauses for the cardholder's device and whose card page this build never lets
|
|
178
178
|
* the agent's browser post to (pre-claim.ts, ekashu.ts); served on the same terms.
|
|
179
|
+
* `cybersource_sa_token_create`: CyberSource Secure Acceptance's token create
|
|
180
|
+
* (POST /silent/embedded/token/create), which this build pauses only as the
|
|
181
|
+
* merchant's hidden frame sends it, binds to a prepared checkout, and answers with
|
|
182
|
+
* CyberSource's own reply page once neither the API nor the cardholder's device
|
|
183
|
+
* finds a card in it (cybersource-secure-acceptance.ts). Served on the same terms,
|
|
184
|
+
* so every other company keeps the stop every build applies to a card posted to
|
|
185
|
+
* Secure Acceptance.
|
|
179
186
|
*/
|
|
180
187
|
export declare const SUPPORTED_CAPABILITIES: readonly string[];
|
|
181
188
|
/**
|
|
@@ -456,6 +463,19 @@ export interface PrepareCheckoutOptions {
|
|
|
456
463
|
* processor.
|
|
457
464
|
*/
|
|
458
465
|
paypageId?: string;
|
|
466
|
+
/**
|
|
467
|
+
* CyberSource Secure Acceptance only: the merchant's Secure Acceptance profile
|
|
468
|
+
* id, CyberSource's UUID shape (36 characters, hex in either case). It names
|
|
469
|
+
* the CyberSource merchant profile the card's token is created for, so it is
|
|
470
|
+
* fixed before the page sends anything: a token request naming another profile
|
|
471
|
+
* id never spends the approval. Required on the sandbox environment (your own
|
|
472
|
+
* test profile on testsecureacceptance.cybersource.com). On production leave it
|
|
473
|
+
* out for a merchant Agentcard reviewed: the SDK fills the one profile id its
|
|
474
|
+
* entry pins for the checkout page's origin, and refuses a checkout it can name
|
|
475
|
+
* none for (`secure_acceptance_profile_id_invalid`). Refused on every other
|
|
476
|
+
* processor.
|
|
477
|
+
*/
|
|
478
|
+
secureAcceptanceProfileId?: string;
|
|
459
479
|
/**
|
|
460
480
|
* CyberSource Flex only: the cardholder's device tokenizes the card when it approves, before
|
|
461
481
|
* Pay, under the capture context of the card fields the page already loaded, so the page's
|
|
@@ -525,6 +545,8 @@ export interface PreparedCheckout {
|
|
|
525
545
|
readonly paymentPageUrl?: string;
|
|
526
546
|
/** Worldpay eProtect only: the paypageId the API confirmed, the one merchant whose registration may spend this approval. */
|
|
527
547
|
readonly paypageId?: string;
|
|
548
|
+
/** CyberSource Secure Acceptance only: the profile id the API confirmed, the one merchant profile whose token create may spend this approval. */
|
|
549
|
+
readonly secureAcceptanceProfileId?: string;
|
|
528
550
|
/**
|
|
529
551
|
* CyberSource Flex, tokenized before Pay: the capture context and fields the cardholder's device
|
|
530
552
|
* tokenized the card under. Only a field request loaded under this context, with these fields,
|