@agent-cards/sdk 0.21.0 → 0.22.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 +47 -0
- package/PREFLIGHT.md +30 -0
- package/README.md +1070 -21
- package/dist/aci.d.ts +173 -0
- package/dist/aci.generated.d.ts +87 -0
- package/dist/aci.generated.js +306 -0
- package/dist/aci.js +371 -0
- package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
- package/dist/adyen-merchant-hosted.generated.js +283 -49
- package/dist/agentcard.d.ts +9 -2
- package/dist/agentcard.js +4 -2
- package/dist/amadeus-checkout.generated.d.ts +57 -0
- package/dist/amadeus-checkout.generated.js +549 -0
- package/dist/amadeus.d.ts +41 -0
- package/dist/amadeus.js +81 -0
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/card-endpoint.d.ts +177 -0
- package/dist/card-endpoint.js +286 -0
- package/dist/card-fields.generated.d.ts +5 -3
- package/dist/card-fields.generated.js +284 -7
- package/dist/cdp.d.ts +5 -1
- package/dist/cdp.js +841 -135
- package/dist/client.d.ts +403 -19
- package/dist/client.js +949 -98
- package/dist/creditguard.generated.d.ts +79 -0
- package/dist/creditguard.generated.js +662 -0
- package/dist/cse-body.js +11 -1
- package/dist/datatrans.d.ts +96 -0
- package/dist/datatrans.generated.d.ts +64 -0
- package/dist/datatrans.generated.js +426 -0
- package/dist/datatrans.js +178 -0
- package/dist/elavon-converge.d.ts +37 -0
- package/dist/elavon-converge.generated.d.ts +49 -0
- package/dist/elavon-converge.generated.js +439 -0
- package/dist/elavon-converge.js +84 -0
- package/dist/eprotect.d.ts +152 -0
- package/dist/eprotect.generated.d.ts +77 -0
- package/dist/eprotect.generated.js +498 -0
- package/dist/eprotect.js +222 -0
- package/dist/hosted-form.d.ts +3 -2
- package/dist/hosted-form.js +3 -2
- package/dist/index.d.ts +17 -3
- package/dist/index.js +6 -1
- package/dist/juspay-gate.d.ts +82 -0
- package/dist/juspay-gate.js +126 -0
- package/dist/juspay.generated.d.ts +89 -0
- package/dist/juspay.generated.js +705 -0
- package/dist/lifecycle.d.ts +57 -6
- package/dist/lifecycle.js +119 -8
- package/dist/merchant-card-endpoints.generated.d.ts +63 -0
- package/dist/merchant-card-endpoints.generated.js +1103 -0
- package/dist/merchant-hosted.d.ts +4 -1
- package/dist/merchant-hosted.js +5 -2
- package/dist/merchant-session-probe.d.ts +48 -0
- package/dist/merchant-session-probe.js +53 -0
- package/dist/merchant-session.generated.d.ts +69 -0
- package/dist/merchant-session.generated.js +919 -0
- package/dist/payrails.d.ts +133 -0
- package/dist/payrails.generated.d.ts +73 -0
- package/dist/payrails.generated.js +596 -0
- package/dist/payrails.js +305 -0
- package/dist/pelecard.d.ts +32 -0
- package/dist/pelecard.generated.d.ts +32 -0
- package/dist/pelecard.generated.js +283 -0
- package/dist/pelecard.js +42 -0
- package/dist/pre-claim.d.ts +72 -2
- package/dist/pre-claim.js +857 -5
- package/dist/preflight-catalog.json +2218 -287
- package/dist/preflight-schemas.json +179 -6
- package/dist/preflight.generated.d.ts +1 -1
- package/dist/preflight.generated.js +233 -4
- package/dist/preparation.d.ts +5 -2
- package/dist/preparation.js +36 -10
- package/dist/prepared-processor.d.ts +84 -5
- package/dist/prepared-processor.js +147 -6
- package/dist/primer.d.ts +53 -0
- package/dist/primer.generated.d.ts +38 -0
- package/dist/primer.generated.js +624 -0
- package/dist/primer.js +82 -0
- package/dist/registry.d.ts +100 -8
- package/dist/registry.js +85 -11
- package/dist/shiji-sts.d.ts +144 -0
- package/dist/shiji-sts.generated.d.ts +72 -0
- package/dist/shiji-sts.generated.js +913 -0
- package/dist/shiji-sts.js +222 -0
- package/dist/stripe-checkout.generated.d.ts +1 -1
- package/dist/stripe-checkout.generated.js +699 -9
- package/dist/stripe-setup.d.ts +16 -0
- package/dist/stripe-setup.js +61 -0
- package/dist/substitutions.generated.js +187 -29
- package/dist/verifone.d.ts +68 -0
- package/dist/verifone.generated.d.ts +51 -0
- package/dist/verifone.generated.js +1401 -0
- package/dist/verifone.js +119 -0
- package/dist/voice-types.d.ts +27 -0
- package/dist/voice.d.ts +6 -1
- package/dist/voice.js +6 -1
- package/dist/wspay.d.ts +41 -0
- package/dist/wspay.generated.d.ts +41 -0
- package/dist/wspay.generated.js +370 -0
- package/dist/wspay.js +65 -0
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
package/dist/pre-claim.js
CHANGED
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
// gates inspect it: Mercado Pago's native Checkout Pro configuration and
|
|
5
5
|
// association, Braintree's shared /graphql endpoint, native Stripe Checkout,
|
|
6
6
|
// Fiserv's card capture, GMO's CreateToken.json, which also carries requests
|
|
7
|
-
// the vault can never pay,
|
|
8
|
-
// capture context rides into the pause)
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// the vault can never pay, CyberSource Flex Microform's field iframes (whose
|
|
8
|
+
// capture context rides into the pause), Datatrans Secure Fields, whose
|
|
9
|
+
// card URL also carries card posts the device could never fill and card posts
|
|
10
|
+
// it already answered, and Pelecard's card submission URL, which also carries
|
|
11
|
+
// saved-token, wallet and keep-on-file bodies, Primer's
|
|
12
|
+
// /payment-instruments, which also carries requests the vault can never pay,
|
|
13
|
+
// Payrails, whose save needs the session key the page captured and whose
|
|
14
|
+
// hosts also carry card payments the vault can never pay, and Elavon
|
|
15
|
+
// Converge's hpp/process URL, which also carries wallet, debit, follow-up and
|
|
16
|
+
// card-saving bodies, WSPay's confirm URL, which also carries bodies with
|
|
17
|
+
// no new card, and Shiji STS's getTokenId and getCvvId, which also carry
|
|
18
|
+
// requests the vault must never pay.
|
|
19
|
+
// The first three used to be
|
|
20
|
+
// written inline in Fetch.requestPaused and again in the page.route handler, so
|
|
21
|
+
// a decision could drift between the two adapters and a new family had to be
|
|
11
22
|
// added in both places.
|
|
23
|
+
// Juspay's tokenize and /txns joined later as one more entry.
|
|
12
24
|
//
|
|
13
25
|
// dispatchPreClaim runs the gates in order and resolves the request to one of:
|
|
14
26
|
// - continue: let the request through untouched, or with a body the gate
|
|
@@ -42,9 +54,23 @@ import { PaymentOutcomeUnknownError, redactUrl } from './client.js';
|
|
|
42
54
|
import { classifyBraintreeRequest } from './braintree.js';
|
|
43
55
|
import { StripeCheckoutClaimConflictError, stripeCheckoutReadinessError } from './stripe-checkout.js';
|
|
44
56
|
import { gmoRequestRefusal, isGmoTokenHost, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
57
|
+
import { JUSPAY_PSP } from './juspay-gate.js';
|
|
58
|
+
import { datatransPauseDecision, isDatatransHost } from './datatrans.js';
|
|
59
|
+
import { DATATRANS_INVALID_REQUEST } from './datatrans.generated.js';
|
|
60
|
+
import { isPelecardHost, pelecardRefusal } from './pelecard.js';
|
|
61
|
+
import { ACI_CHECKOUT_REUSED } from './aci.js';
|
|
62
|
+
import { ACI_EXPIRY_UNSTRIPPABLE } from './aci.generated.js';
|
|
63
|
+
import { CREDITGUARD_PPS_HOSTS, classifyCreditguardRequest, creditguardNuveiRefusal, creditguardProcessRefusal } from './creditguard.generated.js';
|
|
64
|
+
import { isPrimerPciHost, primerRequestVerdict } from './primer.js';
|
|
65
|
+
import { convergeRefusal, isConvergeHost } from './elavon-converge.js';
|
|
66
|
+
import { isWspayHost, wspayRefusal } from './wspay.js';
|
|
67
|
+
import { isShijiStsHost, shijiStsVerdict } from './shiji-sts.js';
|
|
45
68
|
import { classifyProfileRequest, merchantBodyKeyPathSha256, profileTemplatedUrl } from './merchant-hosted.js';
|
|
46
69
|
import { isFiservCaptureHost, isFiservPausableBody } from './fiserv.js';
|
|
47
70
|
import { CYBERSOURCE_FLEX_PSP, isCybersourceFlexHost } from './cybersource-flex.js';
|
|
71
|
+
import { cardEndpointDetail, cardEndpointFor, cardEndpointLive } from './card-endpoint.js';
|
|
72
|
+
import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isPayrailsStaticHost, payrailsKeyRefusal, payrailsRequestKind, payrailsSaveRefusal, } from './payrails.js';
|
|
73
|
+
import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
|
|
48
74
|
/**
|
|
49
75
|
* The URL an event may name for a request. For an armed merchant profile's
|
|
50
76
|
* endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
|
|
@@ -60,8 +86,40 @@ export function eventUrl(vault, url) {
|
|
|
60
86
|
}
|
|
61
87
|
catch { /* not a profile this build reviewed */ }
|
|
62
88
|
}
|
|
89
|
+
// A Verifone merchant's origin: its origin and, for a leg, the leg's path template (verifone.ts).
|
|
90
|
+
const verifone = typeof vault?.verifoneProfileOf === 'function' ? vault.verifoneProfileOf(url) : null;
|
|
91
|
+
if (verifone) {
|
|
92
|
+
try {
|
|
93
|
+
return verifoneTemplatedUrl(verifone.id, url);
|
|
94
|
+
}
|
|
95
|
+
catch { /* not a profile this build reviewed */ }
|
|
96
|
+
}
|
|
63
97
|
return redactUrl(url);
|
|
64
98
|
}
|
|
99
|
+
// A reviewed merchant's own card endpoint (card-endpoint.ts). It runs first: its URLs
|
|
100
|
+
// are merchant origins no processor gate claims, and one never reaches another gate. A
|
|
101
|
+
// POST that is exactly a profile's card request pauses for the claim flow, carrying the
|
|
102
|
+
// profile, when this build's copy of the profile takes payments. In any other status
|
|
103
|
+
// (observe, a fact only a HAR can pin, no reviewed HAR, a recurring charge) it aborts as
|
|
104
|
+
// an unsupported checkout, synchronously and before any create, so the placeholder card
|
|
105
|
+
// never reaches the merchant; the event names the profile and the endpoint origin, never
|
|
106
|
+
// the URL's path or query, which carry the merchant's cart and booking ids. Anything
|
|
107
|
+
// else, a GET or a CORS preflight to the same URL included, defers.
|
|
108
|
+
const cardEndpointPreClaim = (ctx) => {
|
|
109
|
+
const profile = cardEndpointFor(ctx.request.url, ctx.request.method);
|
|
110
|
+
if (!profile)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (cardEndpointLive(profile))
|
|
113
|
+
return { kind: 'pause', stripeStep: null, cardEndpoint: profile };
|
|
114
|
+
return cardEndpointUnsupported(ctx, profile);
|
|
115
|
+
};
|
|
116
|
+
async function cardEndpointUnsupported(ctx, profile) {
|
|
117
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
118
|
+
ctx.lifecycle.unsupported('merchant_card_endpoint_not_enabled');
|
|
119
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { ...cardEndpointDetail(profile), reason: 'merchant_card_endpoint_not_enabled' } });
|
|
120
|
+
await ctx.effects.abort();
|
|
121
|
+
return { kind: 'abort' };
|
|
122
|
+
}
|
|
65
123
|
// Mercado Pago's native Checkout Pro shares one origin between its BIN-lookup
|
|
66
124
|
// configuration and its card association. A request that is not for that origin
|
|
67
125
|
// defers synchronously; a POST to it is handled asynchronously (a document read
|
|
@@ -295,6 +353,71 @@ async function merchantHostedUnsupported(ctx, profile, url, category, reason) {
|
|
|
295
353
|
await ctx.effects.abort();
|
|
296
354
|
return { kind: 'abort' };
|
|
297
355
|
}
|
|
356
|
+
// Verifone merchants (verifone.ts). A request to an armed Verifone profile's endpoint origin is
|
|
357
|
+
// the profile's to judge before the terminal arbiter, which would otherwise continue it (no
|
|
358
|
+
// processor recognizes a merchant's own server). verifone.js sends the card, encrypted in the
|
|
359
|
+
// page, in the merchant's own requests, so every request to that origin is read:
|
|
360
|
+
// - no verifone.js blob (a basket query, a GET, a preflight, a page load, a request with no
|
|
361
|
+
// body at all): defer, and the request is judged as it was before any profile existed. The
|
|
362
|
+
// profile arms the merchant's whole origin, where a bodiless POST or DELETE (a session
|
|
363
|
+
// refresh, a cart edit, an empty beacon) is routine, so only a body the adapter could not read
|
|
364
|
+
// counts as unread, never a missing one;
|
|
365
|
+
// - a body the adapter knows is there and could not read (CDP: a blob or a file), on anything
|
|
366
|
+
// but a GET, HEAD or OPTIONS: not proven to carry no card, so it is judged as a card body the
|
|
367
|
+
// profile cannot finish, as for a merchant-hosted profile. Playwright reports such a body as
|
|
368
|
+
// no body (PreClaimContext.request), so only attachToCdp refuses it;
|
|
369
|
+
// - a card request to a profile this attachment already handed a card to: the page's retry
|
|
370
|
+
// carries the agent's placeholder and the approval is spent, so it aborts;
|
|
371
|
+
// - a profile in observe: report the request's shape (the SHA-256 of its key paths, never a
|
|
372
|
+
// value) with the verdict, then abort as unsupported, so the placeholder never reaches the
|
|
373
|
+
// merchant and no authorization exists;
|
|
374
|
+
// - a blob the profile cannot pay (a BIN lookup, a gift card, a saved card's CVV, a second
|
|
375
|
+
// card entry, a key nobody reviewed, a body the leg did not review): abort as unsupported,
|
|
376
|
+
// with the reason;
|
|
377
|
+
// - the reviewed leg, on an enabled profile, with no preparation on this attachment: abort as
|
|
378
|
+
// preparation_required before any pause, prompt or authorization;
|
|
379
|
+
// - the same request with a preparation: pause for the claim flow, where only a ready
|
|
380
|
+
// preparation for this profile pays it.
|
|
381
|
+
// A reader that throws on the body is not proof it carries no card either: the request is judged
|
|
382
|
+
// as an unreadable card body and aborted. Synchronous until it must abort, like every gate.
|
|
383
|
+
export const verifonePreClaim = (ctx) => {
|
|
384
|
+
const { url, method } = ctx.request;
|
|
385
|
+
const profile = typeof ctx.vault.verifoneProfileOf === 'function' ? ctx.vault.verifoneProfileOf(url) : null;
|
|
386
|
+
if (!profile)
|
|
387
|
+
return undefined;
|
|
388
|
+
const body = ctx.request.body;
|
|
389
|
+
const unread = body === null;
|
|
390
|
+
let verdict;
|
|
391
|
+
try {
|
|
392
|
+
verdict = !unread ? classifyVerifoneProfileRequest(profile, url, method, body)
|
|
393
|
+
: UNREAD_BODY_METHODS.has(String(method).toUpperCase()) ? { verdict: 'pass' } : { verdict: 'abort', reason: MERCHANT_BODY_UNREADABLE };
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
verdict = { verdict: 'abort', reason: MERCHANT_BODY_UNREADABLE };
|
|
397
|
+
}
|
|
398
|
+
if (verdict.verdict === 'pass')
|
|
399
|
+
return undefined;
|
|
400
|
+
const templated = verifoneTemplatedUrl(profile.id, url);
|
|
401
|
+
if (ctx.merchantHandoff?.retried(profile.id))
|
|
402
|
+
return merchantHostedAbort(ctx);
|
|
403
|
+
if (profile.status !== 'enabled') {
|
|
404
|
+
ctx.onEvent?.({ type: 'verifone_profile_observed', detail: { profile: profile.id, url: templated, method, verdict: verdict.verdict,
|
|
405
|
+
...(verdict.verdict === 'abort' ? { reason: verdict.reason } : {}), keyPathSha256: unread ? null : keyPathSha256Of(body) } });
|
|
406
|
+
return verifoneUnsupported(ctx, profile.id, templated, 'merchant_profile_observe', 'Agentcard has not turned on payments to this merchant yet');
|
|
407
|
+
}
|
|
408
|
+
if (verdict.verdict === 'abort')
|
|
409
|
+
return verifoneUnsupported(ctx, profile.id, templated, 'merchant_request_unsupported', verdict.reason);
|
|
410
|
+
if (!ctx.preparationGate.isEngaged())
|
|
411
|
+
return merchantHostedPreparationRequired(ctx);
|
|
412
|
+
return { kind: 'pause', stripeStep: ctx.stripeStep };
|
|
413
|
+
};
|
|
414
|
+
async function verifoneUnsupported(ctx, profile, url, category, reason) {
|
|
415
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
416
|
+
ctx.lifecycle.unsupported(category);
|
|
417
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url, method: ctx.request.method, profile, reason } });
|
|
418
|
+
await ctx.effects.abort();
|
|
419
|
+
return { kind: 'abort' };
|
|
420
|
+
}
|
|
298
421
|
// Fiserv Commerce Hub card capture (fiserv.ts). A POST the registry recognizes on
|
|
299
422
|
// one of Fiserv's two card-capture hosts is this gate's to judge before any claim:
|
|
300
423
|
// - only a capture that carries no Vault card (Fastlane's PaymentToken, ACH's
|
|
@@ -459,6 +582,735 @@ async function cybersourceFlexRefuse(ctx, reason) {
|
|
|
459
582
|
await ctx.effects.abort();
|
|
460
583
|
return { kind: 'abort' };
|
|
461
584
|
}
|
|
585
|
+
// Juspay Express Checkout (juspay-gate.ts; payment-core juspay.js holds the
|
|
586
|
+
// rules). While the attachment serves the Juspay recognizer, every request to a
|
|
587
|
+
// Juspay host is this gate's: the terminal arbiter would continue the page's
|
|
588
|
+
// placeholder tokenize (no recognizer names it) and pause any /txns.
|
|
589
|
+
// - answer_tokenize: the placeholder tokenize is answered here with a
|
|
590
|
+
// synthetic token, remembered for the /txns that follows. Nothing reaches
|
|
591
|
+
// Juspay. The answer runs inside the gate's own try, so a failed fulfill
|
|
592
|
+
// forgets the token and aborts the request; an attachment that is not
|
|
593
|
+
// ready, was cancelled or has latched a terminal failure answers nothing.
|
|
594
|
+
// - pause: the /txns on a token this attachment issued, in the reviewed
|
|
595
|
+
// shape. Synchronous, so the claim flow still sets activeRequest in the
|
|
596
|
+
// paused request's own microtask.
|
|
597
|
+
// - continue: a request that carries no card of the vault's (net banking,
|
|
598
|
+
// UPI, a saved card, the bank's pay/start redirect, a preflight, a read).
|
|
599
|
+
// - abort, this request only: the placeholder's fingerprint lookup (the page
|
|
600
|
+
// carries on without it), a /txns on a synthetic token this attachment
|
|
601
|
+
// never issued, and a body the adapter could not read.
|
|
602
|
+
// - abort as unsupported: every other card-bearing request (save card, EMI,
|
|
603
|
+
// a direct card, a sibling host), before any authorization, holding the
|
|
604
|
+
// attachment the way an unrecognized payment endpoint does.
|
|
605
|
+
const JUSPAY_REFUSED_ALONE = new Set(['placeholder_fingerprint', 'unknown_synthetic_token', 'request_body_unreadable']);
|
|
606
|
+
const juspayPreClaim = (ctx) => {
|
|
607
|
+
const gate = ctx.juspay;
|
|
608
|
+
const decision = gate?.classify(ctx.request);
|
|
609
|
+
if (!gate || !decision)
|
|
610
|
+
return undefined;
|
|
611
|
+
if (decision.decision === 'pause')
|
|
612
|
+
return { kind: 'pause', stripeStep: ctx.stripeStep };
|
|
613
|
+
if (decision.decision === 'answer_tokenize')
|
|
614
|
+
return juspayAnswer(ctx, gate, decision.host, decision.merchantId);
|
|
615
|
+
if (decision.decision === 'continue')
|
|
616
|
+
return juspayContinue(ctx);
|
|
617
|
+
return JUSPAY_REFUSED_ALONE.has(decision.reason) ? juspayRefuse(ctx, decision.reason) : juspayUnsupported(ctx, decision.reason);
|
|
618
|
+
};
|
|
619
|
+
async function juspayAnswer(ctx, gate, host, merchantId) {
|
|
620
|
+
if (!ctx.attachmentReady() || ctx.interceptionAborted() || ctx.lifecycleAborted() || ctx.terminal()) {
|
|
621
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
|
|
622
|
+
await ctx.effects.abort();
|
|
623
|
+
return { kind: 'abort' };
|
|
624
|
+
}
|
|
625
|
+
const { token, response } = gate.issue(host, merchantId);
|
|
626
|
+
try {
|
|
627
|
+
await ctx.effects.answer(response);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
gate.forget(token);
|
|
631
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'juspay_tokenize_answer_failed' });
|
|
632
|
+
await ctx.effects.abort();
|
|
633
|
+
return { kind: 'abort' };
|
|
634
|
+
}
|
|
635
|
+
ctx.onEvent?.({ type: 'checkout_prepared', detail: { processor: JUSPAY_PSP } });
|
|
636
|
+
return { kind: 'answer' };
|
|
637
|
+
}
|
|
638
|
+
async function juspayContinue(ctx) {
|
|
639
|
+
await ctx.effects.continueRequest().catch(() => { });
|
|
640
|
+
return { kind: 'continue' };
|
|
641
|
+
}
|
|
642
|
+
async function juspayRefuse(ctx, reason) {
|
|
643
|
+
ctx.onEvent?.({ type: 'blocked', detail: `juspay_${reason}` });
|
|
644
|
+
await ctx.effects.abort();
|
|
645
|
+
return { kind: 'abort' };
|
|
646
|
+
}
|
|
647
|
+
async function juspayUnsupported(ctx, reason) {
|
|
648
|
+
const { url, method } = ctx.request;
|
|
649
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
650
|
+
ctx.lifecycle.unsupported('juspay_request_unsupported');
|
|
651
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, processor: JUSPAY_PSP, reason } });
|
|
652
|
+
await ctx.effects.abort();
|
|
653
|
+
return { kind: 'abort' };
|
|
654
|
+
}
|
|
655
|
+
// Datatrans Secure Fields (datatrans.ts). The card-number iframe's card URL also takes posts the
|
|
656
|
+
// device could never fill, and British Airways's page submits the same form again on every blur
|
|
657
|
+
// and expiry change, so a POST the registry recognizes on a Datatrans pay host is judged here,
|
|
658
|
+
// before the terminal arbiter would pause it, by payment-core's own pause decision under the
|
|
659
|
+
// card-on-file policy the API served with the recognizer (VaultClient.cardOnFilePolicy):
|
|
660
|
+
// - a sound TOKENIZE card post defers, and the arbiter pauses it for approval, unless the same
|
|
661
|
+
// frame's same form was already answered with an approved token (the repeat rule) and that
|
|
662
|
+
// approval is still the one being handed off (datatransRepeatApproval): then that stored answer
|
|
663
|
+
// is fulfilled here, with no claim, create or prompt. Once the answer reached the page, the
|
|
664
|
+
// application's merchant resolver must first report the order still pending
|
|
665
|
+
// (datatransRepeatAfterHandoff);
|
|
666
|
+
// - a card post the API would refuse at create aborts with a `blocked` event that names the
|
|
667
|
+
// API's own code, `invalid_datatrans_request` or `datatrans_card_on_file_refused` (the latter
|
|
668
|
+
// only while the served policy refuses card on file), before anything is claimed, created or
|
|
669
|
+
// prompted. Like Braintree's unsupported operation it neither holds the attachment nor changes
|
|
670
|
+
// its state, so the page's next post is judged afresh;
|
|
671
|
+
// - a post with no card number (a wallet, bank-account or CVV-only post) defers, and continues
|
|
672
|
+
// untouched through the recognizer's passWithoutCard.
|
|
673
|
+
// Nothing is decided without the registry: an SDK the API did not serve the datatrans recognizer
|
|
674
|
+
// never recognizes a pay host, so the gate defers and every Datatrans request continues as before.
|
|
675
|
+
// paymentField/cardInfo (the BIN lookup) and the iframe documents are not card requests and never
|
|
676
|
+
// reach it. Synchronous until an effect, so a card post pauses with no await. A check that throws
|
|
677
|
+
// blocks the request as invalid rather than letting it reach a prompt.
|
|
678
|
+
const datatransPreClaim = (ctx) => {
|
|
679
|
+
const { url, method } = ctx.request;
|
|
680
|
+
if (method.toUpperCase() !== 'POST' || !isDatatransHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
681
|
+
return undefined;
|
|
682
|
+
let decision;
|
|
683
|
+
try {
|
|
684
|
+
decision = datatransPauseDecision({ url, headers: ctx.request.headers, body: ctx.request.body }, ctx.vault.cardOnFilePolicy?.(url, method));
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
decision = DATATRANS_INVALID_REQUEST;
|
|
688
|
+
}
|
|
689
|
+
if (decision === null)
|
|
690
|
+
return undefined;
|
|
691
|
+
if (decision !== 'card')
|
|
692
|
+
return datatransBlock(ctx, decision);
|
|
693
|
+
const approval = datatransRepeatApproval(ctx);
|
|
694
|
+
const repeat = approval === null ? null : ctx.datatrans?.answerFor(ctx.frame, url, ctx.request.body, approval) ?? null;
|
|
695
|
+
if (!repeat)
|
|
696
|
+
return undefined;
|
|
697
|
+
return ctx.lifecycle.getState().status === 'awaiting_merchant' ? datatransRepeatAfterHandoff(ctx, repeat) : datatransRepeat(ctx, repeat);
|
|
698
|
+
};
|
|
699
|
+
/**
|
|
700
|
+
* The approval whose stored Datatrans answer a repeat may still receive, or null. A stored token
|
|
701
|
+
* answers a re-submit only while the approval it came from is still being handed off: the lifecycle
|
|
702
|
+
* names it and reads awaiting_approval (its answer is being delivered) or awaiting_merchant (the
|
|
703
|
+
* merchant has not confirmed yet), and nothing latched, tore down or cancelled the attachment. The
|
|
704
|
+
* order completing or failing, an unknown outcome, a decline, a hold for the cardholder, a retry
|
|
705
|
+
* after a merchant failure and a new authorization all end it, and the re-submit is then a new
|
|
706
|
+
* question for the terminal arbiter, which holds or pauses it like any card post: TOKENIZE binds no
|
|
707
|
+
* amount, so one approval never answers for a later purchase. Once the answer reached the page
|
|
708
|
+
* (awaiting_merchant), the gate also reconciles before it answers (datatransRepeatAfterHandoff), and
|
|
709
|
+
* the stored answer itself ends once its form goes quiet (DatatransRepeats, DATATRANS_REPEAT_IDLE_MS).
|
|
710
|
+
*/
|
|
711
|
+
function datatransRepeatApproval(ctx) {
|
|
712
|
+
if (ctx.terminal() || ctx.interceptionAborted() || ctx.lifecycleAborted())
|
|
713
|
+
return null;
|
|
714
|
+
const { status, authorizationId } = ctx.lifecycle.getState();
|
|
715
|
+
return status === 'awaiting_approval' || status === 'awaiting_merchant' ? authorizationId : null;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* A re-submit after the approved answer reached the page. The merchant can already hold an order paid
|
|
719
|
+
* with that token, and the post names no amount and no order, so nothing in the browser tells the same
|
|
720
|
+
* checkout's re-submit from a later purchase in the same form. The gate asks the application's merchant
|
|
721
|
+
* resolver first (lifecycle.merchantOrderPending()), and the stored answer goes out only while the
|
|
722
|
+
* resolver reports this approval's order still pending and the checkout still awaits the merchant for
|
|
723
|
+
* it. A completed, failed or unknown order, a resolver that throws or has not answered within
|
|
724
|
+
* MERCHANT_ORDER_READ_MS, and a request in flight make the re-submit a new question for the terminal
|
|
725
|
+
* arbiter, and so does every re-submit on an attachment with no resolver: a later purchase never
|
|
726
|
+
* receives an earlier token without the cardholder, and the paused post never waits on a stalled read.
|
|
727
|
+
*/
|
|
728
|
+
function datatransRepeatAfterHandoff(ctx, repeat) {
|
|
729
|
+
if (!ctx.lifecycle.hasMerchantResolver())
|
|
730
|
+
return undefined;
|
|
731
|
+
return datatransPendingRepeat(ctx, repeat);
|
|
732
|
+
}
|
|
733
|
+
async function datatransPendingRepeat(ctx, repeat) {
|
|
734
|
+
if (!await ctx.lifecycle.merchantOrderPending())
|
|
735
|
+
return undefined;
|
|
736
|
+
// The resolver answered asynchronously: the checkout must still be handing off this approval.
|
|
737
|
+
if (ctx.lifecycle.getState().status !== 'awaiting_merchant' || datatransRepeatApproval(ctx) !== repeat.authorizationId)
|
|
738
|
+
return undefined;
|
|
739
|
+
return datatransRepeat(ctx, repeat);
|
|
740
|
+
}
|
|
741
|
+
async function datatransBlock(ctx, reason) {
|
|
742
|
+
ctx.onEvent?.({ type: 'blocked', detail: reason });
|
|
743
|
+
await ctx.effects.abort();
|
|
744
|
+
return { kind: 'abort' };
|
|
745
|
+
}
|
|
746
|
+
async function datatransRepeat(ctx, repeat) {
|
|
747
|
+
try {
|
|
748
|
+
await ctx.effects.answer(repeat.response);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'datatrans_repeat_unanswered' });
|
|
752
|
+
await ctx.effects.abort();
|
|
753
|
+
return { kind: 'abort' };
|
|
754
|
+
}
|
|
755
|
+
ctx.onEvent?.({ type: 'repeat_answered', detail: { psp: 'datatrans', authorizationId: repeat.authorizationId } });
|
|
756
|
+
return { kind: 'answer' };
|
|
757
|
+
}
|
|
758
|
+
// Pelecard Iframe/Redirect 2.0 (pelecard.ts). The payment page's card URL, POST
|
|
759
|
+
// /PaymentGW/Transaction on gateway21 or gateway20.pelecard.biz, also carries the wallet, Bit,
|
|
760
|
+
// PIN pad and bank tabs with no card, and card-shaped bodies the device could never fill: a saved
|
|
761
|
+
// token, a card track, a phone authorization, installments, a buyer-typed total. So a POST the
|
|
762
|
+
// registry recognizes on a Pelecard host that carries a card is judged here, before the terminal
|
|
763
|
+
// arbiter would pause it, by the whole-request check the API runs at create:
|
|
764
|
+
// - the page's own one-time new-card submission defers, and the arbiter pauses it for approval;
|
|
765
|
+
// - anything else aborts with a `blocked` event that names why, with no claim, create or
|
|
766
|
+
// prompt: `invalid_pelecard_request`, or `pelecard_keep_on_file_unsupported` for a
|
|
767
|
+
// submission that asks Pelecard to keep the card on file. Like Braintree's unsupported
|
|
768
|
+
// operation it neither holds the attachment nor changes its state, so the page's next
|
|
769
|
+
// submission (the buyer unticks "save my card") is judged afresh.
|
|
770
|
+
// A body with no card defers too, and the adapter continues it untouched in every state
|
|
771
|
+
// (passWithoutCard); so does a body the adapter could not read, which the adapter fails on its
|
|
772
|
+
// own as unreadable, that request only. Nothing is decided without the registry: an SDK that did
|
|
773
|
+
// not sync with the pelecard_transaction capability, or whose org Agentcard has not enabled for
|
|
774
|
+
// Pelecard (the API then serves no pelecard recognizer), never recognizes a Pelecard host, so the
|
|
775
|
+
// gate defers and the request continues as before. Synchronous until the abort, so a card
|
|
776
|
+
// submission pauses with no await. A check that throws blocks the request as invalid rather than
|
|
777
|
+
// letting it reach a prompt.
|
|
778
|
+
const pelecardPreClaim = (ctx) => {
|
|
779
|
+
const { url, method } = ctx.request;
|
|
780
|
+
if (method.toUpperCase() !== 'POST' || !isPelecardHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
781
|
+
return undefined;
|
|
782
|
+
if (ctx.request.body == null || ctx.vault.withoutCard?.(url, ctx.request.body) === 'continue')
|
|
783
|
+
return undefined;
|
|
784
|
+
let reason;
|
|
785
|
+
try {
|
|
786
|
+
reason = pelecardRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
reason = 'invalid_pelecard_request';
|
|
790
|
+
}
|
|
791
|
+
return reason === null ? undefined : pelecardBlock(ctx, reason);
|
|
792
|
+
};
|
|
793
|
+
async function pelecardBlock(ctx, reason) {
|
|
794
|
+
ctx.onEvent?.({ type: 'blocked', detail: reason });
|
|
795
|
+
await ctx.effects.abort();
|
|
796
|
+
return { kind: 'abort' };
|
|
797
|
+
}
|
|
798
|
+
// ACI COPYandPAY, once the registry serves it (a synced build that declares its
|
|
799
|
+
// capability); for every other client the gate defers on everything, so nothing
|
|
800
|
+
// changes. On a POST to one of ACI's checkouts (aci.ts, payment-core aci.js):
|
|
801
|
+
// - the card-number request defers to the arbiter, which pauses it for the
|
|
802
|
+
// vault claim flow (a prepared checkout only: an unprepared one is refused
|
|
803
|
+
// there with PreparationRequiredError before any create), unless its
|
|
804
|
+
// checkout's card-number request is running or was answered here: that one
|
|
805
|
+
// is refused as aci_checkout_reused before it claims anything;
|
|
806
|
+
// - the page's CVC and expiry requests are held until that request's
|
|
807
|
+
// approval ends, then answered with its answer, never forwarded; they fail
|
|
808
|
+
// when it failed or never began;
|
|
809
|
+
// - the holder request continues untouched;
|
|
810
|
+
// - the /payment submission continues once per answered card-number request,
|
|
811
|
+
// as a frame navigation, with the placeholder expiry a classic-mode page
|
|
812
|
+
// puts there removed (only where the browser sends a replaced navigation
|
|
813
|
+
// body, else aci_expiry_unstrippable), or is refused with the judge's code;
|
|
814
|
+
// - anything else is refused with payment-core's code: a request that asks ACI
|
|
815
|
+
// to keep the card or pay with a stored one (aci_card_on_file_refused, which
|
|
816
|
+
// also retires a ready preparation) or one the widget never sends.
|
|
817
|
+
// Everything else on ACI's hosts (the widget script, /bins, /v1/collect, GETs)
|
|
818
|
+
// is not a POST to a checkout and defers. Synchronous for the card-number
|
|
819
|
+
// request, so it pauses in the microtask it paused in.
|
|
820
|
+
const aciPreClaim = (ctx) => {
|
|
821
|
+
const leg = ctx.aci?.classify(ctx.request.url, ctx.request.method, ctx.request.body);
|
|
822
|
+
if (!leg)
|
|
823
|
+
return undefined;
|
|
824
|
+
if (leg.leg === 'refuse') {
|
|
825
|
+
ctx.aci.refused(leg);
|
|
826
|
+
return aciRefuse(ctx, leg.code);
|
|
827
|
+
}
|
|
828
|
+
if (leg.leg === 'pass')
|
|
829
|
+
return aciContinue(ctx);
|
|
830
|
+
if (leg.leg === 'absorb')
|
|
831
|
+
return aciHold(ctx, leg);
|
|
832
|
+
if (leg.leg === 'continuation')
|
|
833
|
+
return aciPayment(ctx);
|
|
834
|
+
// The card-number request: the arbiter pauses it for the vault claim flow,
|
|
835
|
+
// unless this checkout already has one running or answered (AciGate.begin).
|
|
836
|
+
if (ctx.aci.reused(leg))
|
|
837
|
+
return aciRefuse(ctx, ACI_CHECKOUT_REUSED);
|
|
838
|
+
return undefined;
|
|
839
|
+
};
|
|
840
|
+
async function aciContinue(ctx) {
|
|
841
|
+
await ctx.effects.continueRequest().catch(() => { });
|
|
842
|
+
return { kind: 'continue' };
|
|
843
|
+
}
|
|
844
|
+
async function aciHold(ctx, leg) {
|
|
845
|
+
const answer = await ctx.aci.hold(leg);
|
|
846
|
+
if (answer && !ctx.interceptionAborted() && !ctx.lifecycleAborted()) {
|
|
847
|
+
try {
|
|
848
|
+
await ctx.effects.answer(answer);
|
|
849
|
+
ctx.onEvent?.({ type: 'aci_leg_absorbed', detail: { url: redactUrl(ctx.request.url) } });
|
|
850
|
+
return { kind: 'answer' };
|
|
851
|
+
}
|
|
852
|
+
catch { /* the page's request is gone; nothing was forwarded */ }
|
|
853
|
+
}
|
|
854
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'aci_leg_unanswered' });
|
|
855
|
+
await ctx.effects.abort();
|
|
856
|
+
return { kind: 'abort' };
|
|
857
|
+
}
|
|
858
|
+
async function aciPayment(ctx) {
|
|
859
|
+
const { url, method, body } = ctx.request;
|
|
860
|
+
const stopped = () => ctx.interceptionAborted() || ctx.lifecycleAborted() || ctx.terminal();
|
|
861
|
+
let verdict = stopped() ? { outcome: 'refuse', code: 'checkout_interception_not_ready' }
|
|
862
|
+
: ctx.aci.payment(url, method, body, { navigation: ctx.request.navigation === true });
|
|
863
|
+
// A classic-mode body continues only where the replaced body is known to
|
|
864
|
+
// reach ACI on a navigation; elsewhere the placeholder expiry would, so it is
|
|
865
|
+
// refused and the reservation stays taken. That check can wait on the
|
|
866
|
+
// browser, so the stop conditions are read again after it.
|
|
867
|
+
if (verdict.outcome === 'continue' && verdict.stripped && !(await replacesNavigationBody(ctx))) {
|
|
868
|
+
verdict = { outcome: 'refuse', code: ACI_EXPIRY_UNSTRIPPABLE };
|
|
869
|
+
}
|
|
870
|
+
if (verdict.outcome === 'continue' && stopped())
|
|
871
|
+
verdict = { outcome: 'refuse', code: 'checkout_interception_not_ready' };
|
|
872
|
+
if (verdict.outcome === 'refuse') {
|
|
873
|
+
ctx.onEvent?.({ type: 'blocked', detail: verdict.code });
|
|
874
|
+
await ctx.effects.abort();
|
|
875
|
+
return { kind: 'abort' };
|
|
876
|
+
}
|
|
877
|
+
// A classic-mode body continues with the placeholder expiry taken out; an
|
|
878
|
+
// untouched one continues exactly as the page sent it. The reservation stays
|
|
879
|
+
// taken either way, so a failed continue is never followed by a second post.
|
|
880
|
+
try {
|
|
881
|
+
await ctx.effects.continueRequest(verdict.stripped ? verdict.body : undefined);
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'aci_payment_unsent' });
|
|
885
|
+
return { kind: 'continue' };
|
|
886
|
+
}
|
|
887
|
+
ctx.onEvent?.({ type: 'aci_payment_continued', detail: { authorizationId: verdict.authorizationId, stripped: verdict.stripped } });
|
|
888
|
+
return { kind: 'continue' };
|
|
889
|
+
}
|
|
890
|
+
/** Whether this browser sends the body the SDK replaced on a navigation; an absent or failing answer is no. */
|
|
891
|
+
async function replacesNavigationBody(ctx) {
|
|
892
|
+
try {
|
|
893
|
+
return (await ctx.replacesNavigationBody?.()) === true;
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function aciRefuse(ctx, code) {
|
|
900
|
+
// A card that may not be kept on file can never pay this checkout: a ready
|
|
901
|
+
// preparation is retired with that reason rather than left to expire.
|
|
902
|
+
if (code === 'aci_card_on_file_refused')
|
|
903
|
+
ctx.preparationGate.invalidate(code);
|
|
904
|
+
ctx.onEvent?.({ type: 'blocked', detail: code });
|
|
905
|
+
await ctx.effects.abort();
|
|
906
|
+
return { kind: 'abort' };
|
|
907
|
+
}
|
|
908
|
+
// CreditGuard's hosted payment page (Hyp PPS), in two parts the generic card
|
|
909
|
+
// path cannot judge. Every other request on the PPS hosts (the page, its
|
|
910
|
+
// transaction read, /card/details, /cg3ds, assets) and the stock new-card
|
|
911
|
+
// submit defer, synchronously, and the terminal arbiter continues or pauses
|
|
912
|
+
// them; the pause then needs a ready preparation for this page's transaction.
|
|
913
|
+
// - A /process submit the cardholder's device would not fill (a saved card,
|
|
914
|
+
// a wallet, "save my card", installments, DCC, an Israeli ID, a gift card,
|
|
915
|
+
// another method, an unreadable body or other headers) is aborted with the device's own
|
|
916
|
+
// reason, before any claim, create or prompt. A ready preparation stays
|
|
917
|
+
// ready, so the page's corrected submit can still pay.
|
|
918
|
+
// - PPS runs 3-D Secure itself, sending the placeholder card to Nuvei from
|
|
919
|
+
// inside its frame. The nuvei recognizer would pause that as a Nuvei
|
|
920
|
+
// payment. A request to a Nuvei host that a PPS document sent, or that any
|
|
921
|
+
// frame inside a PPS document sent, is aborted instead, the checkout
|
|
922
|
+
// becomes unsupported and any preparation is retired: the page's flow is
|
|
923
|
+
// one the Vault refuses. See nuveiPreClaim for which requests wait on a
|
|
924
|
+
// frame lookup; no request to another host ever does.
|
|
925
|
+
const PPS_DOCUMENT = 'https://pps.creditguard.co.il/';
|
|
926
|
+
const sendsToNuvei = (url) => creditguardNuveiRefusal(url, PPS_DOCUMENT) !== null;
|
|
927
|
+
const headerValue = (headers, name) => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
928
|
+
const onPpsHost = (url) => {
|
|
929
|
+
try {
|
|
930
|
+
return CREDITGUARD_PPS_HOSTS.includes(new URL(url).hostname.toLowerCase());
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
// ctx.request.headers is read only once a request is CreditGuard's or Nuvei's: the adapters read
|
|
937
|
+
// headers lazily, and a request mock without them must still pass every other gate.
|
|
938
|
+
const creditguardPreClaim = (ctx) => {
|
|
939
|
+
const { url, method } = ctx.request;
|
|
940
|
+
if (sendsToNuvei(url))
|
|
941
|
+
return nuveiPreClaim(ctx);
|
|
942
|
+
// The body is read only for a request to a PPS host, as every gate reads one only on its own hosts:
|
|
943
|
+
// the adapters read it lazily, and a body they cannot read still reaches the rest of the list.
|
|
944
|
+
if (!onPpsHost(url))
|
|
945
|
+
return undefined;
|
|
946
|
+
const { body } = ctx.request;
|
|
947
|
+
// Any method reaches the /process check: payment-core blocks a non-POST, a query or an unreadable
|
|
948
|
+
// body there as well, so nothing but the stock POST ever continues to CreditGuard.
|
|
949
|
+
const verb = method.toUpperCase();
|
|
950
|
+
const verdict = classifyCreditguardRequest(url, verb, body ?? '', undefined);
|
|
951
|
+
if (verdict === null)
|
|
952
|
+
return undefined;
|
|
953
|
+
const refusal = creditguardProcessRefusal({ url, method: verb, headers: ctx.request.headers, body: body ?? '' }, undefined);
|
|
954
|
+
if (verdict === 'card' && refusal === null)
|
|
955
|
+
return undefined;
|
|
956
|
+
return creditguardBlock(ctx, refusal ?? 'malformed_body');
|
|
957
|
+
};
|
|
958
|
+
/**
|
|
959
|
+
* Who sent a request to a Nuvei host, as far as it decides the request:
|
|
960
|
+
* 1. The browser sets Origin and Referer, and no page can make either name another page's
|
|
961
|
+
* document, so one that names PPS settles it at once, for any request.
|
|
962
|
+
* 2. Only a card request the nuvei recognizer would pause can be approved as a Nuvei payment.
|
|
963
|
+
* Anything else (a GET, another Nuvei path) continues as it always has, with no lookup.
|
|
964
|
+
* 3. Otherwise the frames that sent it are read, up to the top, and a PPS document anywhere among
|
|
965
|
+
* them refuses the request, whichever page its Origin names: the sender may be the merchant's
|
|
966
|
+
* own page running Nuvei's Web SDK, one of Nuvei's own frames, which sends for whichever page
|
|
967
|
+
* frames it, or any other frame, and only the frames above it say whether PPS framed it. With
|
|
968
|
+
* an adapter that reads the chain at once (Playwright) the decision still comes in the
|
|
969
|
+
* request's own microtask. A chain the adapter cannot read leaves the request to the nuvei
|
|
970
|
+
* recognizer, as before; the prepare-time refusal of a 3-D Secure transaction still keeps the
|
|
971
|
+
* CreditGuard payment from starting.
|
|
972
|
+
*/
|
|
973
|
+
function nuveiPreClaim(ctx) {
|
|
974
|
+
const { url, method, headers } = ctx.request;
|
|
975
|
+
if ([headerValue(headers, 'origin'), headerValue(headers, 'referer')].some(sender => creditguardNuveiRefusal(url, sender) !== null))
|
|
976
|
+
return creditguardThreeDSecure(ctx);
|
|
977
|
+
if (!ctx.vault.isCardRequest(url, method))
|
|
978
|
+
return undefined;
|
|
979
|
+
const decide = (frames) => frames?.some(frame => creditguardNuveiRefusal(url, frame) !== null) ? creditguardThreeDSecure(ctx) : undefined;
|
|
980
|
+
let frames;
|
|
981
|
+
try {
|
|
982
|
+
frames = ctx.initiatorFrames?.();
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
frames = undefined;
|
|
986
|
+
}
|
|
987
|
+
return frames instanceof Promise ? frames.then(decide, () => undefined) : decide(frames);
|
|
988
|
+
}
|
|
989
|
+
async function creditguardBlock(ctx, refusal) {
|
|
990
|
+
ctx.onEvent?.({ type: 'blocked', detail: { reason: 'creditguard_request_unsupported', refusal } });
|
|
991
|
+
await ctx.effects.abort();
|
|
992
|
+
return { kind: 'abort' };
|
|
993
|
+
}
|
|
994
|
+
async function creditguardThreeDSecure(ctx) {
|
|
995
|
+
const { url, method } = ctx.request;
|
|
996
|
+
ctx.preparationGate.invalidate('creditguard_three_d_secure');
|
|
997
|
+
ctx.lifecycle.unsupported('creditguard_three_d_secure');
|
|
998
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, processor: 'creditguard', reason: 'creditguard_three_d_secure' } });
|
|
999
|
+
await ctx.effects.abort();
|
|
1000
|
+
return { kind: 'abort' };
|
|
1001
|
+
}
|
|
1002
|
+
// Primer (primer.ts). Primer's /payment-instruments also carries requests the vault can never pay,
|
|
1003
|
+
// so a POST the registry recognizes on a Primer pci host is judged here, before the terminal
|
|
1004
|
+
// arbiter would pause it, by payment-core's own pause decision (classifyPrimerRequest), the rule
|
|
1005
|
+
// the API runs at create:
|
|
1006
|
+
// - a one-time new card from the legacy v2 controller defers, and the arbiter pauses it; a body
|
|
1007
|
+
// with no card (PayPal, Apple Pay, Google Pay, Klarna) defers too, and the card-field rule
|
|
1008
|
+
// after the pause continues it untouched;
|
|
1009
|
+
// - a body that asks Primer to keep the card is aborted alone with a `blocked` event,
|
|
1010
|
+
// `primer_keep_on_file`: a save-card choice on the page decides it, so the attachment is not
|
|
1011
|
+
// held and the next request is judged again;
|
|
1012
|
+
// - Primer Checkout (primer-js) and any other shape the legacy controller would not send abort as
|
|
1013
|
+
// an unsupported checkout, `unsupported_checkout` with reason `unsupported_primer_sdk` or
|
|
1014
|
+
// `unsupported_primer_request`, with no claim, create or prompt. The attachment then reads
|
|
1015
|
+
// `unsupported` and holds: the merchant's integration decides that shape.
|
|
1016
|
+
// Nothing is decided without the registry: an SDK the API did not serve Primer (a build without the
|
|
1017
|
+
// capability, or an org Agentcard has not enabled) recognizes no Primer URL, so the gate defers and
|
|
1018
|
+
// every Primer request continues as before. The preflight OPTIONS and every other method continue
|
|
1019
|
+
// through the arbiter. A body the adapter could not read says nothing about the page, so that
|
|
1020
|
+
// request alone is aborted (`unreadable_primer_request`). Synchronous until the abort, so a card
|
|
1021
|
+
// request pauses with no await.
|
|
1022
|
+
const primerPreClaim = (ctx) => {
|
|
1023
|
+
const { url, method } = ctx.request;
|
|
1024
|
+
if (method.toUpperCase() !== 'POST' || !isPrimerPciHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
1025
|
+
return undefined;
|
|
1026
|
+
const verdict = primerRequestVerdict(ctx.request);
|
|
1027
|
+
if (verdict.kind === 'block')
|
|
1028
|
+
return primerBlocked(ctx, verdict.reason);
|
|
1029
|
+
if (verdict.kind === 'unsupported')
|
|
1030
|
+
return primerUnsupported(ctx, verdict.reason);
|
|
1031
|
+
return undefined;
|
|
1032
|
+
};
|
|
1033
|
+
async function primerBlocked(ctx, reason) {
|
|
1034
|
+
ctx.onEvent?.({ type: 'blocked', detail: reason });
|
|
1035
|
+
await ctx.effects.abort();
|
|
1036
|
+
return { kind: 'abort' };
|
|
1037
|
+
}
|
|
1038
|
+
async function primerUnsupported(ctx, reason) {
|
|
1039
|
+
const { url, method } = ctx.request;
|
|
1040
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
1041
|
+
ctx.lifecycle.unsupported(reason);
|
|
1042
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason } });
|
|
1043
|
+
await ctx.effects.abort();
|
|
1044
|
+
return { kind: 'abort' };
|
|
1045
|
+
}
|
|
1046
|
+
// Payrails web SDK v6 (payrails.ts). The adapter captures Payrails sessions only while the synced
|
|
1047
|
+
// registry pauses Payrails' card save (an org the API served the dark entry to); otherwise
|
|
1048
|
+
// ctx.payrails is null, this gate defers, and every Payrails request continues as before. For a
|
|
1049
|
+
// POST to a Payrails host:
|
|
1050
|
+
// - a POST that carries no body the gate can read aborts that request alone
|
|
1051
|
+
// (`unreadable_payrails_request`): no body, an empty one, or one the adapter could not read.
|
|
1052
|
+
// CDP reports '' for the first two and null for the third (a streamed upload, for one);
|
|
1053
|
+
// Playwright reports null for all three, and no Playwright call tells them apart. So the gate
|
|
1054
|
+
// refuses '' and null alike: both adapters then agree, and a streamed card-form payment cannot
|
|
1055
|
+
// pass unread on Playwright. payrails.js 6.5.0 never posts without a body (its request helper
|
|
1056
|
+
// sends JSON.stringify(body || {}) on every POST), and the secure frame v6 posts JSON. Payrails'
|
|
1057
|
+
// static host (assets.payrails.io) is the one exception: it serves the SDK's and the secure
|
|
1058
|
+
// frame's files and takes no payment, so another script's bodyless POST there continues
|
|
1059
|
+
// untouched on both adapters;
|
|
1060
|
+
// - a payment that carries an encrypted card inside paymentComposition (the card form paying,
|
|
1061
|
+
// a stored card with a CVC field) aborts as an unsupported checkout,
|
|
1062
|
+
// `unsupported_payrails_card_payment`, and the attachment holds;
|
|
1063
|
+
// - a card save the registry recognizes is judged first by payment-core's guard and its
|
|
1064
|
+
// card-on-file policy: a refusal aborts as an unsupported checkout with that code, and the
|
|
1065
|
+
// attachment holds. Then it is bound to the one captured session its token, URL,
|
|
1066
|
+
// holderReference and providerConfigId name, and the session's key is checked against the key
|
|
1067
|
+
// the page's frame wrapped. A miss aborts that request alone (`payrails_session_unavailable`,
|
|
1068
|
+
// or the key's code) with a message to reload the checkout, and the attachment does not hold,
|
|
1069
|
+
// so the save after a reload is judged afresh. A bound save pauses carrying the key;
|
|
1070
|
+
// - anything else defers to the arbiter: a save with no card pauses there and the claim flow
|
|
1071
|
+
// continues it (passWithoutCard), and every other request continues.
|
|
1072
|
+
// Synchronous unless answer bodies are still being read (the save then waits for them, briefly)
|
|
1073
|
+
// or the request aborts.
|
|
1074
|
+
const payrailsPreClaim = (ctx) => {
|
|
1075
|
+
const capture = ctx.payrails;
|
|
1076
|
+
const { url, method } = ctx.request;
|
|
1077
|
+
if (!capture || method.toUpperCase() !== 'POST' || !isPayrailsHost(url))
|
|
1078
|
+
return undefined;
|
|
1079
|
+
const body = ctx.request.body;
|
|
1080
|
+
if (body === null || body === '')
|
|
1081
|
+
return isPayrailsStaticHost(url) ? undefined : payrailsUnreadable(ctx);
|
|
1082
|
+
const kind = payrailsRequestKind(url, method, body);
|
|
1083
|
+
if (kind === 'card_payment')
|
|
1084
|
+
return payrailsUnsupported(ctx, 'unsupported_payrails_card_payment');
|
|
1085
|
+
if (kind !== 'card' || !ctx.vault.isCardRequest(url, method))
|
|
1086
|
+
return undefined;
|
|
1087
|
+
const refusal = payrailsSaveRefusal(body);
|
|
1088
|
+
if (refusal)
|
|
1089
|
+
return payrailsUnsupported(ctx, refusal);
|
|
1090
|
+
return capture.pending ? capture.settle().then(() => payrailsBind(ctx, capture, body)) : payrailsBind(ctx, capture, body);
|
|
1091
|
+
};
|
|
1092
|
+
function payrailsBind(ctx, capture, body) {
|
|
1093
|
+
const key = capture.keyFor({ url: ctx.request.url, headers: ctx.request.headers, body });
|
|
1094
|
+
if (key === null)
|
|
1095
|
+
return payrailsUnsupported(ctx, 'payrails_session_unavailable');
|
|
1096
|
+
const refusal = payrailsKeyRefusal(key, body);
|
|
1097
|
+
if (refusal)
|
|
1098
|
+
return payrailsUnsupported(ctx, refusal);
|
|
1099
|
+
return { kind: 'pause', stripeStep: ctx.stripeStep, payrails: { encryption_public_key: key } };
|
|
1100
|
+
}
|
|
1101
|
+
async function payrailsUnreadable(ctx) {
|
|
1102
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_payrails_request' });
|
|
1103
|
+
await ctx.effects.abort();
|
|
1104
|
+
return { kind: 'abort' };
|
|
1105
|
+
}
|
|
1106
|
+
async function payrailsUnsupported(ctx, reason) {
|
|
1107
|
+
const { url, method } = ctx.request;
|
|
1108
|
+
const holds = PAYRAILS_HOLDING_REASONS.includes(reason);
|
|
1109
|
+
if (holds) {
|
|
1110
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
1111
|
+
ctx.lifecycle.unsupported(reason);
|
|
1112
|
+
}
|
|
1113
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason,
|
|
1114
|
+
...(holds ? {} : { message: PAYRAILS_SESSION_MESSAGE }) } });
|
|
1115
|
+
await ctx.effects.abort();
|
|
1116
|
+
return { kind: 'abort' };
|
|
1117
|
+
}
|
|
1118
|
+
// Elavon Converge's Hosted Payment Page (elavon-converge.ts). Its card URL, POST
|
|
1119
|
+
// /hosted-payments/service/payment/hpp/process on api.convergepay.com or
|
|
1120
|
+
// api.demo.convergepay.com, also carries electronic check and financing payments
|
|
1121
|
+
// with no card, and card-shaped bodies the Vault never pays: a wallet with a card
|
|
1122
|
+
// number the server supplied, the debit, gift card and push-to-card tabs, the
|
|
1123
|
+
// follow-ups after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
|
|
1124
|
+
// service fee, recurring or installment fields, card saving, a tip, another
|
|
1125
|
+
// currency. So a POST the registry recognizes on a Converge host that carries a
|
|
1126
|
+
// card is judged here, before the terminal arbiter would pause it, by the
|
|
1127
|
+
// whole-request check the API runs at create:
|
|
1128
|
+
// - the page's own first credit card sale defers, and the arbiter pauses it
|
|
1129
|
+
// for approval;
|
|
1130
|
+
// - anything else aborts with a `blocked` event naming payment-core's code
|
|
1131
|
+
// (converge_wallet_refused, converge_3ds_follow_up_refused, and so on), with
|
|
1132
|
+
// no claim, create or prompt. Like Braintree's unsupported operation it
|
|
1133
|
+
// neither holds the attachment nor changes its state, so the page's next
|
|
1134
|
+
// payment (the buyer leaves Google Pay for the card form) is judged afresh.
|
|
1135
|
+
// A body with no card defers too, and the adapter continues it untouched in every
|
|
1136
|
+
// state (passWithoutCard); so does a body the adapter could not read, which the
|
|
1137
|
+
// adapter fails on its own as unreadable, that request only. Nothing is decided
|
|
1138
|
+
// without the registry: an SDK that did not sync with the converge_hpp_process
|
|
1139
|
+
// capability, or whose org Agentcard has not enabled for Converge (the API then
|
|
1140
|
+
// serves no elavon_converge recognizer), never recognizes a Converge host, so the
|
|
1141
|
+
// gate defers and the request continues as before. Synchronous until the abort,
|
|
1142
|
+
// so a card request pauses with no await. A check that throws blocks the request
|
|
1143
|
+
// as not the page's own rather than letting it reach a prompt.
|
|
1144
|
+
const convergePreClaim = (ctx) => {
|
|
1145
|
+
const { url, method } = ctx.request;
|
|
1146
|
+
if (method.toUpperCase() !== 'POST' || !isConvergeHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
1147
|
+
return undefined;
|
|
1148
|
+
if (ctx.request.body == null || ctx.vault.withoutCard?.(url, ctx.request.body) === 'continue')
|
|
1149
|
+
return undefined;
|
|
1150
|
+
let reason;
|
|
1151
|
+
try {
|
|
1152
|
+
reason = convergeRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
reason = 'converge_body_malformed';
|
|
1156
|
+
}
|
|
1157
|
+
return reason === null ? undefined : convergeBlock(ctx, reason);
|
|
1158
|
+
};
|
|
1159
|
+
async function convergeBlock(ctx, reason) {
|
|
1160
|
+
ctx.onEvent?.({ type: 'blocked', detail: reason });
|
|
1161
|
+
await ctx.effects.abort();
|
|
1162
|
+
return { kind: 'abort' };
|
|
1163
|
+
}
|
|
1164
|
+
// Worldpay eProtect, once the registry serves it (a synced build that declares
|
|
1165
|
+
// its capability, for an org the API enabled); for every other client the gate
|
|
1166
|
+
// defers on everything, so nothing changes. On eProtect's hosts:
|
|
1167
|
+
// - the registration GET the recognizer admits defers to the arbiter, which
|
|
1168
|
+
// pauses it for the vault claim flow; one the registry does not cover aborts
|
|
1169
|
+
// as unsupported instead of reaching Worldpay;
|
|
1170
|
+
// - any other request on /eProtect/paypage (a CVV-only, PIN, wallet or
|
|
1171
|
+
// private-label variant, the native-app Mobile API form post) aborts as an
|
|
1172
|
+
// unsupported checkout, so the placeholder's ciphertext never leaves;
|
|
1173
|
+
// - the vendor's failover re-send of a registration this attachment saw
|
|
1174
|
+
// aborts quietly: it repeats the placeholder's ciphertext and never prompts
|
|
1175
|
+
// or claims a preparation. A re-send of nothing seen means the registration
|
|
1176
|
+
// went somewhere this SDK does not cover, and aborts as unsupported;
|
|
1177
|
+
// - the vendor's give-up report (/eProtect/ppstats 900 or 901) ends the
|
|
1178
|
+
// approval running for its registration, then continues to Worldpay as the
|
|
1179
|
+
// page's own telemetry;
|
|
1180
|
+
// - everything else there (the scripts, the payframe) continues.
|
|
1181
|
+
// Synchronous for a registration, so it pauses in the microtask it paused in.
|
|
1182
|
+
const eprotectPreClaim = (ctx) => {
|
|
1183
|
+
const request = ctx.eprotect?.classify(ctx.request.url, ctx.request.method);
|
|
1184
|
+
if (!request || request.kind === 'sibling')
|
|
1185
|
+
return undefined;
|
|
1186
|
+
if (request.kind === 'registration') {
|
|
1187
|
+
ctx.eprotect.saw(request);
|
|
1188
|
+
return ctx.vault.isCardRequest(ctx.request.url, ctx.request.method) ? undefined : unsupportedAbort(ctx);
|
|
1189
|
+
}
|
|
1190
|
+
if (request.kind === 'give_up')
|
|
1191
|
+
return eprotectGiveUp(ctx, request, request.code);
|
|
1192
|
+
if (request.kind === 'failover' && ctx.eprotect.repeats(request))
|
|
1193
|
+
return eprotectFailoverAbort(ctx);
|
|
1194
|
+
return unsupportedAbort(ctx);
|
|
1195
|
+
};
|
|
1196
|
+
async function eprotectFailoverAbort(ctx) {
|
|
1197
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'eprotect_failover_aborted' });
|
|
1198
|
+
await ctx.effects.abort();
|
|
1199
|
+
return { kind: 'abort' };
|
|
1200
|
+
}
|
|
1201
|
+
async function eprotectGiveUp(ctx, attempt, code) {
|
|
1202
|
+
const ended = ctx.eprotect.gaveUp(attempt);
|
|
1203
|
+
if (ended)
|
|
1204
|
+
ctx.onEvent?.({ type: 'merchant_request_abandoned', detail: { processor: 'worldpay_eprotect', code, answered: ended === 'answered' } });
|
|
1205
|
+
await ctx.effects.continueRequest().catch(() => { });
|
|
1206
|
+
return { kind: 'continue' };
|
|
1207
|
+
}
|
|
1208
|
+
// WSPay Components (wspay.ts). The card frame's confirm URL also carries saved_card and
|
|
1209
|
+
// temp_card_id bodies with no new card, and the page builds the transaction object itself, so a
|
|
1210
|
+
// POST the registry recognizes on a WSPay host is judged here, before the terminal arbiter would
|
|
1211
|
+
// pause it, by the whole-request check the API runs at create:
|
|
1212
|
+
// - a new card for a one-time payment defers, and the arbiter pauses it for approval;
|
|
1213
|
+
// - anything else aborts with a `blocked` event that names why, with no claim, create or
|
|
1214
|
+
// prompt: `unsupported_wspay_payment_method`, or `wspay_keep_on_file_unsupported` for a
|
|
1215
|
+
// confirm that asks WSPay to keep the card on file. Like Braintree's unsupported operation
|
|
1216
|
+
// it neither holds the attachment nor changes its state, so the page's next confirm (a new
|
|
1217
|
+
// card instead of a saved one) is judged afresh.
|
|
1218
|
+
// Nothing is decided without the registry: an SDK that did not sync with the wspay_confirm
|
|
1219
|
+
// capability, or whose org Agentcard has not enabled for WSPay (the API then serves no WSPay
|
|
1220
|
+
// recognizer), never recognizes a WSPay host, so the gate defers and the request continues as
|
|
1221
|
+
// before. GET /show and /status and POST /pan-info are not card requests and continue through
|
|
1222
|
+
// the arbiter. Synchronous until the abort, so a card confirm pauses with no await. A check that
|
|
1223
|
+
// throws blocks the request as unsupported rather than letting it reach a prompt.
|
|
1224
|
+
const wspayPreClaim = (ctx) => {
|
|
1225
|
+
const { url, method } = ctx.request;
|
|
1226
|
+
if (method.toUpperCase() !== 'POST' || !isWspayHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
1227
|
+
return undefined;
|
|
1228
|
+
let reason;
|
|
1229
|
+
try {
|
|
1230
|
+
reason = wspayRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
reason = 'unsupported_wspay_payment_method';
|
|
1234
|
+
}
|
|
1235
|
+
return reason === null ? undefined : wspayBlock(ctx, reason);
|
|
1236
|
+
};
|
|
1237
|
+
async function wspayBlock(ctx, reason) {
|
|
1238
|
+
ctx.onEvent?.({ type: 'blocked', detail: reason });
|
|
1239
|
+
await ctx.effects.abort();
|
|
1240
|
+
return { kind: 'abort' };
|
|
1241
|
+
}
|
|
1242
|
+
// Shiji STS (shiji-sts.ts). Its two endpoints also carry requests the vault must never pay, so a
|
|
1243
|
+
// POST the registry recognizes on an STS host is judged here, before the terminal arbiter would
|
|
1244
|
+
// pause it, by payment-core's classifier:
|
|
1245
|
+
// - a getTokenId in the proxy's shape defers, and the arbiter pauses it: the one prompt. While the
|
|
1246
|
+
// card-on-file decision is open the classifier never answers that, and every getTokenId aborts
|
|
1247
|
+
// as an unsupported checkout, `unsupported_checkout` with reason `shiji_sts_keep_on_file`, with
|
|
1248
|
+
// no claim, create or prompt; the attachment then reads `unsupported` and holds;
|
|
1249
|
+
// - a getCvvId never pauses. It continues with the device's CVV ciphertext only when the
|
|
1250
|
+
// attachment's companion binding answers for it (the getTokenId it continued, same page, same
|
|
1251
|
+
// accessToken, within 120 s, once), and aborts as an unsupported checkout otherwise, so the
|
|
1252
|
+
// agent's placeholder CVV never rides beside a real card;
|
|
1253
|
+
// - anything else on those URLs (another shape, a query, a card field under another key) aborts
|
|
1254
|
+
// the same way.
|
|
1255
|
+
// Nothing is decided without the registry: an SDK the API did not serve Shiji (a build without the
|
|
1256
|
+
// capability, or an org Agentcard has not enabled) recognizes neither URL, so the gate defers and
|
|
1257
|
+
// every Shiji request continues as before. The preflight OPTIONS and every other method continue
|
|
1258
|
+
// through the arbiter. A body the adapter could not read aborts that request alone with a `blocked`
|
|
1259
|
+
// event, `unreadable_shiji_sts_request`, and the attachment is not held. Synchronous until the
|
|
1260
|
+
// abort or the companion's continue, so a card request pauses with no await.
|
|
1261
|
+
const shijiStsPreClaim = (ctx) => {
|
|
1262
|
+
const { url, method } = ctx.request;
|
|
1263
|
+
if (method.toUpperCase() !== 'POST' || !isShijiStsHost(url) || !ctx.vault.isCardRequest(url, method))
|
|
1264
|
+
return undefined;
|
|
1265
|
+
const body = ctx.request.body;
|
|
1266
|
+
if (body === null)
|
|
1267
|
+
return shijiStsUnreadable(ctx);
|
|
1268
|
+
const verdict = shijiStsVerdict({ url, method, body });
|
|
1269
|
+
if (verdict.kind === 'pause')
|
|
1270
|
+
return undefined;
|
|
1271
|
+
if (verdict.kind === 'refuse')
|
|
1272
|
+
return shijiStsUnsupported(ctx, verdict.reason);
|
|
1273
|
+
return shijiStsCompanion(ctx, body);
|
|
1274
|
+
};
|
|
1275
|
+
async function shijiStsUnreadable(ctx) {
|
|
1276
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_shiji_sts_request' });
|
|
1277
|
+
await ctx.effects.abort();
|
|
1278
|
+
return { kind: 'abort' };
|
|
1279
|
+
}
|
|
1280
|
+
async function shijiStsUnsupported(ctx, reason) {
|
|
1281
|
+
const { url, method } = ctx.request;
|
|
1282
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
1283
|
+
ctx.lifecycle.unsupported(reason);
|
|
1284
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason } });
|
|
1285
|
+
await ctx.effects.abort();
|
|
1286
|
+
return { kind: 'abort' };
|
|
1287
|
+
}
|
|
1288
|
+
async function shijiStsCompanion(ctx, body) {
|
|
1289
|
+
const { url, method } = ctx.request;
|
|
1290
|
+
const shiji = ctx.shijiSts;
|
|
1291
|
+
if (!shiji)
|
|
1292
|
+
return shijiStsUnsupported(ctx, 'shiji_sts_companion_unbound');
|
|
1293
|
+
if (ctx.interceptionAborted() || ctx.lifecycleAborted()) {
|
|
1294
|
+
// The checkout was cancelled or the attachment is tearing down: the CVV stays on the device's side.
|
|
1295
|
+
shiji.companions.release();
|
|
1296
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'checkout_cancelled' });
|
|
1297
|
+
await ctx.effects.abort();
|
|
1298
|
+
return { kind: 'abort' };
|
|
1299
|
+
}
|
|
1300
|
+
const claimed = shiji.companions.claim({ url, method, body }, await shiji.page());
|
|
1301
|
+
if ('reason' in claimed)
|
|
1302
|
+
return shijiStsUnsupported(ctx, claimed.reason);
|
|
1303
|
+
try {
|
|
1304
|
+
await ctx.effects.continueRequest(claimed.body);
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
ctx.onEvent?.({ type: 'failed', detail: ctx.failureSummary(error) });
|
|
1308
|
+
await ctx.effects.abort().catch(() => { });
|
|
1309
|
+
return { kind: 'abort' };
|
|
1310
|
+
}
|
|
1311
|
+
ctx.onEvent?.({ type: 'companion_continued', detail: { url: redactUrl(url), psp: 'shiji_sts', fields: ['cvv'] } });
|
|
1312
|
+
return { kind: 'continue' };
|
|
1313
|
+
}
|
|
462
1314
|
// The terminal arbiter, always last. A request that no gate claimed and that is
|
|
463
1315
|
// neither a native Stripe step nor a recognized card request continues, unless
|
|
464
1316
|
// it matches a declared payment endpoint we do not support (then it aborts as an
|
|
@@ -492,7 +1344,7 @@ async function unsupportedContinue(ctx) {
|
|
|
492
1344
|
* arbiter runs after all of them and is not part of this list. A gate appended
|
|
493
1345
|
* after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
|
|
494
1346
|
* pauses each one itself. */
|
|
495
|
-
export const PSP_PRE_CLAIM_GATES = [mercadoPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim];
|
|
1347
|
+
export const PSP_PRE_CLAIM_GATES = [cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim];
|
|
496
1348
|
/** Run each gate in order; the first to return a decision wins. If none does,
|
|
497
1349
|
* the terminal arbiter decides between continue, abort (unsupported) and pause.
|
|
498
1350
|
* Returns synchronously while every gate defers synchronously (an ordinary card
|