@agent-cards/checkout 0.19.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/README.md +6 -753
- package/cdp.d.ts +1 -0
- package/cdp.js +2 -0
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/package.json +33 -33
- package/playwright.d.ts +1 -0
- package/playwright.js +2 -0
- package/preflight.d.ts +1 -0
- package/preflight.js +2 -0
- package/CHANGELOG.md +0 -132
- package/PREFLIGHT.md +0 -312
- package/dist/adyen-merchant-hosted.generated.d.ts +0 -277
- package/dist/adyen-merchant-hosted.generated.js +0 -1902
- package/dist/adyen.generated.d.ts +0 -24
- package/dist/adyen.generated.js +0 -64
- package/dist/attachment.d.ts +0 -11
- package/dist/attachment.js +0 -50
- package/dist/braintree.d.ts +0 -2
- package/dist/braintree.generated.d.ts +0 -10
- package/dist/braintree.generated.js +0 -302
- package/dist/braintree.js +0 -2
- package/dist/builtin-registry.generated.d.ts +0 -2
- package/dist/builtin-registry.generated.js +0 -1
- package/dist/card-fields.generated.d.ts +0 -3
- package/dist/card-fields.generated.js +0 -46
- package/dist/cdp.d.ts +0 -192
- package/dist/cdp.js +0 -2393
- package/dist/checkout-com.generated.d.ts +0 -4
- package/dist/checkout-com.generated.js +0 -183
- package/dist/client.d.ts +0 -849
- package/dist/client.js +0 -1754
- package/dist/cse-body.d.ts +0 -25
- package/dist/cse-body.js +0 -41
- package/dist/fiserv.d.ts +0 -65
- package/dist/fiserv.generated.d.ts +0 -73
- package/dist/fiserv.generated.js +0 -830
- package/dist/fiserv.js +0 -104
- package/dist/hosted-form.d.ts +0 -44
- package/dist/hosted-form.js +0 -78
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -10
- package/dist/lifecycle.d.ts +0 -179
- package/dist/lifecycle.js +0 -395
- package/dist/mercado-checkout.d.ts +0 -20
- package/dist/mercado-checkout.generated.d.ts +0 -52
- package/dist/mercado-checkout.generated.js +0 -198
- package/dist/mercado-checkout.js +0 -108
- package/dist/merchant-handoff.d.ts +0 -54
- package/dist/merchant-handoff.js +0 -100
- package/dist/merchant-hosted.d.ts +0 -140
- package/dist/merchant-hosted.js +0 -170
- package/dist/merchant-total-watch.d.ts +0 -115
- package/dist/merchant-total-watch.js +0 -268
- package/dist/merchant-total.d.ts +0 -257
- package/dist/merchant-total.js +0 -383
- package/dist/owned-shop.generated.d.ts +0 -24
- package/dist/owned-shop.generated.js +0 -108
- package/dist/paysafe.generated.d.ts +0 -12
- package/dist/paysafe.generated.js +0 -87
- package/dist/playwright.d.ts +0 -3
- package/dist/playwright.js +0 -3
- package/dist/pre-claim.d.ts +0 -123
- package/dist/pre-claim.js +0 -386
- package/dist/preflight-capabilities.generated.d.ts +0 -1253
- package/dist/preflight-capabilities.generated.js +0 -1929
- package/dist/preflight-catalog.json +0 -4727
- package/dist/preflight-playwright.d.ts +0 -34
- package/dist/preflight-playwright.js +0 -355
- package/dist/preflight-schemas.json +0 -1122
- package/dist/preflight.d.ts +0 -1
- package/dist/preflight.generated.d.ts +0 -1965
- package/dist/preflight.generated.js +0 -570
- package/dist/preflight.js +0 -2
- package/dist/preparation.d.ts +0 -38
- package/dist/preparation.js +0 -191
- package/dist/prepared-processor.d.ts +0 -43
- package/dist/prepared-processor.js +0 -172
- package/dist/recurly.generated.d.ts +0 -1
- package/dist/recurly.generated.js +0 -87
- package/dist/registry.d.ts +0 -121
- package/dist/registry.js +0 -310
- package/dist/spreedly.generated.d.ts +0 -10
- package/dist/spreedly.generated.js +0 -332
- package/dist/stripe-checkout.d.ts +0 -81
- package/dist/stripe-checkout.generated.d.ts +0 -82
- package/dist/stripe-checkout.generated.js +0 -1093
- package/dist/stripe-checkout.js +0 -140
- package/dist/substitute.d.ts +0 -38
- package/dist/substitute.js +0 -23
- package/dist/substitutions.generated.d.ts +0 -11
- package/dist/substitutions.generated.js +0 -818
- package/examples/existing-browser.mjs +0 -63
- package/examples/preflight/classify-direct.mjs +0 -21
- package/examples/preflight/classify-kernel.mjs +0 -30
- package/examples/preflight/inspect-browser.mjs +0 -44
- package/examples/preflight/kernel-native/README.md +0 -112
- package/examples/preflight/kernel-native/documented-adapters.json +0 -113
- package/examples/preflight/kernel-native/inventory.json +0 -233
- package/examples/preflight/kernel-native/qualification.mjs +0 -182
- package/examples/preflight/kernel-profile.empty.json +0 -11
- package/examples/preflight/mollie-hosted.observations.json +0 -23
- package/examples/preflight/mollie-hosted.result.json +0 -103
- package/examples/preflight/stripe-script.direct.result.json +0 -92
- package/examples/preflight/stripe-script.observations.json +0 -16
- package/examples/preflight/stripe-script.result.json +0 -87
package/dist/merchant-hosted.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adyen merchant-hosted checkouts, as the agent's browser sees them.
|
|
3
|
-
*
|
|
4
|
-
* Some Adyen merchants (Dunelm, Cinemark) have adyen-web encrypt the card into
|
|
5
|
-
* four fields and post them to THEIR OWN server, which then charges the card
|
|
6
|
-
* through Adyen. No processor host is involved, so the processor registry can
|
|
7
|
-
* never recognize that request. Agentcard reviews each such merchant instead and
|
|
8
|
-
* pins its endpoint, body rules and Adyen key in a profile (payment-core
|
|
9
|
-
* merchant-profiles.js). This build carries those profiles as a generated
|
|
10
|
-
* projection (adyen-merchant-hosted.generated.ts) and arms every one of them from
|
|
11
|
-
* the start, in 'observe' until the API says this client may pay it
|
|
12
|
-
* (VaultClient.syncRegistry asks with merchant_profiles=1). A sync that has not run
|
|
13
|
-
* or failed, or an API that serves no profile, leaves each one in 'observe'.
|
|
14
|
-
*
|
|
15
|
-
* A profile's `status` keeps it dark: 'observe' pauses its card request, reports
|
|
16
|
-
* the request's shape (a hash of its key paths, never a value) and aborts it, so
|
|
17
|
-
* the agent's dummy card never reaches the merchant and nothing is charged;
|
|
18
|
-
* 'enabled' lets a prepared checkout pay it. The API can hold a profile in
|
|
19
|
-
* 'observe' for a client but never enable it past this build: a profile is
|
|
20
|
-
* 'enabled' only when both say so. Every profile ships in 'observe' today.
|
|
21
|
-
*/
|
|
22
|
-
import { MERCHANT_PROFILE_RECOGNIZERS as SHARED_PROFILES, classifyDeclaredMerchantRequest as sharedClassifyDeclared, declaredTemplatedUrl as sharedDeclaredTemplatedUrl, declaredUrlGlobs as sharedDeclaredUrlGlobs, declaredUrlRelation as sharedDeclaredUrlRelation, declaredEndpointRefusal as sharedDeclaredEndpointRefusal, classifyMerchantRequest as sharedClassify, merchantBodyKeyPathSha256 as sharedKeyPathSha256, merchantProfileEnvironment as sharedEnvironment, merchantProfileRulesMatch as sharedRulesMatch, merchantProfileTemplatedUrl as sharedTemplatedUrl, merchantProfileUrlGlobs as sharedUrlGlobs, merchantProfileUrlRelation as sharedUrlRelation, sha256Hex as sharedSha256Hex, } from './adyen-merchant-hosted.generated.js';
|
|
23
|
-
import { SubstitutionError, substituteMerchantHosted as sharedSubstitute } from './substitutions.generated.js';
|
|
24
|
-
import { BUILTIN_REGISTRY } from './registry.js';
|
|
25
|
-
/**
|
|
26
|
-
* The profile's verdict on a paused request: pause (its reviewed card request),
|
|
27
|
-
* abort (encrypted card data it cannot finish, with the reason) or pass (no
|
|
28
|
-
* encrypted card data, or not its endpoint or a sibling of it). Reads only;
|
|
29
|
-
* throws for a profile id this build did not review.
|
|
30
|
-
*/
|
|
31
|
-
export const classifyMerchantRequest = sharedClassify;
|
|
32
|
-
/** The endpoint origin, the path TEMPLATE and the profile's own query: what an event may name. */
|
|
33
|
-
export const merchantProfileTemplatedUrl = sharedTemplatedUrl;
|
|
34
|
-
/** SHA-256 of a JSON body's sorted key paths, no value in it; null for a body that is not JSON. */
|
|
35
|
-
export const merchantBodyKeyPathSha256 = sharedKeyPathSha256;
|
|
36
|
-
/** 'sandbox' for a profile on Adyen's TEST platform, 'production' for every live environment. */
|
|
37
|
-
export const merchantProfileEnvironment = sharedEnvironment;
|
|
38
|
-
const urlRelation = sharedUrlRelation;
|
|
39
|
-
const urlGlobs = sharedUrlGlobs;
|
|
40
|
-
const rulesMatch = sharedRulesMatch;
|
|
41
|
-
const sha256Hex = sharedSha256Hex;
|
|
42
|
-
const substitute = sharedSubstitute;
|
|
43
|
-
/** classifyMerchantRequest for a sandbox declaration: the profile's body rules at the declared endpoint instead of the profile's. */
|
|
44
|
-
export const classifyDeclaredMerchantRequest = sharedClassifyDeclared;
|
|
45
|
-
const declaredRelation = sharedDeclaredUrlRelation;
|
|
46
|
-
const declaredTemplated = sharedDeclaredTemplatedUrl;
|
|
47
|
-
const declaredGlobs = sharedDeclaredUrlGlobs;
|
|
48
|
-
/** payment-core's endpoint rule for a declaration, the part that needs no processor catalog: why an endpoint cannot be declared, or null. */
|
|
49
|
-
const declaredEndpointRefusal = sharedDeclaredEndpointRefusal;
|
|
50
|
-
const BUILT = SHARED_PROFILES;
|
|
51
|
-
/**
|
|
52
|
-
* The profiles this client arms: every profile this build reviewed, once each, in
|
|
53
|
-
* this build's order. A served entry is `{ merchant_profile: <the profile's wire
|
|
54
|
-
* projection> }`, and the first one that names a profile with exactly this build's
|
|
55
|
-
* rules (endpoint, body rules, holder, fields, key: everything but `status`) and a
|
|
56
|
-
* status this build knows decides it: 'enabled' only when both that entry and this
|
|
57
|
-
* build say so. Every other profile stays in 'observe': one the API did not serve (no
|
|
58
|
-
* sync yet, a failed sync, an API that predates profiles) and one the API reviewed
|
|
59
|
-
* again after this build was cut, which this build cannot pay. In 'observe' its card
|
|
60
|
-
* request is still paused and aborted, so the agent's dummy card never reaches a
|
|
61
|
-
* reviewed merchant, whatever the API answered.
|
|
62
|
-
*/
|
|
63
|
-
export function armServedProfiles(entries = []) {
|
|
64
|
-
const decided = new Map();
|
|
65
|
-
for (const entry of entries) {
|
|
66
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
67
|
-
continue;
|
|
68
|
-
const served = entry.merchant_profile;
|
|
69
|
-
if (!served || typeof served !== 'object' || Array.isArray(served))
|
|
70
|
-
continue;
|
|
71
|
-
const id = served.id;
|
|
72
|
-
const status = served.status;
|
|
73
|
-
const built = typeof id === 'string' ? BUILT.find((profile) => profile.id === id) : undefined;
|
|
74
|
-
if (!built || decided.has(built.id) || (status !== 'observe' && status !== 'enabled') || !rulesMatch(built.id, served))
|
|
75
|
-
continue;
|
|
76
|
-
decided.set(built.id, status === 'enabled' && built.status === 'enabled' ? 'enabled' : 'observe');
|
|
77
|
-
}
|
|
78
|
-
return BUILT.map((built) => Object.freeze({ id: built.id, merchant: built.merchant, status: decided.get(built.id) ?? 'observe' }));
|
|
79
|
-
}
|
|
80
|
-
const TEST_CLIENT_KEY = /^test_[A-Za-z0-9]{32}$/;
|
|
81
|
-
/**
|
|
82
|
-
* Arms a sandbox declaration: the reviewed profile's card request at your own
|
|
83
|
-
* endpoint, under your own Adyen TEST key. Throws a TypeError for a profile this
|
|
84
|
-
* build did not review, an endpoint the API would refuse, or a key that is not a
|
|
85
|
-
* test_ key. The endpoint rule is the API's own (payment-core sandboxDeclarationRefusal):
|
|
86
|
-
* an HTTPS URL in the exact form the browser writes it (no port, credentials or
|
|
87
|
-
* fragment), on a public domain name outside Adyen's domains, Agentcard's and every
|
|
88
|
-
* reviewed merchant's domain (declaredEndpointRefusal), and on no host this build's
|
|
89
|
-
* processor registry names. The armed entry names its host as the merchant, and is
|
|
90
|
-
* 'enabled' whatever the reviewed profile's status: it never pays the reviewed
|
|
91
|
-
* merchant, the API refuses it from a live client and from an org it has not turned
|
|
92
|
-
* declarations on for, and the Vault encrypts only Adyen's documented test cards
|
|
93
|
-
* under a test_ key.
|
|
94
|
-
*/
|
|
95
|
-
export function declareSandboxMerchant(input) {
|
|
96
|
-
const built = input && typeof input.profile === 'string' ? BUILT.find((profile) => profile.id === input.profile) : undefined;
|
|
97
|
-
if (!built)
|
|
98
|
-
throw new TypeError('sandboxMerchants: profile is not a merchant profile this SDK build reviewed.');
|
|
99
|
-
const refusal = declaredEndpointRefusal(input.endpoint);
|
|
100
|
-
if (refusal)
|
|
101
|
-
throw new TypeError(`sandboxMerchants: ${refusal}.`);
|
|
102
|
-
const url = new URL(input.endpoint);
|
|
103
|
-
const host = url.hostname.replace(/\.$/, '');
|
|
104
|
-
if (BUILTIN_REGISTRY.some((rec) => (rec.hosts ?? []).some((source) => new RegExp(source, 'i').test(host)))) {
|
|
105
|
-
throw new TypeError(`sandboxMerchants: ${url.hostname} is a payment processor's host, not a merchant's own endpoint.`);
|
|
106
|
-
}
|
|
107
|
-
if (typeof input.clientKey !== 'string' || !TEST_CLIENT_KEY.test(input.clientKey)) {
|
|
108
|
-
throw new TypeError('sandboxMerchants: clientKey must be an Adyen TEST client key (test_ followed by 32 letters and digits).');
|
|
109
|
-
}
|
|
110
|
-
return Object.freeze({ id: built.id, merchant: url.hostname, status: 'enabled',
|
|
111
|
-
sandboxDeclaration: Object.freeze({ endpoint: input.endpoint, clientKey: input.clientKey }) });
|
|
112
|
-
}
|
|
113
|
-
/** How a URL relates to an armed profile: its declared endpoint when it has one, else the reviewed endpoint. */
|
|
114
|
-
function relationTo(profile, url) {
|
|
115
|
-
return profile.sandboxDeclaration ? declaredRelation(profile.sandboxDeclaration.endpoint, url) : urlRelation(profile.id, url);
|
|
116
|
-
}
|
|
117
|
-
/** The armed profile whose endpoint, or a sibling of it (same origin and path shape), a URL is; null for any other URL. */
|
|
118
|
-
export function merchantProfileFor(profiles, url) {
|
|
119
|
-
if (typeof url !== 'string')
|
|
120
|
-
return null;
|
|
121
|
-
return profiles.find((profile) => relationTo(profile, url) !== null) ?? null;
|
|
122
|
-
}
|
|
123
|
-
/** The Fetch.enable globs that pause every armed profile's endpoint and its siblings, a declared endpoint included. */
|
|
124
|
-
export function merchantProfileUrlPatterns(profiles) {
|
|
125
|
-
return [...new Set(profiles.flatMap((profile) => (profile.sandboxDeclaration
|
|
126
|
-
? declaredGlobs(profile.sandboxDeclaration.endpoint) : urlGlobs(profile.id))))];
|
|
127
|
-
}
|
|
128
|
-
/** classifyMerchantRequest for an armed profile: at its declared endpoint when it has one. */
|
|
129
|
-
export function classifyProfileRequest(profile, url, method, body) {
|
|
130
|
-
return profile.sandboxDeclaration
|
|
131
|
-
? classifyDeclaredMerchantRequest(profile.id, profile.sandboxDeclaration.endpoint, url, method, body)
|
|
132
|
-
: classifyMerchantRequest(profile.id, url, method, body);
|
|
133
|
-
}
|
|
134
|
-
/** merchantProfileTemplatedUrl for an armed profile: a declared endpoint's origin and path, never its query. */
|
|
135
|
-
export function profileTemplatedUrl(profile, url) {
|
|
136
|
-
return profile.sandboxDeclaration ? declaredTemplated(profile.sandboxDeclaration.endpoint) : merchantProfileTemplatedUrl(profile.id, url);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* The Fetch.enable globs for every profile this build reviewed. Every one of them is
|
|
140
|
-
* armed from the start (armServedProfiles), so attachToCdp arms all of them at attach
|
|
141
|
-
* and a syncRegistry() before or after it changes only a profile's status: its card
|
|
142
|
-
* request is paused and judged under CDP exactly as attachToPlaywright's per-request
|
|
143
|
-
* route judges it.
|
|
144
|
-
*/
|
|
145
|
-
export function reviewedMerchantProfileUrlPatterns() {
|
|
146
|
-
return [...new Set(BUILT.flatMap((profile) => urlGlobs(profile.id)))];
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* The continuation of a merchant-hosted approval: the live paused body with the
|
|
150
|
-
* Vault's ciphertext in place of the dummy fields. The API never sees the body the
|
|
151
|
-
* browser continues, so the live body must first hash to the one the API checked
|
|
152
|
-
* when the authorization was created (`bodySha256`); a page that rewrote its body
|
|
153
|
-
* while the cardholder decided is refused rather than paid. payment-core's
|
|
154
|
-
* substituteMerchantHosted then reads the profile's body rules again on the live
|
|
155
|
-
* body, writes only the profile's four fields, and refuses a profile that is not
|
|
156
|
-
* enabled, unless this client armed the endpoint from its own sandbox declaration
|
|
157
|
-
* (the replay carries `sandboxDeclaration`), where every body rule still applies.
|
|
158
|
-
* Throws SubstitutionError; nothing leaves the browser when it does.
|
|
159
|
-
*/
|
|
160
|
-
export function substituteMerchantHostedBody(body, replay) {
|
|
161
|
-
if (typeof body !== 'string' || sha256Hex(body) !== replay.bodySha256) {
|
|
162
|
-
throw new SubstitutionError('the paused body is not the one Agentcard checked when this checkout was created');
|
|
163
|
-
}
|
|
164
|
-
const { set, ...substitution } = replay.substitutions;
|
|
165
|
-
return substitute(body, set && set.length ? { ...substitution, set } : substitution, { sandboxDeclared: replay.sandboxDeclaration !== undefined });
|
|
166
|
-
}
|
|
167
|
-
/** The URL an event names for a merchant-hosted replay's request: its declared endpoint's origin and path, or the profile's template. */
|
|
168
|
-
export function replayTemplatedUrl(replay, url) {
|
|
169
|
-
return replay.sandboxDeclaration ? declaredTemplated(replay.sandboxDeclaration.endpoint) : merchantProfileTemplatedUrl(replay.profile, url);
|
|
170
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { type MerchantHostedReplay } from './client.js';
|
|
2
|
-
import { MerchantExchangeLog, type MerchantRequestSeen, type MerchantTotalCapture } from './merchant-total.js';
|
|
3
|
-
/** onEvent is ordinary integrator telemetry; the shape matches AttachOptions.onEvent. */
|
|
4
|
-
type TotalEvent = {
|
|
5
|
-
type: string;
|
|
6
|
-
detail?: unknown;
|
|
7
|
-
};
|
|
8
|
-
type ChargeReporter = {
|
|
9
|
-
reportMerchantCharge?: (authorizationId: string, report: any) => Promise<{
|
|
10
|
-
verdict: string;
|
|
11
|
-
alerted: boolean;
|
|
12
|
-
}>;
|
|
13
|
-
};
|
|
14
|
-
type Canceller = {
|
|
15
|
-
cancelAuthorization?: (authorizationId: string, reason: 'merchant_never_retried') => Promise<unknown>;
|
|
16
|
-
};
|
|
17
|
-
/** What the watch reads from the adapter's VaultClient: the profile a URL pays, and this client's own declarations. */
|
|
18
|
-
type ProfileSource = {
|
|
19
|
-
merchantProfile?: (id: string) => {
|
|
20
|
-
id: string;
|
|
21
|
-
sandboxDeclaration?: {
|
|
22
|
-
endpoint: string;
|
|
23
|
-
};
|
|
24
|
-
} | null;
|
|
25
|
-
merchantProfileOf?: (url: string) => {
|
|
26
|
-
id: string;
|
|
27
|
-
} | null;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* How long a paused card request's page is waited for. Chromium can deliver a request's
|
|
31
|
-
* Network event (which names its document) after the Fetch pause; it follows within
|
|
32
|
-
* milliseconds, so this bound only matters when it never comes, and the read then has no page.
|
|
33
|
-
*/
|
|
34
|
-
export declare const MERCHANT_PAGE_WAIT_MS = 1000;
|
|
35
|
-
/**
|
|
36
|
-
* The pauses before the after-payment report is tried again when it could not go out (no
|
|
37
|
-
* confirmation read yet, or the report request failed): a confirmation the page asks for
|
|
38
|
-
* after the card request's answer (Dick's order submit) still gets reported.
|
|
39
|
-
*/
|
|
40
|
-
export declare const MERCHANT_CHARGE_RETRY_DELAYS_MS: readonly number[];
|
|
41
|
-
/** How long after the merchant answered a confirmation that arrives late is still reported. */
|
|
42
|
-
export declare const MERCHANT_CHARGE_REPORT_WINDOW_MS: number;
|
|
43
|
-
/**
|
|
44
|
-
* The merchant's own total around one attached page, shared by the CDP and Playwright
|
|
45
|
-
* adapters so both read it the same way (merchant-total.ts says what it proves). From
|
|
46
|
-
* attach on, the adapter tells it about every request that leaves, every answer and every
|
|
47
|
-
* failure; it keeps, read-only, the ones a priced profile's rules read at the origins they
|
|
48
|
-
* read (a client's sandbox declaration moves a profile's to its declared endpoint), and
|
|
49
|
-
* the page each request came from. At a pause it hands the client a capture; at release it
|
|
50
|
-
* reads the total again and holds the card request unless it is still the approved amount;
|
|
51
|
-
* once the merchant answered the continued request it reports what the merchant says it
|
|
52
|
-
* charged.
|
|
53
|
-
*/
|
|
54
|
-
export declare class MerchantTotalWatch {
|
|
55
|
-
private readonly onEvent?;
|
|
56
|
-
private readonly vault?;
|
|
57
|
-
private readonly waitMs;
|
|
58
|
-
private readonly settleMs;
|
|
59
|
-
private readonly retryDelaysMs;
|
|
60
|
-
readonly log: MerchantExchangeLog;
|
|
61
|
-
private readonly pages;
|
|
62
|
-
private readonly pageWaiters;
|
|
63
|
-
/** Approvals whose charge report the API took (the newest REPORTS_MAX). */
|
|
64
|
-
private readonly reported;
|
|
65
|
-
/** Approvals a report attempt is running or scheduled for. */
|
|
66
|
-
private readonly reporting;
|
|
67
|
-
/**
|
|
68
|
-
* Approvals still waiting for their report (the newest REPORTS_MAX), each with until when a
|
|
69
|
-
* confirmation that comes late reports it. One per approval, so a second payment on the
|
|
70
|
-
* same page never takes the first one's place.
|
|
71
|
-
*/
|
|
72
|
-
private readonly awaiting;
|
|
73
|
-
constructor(onEvent?: ((event: TotalEvent) => void) | undefined, vault?: ProfileSource | undefined, waitMs?: number, settleMs?: number, retryDelaysMs?: readonly number[]);
|
|
74
|
-
/** A request left: remember its page, and record it when a priced profile's rules read it. */
|
|
75
|
-
sent(key: string, request: MerchantRequestSeen): void;
|
|
76
|
-
/** The page a request came from, when the adapter named one. */
|
|
77
|
-
pageOf(key: string): string | undefined;
|
|
78
|
-
/**
|
|
79
|
-
* The page a request came from, waiting up to `timeoutMs` for its own event to arrive when
|
|
80
|
-
* the adapter has not seen it yet; undefined when that event names none or never comes.
|
|
81
|
-
*/
|
|
82
|
-
pageFor(key: string, timeoutMs: number, signal?: AbortSignal): Promise<string | undefined>;
|
|
83
|
-
/** Whether a paused request is the card request of an armed profile the merchant's own total prices. */
|
|
84
|
-
priced(url: string): boolean;
|
|
85
|
-
/** The capture the client reads for a card request that paused at `pausedAt`. */
|
|
86
|
-
capture(pausedAt: number, page: string | undefined): MerchantTotalCapture;
|
|
87
|
-
/**
|
|
88
|
-
* The read at release. Resolves when the card request may go out; otherwise retires the
|
|
89
|
-
* approval (the card never left this browser, so merchant_never_retried, which stops the
|
|
90
|
-
* API serving its ciphertext) and throws. The retirement is awaited: once the API confirms
|
|
91
|
-
* it, a MerchantTotalError at stage 'release'; when it does not (or this vault cannot
|
|
92
|
-
* retire), the approved row may still serve its ciphertext, so a PaymentOutcomeUnknownError
|
|
93
|
-
* ('authorization_cancel_unconfirmed') the application must reconcile, as every retirement
|
|
94
|
-
* the API did not confirm ends. `merchant_total_held` says which (`retired`).
|
|
95
|
-
*/
|
|
96
|
-
release(replay: MerchantHostedReplay, vault: Canceller | undefined, signal?: AbortSignal): Promise<void>;
|
|
97
|
-
/**
|
|
98
|
-
* The merchant answered the continued card request (both adapters call this when its
|
|
99
|
-
* final answer arrives, whatever the profile's after-payment rules read): after a short
|
|
100
|
-
* settle for the rest of its confirmation, report what it says it charged, once per
|
|
101
|
-
* approval. The approval counts as reported only once the API took the report. One that
|
|
102
|
-
* could not go out (no confirmation read yet, or the report request failed) is tried again
|
|
103
|
-
* after each of MERCHANT_CHARGE_RETRY_DELAYS_MS; after the last, a confirmation the log reads
|
|
104
|
-
* within MERCHANT_CHARGE_REPORT_WINDOW_MS of the answer is still reported (lateConfirmation).
|
|
105
|
-
* The first attempt keeps a Node process alive, since the confirmation may already be read;
|
|
106
|
-
* later ones do not. Never throws; each outcome is an event, and `merchant_charge_unreported`
|
|
107
|
-
* says whether another attempt follows (`retrying`).
|
|
108
|
-
*/
|
|
109
|
-
answered(replay: MerchantHostedReplay, vault: ChargeReporter | undefined): void;
|
|
110
|
-
/** A newly read answer: each approval's confirmation that came after its last attempt is reported now, under that approval. */
|
|
111
|
-
private lateConfirmation;
|
|
112
|
-
/** One report attempt, `tries` into its retries; never rejects. */
|
|
113
|
-
private attempt;
|
|
114
|
-
}
|
|
115
|
-
export {};
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { MerchantTotalError, PaymentOutcomeUnknownError } from './client.js';
|
|
2
|
-
import { MERCHANT_CHARGE_SETTLE_MS, MERCHANT_LOG_TEXT_MAX, MERCHANT_TOTAL_WAIT_MS, MerchantExchangeLog, merchantChargeReport, merchantProfilePricedBySource, merchantTotalAtRelease, } from './merchant-total.js';
|
|
3
|
-
import { MERCHANT_PROFILE_RECOGNIZERS } from './adyen-merchant-hosted.generated.js';
|
|
4
|
-
/** Every profile this build reviewed that the merchant's own responses price. */
|
|
5
|
-
const PRICED_IDS = MERCHANT_PROFILE_RECOGNIZERS
|
|
6
|
-
.map((profile) => profile.id).filter((id) => merchantProfilePricedBySource(id));
|
|
7
|
-
/** How many request pages are remembered, oldest dropped first. */
|
|
8
|
-
const PAGES_MAX = 512;
|
|
9
|
-
/**
|
|
10
|
-
* How long a paused card request's page is waited for. Chromium can deliver a request's
|
|
11
|
-
* Network event (which names its document) after the Fetch pause; it follows within
|
|
12
|
-
* milliseconds, so this bound only matters when it never comes, and the read then has no page.
|
|
13
|
-
*/
|
|
14
|
-
export const MERCHANT_PAGE_WAIT_MS = 1_000;
|
|
15
|
-
/**
|
|
16
|
-
* The pauses before the after-payment report is tried again when it could not go out (no
|
|
17
|
-
* confirmation read yet, or the report request failed): a confirmation the page asks for
|
|
18
|
-
* after the card request's answer (Dick's order submit) still gets reported.
|
|
19
|
-
*/
|
|
20
|
-
export const MERCHANT_CHARGE_RETRY_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
21
|
-
/** How long after the merchant answered a confirmation that arrives late is still reported. */
|
|
22
|
-
export const MERCHANT_CHARGE_REPORT_WINDOW_MS = 10 * 60_000;
|
|
23
|
-
/** How many approvals the watch remembers as reported, or as waiting for a late confirmation. */
|
|
24
|
-
const REPORTS_MAX = 16;
|
|
25
|
-
/**
|
|
26
|
-
* The merchant's own total around one attached page, shared by the CDP and Playwright
|
|
27
|
-
* adapters so both read it the same way (merchant-total.ts says what it proves). From
|
|
28
|
-
* attach on, the adapter tells it about every request that leaves, every answer and every
|
|
29
|
-
* failure; it keeps, read-only, the ones a priced profile's rules read at the origins they
|
|
30
|
-
* read (a client's sandbox declaration moves a profile's to its declared endpoint), and
|
|
31
|
-
* the page each request came from. At a pause it hands the client a capture; at release it
|
|
32
|
-
* reads the total again and holds the card request unless it is still the approved amount;
|
|
33
|
-
* once the merchant answered the continued request it reports what the merchant says it
|
|
34
|
-
* charged.
|
|
35
|
-
*/
|
|
36
|
-
export class MerchantTotalWatch {
|
|
37
|
-
onEvent;
|
|
38
|
-
vault;
|
|
39
|
-
waitMs;
|
|
40
|
-
settleMs;
|
|
41
|
-
retryDelaysMs;
|
|
42
|
-
log;
|
|
43
|
-
pages = new Map();
|
|
44
|
-
pageWaiters = new Map();
|
|
45
|
-
/** Approvals whose charge report the API took (the newest REPORTS_MAX). */
|
|
46
|
-
reported = new Set();
|
|
47
|
-
/** Approvals a report attempt is running or scheduled for. */
|
|
48
|
-
reporting = new Set();
|
|
49
|
-
/**
|
|
50
|
-
* Approvals still waiting for their report (the newest REPORTS_MAX), each with until when a
|
|
51
|
-
* confirmation that comes late reports it. One per approval, so a second payment on the
|
|
52
|
-
* same page never takes the first one's place.
|
|
53
|
-
*/
|
|
54
|
-
awaiting = new Map();
|
|
55
|
-
constructor(onEvent, vault, waitMs = MERCHANT_TOTAL_WAIT_MS, settleMs = MERCHANT_CHARGE_SETTLE_MS, retryDelaysMs = MERCHANT_CHARGE_RETRY_DELAYS_MS) {
|
|
56
|
-
this.onEvent = onEvent;
|
|
57
|
-
this.vault = vault;
|
|
58
|
-
this.waitMs = waitMs;
|
|
59
|
-
this.settleMs = settleMs;
|
|
60
|
-
this.retryDelaysMs = retryDelaysMs;
|
|
61
|
-
// A client's declarations are fixed when it is built (VaultClientOptions.sandboxMerchants),
|
|
62
|
-
// so where each priced profile is read is settled once, here.
|
|
63
|
-
const scopes = PRICED_IDS.map((id) => {
|
|
64
|
-
let endpoint;
|
|
65
|
-
try {
|
|
66
|
-
endpoint = typeof vault?.merchantProfile === 'function' ? vault.merchantProfile(id)?.sandboxDeclaration?.endpoint : undefined;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
endpoint = undefined;
|
|
70
|
-
}
|
|
71
|
-
return typeof endpoint === 'string' ? { id, declaredEndpoint: endpoint } : id;
|
|
72
|
-
});
|
|
73
|
-
// Every answer the log reads can be a confirmation that came after the report's last attempt.
|
|
74
|
-
this.log = new MerchantExchangeLog(() => scopes, Date.now, MERCHANT_LOG_TEXT_MAX, () => this.lateConfirmation());
|
|
75
|
-
}
|
|
76
|
-
/** A request left: remember its page, and record it when a priced profile's rules read it. */
|
|
77
|
-
sent(key, request) {
|
|
78
|
-
if (request.page) {
|
|
79
|
-
this.pages.delete(key);
|
|
80
|
-
this.pages.set(key, request.page);
|
|
81
|
-
while (this.pages.size > PAGES_MAX)
|
|
82
|
-
this.pages.delete(this.pages.keys().next().value);
|
|
83
|
-
}
|
|
84
|
-
this.log.sent(key, request);
|
|
85
|
-
// A pause waiting for this request's page learns it now (or that its event names none).
|
|
86
|
-
for (const waiter of [...(this.pageWaiters.get(key) ?? [])])
|
|
87
|
-
waiter();
|
|
88
|
-
}
|
|
89
|
-
/** The page a request came from, when the adapter named one. */
|
|
90
|
-
pageOf(key) {
|
|
91
|
-
return this.pages.get(key);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* The page a request came from, waiting up to `timeoutMs` for its own event to arrive when
|
|
95
|
-
* the adapter has not seen it yet; undefined when that event names none or never comes.
|
|
96
|
-
*/
|
|
97
|
-
async pageFor(key, timeoutMs, signal) {
|
|
98
|
-
const known = this.pages.get(key);
|
|
99
|
-
if (known !== undefined || timeoutMs <= 0 || signal?.aborted)
|
|
100
|
-
return known;
|
|
101
|
-
await new Promise((resolve) => {
|
|
102
|
-
const waiters = this.pageWaiters.get(key) ?? new Set();
|
|
103
|
-
const done = () => {
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
waiters.delete(done);
|
|
106
|
-
if (waiters.size === 0 && this.pageWaiters.get(key) === waiters)
|
|
107
|
-
this.pageWaiters.delete(key);
|
|
108
|
-
signal?.removeEventListener('abort', done);
|
|
109
|
-
resolve();
|
|
110
|
-
};
|
|
111
|
-
const timer = setTimeout(done, timeoutMs);
|
|
112
|
-
waiters.add(done);
|
|
113
|
-
this.pageWaiters.set(key, waiters);
|
|
114
|
-
signal?.addEventListener('abort', done, { once: true });
|
|
115
|
-
});
|
|
116
|
-
return this.pages.get(key);
|
|
117
|
-
}
|
|
118
|
-
/** Whether a paused request is the card request of an armed profile the merchant's own total prices. */
|
|
119
|
-
priced(url) {
|
|
120
|
-
try {
|
|
121
|
-
const profile = typeof this.vault?.merchantProfileOf === 'function' ? this.vault.merchantProfileOf(url) : null;
|
|
122
|
-
return !!profile && merchantProfilePricedBySource(profile.id);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/** The capture the client reads for a card request that paused at `pausedAt`. */
|
|
129
|
-
capture(pausedAt, page) {
|
|
130
|
-
return this.log.capture(pausedAt, page, this.waitMs);
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* The read at release. Resolves when the card request may go out; otherwise retires the
|
|
134
|
-
* approval (the card never left this browser, so merchant_never_retried, which stops the
|
|
135
|
-
* API serving its ciphertext) and throws. The retirement is awaited: once the API confirms
|
|
136
|
-
* it, a MerchantTotalError at stage 'release'; when it does not (or this vault cannot
|
|
137
|
-
* retire), the approved row may still serve its ciphertext, so a PaymentOutcomeUnknownError
|
|
138
|
-
* ('authorization_cancel_unconfirmed') the application must reconcile, as every retirement
|
|
139
|
-
* the API did not confirm ends. `merchant_total_held` says which (`retired`).
|
|
140
|
-
*/
|
|
141
|
-
async release(replay, vault, signal) {
|
|
142
|
-
if (!replay.merchantTotal)
|
|
143
|
-
return;
|
|
144
|
-
const refusal = await merchantTotalAtRelease(this.log, replay.profile, replay.merchantTotal, Date.now(), this.waitMs, signal);
|
|
145
|
-
if (!refusal)
|
|
146
|
-
return;
|
|
147
|
-
let retired = false;
|
|
148
|
-
if (typeof vault?.cancelAuthorization === 'function') {
|
|
149
|
-
try {
|
|
150
|
-
await vault.cancelAuthorization(replay.authorizationId, 'merchant_never_retried');
|
|
151
|
-
retired = true;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
retired = false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
this.onEvent?.({ type: 'merchant_total_held', detail: { authorizationId: replay.authorizationId, profile: replay.profile, reason: refusal.code, retired } });
|
|
158
|
-
if (!retired)
|
|
159
|
-
throw new PaymentOutcomeUnknownError(replay.authorizationId, 'authorization_cancel_unconfirmed');
|
|
160
|
-
throw new MerchantTotalError(replay.authorizationId, 'merchant_total_changed', 'release', refusal.code, `The card request was held: ${refusal.reason}`);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* The merchant answered the continued card request (both adapters call this when its
|
|
164
|
-
* final answer arrives, whatever the profile's after-payment rules read): after a short
|
|
165
|
-
* settle for the rest of its confirmation, report what it says it charged, once per
|
|
166
|
-
* approval. The approval counts as reported only once the API took the report. One that
|
|
167
|
-
* could not go out (no confirmation read yet, or the report request failed) is tried again
|
|
168
|
-
* after each of MERCHANT_CHARGE_RETRY_DELAYS_MS; after the last, a confirmation the log reads
|
|
169
|
-
* within MERCHANT_CHARGE_REPORT_WINDOW_MS of the answer is still reported (lateConfirmation).
|
|
170
|
-
* The first attempt keeps a Node process alive, since the confirmation may already be read;
|
|
171
|
-
* later ones do not. Never throws; each outcome is an event, and `merchant_charge_unreported`
|
|
172
|
-
* says whether another attempt follows (`retrying`).
|
|
173
|
-
*/
|
|
174
|
-
answered(replay, vault) {
|
|
175
|
-
const id = replay.authorizationId;
|
|
176
|
-
if (!replay.merchantTotal || this.reported.has(id) || this.reporting.has(id))
|
|
177
|
-
return;
|
|
178
|
-
this.awaiting.delete(id);
|
|
179
|
-
this.awaiting.set(id, { replay, vault, until: Date.now() + MERCHANT_CHARGE_REPORT_WINDOW_MS });
|
|
180
|
-
while (this.awaiting.size > REPORTS_MAX)
|
|
181
|
-
this.awaiting.delete(this.awaiting.keys().next().value);
|
|
182
|
-
this.reporting.add(id);
|
|
183
|
-
setTimeout(() => { void this.attempt(replay, vault, 0); }, this.settleMs);
|
|
184
|
-
}
|
|
185
|
-
/** A newly read answer: each approval's confirmation that came after its last attempt is reported now, under that approval. */
|
|
186
|
-
lateConfirmation() {
|
|
187
|
-
const now = Date.now();
|
|
188
|
-
for (const [id, awaiting] of [...this.awaiting]) {
|
|
189
|
-
if (this.reported.has(id) || now > awaiting.until || !awaiting.replay.merchantTotal) {
|
|
190
|
-
this.awaiting.delete(id);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (this.reporting.has(id))
|
|
194
|
-
continue;
|
|
195
|
-
let readable = false;
|
|
196
|
-
try {
|
|
197
|
-
readable = merchantChargeReport(this.log, awaiting.replay.profile, awaiting.replay.merchantTotal) !== null;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
readable = false;
|
|
201
|
-
}
|
|
202
|
-
if (!readable)
|
|
203
|
-
continue;
|
|
204
|
-
this.reporting.add(id);
|
|
205
|
-
void this.attempt(awaiting.replay, awaiting.vault, this.retryDelaysMs.length);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
/** One report attempt, `tries` into its retries; never rejects. */
|
|
209
|
-
async attempt(replay, vault, tries) {
|
|
210
|
-
const id = replay.authorizationId;
|
|
211
|
-
const approved = replay.merchantTotal;
|
|
212
|
-
const detail = { authorizationId: id, profile: replay.profile };
|
|
213
|
-
let retrying = false;
|
|
214
|
-
try {
|
|
215
|
-
if (!approved)
|
|
216
|
-
return;
|
|
217
|
-
// The confirmation's text is read after it finished: wait (bounded) for every answer
|
|
218
|
-
// the after-payment rules read, then report what arrived.
|
|
219
|
-
await this.log.settle(replay.profile, 'charged', approved.card, Date.now(), this.waitMs).catch(() => { });
|
|
220
|
-
const report = merchantChargeReport(this.log, replay.profile, approved);
|
|
221
|
-
let reason;
|
|
222
|
-
if (!report)
|
|
223
|
-
reason = 'no_confirmation_read';
|
|
224
|
-
else if (typeof vault?.reportMerchantCharge !== 'function')
|
|
225
|
-
reason = 'no_reporter';
|
|
226
|
-
else {
|
|
227
|
-
let result = null;
|
|
228
|
-
try {
|
|
229
|
-
result = await vault.reportMerchantCharge(id, report);
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
result = null;
|
|
233
|
-
}
|
|
234
|
-
if (result) {
|
|
235
|
-
this.reported.add(id);
|
|
236
|
-
while (this.reported.size > REPORTS_MAX)
|
|
237
|
-
this.reported.delete(this.reported.values().next().value);
|
|
238
|
-
this.awaiting.delete(id);
|
|
239
|
-
this.onEvent?.({ type: 'merchant_charge_reported', detail: { ...detail, verdict: result.verdict, alerted: result.alerted } });
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
reason = 'report_failed';
|
|
243
|
-
}
|
|
244
|
-
// Only a confirmation still to come can change a missing one; a failed report or a
|
|
245
|
-
// missing reporter is not tried again by a later answer.
|
|
246
|
-
if (reason !== 'no_confirmation_read')
|
|
247
|
-
this.awaiting.delete(id);
|
|
248
|
-
// No reporter never changes, so it is not tried again at all.
|
|
249
|
-
retrying = reason !== 'no_reporter' && tries < this.retryDelaysMs.length;
|
|
250
|
-
this.onEvent?.({ type: 'merchant_charge_unreported', detail: { ...detail, reason, retrying } });
|
|
251
|
-
if (retrying)
|
|
252
|
-
later(() => { void this.attempt(replay, vault, tries + 1); }, this.retryDelaysMs[tries]);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
// A report never breaks the page: an attempt that threw is the last one.
|
|
256
|
-
retrying = false;
|
|
257
|
-
}
|
|
258
|
-
finally {
|
|
259
|
-
if (!retrying)
|
|
260
|
-
this.reporting.delete(id);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
/** A timer that never keeps a Node process alive on its own: a retry of the report is best effort. */
|
|
265
|
-
function later(run, ms) {
|
|
266
|
-
const timer = setTimeout(run, ms);
|
|
267
|
-
timer.unref?.();
|
|
268
|
-
}
|