@agent-cards/sdk 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -8
- package/PREFLIGHT.md +3 -1
- package/README.md +120 -15
- package/dist/adyen-merchant-hosted.generated.d.ts +2 -1
- package/dist/adyen-merchant-hosted.generated.js +51 -39
- package/dist/card-endpoint-facts.d.ts +96 -0
- package/dist/card-endpoint-facts.js +275 -0
- package/dist/card-endpoint-page.d.ts +54 -0
- package/dist/card-endpoint-page.js +69 -0
- package/dist/card-endpoint.d.ts +132 -11
- package/dist/card-endpoint.js +169 -17
- package/dist/cdp.js +336 -28
- package/dist/client.d.ts +61 -9
- package/dist/client.js +133 -16
- package/dist/creditguard.generated.d.ts +1 -1
- package/dist/cybersource-flex.d.ts +30 -0
- package/dist/cybersource-flex.generated.d.ts +27 -1
- package/dist/cybersource-flex.generated.js +42 -2
- package/dist/cybersource-flex.js +46 -0
- package/dist/ekashu.d.ts +35 -0
- package/dist/ekashu.js +55 -0
- package/dist/lifecycle.d.ts +7 -4
- package/dist/lifecycle.js +6 -3
- package/dist/merchant-card-endpoints.generated.d.ts +60 -1
- package/dist/merchant-card-endpoints.generated.js +3264 -95
- package/dist/pre-claim.d.ts +9 -0
- package/dist/pre-claim.js +179 -13
- package/dist/preflight-catalog.json +48 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +11 -1
- package/dist/preparation.d.ts +7 -1
- package/dist/preparation.js +27 -2
- package/dist/stripe-checkout.generated.js +66 -1
- package/dist/verifone.generated.js +20 -8
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +2 -2
package/dist/cdp.js
CHANGED
|
@@ -11,6 +11,7 @@ import { dispatchPreClaim, eventUrl } from './pre-claim.js';
|
|
|
11
11
|
import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
12
12
|
import { DatatransRepeats } from './datatrans.js';
|
|
13
13
|
import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
|
|
14
|
+
import { isArmedEkashuUrl } from './ekashu.js';
|
|
14
15
|
import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
|
|
15
16
|
import { cseBody } from './cse-body.js';
|
|
16
17
|
import { STRIPE_SETUP_INTENT_CONFIRM, setupConfirmPayingWith } from './stripe-setup.js';
|
|
@@ -21,9 +22,11 @@ import { MERCHANT_PAGE_WAIT_MS, MerchantTotalWatch } from './merchant-total-watc
|
|
|
21
22
|
import { hostedFormSubmittedPage } from './hosted-form.js';
|
|
22
23
|
import { amadeusSendWindowMs, armSendWindow, failsAsNetworkError } from './amadeus.js';
|
|
23
24
|
import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
24
|
-
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, refusedRequestFailure } from './cybersource-flex.js';
|
|
25
|
-
import { CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointFor, cardEndpointPatterns, } from './card-endpoint.js';
|
|
25
|
+
import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, loadedCardMicroformFromUrls, refusedRequestFailure, } from './cybersource-flex.js';
|
|
26
|
+
import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointFor, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointResendFor, } from './card-endpoint.js';
|
|
27
|
+
import { CardEndpointFactsWatch } from './card-endpoint-facts.js';
|
|
26
28
|
import { PAYRAILS_GUARD_PATTERNS, PAYRAILS_INIT_BODY_LIMIT, PayrailsSessionCapture, isPayrailsHost, payrailsInitCandidate, recognizesPayrails, } from './payrails.js';
|
|
29
|
+
import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoint-page.js';
|
|
27
30
|
/**
|
|
28
31
|
* How an adapter fails a paused request it will not let through: as a network error for a
|
|
29
32
|
* CyberSource Flex request (refusedRequestFailure, so the Microform field reports it to the page)
|
|
@@ -620,10 +623,24 @@ export function withCorsHeaders(headers, cors) {
|
|
|
620
623
|
/**
|
|
621
624
|
* The request a card endpoint approval is asked for: `request` names the URL and
|
|
622
625
|
* nothing else, and the profile travels with the captured session in
|
|
623
|
-
* merchant_card_endpoint, the one member the API seals
|
|
626
|
+
* merchant_card_endpoint, the one member the API seals, with the merchant's own cut
|
|
627
|
+
* record of the order for a profile that reads one.
|
|
624
628
|
*/
|
|
625
|
-
function cardEndpointRequest(profile, session) {
|
|
626
|
-
return { url: session.url, method: 'POST', headers: {}, body: '', merchant_card_endpoint: { profile: profile.id, session } };
|
|
629
|
+
function cardEndpointRequest(profile, session, facts = null) {
|
|
630
|
+
return { url: session.url, method: 'POST', headers: {}, body: '', merchant_card_endpoint: { profile: profile.id, session, ...(facts ? { facts } : {}) } };
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* The merchant's own record of the order for a paused card request of a profile that
|
|
634
|
+
* reads one (card-endpoint-facts.ts), or null for any other profile. A request with no
|
|
635
|
+
* record to send is refused before any create, naming no value.
|
|
636
|
+
*/
|
|
637
|
+
async function cardEndpointFacts(watch, profile, url, pausedAt, signal) {
|
|
638
|
+
if (!profile.facts)
|
|
639
|
+
return null;
|
|
640
|
+
const captured = await watch.capture(profile, url, pausedAt, signal);
|
|
641
|
+
if (!captured.record)
|
|
642
|
+
throw new CardEndpointRefusedError(profile.id, 'facts_unavailable', captured.refusal);
|
|
643
|
+
return captured.record;
|
|
627
644
|
}
|
|
628
645
|
/** A card endpoint refusal as an event: the profile, the endpoint origin, the reason, and the fields it is about, never a value. */
|
|
629
646
|
function reportCardEndpointRefusal(opts, profile, error) {
|
|
@@ -651,6 +668,75 @@ function playwrightCookieParams(cookie) {
|
|
|
651
668
|
secure: cookie.secure, httpOnly: cookie.httpOnly,
|
|
652
669
|
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}), ...(cookie.expires !== undefined ? { expires: cookie.expires } : {}) };
|
|
653
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* The page a stopped card form navigation is answered with (card-endpoint-page.ts): the
|
|
673
|
+
* card never left the device, it reached the merchant and the answer cannot be shown, or
|
|
674
|
+
* a payment for this checkout is waiting or may have reached the merchant.
|
|
675
|
+
*/
|
|
676
|
+
function stoppedNavigationPage(profile, outcome, reason, authorizationId) {
|
|
677
|
+
return cardEndpointStoppedPage({ outcome, reason, merchant: profile.merchant, authorizationId });
|
|
678
|
+
}
|
|
679
|
+
/** The API refused the create because this checkout already has a payment waiting or sent (409 merchant_checkout_in_use). */
|
|
680
|
+
function checkoutInUse(error) {
|
|
681
|
+
return error instanceof CheckoutApiError && error.code === 'merchant_checkout_in_use';
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* How a card form navigation the attachment refuses before any approval (not ready, held,
|
|
685
|
+
* busy, quiet, or after a failure that ends every later request) is answered. The API's
|
|
686
|
+
* refusal of this checkout as already paying keeps saying so on every later post. Otherwise
|
|
687
|
+
* the page reads the attachment (refusedNavigationOutcome): while an approval is outstanding,
|
|
688
|
+
* or once a payment may exist, it says a payment is in progress and names its authorization,
|
|
689
|
+
* never that nothing was sent.
|
|
690
|
+
*/
|
|
691
|
+
function heldNavigationRefusal(terminal, awaitingApproval, lifecycle) {
|
|
692
|
+
if (checkoutInUse(terminal))
|
|
693
|
+
return { outcome: 'payment_in_progress', reason: 'checkout_in_use', authorizationId: null };
|
|
694
|
+
const state = lifecycle.getState();
|
|
695
|
+
const outcome = refusedNavigationOutcome({ awaitingApproval, status: state.status });
|
|
696
|
+
return { outcome, reason: 'blocked', authorizationId: outcome === 'payment_in_progress' ? state.authorizationId : null };
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* How a card form navigation whose approval failed is answered, for `authorizationId`. Once
|
|
700
|
+
* the device approved or the hand-off began (`sent`), the card reached the merchant. Before
|
|
701
|
+
* that it may have left only while the authorization stood: a PaymentOutcomeUnknownError
|
|
702
|
+
* whose authorization the API confirmed retired before any processor request
|
|
703
|
+
* (processorRequestStarted false) sent nothing, and any other one may have reached the
|
|
704
|
+
* merchant. The API's refusal of a checkout already paying says a payment is in progress.
|
|
705
|
+
* Every other failure stopped the card before it left.
|
|
706
|
+
*/
|
|
707
|
+
function failedNavigationRefusal(error, sent, authorizationId) {
|
|
708
|
+
if (sent)
|
|
709
|
+
return { outcome: 'sent_answer_not_shown', reason: 'outcome_unknown', authorizationId };
|
|
710
|
+
if (error instanceof PaymentOutcomeUnknownError) {
|
|
711
|
+
return error.processorRequestStarted === false ? { outcome: 'stopped_before_send', reason: 'not_approved', authorizationId }
|
|
712
|
+
: { outcome: 'payment_in_progress', reason: 'outcome_unknown', authorizationId };
|
|
713
|
+
}
|
|
714
|
+
if (checkoutInUse(error))
|
|
715
|
+
return { outcome: 'payment_in_progress', reason: 'checkout_in_use', authorizationId };
|
|
716
|
+
return { outcome: 'stopped_before_send', reason: stoppedBeforeSendReason(error), authorizationId };
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* What a Playwright request navigates: 'top' for the page, 'frame' for an iframe, or null
|
|
720
|
+
* for a fetch, an XHR or a subresource, and for a request whose frame cannot be read.
|
|
721
|
+
*/
|
|
722
|
+
function playwrightNavigation(request) {
|
|
723
|
+
try {
|
|
724
|
+
const navigating = typeof request.isNavigationRequest === 'function' ? request.isNavigationRequest() === true : request.resourceType?.() === 'document';
|
|
725
|
+
if (!navigating)
|
|
726
|
+
return null;
|
|
727
|
+
const frame = request.frame?.();
|
|
728
|
+
if (!frame)
|
|
729
|
+
return null;
|
|
730
|
+
return frame.parentFrame?.() ? 'frame' : 'top';
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/** Why a card form navigation stopped before its card left, from the error that stopped it. */
|
|
737
|
+
function stoppedBeforeSendReason(error) {
|
|
738
|
+
return error instanceof CardEndpointRefusedError ? (error.reason === 'not_enabled' ? 'not_enabled' : 'request_unsupported') : 'not_approved';
|
|
739
|
+
}
|
|
654
740
|
/**
|
|
655
741
|
* Answer a paused card endpoint request with what the cardholder's device reported.
|
|
656
742
|
* A withheld answer (it carried the card, or a stream the device cannot read) becomes
|
|
@@ -661,15 +747,24 @@ function playwrightCookieParams(cookie) {
|
|
|
661
747
|
* merchant's status, the profile's answer
|
|
662
748
|
* headers, the CORS answer the request needs, and the body. Resolves false for a
|
|
663
749
|
* withheld answer and true when the merchant's answer was handed over.
|
|
750
|
+
*
|
|
751
|
+
* A profile whose page submits its form as a navigation is answered as a page load: a
|
|
752
|
+
* withheld answer, or a redirect that would make the browser post the form again, gets
|
|
753
|
+
* the stopped page (the card reached the merchant, the answer is not shown); a 301, 302
|
|
754
|
+
* or 303 is handed over as a redirect to its one https Location, which the browser
|
|
755
|
+
* follows in its own session with the merchant's cookies written first; any other
|
|
756
|
+
* answer is the page itself. No CORS answer rides on a page load.
|
|
664
757
|
*/
|
|
665
758
|
async function deliverCardEndpointAnswer(opts, lifecycle, profile, replay, request, io) {
|
|
666
759
|
// Only the request that named this profile is answered from its merchant's answer.
|
|
667
760
|
if (!profile || replay.profile !== profile.id)
|
|
668
761
|
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'approved_mode_unsupported');
|
|
669
762
|
const cors = corsDecision(request.url, request.headers);
|
|
763
|
+
const navigation = profile.navigation !== null;
|
|
670
764
|
if (replay.withheld) {
|
|
671
|
-
|
|
672
|
-
|
|
765
|
+
const fixed = replay.withheld === 'refused_step_in_response' ? CARD_ENDPOINT_REFUSED_STEP_ANSWER : CARD_ENDPOINT_WITHHELD_ANSWER;
|
|
766
|
+
await io.fulfill(navigation ? stoppedNavigationPage(profile, 'sent_answer_not_shown', 'answer_withheld', replay.authorizationId)
|
|
767
|
+
: { status: fixed.status, headers: withCorsHeaders({ ...fixed.headers }, cors.headers), body: fixed.body });
|
|
673
768
|
io.assertLive();
|
|
674
769
|
lifecycle.failed(new PaymentOutcomeUnknownError(replay.authorizationId, 'merchant_answer_withheld'), true);
|
|
675
770
|
opts.onEvent?.({ type: 'merchant_answer_withheld', detail: { authorizationId: replay.authorizationId, ...cardEndpointDetail(profile), reason: replay.withheld } });
|
|
@@ -687,6 +782,15 @@ async function deliverCardEndpointAnswer(opts, lifecycle, profile, replay, reque
|
|
|
687
782
|
}
|
|
688
783
|
}
|
|
689
784
|
const { cookies, skipped } = cardEndpointCookies(profile, request.url, replay.setCookies, Date.now(), jar);
|
|
785
|
+
// A navigation's answer is judged before any cookie is written: a redirect it cannot follow ends on the stopped page.
|
|
786
|
+
const verdict = navigation ? cardEndpointNavigationAnswer(profile, { status: replay.status, headers: replay.headers }, request.url) : { kind: 'page' };
|
|
787
|
+
if (verdict.kind === 'refused') {
|
|
788
|
+
await io.fulfill(stoppedNavigationPage(profile, 'sent_answer_not_shown', verdict.reason, replay.authorizationId));
|
|
789
|
+
io.assertLive();
|
|
790
|
+
lifecycle.failed(new PaymentOutcomeUnknownError(replay.authorizationId, 'merchant_redirect_refused'), true);
|
|
791
|
+
opts.onEvent?.({ type: 'merchant_answer_withheld', detail: { authorizationId: replay.authorizationId, ...cardEndpointDetail(profile), reason: verdict.reason } });
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
690
794
|
let written = 0;
|
|
691
795
|
let refused = 0;
|
|
692
796
|
for (const cookie of cookies) {
|
|
@@ -701,10 +805,20 @@ async function deliverCardEndpointAnswer(opts, lifecycle, profile, replay, reque
|
|
|
701
805
|
}
|
|
702
806
|
}
|
|
703
807
|
io.assertLive();
|
|
704
|
-
|
|
808
|
+
if (verdict.kind === 'redirect') {
|
|
809
|
+
await io.fulfill({ status: verdict.status, headers: { location: verdict.location, 'cache-control': 'no-store' }, body: '' });
|
|
810
|
+
}
|
|
811
|
+
else if (navigation) {
|
|
812
|
+
const headers = cardEndpointAnswerHeaders(profile, replay.headers);
|
|
813
|
+
delete headers.location;
|
|
814
|
+
await io.fulfill({ status: replay.status, headers, body: replay.body });
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
await io.fulfill({ status: replay.status, headers: withCorsHeaders(cardEndpointAnswerHeaders(profile, replay.headers), cors.headers), body: replay.body });
|
|
818
|
+
}
|
|
705
819
|
io.assertLive();
|
|
706
820
|
opts.onEvent?.({ type: 'authorized', detail: { mode: 'card_endpoint', authorizationId: replay.authorizationId, ...cardEndpointDetail(profile),
|
|
707
|
-
status: replay.status, cors: cors.outcome, cookies: { written, skipped: skipped + refused } } });
|
|
821
|
+
status: replay.status, ...(navigation ? { answered: verdict.kind } : { cors: cors.outcome }), cookies: { written, skipped: skipped + refused } } });
|
|
708
822
|
return true;
|
|
709
823
|
}
|
|
710
824
|
/** CDP's header shape: `{ name, value }` entries. */
|
|
@@ -745,6 +859,21 @@ function pageOriginFrom(documentUrl) {
|
|
|
745
859
|
return undefined;
|
|
746
860
|
}
|
|
747
861
|
}
|
|
862
|
+
/** The URL of the document the frame `frameId` holds, from the frame tree of the CDP session a request paused on. */
|
|
863
|
+
async function frameUrlIn(cdp, sessionId, frameId) {
|
|
864
|
+
if (typeof frameId !== 'string' || frameId === '')
|
|
865
|
+
throw new Error('merchant_document_unavailable');
|
|
866
|
+
const tree = await cdp.send('Page.getFrameTree', {}, sessionId);
|
|
867
|
+
const stack = tree?.frameTree ? [tree.frameTree] : [];
|
|
868
|
+
while (stack.length > 0) {
|
|
869
|
+
const node = stack.pop();
|
|
870
|
+
if (node?.frame?.id === frameId && typeof node.frame.url === 'string')
|
|
871
|
+
return node.frame.url;
|
|
872
|
+
for (const child of node?.childFrames ?? [])
|
|
873
|
+
stack.push(child);
|
|
874
|
+
}
|
|
875
|
+
throw new Error('merchant_document_unavailable');
|
|
876
|
+
}
|
|
748
877
|
/**
|
|
749
878
|
* The top-level document's URL, read once with a one-second bound when a card
|
|
750
879
|
* request pauses: pageOriginFrom derives the merchant origin from it, and the
|
|
@@ -1530,7 +1659,58 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1530
1659
|
preparationFrameId = tree.frameTree.frame.id;
|
|
1531
1660
|
return tree.frameTree.frame.url;
|
|
1532
1661
|
};
|
|
1533
|
-
|
|
1662
|
+
// prepare({ tokenizeBeforePay: true }) sends the card Microform whose field documents are committed
|
|
1663
|
+
// now. A field's commit can come before this attachment enabled its session, so no event reports
|
|
1664
|
+
// it; each iframe session's own frame tree, and the page's, is read once first (as rereadMicroforms
|
|
1665
|
+
// reads the pausing one), and every field found there is recorded (cybersource-flex.ts MicroformFrames).
|
|
1666
|
+
const readLoadedMicroform = async () => {
|
|
1667
|
+
const sessions = [pageSessionId, ...[...ownedSessions].filter(session => sessionTypes.get(session) === 'iframe' && !sessionDetached(session))];
|
|
1668
|
+
await Promise.all(sessions.map(session => rereadMicroforms(session, '').catch(() => undefined)));
|
|
1669
|
+
return microforms.loadedCard();
|
|
1670
|
+
};
|
|
1671
|
+
/**
|
|
1672
|
+
* The frame a paused document request navigates, read from the frame tree of the
|
|
1673
|
+
* session it paused on: 'top' for the page itself, 'frame' for an iframe (a frame with
|
|
1674
|
+
* a parent, or the root of an iframe's own session), with that frame's current
|
|
1675
|
+
* document URL, where the form was submitted. Null when the frame is not in the tree.
|
|
1676
|
+
*/
|
|
1677
|
+
const documentFrame = async (sessionId, frameId) => {
|
|
1678
|
+
if (typeof frameId !== 'string' || !frameId)
|
|
1679
|
+
return null;
|
|
1680
|
+
const tree = await cdp.send('Page.getFrameTree', {}, sessionId);
|
|
1681
|
+
const find = (node, depth) => {
|
|
1682
|
+
if (!node || depth > 32)
|
|
1683
|
+
return null;
|
|
1684
|
+
if (node.frame?.id === frameId)
|
|
1685
|
+
return node.frame;
|
|
1686
|
+
for (const child of Array.isArray(node.childFrames) ? node.childFrames : []) {
|
|
1687
|
+
const hit = find(child, depth + 1);
|
|
1688
|
+
if (hit)
|
|
1689
|
+
return hit;
|
|
1690
|
+
}
|
|
1691
|
+
return null;
|
|
1692
|
+
};
|
|
1693
|
+
const frame = find(tree?.frameTree, 0);
|
|
1694
|
+
if (!frame)
|
|
1695
|
+
return null;
|
|
1696
|
+
return { navigation: frame.parentId || sessionId !== pageSessionId ? 'frame' : 'top', url: typeof frame.url === 'string' ? frame.url : undefined };
|
|
1697
|
+
};
|
|
1698
|
+
/**
|
|
1699
|
+
* Refuse a paused request after the pre-claim gates: a card form navigation gets the
|
|
1700
|
+
* stopped page (card-endpoint-page.ts) that `refusal` describes, since an aborted page
|
|
1701
|
+
* load renders nothing and invites another click on Pay; any other request fails as
|
|
1702
|
+
* cdpFailure says (a network error for CyberSource Flex and Amadeus, else an abort).
|
|
1703
|
+
*/
|
|
1704
|
+
const refusePaused = async (requestId, sessionId, url, profile, refusal) => {
|
|
1705
|
+
if (profile && profile.navigation !== null) {
|
|
1706
|
+
const page = stoppedNavigationPage(profile, refusal.outcome, refusal.reason, refusal.authorizationId);
|
|
1707
|
+
await cdp.send('Fetch.fulfillRequest', { requestId, responseCode: page.status, responseHeaders: headerEntries(page.headers),
|
|
1708
|
+
body: Buffer.from(page.body).toString('base64') }, sessionId).catch(() => { });
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: cdpFailure(url) }, sessionId).catch(() => { });
|
|
1712
|
+
};
|
|
1713
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl, readLoadedMicroform);
|
|
1534
1714
|
// A merchant-hosted request this attachment continued with the Vault's
|
|
1535
1715
|
// ciphertext, and the Network identity its answer arrives under.
|
|
1536
1716
|
const merchantHandoff = new MerchantHandoffWatch(lifecycle, opts.onEvent);
|
|
@@ -1540,6 +1720,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1540
1720
|
// The merchant's own total (merchant-total-watch.ts): its checkout responses, recorded
|
|
1541
1721
|
// read-only from attach on, and the approval a hand-off answers, for the charge report.
|
|
1542
1722
|
const merchantTotal = new MerchantTotalWatch(opts.onEvent, opts.vault);
|
|
1723
|
+
// A reviewed card endpoint merchant's own record of the order (card-endpoint-facts.ts).
|
|
1724
|
+
const cardEndpointRecord = new CardEndpointFactsWatch();
|
|
1543
1725
|
let handoffReplay = null;
|
|
1544
1726
|
const guards = paymentEndpointGuards(opts.paymentEndpoints);
|
|
1545
1727
|
// Payrails sessions this page's own XHR and fetch answers carried, read only
|
|
@@ -1722,8 +1904,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1722
1904
|
// GMO token v1's JSONP GET is armed only when the synced registry recognizes
|
|
1723
1905
|
// GMO (an org the API served the dark entry to), so the gate can refuse it;
|
|
1724
1906
|
// for every other org it is never intercepted, as before (see gmo.ts).
|
|
1725
|
-
// Every reviewed card endpoint is armed in any status:
|
|
1726
|
-
// paused only to be refused, so the placeholder card never reaches the
|
|
1907
|
+
// Every reviewed card endpoint is armed in any status: a card request to one this build
|
|
1908
|
+
// does not pay is paused only to be refused, so the placeholder card never reaches the
|
|
1909
|
+
// merchant, and a request with no card in it continues untouched.
|
|
1727
1910
|
// Every Payrails host is intercepted only for an org the API served Payrails
|
|
1728
1911
|
// to, so the gate can abort a card-form payment (see payrails.ts).
|
|
1729
1912
|
const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
|
|
@@ -1884,6 +2067,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1884
2067
|
merchantTotal.sent(key, { method: params.request.method, url: params.request.url,
|
|
1885
2068
|
...(typeof params.request.postData === 'string' ? { requestBody: params.request.postData } : {}),
|
|
1886
2069
|
...(typeof params.loaderId === 'string' && params.loaderId ? { page: `${owner}:${params.loaderId}` } : {}) });
|
|
2070
|
+
cardEndpointRecord.sent(key, { method: params.request.method, url: params.request.url, resourceType: params.type ?? null, networkId: params.requestId });
|
|
1887
2071
|
}
|
|
1888
2072
|
if (matchesActive) {
|
|
1889
2073
|
try {
|
|
@@ -1906,6 +2090,16 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1906
2090
|
if (method === 'Network.loadingFinished') {
|
|
1907
2091
|
const key = requestKey(sessionId, params.requestId);
|
|
1908
2092
|
requestSources.delete(key);
|
|
2093
|
+
// An out-of-process frame's document finishes on the frame's own session, under the
|
|
2094
|
+
// network id its request was reported with on the parent's (keyFor).
|
|
2095
|
+
const recordKey = cardEndpointRecord.keyFor(key, params.requestId);
|
|
2096
|
+
if (recordKey) {
|
|
2097
|
+
const completedAt = Date.now();
|
|
2098
|
+
// Its place among the answers that finished, before its text is read: texts can come back out of order.
|
|
2099
|
+
cardEndpointRecord.finished(recordKey);
|
|
2100
|
+
cdp.send('Network.getResponseBody', { requestId: params.requestId }, sessionId).then((read) => cardEndpointRecord.read(recordKey, typeof read?.body === 'string'
|
|
2101
|
+
? (read.base64Encoded ? Buffer.from(read.body, 'base64').toString('utf8') : read.body) : null, completedAt), () => cardEndpointRecord.read(recordKey, null, completedAt));
|
|
2102
|
+
}
|
|
1909
2103
|
if (merchantTotal.log.tracks(key)) {
|
|
1910
2104
|
// When it finished, then its text: a merchant response the total or the charge is read from.
|
|
1911
2105
|
const completedAt = Date.now();
|
|
@@ -1926,6 +2120,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1926
2120
|
}
|
|
1927
2121
|
if (method === 'Network.responseReceived') {
|
|
1928
2122
|
merchantTotal.log.answered(requestKey(sessionId, params.requestId), params.response?.status);
|
|
2123
|
+
const recordKey = cardEndpointRecord.keyFor(requestKey(sessionId, params.requestId), params.requestId);
|
|
2124
|
+
if (recordKey)
|
|
2125
|
+
cardEndpointRecord.answered(recordKey, params.response?.status);
|
|
1929
2126
|
// The merchant's answer to a continued merchant-hosted request: 5xx is an unknown outcome.
|
|
1930
2127
|
if (handoffNetwork && params.requestId === handoffNetwork.networkId && sessionId === handoffNetwork.sessionId) {
|
|
1931
2128
|
merchantHandoff.responded(params.response?.status);
|
|
@@ -1947,6 +2144,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1947
2144
|
}
|
|
1948
2145
|
if (method === 'Network.loadingFailed') {
|
|
1949
2146
|
merchantTotal.log.failed(requestKey(sessionId, params.requestId));
|
|
2147
|
+
const recordKey = cardEndpointRecord.keyFor(requestKey(sessionId, params.requestId), params.requestId);
|
|
2148
|
+
if (recordKey)
|
|
2149
|
+
cardEndpointRecord.failed(recordKey);
|
|
1950
2150
|
if (handoffNetwork && params.requestId === handoffNetwork.networkId && sessionId === handoffNetwork.sessionId)
|
|
1951
2151
|
merchantHandoff.failed();
|
|
1952
2152
|
payrailsAnswers.delete(requestKey(sessionId, params.requestId));
|
|
@@ -2109,6 +2309,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2109
2309
|
// navigation (the ACI browser test's classic case, design gate G3).
|
|
2110
2310
|
replacesNavigationBody: async () => true,
|
|
2111
2311
|
readDocumentUrl, lifecycle, preparationGate, onEvent: opts.onEvent, failureSummary,
|
|
2312
|
+
// Read only for a card endpoint whose page submits its form as a navigation.
|
|
2313
|
+
navigation: async () => (resourceType === 'Document' ? (await documentFrame(sessionId, frameId))?.navigation ?? null : null),
|
|
2112
2314
|
effects: {
|
|
2113
2315
|
continueRequest: (postData) => cdp.send('Fetch.continueRequest', { requestId, ...(postData !== undefined ? { postData: Buffer.from(postData).toString('base64') } : {}) }, sessionId),
|
|
2114
2316
|
answer: (response) => cdp.send('Fetch.fulfillRequest', { requestId, responseCode: response.status,
|
|
@@ -2145,13 +2347,15 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2145
2347
|
const stripeStep = preClaim.stripeStep;
|
|
2146
2348
|
// The capture context and fields a CyberSource Flex create carries, and its retry shape.
|
|
2147
2349
|
const flexPause = preClaim.cybersourceFlex;
|
|
2148
|
-
// A reviewed merchant's own card endpoint this build pays (see card-endpoint.ts)
|
|
2350
|
+
// A reviewed merchant's own card endpoint this build pays (see card-endpoint.ts), and
|
|
2351
|
+
// for one whose page submits its form as a navigation, which frame it navigates.
|
|
2149
2352
|
const cardEndpoint = preClaim.cardEndpoint ?? null;
|
|
2150
2353
|
// A Payrails card save the gate bound to a captured session: its key rides on the create.
|
|
2151
2354
|
const payrailsSidecar = preClaim.payrails;
|
|
2355
|
+
const cardNavigation = preClaim.navigation;
|
|
2152
2356
|
if (!attachmentReady || arming.has(sessionId ?? '__root__') || setupStop.signal.aborted) {
|
|
2153
2357
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
2154
|
-
await
|
|
2358
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2155
2359
|
return;
|
|
2156
2360
|
}
|
|
2157
2361
|
// A request the catalog marks as routine without a card (a confirmation
|
|
@@ -2238,7 +2442,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2238
2442
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
2239
2443
|
if (preparation)
|
|
2240
2444
|
preparationGate.retireUnboundClaim();
|
|
2241
|
-
|
|
2445
|
+
// A card form navigation never reads that nothing was sent while a payment for this checkout may exist.
|
|
2446
|
+
await refusePaused(requestId, sessionId, request.url, cardEndpoint, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
2242
2447
|
if (repeatedAuthorizationId)
|
|
2243
2448
|
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
2244
2449
|
return;
|
|
@@ -2341,12 +2546,23 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2341
2546
|
? await merchantTotal.pageFor(cardKey, MERCHANT_PAGE_WAIT_MS, attempt.signal) : merchantTotal.pageOf(cardKey);
|
|
2342
2547
|
// The card endpoint's session: the profile's own headers from the paused request,
|
|
2343
2548
|
// the endpoint's cookie jar for a cookie profile (Chrome keeps it out of the
|
|
2344
|
-
// paused headers, HttpOnly cookies included), and the
|
|
2549
|
+
// paused headers, HttpOnly cookies included), and a Referer on the origin the
|
|
2550
|
+
// request is sent from: the profile's card vault frame when it names one, else
|
|
2551
|
+
// the checkout page.
|
|
2552
|
+
// A form inside an iframe is judged by the iframe's own document, where it was submitted, and
|
|
2553
|
+
// a checkout in a frame the merchant's page embeds by that frame's own document.
|
|
2554
|
+
const capturedOrigin = cardEndpoint?.navigation === 'frame'
|
|
2555
|
+
? pageOriginFrom((await documentFrame(sessionId, frameId).catch(() => null))?.url)
|
|
2556
|
+
: cardEndpoint?.checkout_frame
|
|
2557
|
+
? pageOriginFrom(await documentUrlOf(() => frameUrlIn(cdp, sessionId, frameId), attempt.signal)) : pageOrigin;
|
|
2345
2558
|
const session = cardEndpoint ? await captureCardEndpointSession(cardEndpoint, {
|
|
2346
|
-
url: request.url, headers: request.headers, body, pageOrigin,
|
|
2559
|
+
url: request.url, headers: request.headers, body, pageOrigin: capturedOrigin, navigation: cardNavigation,
|
|
2347
2560
|
cookies: async () => (await cdp.send('Network.getCookies', { urls: [request.url] }, sessionId))?.cookies ?? [],
|
|
2348
2561
|
}) : null;
|
|
2349
2562
|
attempt.assertLive();
|
|
2563
|
+
// The merchant's own record of the order, as of the pause, for a profile that reads one.
|
|
2564
|
+
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url, pausedAt, attempt.signal) : null;
|
|
2565
|
+
attempt.assertLive();
|
|
2350
2566
|
// Decision 176: pause only. The business creates the authorization from the paused request and answers
|
|
2351
2567
|
// through controller.resume; the replay it hands back goes down the same delivery path as a create's.
|
|
2352
2568
|
const pause = opts.pauseOnly ? pauseForBusiness(opts, lifecycle, attempt.signal, { url: request.url, method: request.method, headers: request.headers ?? {}, body }, pageOrigin) : null;
|
|
@@ -2376,7 +2592,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2376
2592
|
lifecycle.approvalUrl(url);
|
|
2377
2593
|
return opts.onApprovalUrl?.(url);
|
|
2378
2594
|
} },
|
|
2379
|
-
request: stripeStep?.phase === 'final' ? stripeStep.request : cardEndpoint && session ? cardEndpointRequest(cardEndpoint, session)
|
|
2595
|
+
request: stripeStep?.phase === 'final' ? stripeStep.request : cardEndpoint && session ? cardEndpointRequest(cardEndpoint, session, cardRecord)
|
|
2380
2596
|
: { url: request.url, method: request.method, headers: request.headers, body,
|
|
2381
2597
|
...(mercadoCheckout.requiresDocument(request.url, request.method)
|
|
2382
2598
|
? { mercado_checkout: mercadoCheckout.claimToken(request.url, request.method, await readDocumentUrl()) } : {}),
|
|
@@ -2565,7 +2781,9 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2565
2781
|
reportCardEndpointRefusal(opts, cardEndpoint, err);
|
|
2566
2782
|
opts.onEvent?.({ type: 'failed', detail: failureSummary(err) });
|
|
2567
2783
|
}
|
|
2568
|
-
|
|
2784
|
+
// A card form navigation ends on the stopped page: its card reached the merchant once the
|
|
2785
|
+
// device approved (the merchant answered it) or the hand-off began; see failedNavigationRefusal.
|
|
2786
|
+
await refusePaused(dead.requestId, dead.sessionId, request.url, cardEndpoint, failedNavigationRefusal(err, handoffStarted || approvedId !== null, approvedId ?? lifecycle.getState().authorizationId));
|
|
2569
2787
|
settlePause();
|
|
2570
2788
|
}
|
|
2571
2789
|
finally {
|
|
@@ -2675,7 +2893,17 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2675
2893
|
throw new Error('merchant_document_unavailable');
|
|
2676
2894
|
return page.url();
|
|
2677
2895
|
};
|
|
2678
|
-
|
|
2896
|
+
// prepare({ tokenizeBeforePay: true }) sends the card Microform whose fields are on the page now.
|
|
2897
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl, () => {
|
|
2898
|
+
let frames = [];
|
|
2899
|
+
try {
|
|
2900
|
+
frames = page.frames?.() ?? [];
|
|
2901
|
+
}
|
|
2902
|
+
catch {
|
|
2903
|
+
frames = [];
|
|
2904
|
+
}
|
|
2905
|
+
return loadedCardMicroformFromUrls(frames.map(frameUrlOf));
|
|
2906
|
+
});
|
|
2679
2907
|
// A merchant-hosted request this attachment continued, and its answer; see watchPlaywrightHandoff.
|
|
2680
2908
|
const merchantHandoff = new MerchantHandoffWatch(lifecycle, opts.onEvent);
|
|
2681
2909
|
let handoffAnswer = null;
|
|
@@ -2686,6 +2914,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2686
2914
|
// but sends no document request, so it keeps the page id, as CDP's loaderId does. Nor does
|
|
2687
2915
|
// a document request that commits nothing: one that failed, or one answered 204 or 205.
|
|
2688
2916
|
const merchantTotal = new MerchantTotalWatch(opts.onEvent, opts.vault);
|
|
2917
|
+
// A reviewed card endpoint merchant's own record of the order (card-endpoint-facts.ts).
|
|
2918
|
+
const cardEndpointRecord = new CardEndpointFactsWatch();
|
|
2689
2919
|
let documentSerial = 1;
|
|
2690
2920
|
// The main frame's document request that no navigation has committed yet.
|
|
2691
2921
|
let documentRequest = null;
|
|
@@ -2713,6 +2943,18 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2713
2943
|
// a new document (the handlers below).
|
|
2714
2944
|
const shijiCompanions = new ShijiStsCompanions();
|
|
2715
2945
|
lifecycle.abort.signal.addEventListener('abort', () => shijiCompanions.release(), { once: true });
|
|
2946
|
+
// An answer to a request the facts watch records began to arrive: the watch orders what the page sent after a
|
|
2947
|
+
// record by this moment, since Playwright reports the answer's end after the page's next request. Listened to only
|
|
2948
|
+
// once the page sent such a request, so a page that never meets a merchant with facts carries no extra listener.
|
|
2949
|
+
let factsAnswers = false;
|
|
2950
|
+
const factsAnswer = (response) => {
|
|
2951
|
+
try {
|
|
2952
|
+
const key = keyOf(response.request());
|
|
2953
|
+
if (cardEndpointRecord.tracks(key))
|
|
2954
|
+
cardEndpointRecord.answered(key, response.status());
|
|
2955
|
+
}
|
|
2956
|
+
catch { /* a response the page already dropped */ }
|
|
2957
|
+
};
|
|
2716
2958
|
page.on?.('request', (request) => {
|
|
2717
2959
|
// A new document is on its way: the main frame's next navigation commits it.
|
|
2718
2960
|
if (mainDocument(request))
|
|
@@ -2723,6 +2965,16 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2723
2965
|
...(typeof body === 'string' ? { requestBody: body } : {}), page: `doc:${documentSerial}` });
|
|
2724
2966
|
}
|
|
2725
2967
|
catch { /* a request the page already dropped */ }
|
|
2968
|
+
try {
|
|
2969
|
+
cardEndpointRecord.sent(keyOf(request), { method: request.method(), url: request.url(),
|
|
2970
|
+
resourceType: typeof request.resourceType === 'function' ? request.resourceType() : null });
|
|
2971
|
+
// From the first request the watch records, it also hears when each answer begins to arrive (factsAnswer).
|
|
2972
|
+
if (!factsAnswers && cardEndpointRecord.tracks(keyOf(request))) {
|
|
2973
|
+
factsAnswers = true;
|
|
2974
|
+
page.on?.('response', factsAnswer);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
catch { /* a request the page already dropped */ }
|
|
2726
2978
|
});
|
|
2727
2979
|
page.on?.('requestfinished', (request) => {
|
|
2728
2980
|
// A document answered 204 or 205 loads nothing, so no navigation will commit it.
|
|
@@ -2733,6 +2985,27 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2733
2985
|
}).catch(() => { });
|
|
2734
2986
|
}
|
|
2735
2987
|
const key = keyOf(request);
|
|
2988
|
+
if (cardEndpointRecord.tracks(key)) {
|
|
2989
|
+
const finishedAt = Date.now();
|
|
2990
|
+
// Its place among the answers that finished, before its text is read: texts can come back out of order.
|
|
2991
|
+
cardEndpointRecord.finished(key);
|
|
2992
|
+
Promise.resolve().then(async () => {
|
|
2993
|
+
const response = await request.response();
|
|
2994
|
+
if (!response) {
|
|
2995
|
+
cardEndpointRecord.failed(key);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
cardEndpointRecord.answered(key, response.status());
|
|
2999
|
+
let text = null;
|
|
3000
|
+
try {
|
|
3001
|
+
text = await response.text();
|
|
3002
|
+
}
|
|
3003
|
+
catch {
|
|
3004
|
+
text = null;
|
|
3005
|
+
}
|
|
3006
|
+
cardEndpointRecord.read(key, text, finishedAt);
|
|
3007
|
+
}).catch(() => cardEndpointRecord.failed(key));
|
|
3008
|
+
}
|
|
2736
3009
|
if (!merchantTotal.log.tracks(key))
|
|
2737
3010
|
return;
|
|
2738
3011
|
const completedAt = Date.now();
|
|
@@ -2802,6 +3075,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2802
3075
|
if (request === documentRequest)
|
|
2803
3076
|
documentRequest = null;
|
|
2804
3077
|
merchantTotal.log.failed(keyOf(request));
|
|
3078
|
+
cardEndpointRecord.failed(keyOf(request));
|
|
2805
3079
|
if (activeRequest && activeRequest.request === request && activeRequest.attempt.lose()) {
|
|
2806
3080
|
// The page's own script gave up on its request; the cardholder's
|
|
2807
3081
|
// approval did not (see merchantAttempt). Forget the request's identity
|
|
@@ -2899,11 +3173,17 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2899
3173
|
// GMO token v1's JSONP GET, routed only for an org the API served GMO to (see attachToCdp).
|
|
2900
3174
|
|| (isGmoTokenV1Request(url.toString()) && recognizesGmo(opts.vault))
|
|
2901
3175
|
|| juspayCheckout.matches(url.toString())
|
|
2902
|
-
// Every reviewed card endpoint, in any status (the pre-claim gate
|
|
3176
|
+
// Every reviewed card endpoint, in any status (the pre-claim gate holds only a request that carries a card,
|
|
3177
|
+
// and refuses one this build does not pay).
|
|
2903
3178
|
|| cardEndpointFor(url.toString(), 'POST') !== null
|
|
3179
|
+
// A request that sends the typed card to such a merchant again, refused in every status.
|
|
3180
|
+
|| cardEndpointResendFor(url.toString(), 'POST') !== null
|
|
2904
3181
|
|| aci.matches(url.toString())
|
|
2905
3182
|
// Every Payrails host, routed only for an org the API served Payrails to (see attachToCdp).
|
|
2906
|
-
|| (payrails !== null && isPayrailsHost(url.toString()))
|
|
3183
|
+
|| (payrails !== null && isPayrailsHost(url.toString()))
|
|
3184
|
+
// Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
|
|
3185
|
+
// host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
|
|
3186
|
+
|| isArmedEkashuUrl(opts.vault, url.toString()), async (route) => {
|
|
2907
3187
|
const request = route.request();
|
|
2908
3188
|
// When this request paused, and the page it came from: the merchant's total is read as of now.
|
|
2909
3189
|
const pausedAt = Date.now();
|
|
@@ -2944,6 +3224,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
2944
3224
|
// the body route.continue replaces on a navigation (aci.ts playwrightReplacesNavigationBody).
|
|
2945
3225
|
replacesNavigationBody: () => (navigationBodyReplaced ??= playwrightReplacesNavigationBody(page)),
|
|
2946
3226
|
readDocumentUrl, lifecycle, preparationGate, onEvent: opts.onEvent, failureSummary,
|
|
3227
|
+
// Read only for a card endpoint whose page submits its form as a navigation (see attachToCdp).
|
|
3228
|
+
navigation: async () => playwrightNavigation(request),
|
|
2947
3229
|
effects: {
|
|
2948
3230
|
continueRequest: (postData) => postData !== undefined ? route.continue({ postData }) : route.fallback(),
|
|
2949
3231
|
answer: (response) => route.fulfill({ ...response,
|
|
@@ -3004,13 +3286,20 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3004
3286
|
const stripeStep = preClaim.stripeStep;
|
|
3005
3287
|
// The capture context and fields a CyberSource Flex create carries, and its retry shape.
|
|
3006
3288
|
const flexPause = preClaim.cybersourceFlex;
|
|
3007
|
-
// A reviewed merchant's own card endpoint this build pays (see card-endpoint.ts)
|
|
3289
|
+
// A reviewed merchant's own card endpoint this build pays (see card-endpoint.ts), and
|
|
3290
|
+
// for one whose page submits its form as a navigation, which frame it navigates.
|
|
3008
3291
|
const cardEndpoint = preClaim.cardEndpoint ?? null;
|
|
3009
3292
|
// A Payrails card save the gate bound to a captured session: its key rides on the create.
|
|
3010
3293
|
const payrailsSidecar = preClaim.payrails;
|
|
3294
|
+
const cardNavigation = preClaim.navigation;
|
|
3295
|
+
// A card form navigation is refused with the stopped page, anything else as refusalFailure says (see attachToCdp
|
|
3296
|
+
// refusePaused).
|
|
3297
|
+
const refuseRoute = (target, refusal) => (cardEndpoint && cardEndpoint.navigation !== null
|
|
3298
|
+
? target.fulfill(stoppedNavigationPage(cardEndpoint, refusal.outcome, refusal.reason, refusal.authorizationId))
|
|
3299
|
+
: target.abort(refusalFailure(request.url())));
|
|
3011
3300
|
if (!attachmentReady || setupStop.signal.aborted) {
|
|
3012
3301
|
opts.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
3013
|
-
return route
|
|
3302
|
+
return refuseRoute(route, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
3014
3303
|
}
|
|
3015
3304
|
// Same order as attachToCdp: a routine card-free request continues in
|
|
3016
3305
|
// every state, before the preparation claim, so it neither spends a
|
|
@@ -3082,7 +3371,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3082
3371
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
3083
3372
|
if (preparation)
|
|
3084
3373
|
preparationGate.retireUnboundClaim();
|
|
3085
|
-
|
|
3374
|
+
// As in attachToCdp: never that nothing was sent while a payment for this checkout may exist.
|
|
3375
|
+
const refused = await refuseRoute(route, heldNavigationRefusal(terminal, awaitingApproval, lifecycle));
|
|
3086
3376
|
if (repeatedAuthorizationId)
|
|
3087
3377
|
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
3088
3378
|
return refused;
|
|
@@ -3171,11 +3461,28 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3171
3461
|
assertRequestLive();
|
|
3172
3462
|
// The card endpoint's session (see attachToCdp): request.headers() never carries
|
|
3173
3463
|
// the cookie jar, so the endpoint's cookies are read from the context.
|
|
3464
|
+
// A form inside an iframe is judged by the iframe's own document, where it was submitted, and
|
|
3465
|
+
// a checkout in a frame the merchant's page embeds by that frame's own document.
|
|
3466
|
+
let capturedOrigin = pageOrigin;
|
|
3467
|
+
if (cardEndpoint?.navigation === 'frame') {
|
|
3468
|
+
try {
|
|
3469
|
+
capturedOrigin = pageOriginFrom(request.frame?.()?.url?.());
|
|
3470
|
+
}
|
|
3471
|
+
catch {
|
|
3472
|
+
capturedOrigin = undefined;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
else if (cardEndpoint?.checkout_frame) {
|
|
3476
|
+
capturedOrigin = pageOriginFrom(await documentUrlOf(async () => String(request.frame().url()), attempt.signal));
|
|
3477
|
+
}
|
|
3174
3478
|
const session = cardEndpoint ? await captureCardEndpointSession(cardEndpoint, {
|
|
3175
|
-
url: request.url(), headers: request.headers(), body, pageOrigin,
|
|
3479
|
+
url: request.url(), headers: request.headers(), body, pageOrigin: capturedOrigin, navigation: cardNavigation,
|
|
3176
3480
|
cookies: async () => await page.context().cookies(request.url()),
|
|
3177
3481
|
}) : null;
|
|
3178
3482
|
assertRequestLive();
|
|
3483
|
+
// The merchant's own record of the order, as of the pause (see attachToCdp).
|
|
3484
|
+
const cardRecord = cardEndpoint ? await cardEndpointFacts(cardEndpointRecord, cardEndpoint, request.url(), pausedAt, attempt.signal) : null;
|
|
3485
|
+
assertRequestLive();
|
|
3179
3486
|
const replay = await opts.vault.authorize({
|
|
3180
3487
|
user: opts.user,
|
|
3181
3488
|
merchant: opts.merchant,
|
|
@@ -3200,7 +3507,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3200
3507
|
lifecycle.approvalUrl(url);
|
|
3201
3508
|
return opts.onApprovalUrl?.(url);
|
|
3202
3509
|
} },
|
|
3203
|
-
request: stripeStep?.phase === 'final' ? stripeStep.request : cardEndpoint && session ? cardEndpointRequest(cardEndpoint, session)
|
|
3510
|
+
request: stripeStep?.phase === 'final' ? stripeStep.request : cardEndpoint && session ? cardEndpointRequest(cardEndpoint, session, cardRecord)
|
|
3204
3511
|
: { url: request.url(), method: request.method(), headers: request.headers(), body,
|
|
3205
3512
|
...(mercadoCheckout.requiresDocument(request.url(), request.method())
|
|
3206
3513
|
? { mercado_checkout: mercadoCheckout.claimToken(request.url(), request.method(), await readDocumentUrl()) } : {}),
|
|
@@ -3340,7 +3647,8 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3340
3647
|
reportCardEndpointRefusal(opts, cardEndpoint, err);
|
|
3341
3648
|
opts.onEvent?.({ type: 'failed', detail: failureSummary(err) });
|
|
3342
3649
|
}
|
|
3343
|
-
|
|
3650
|
+
// As in attachToCdp: a card form navigation ends on the stopped page (failedNavigationRefusal).
|
|
3651
|
+
await Promise.resolve().then(() => refuseRoute(dead, failedNavigationRefusal(err, handoffStarted || approvedId !== null, approvedId ?? lifecycle.getState().authorizationId))).catch(() => { });
|
|
3344
3652
|
}
|
|
3345
3653
|
finally {
|
|
3346
3654
|
disarmSendWindow?.();
|