@visa/cli 4.1.0-rc.24 → 4.1.0-rc.241
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 +262 -227
- package/dist/checkout-engine/adapters/generic.d.ts +69 -0
- package/dist/checkout-engine/adapters/generic.js +383 -58
- package/dist/checkout-engine/adapters/index.d.ts +4 -1
- package/dist/checkout-engine/adapters/index.js +10 -3
- package/dist/checkout-engine/adapters/shopify.d.ts +80 -0
- package/dist/checkout-engine/adapters/shopify.js +688 -0
- package/dist/checkout-engine/amount.d.ts +15 -0
- package/dist/checkout-engine/amount.js +72 -0
- package/dist/checkout-engine/browser-launch.d.ts +9 -4
- package/dist/checkout-engine/browser-launch.js +19 -4
- package/dist/checkout-engine/browserbase-browser.d.ts +24 -0
- package/dist/checkout-engine/browserbase-browser.js +186 -0
- package/dist/checkout-engine/cli-engine.d.ts +297 -4
- package/dist/checkout-engine/cli-engine.js +942 -48
- package/dist/checkout-engine/confirmed-merchants.d.ts +31 -0
- package/dist/checkout-engine/confirmed-merchants.js +165 -0
- package/dist/checkout-engine/detect.d.ts +1 -1
- package/dist/checkout-engine/detect.js +26 -0
- package/dist/checkout-engine/evidence.d.ts +4 -1
- package/dist/checkout-engine/evidence.js +51 -6
- package/dist/checkout-engine/executor.d.ts +64 -4
- package/dist/checkout-engine/executor.js +604 -145
- package/dist/checkout-engine/hosted-approval.d.ts +124 -7
- package/dist/checkout-engine/hosted-approval.js +384 -54
- package/dist/checkout-engine/index.d.ts +9 -2
- package/dist/checkout-engine/index.js +8 -1
- package/dist/checkout-engine/instrument.d.ts +7 -0
- package/dist/checkout-engine/instrument.js +4 -0
- package/dist/checkout-engine/known-merchants.d.ts +10 -0
- package/dist/checkout-engine/known-merchants.js +38 -0
- package/dist/checkout-engine/live-fill-approval.d.ts +5 -20
- package/dist/checkout-engine/live-fill-approval.js +20 -51
- package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
- package/dist/checkout-engine/mandate/card-mandate.js +226 -0
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +174 -0
- package/dist/checkout-engine/mandate/mandate-ledger.js +410 -0
- package/dist/checkout-engine/outcome.d.ts +2 -2
- package/dist/checkout-engine/outcome.js +36 -1
- package/dist/checkout-engine/owner-only-file.d.ts +9 -0
- package/dist/checkout-engine/owner-only-file.js +20 -1
- package/dist/checkout-engine/receipt-dir.d.ts +6 -0
- package/dist/checkout-engine/receipt-dir.js +8 -0
- package/dist/checkout-engine/receipt.d.ts +56 -2
- package/dist/checkout-engine/receipt.js +55 -16
- package/dist/checkout-engine/trace-handles.d.ts +8 -0
- package/dist/checkout-engine/trace-handles.js +12 -0
- package/dist/checkout-engine/types.d.ts +28 -2
- package/dist/checkout-engine/unresolved-charges.d.ts +34 -0
- package/dist/checkout-engine/unresolved-charges.js +134 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +53 -1
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +78 -10
- package/dist/checkout-engine/vgs-live-instrument.d.ts +38 -35
- package/dist/checkout-engine/vgs-live-instrument.js +51 -74
- package/dist/checkout-engine/vic-confirmation.d.ts +18 -0
- package/dist/checkout-engine/vic-confirmation.js +9 -3
- package/dist/checkout-engine/web-bot-auth.d.ts +98 -0
- package/dist/checkout-engine/web-bot-auth.js +218 -0
- package/dist/cli.js +968 -505
- package/dist/mcp-apps/ucp-checkout.html +280 -0
- package/dist/mcp-server/index.js +807 -175
- package/dist/skills/pair-visa-agent/RUNTIMES.md +122 -79
- package/dist/skills/pair-visa-agent/SKILL.md +436 -248
- package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
- package/dist/skills/visa-shopify-checkout/SKILL.md +122 -0
- package/dist/skills/visa-shopify-checkout/references/evidence-and-states.md +37 -0
- package/dist/skills/visa-ucp-shopping/SKILL.md +84 -0
- package/dist/subway-direct.mjs +1 -0
- package/install.ps1 +9 -46
- package/install.sh +5 -37
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +32 -28
- package/server.json +4 -4
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/pay-args.d.ts +0 -14
- package/dist/checkout-engine/pay-args.js +0 -44
- package/dist/checkout-engine/pay.d.ts +0 -1
- package/dist/checkout-engine/pay.js +0 -13
- package/dist/checkout-engine/repo-env.d.ts +0 -11
- package/dist/checkout-engine/repo-env.js +0 -23
- package/dist/checkout-engine/run-live-fill.d.ts +0 -1
- package/dist/checkout-engine/run-live-fill.js +0 -443
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** One confirmed charge, in the receipt's own terms. */
|
|
2
|
+
export type ConfirmedCharge = {
|
|
3
|
+
recordedAt: string;
|
|
4
|
+
amount: string;
|
|
5
|
+
currency: string;
|
|
6
|
+
/** Receipt file basename, so an operator can open the evidence log. */
|
|
7
|
+
receiptFile: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConfirmedMerchant = {
|
|
10
|
+
/** The checkout page the card went through. */
|
|
11
|
+
url: string;
|
|
12
|
+
host: string;
|
|
13
|
+
/**
|
|
14
|
+
* Curated human identity (known-merchants.ts), present only when someone has
|
|
15
|
+
* identified who is behind this checkout URL. Hosted payment links carry an
|
|
16
|
+
* opaque path on the PSP's host, so without this a row names nobody.
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
category?: string;
|
|
20
|
+
website?: string;
|
|
21
|
+
confirmedCount: number;
|
|
22
|
+
lastConfirmedAt: string;
|
|
23
|
+
charges: ConfirmedCharge[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Merchants this device has completed a real card checkout at, newest first.
|
|
27
|
+
*
|
|
28
|
+
* Never throws: a missing directory (nothing has ever been checked out here)
|
|
29
|
+
* and an unreadable one both read as an empty registry.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readConfirmedMerchants(receiptDir?: string): Promise<ConfirmedMerchant[]>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// "Where can I use my card?", the merchant registry derived from this
|
|
2
|
+
// device's checkout receipts.
|
|
3
|
+
//
|
|
4
|
+
// Receipts (receipt.ts) are the only local record that a real merchant
|
|
5
|
+
// checkout completed on this box. This module reads them back and answers one
|
|
6
|
+
// question: which checkout pages has this card actually gone through?
|
|
7
|
+
//
|
|
8
|
+
// A merchant only counts when BOTH halves agree. The engine's own outcome must
|
|
9
|
+
// be 'confirmed' (the merchant showed a definitive success) AND the VIC
|
|
10
|
+
// confirmation must have posted APPROVED (the network side of the same
|
|
11
|
+
// purchase). Either half alone is a claim, not a completion: a 'confirmed' with
|
|
12
|
+
// no posted confirmation is exactly the reconciliation case receipt.ts flags,
|
|
13
|
+
// and a posted DECLINED is a completed report of a failure.
|
|
14
|
+
//
|
|
15
|
+
// Reading is best-effort by construction. The receipts directory is an operator
|
|
16
|
+
// artifact that anything on the box can touch, so an unreadable or malformed
|
|
17
|
+
// file is skipped rather than failing the whole read. One bad file must not
|
|
18
|
+
// hide every merchant behind it.
|
|
19
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { KNOWN_MERCHANT_IDENTITIES } from './known-merchants.js';
|
|
22
|
+
import { RECEIPT_DIR } from './receipt-dir.js';
|
|
23
|
+
/**
|
|
24
|
+
* The URL a receipt's charge happened at.
|
|
25
|
+
*
|
|
26
|
+
* A compact v2 receipt carries an exact checkout URL only when it is already in
|
|
27
|
+
* the curated identity map; otherwise it carries the host. Legacy v1 receipts
|
|
28
|
+
* keep the URL in the evidence log (the executor's first 'navigation' step is
|
|
29
|
+
* the page the run opened), so recover it there and fall back to the host when
|
|
30
|
+
* that evidence is absent or shaped differently.
|
|
31
|
+
*/
|
|
32
|
+
function checkoutUrlOf(receipt) {
|
|
33
|
+
if (receipt.schema === 'checkout-agent-receipt/v2') {
|
|
34
|
+
const checkoutUrl = receipt.merchant.checkoutUrl;
|
|
35
|
+
return checkoutUrl && KNOWN_MERCHANT_IDENTITIES[checkoutUrl]
|
|
36
|
+
? checkoutUrl
|
|
37
|
+
: `https://${receipt.merchant.host}/`;
|
|
38
|
+
}
|
|
39
|
+
const navigation = receipt.evidence.steps.find((step) => step?.type === 'navigation');
|
|
40
|
+
const data = navigation?.data;
|
|
41
|
+
const url = typeof data === 'object' && data !== null ? data.url : undefined;
|
|
42
|
+
return typeof url === 'string' && url.length > 0 ? url : `https://${receipt.merchant.host}/`;
|
|
43
|
+
}
|
|
44
|
+
function isConfirmedCompletion(receipt) {
|
|
45
|
+
if (receipt.schema === 'checkout-agent-receipt/v2') {
|
|
46
|
+
return receipt.outcome === 'confirmed' && receipt.network.confirmation === 'APPROVED';
|
|
47
|
+
}
|
|
48
|
+
return (receipt.outcome === 'confirmed' &&
|
|
49
|
+
receipt.vicConfirmation?.posted === true &&
|
|
50
|
+
receipt.vicConfirmation.transactionStatus === 'APPROVED');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse one supported receipt file, or null when its fields cannot safely feed
|
|
54
|
+
* the registry. Deliberately permissive about everything the grouping does not
|
|
55
|
+
* touch: older and newer engines only have to carry the fields read below.
|
|
56
|
+
*/
|
|
57
|
+
function parseReceipt(json) {
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(json);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
66
|
+
return null;
|
|
67
|
+
const receipt = parsed;
|
|
68
|
+
if (receipt.schema === 'checkout-agent-receipt/v1') {
|
|
69
|
+
const v1 = receipt;
|
|
70
|
+
if (typeof v1.recordedAt !== 'string')
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof v1.merchant?.host !== 'string')
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof v1.transaction?.amount !== 'string')
|
|
75
|
+
return null;
|
|
76
|
+
if (typeof v1.transaction?.currency !== 'string')
|
|
77
|
+
return null;
|
|
78
|
+
if (!Array.isArray(v1.evidence?.steps))
|
|
79
|
+
return null;
|
|
80
|
+
return v1;
|
|
81
|
+
}
|
|
82
|
+
if (receipt.schema === 'checkout-agent-receipt/v2') {
|
|
83
|
+
const v2 = receipt;
|
|
84
|
+
if (typeof v2.recordedAt !== 'string')
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof v2.merchant?.host !== 'string')
|
|
87
|
+
return null;
|
|
88
|
+
if (v2.merchant.checkoutUrl !== null && typeof v2.merchant.checkoutUrl !== 'string')
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof v2.transaction?.amount !== 'string')
|
|
91
|
+
return null;
|
|
92
|
+
if (typeof v2.transaction?.currency !== 'string')
|
|
93
|
+
return null;
|
|
94
|
+
if (v2.network?.confirmation !== null &&
|
|
95
|
+
v2.network?.confirmation !== 'APPROVED' &&
|
|
96
|
+
v2.network?.confirmation !== 'DECLINED') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return v2;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Merchants this device has completed a real card checkout at, newest first.
|
|
105
|
+
*
|
|
106
|
+
* Never throws: a missing directory (nothing has ever been checked out here)
|
|
107
|
+
* and an unreadable one both read as an empty registry.
|
|
108
|
+
*/
|
|
109
|
+
export async function readConfirmedMerchants(receiptDir = RECEIPT_DIR) {
|
|
110
|
+
let names;
|
|
111
|
+
try {
|
|
112
|
+
names = await readdir(receiptDir);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const byUrl = new Map();
|
|
118
|
+
for (const name of names) {
|
|
119
|
+
if (!name.endsWith('.json'))
|
|
120
|
+
continue;
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = await readFile(join(receiptDir, name), 'utf8');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const receipt = parseReceipt(raw);
|
|
129
|
+
if (!receipt || !isConfirmedCompletion(receipt))
|
|
130
|
+
continue;
|
|
131
|
+
const url = checkoutUrlOf(receipt);
|
|
132
|
+
const charge = {
|
|
133
|
+
recordedAt: receipt.recordedAt,
|
|
134
|
+
amount: receipt.transaction.amount,
|
|
135
|
+
currency: receipt.transaction.currency,
|
|
136
|
+
receiptFile: name,
|
|
137
|
+
};
|
|
138
|
+
const existing = byUrl.get(url);
|
|
139
|
+
if (existing) {
|
|
140
|
+
existing.confirmedCount += 1;
|
|
141
|
+
existing.charges.push(charge);
|
|
142
|
+
if (charge.recordedAt > existing.lastConfirmedAt) {
|
|
143
|
+
existing.lastConfirmedAt = charge.recordedAt;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const identity = KNOWN_MERCHANT_IDENTITIES[url];
|
|
148
|
+
byUrl.set(url, {
|
|
149
|
+
url,
|
|
150
|
+
host: receipt.merchant.host,
|
|
151
|
+
// Conditional spread on purpose: an unidentified merchant carries no
|
|
152
|
+
// identity keys at all, rather than keys holding undefined.
|
|
153
|
+
...(identity ?? {}),
|
|
154
|
+
confirmedCount: 1,
|
|
155
|
+
lastConfirmedAt: charge.recordedAt,
|
|
156
|
+
charges: [charge],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const merchants = [...byUrl.values()];
|
|
160
|
+
for (const merchant of merchants) {
|
|
161
|
+
merchant.charges.sort((left, right) => (left.recordedAt < right.recordedAt ? 1 : -1));
|
|
162
|
+
}
|
|
163
|
+
merchants.sort((left, right) => (left.lastConfirmedAt < right.lastConfirmedAt ? 1 : -1));
|
|
164
|
+
return merchants;
|
|
165
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Page } from 'playwright-core';
|
|
2
2
|
export type FieldSource = 'autocomplete' | 'attr-heuristic' | 'label-text' | 'iframe-psp' | 'type-inference';
|
|
3
|
-
export type FieldRole = 'number' | 'expMonth' | 'expYear' | 'expCombined' | 'cvc' | 'name' | 'nameFirst' | 'nameLast' | 'email' | 'addressLine1' | 'addressLine2' | 'city' | 'state' | 'postalCode' | 'country';
|
|
3
|
+
export type FieldRole = 'number' | 'expMonth' | 'expYear' | 'expCombined' | 'cvc' | 'name' | 'nameFirst' | 'nameLast' | 'email' | 'phone' | 'oneTimeCode' | 'addressLine1' | 'addressLine2' | 'city' | 'state' | 'postalCode' | 'country';
|
|
4
4
|
export declare const ALL_ROLES: FieldRole[];
|
|
5
5
|
export type SelectOption = {
|
|
6
6
|
value: string;
|
|
@@ -21,6 +21,8 @@ export const ALL_ROLES = [
|
|
|
21
21
|
'nameFirst',
|
|
22
22
|
'nameLast',
|
|
23
23
|
'email',
|
|
24
|
+
'phone',
|
|
25
|
+
'oneTimeCode',
|
|
24
26
|
'addressLine1',
|
|
25
27
|
'addressLine2',
|
|
26
28
|
'city',
|
|
@@ -37,6 +39,18 @@ const expMonthRe = /(exp.?month|expmonth|exp_mm|(^|[^a-z])mm([^a-z]|$)|(^|[^a-z]
|
|
|
37
39
|
const expYearRe = /(exp.?year|expyear|exp_yy|(^|[^a-z])yy(yy)?([^a-z]|$)|(^|[^a-z])year([^a-z]|$)|jahr|annee|(^|[^a-z])ano([^a-z]|$))/;
|
|
38
40
|
const expCombinedRe = /(exp(iry|iration)?(.?date)?|mm.?\/.?yy|mm.?jj|valid.?thru|ablauf|caducidad)/;
|
|
39
41
|
const emailRe = /(e-?mail|correo|courriel)/;
|
|
42
|
+
const phoneRe = /(phone|telephone|mobile|(^|[^a-z])tel([^a-z]|$)|telefono|téléphone)/;
|
|
43
|
+
// One-time verification code (merchant email/SMS OTP). Ordered AFTER cvcRe in
|
|
44
|
+
// attrClassify so a card CVC ("security code"/"card code") still wins — this
|
|
45
|
+
// regex deliberately omits those card phrasings. It also OMITS a bare "code":
|
|
46
|
+
// `postal_code`/`zip_code`/`country_code`/`promo_code` all contain "code" and
|
|
47
|
+
// would otherwise misclassify as an OTP field and starve the address fill. A
|
|
48
|
+
// bare "code" is matched LAST (bareCodeRe), after every address role.
|
|
49
|
+
const oneTimeCodeRe = /(one[-_ ]?time|(^|[^a-z])otp([^a-z]|$)|passcode|(^|[^a-z])pin([^a-z]|$)|verif(y|ication)?.?code|auth.?code)/;
|
|
50
|
+
// Standalone verification "code" — a last-resort OTP match tried only after the
|
|
51
|
+
// specific address roles (postal/country/state/city/line) have had their say,
|
|
52
|
+
// so a compound `*_code` address/product field is never stolen by the OTP role.
|
|
53
|
+
const bareCodeRe = /(^|[^a-z])code([^a-z]|$)/;
|
|
40
54
|
const postalRe = /(zip|postal|postcode|(^|[^a-z])plz([^a-z]|$)|(^|[^a-z])cep([^a-z]|$)|codigo.?postal)/;
|
|
41
55
|
const countryRe = /(country|(^|[^a-z])land([^a-z]|$)|(^|[^a-z])pais)/;
|
|
42
56
|
const stateRe = /((^|[^a-z])state([^a-z]|$)|province|region|bundesland|provincia)/;
|
|
@@ -55,6 +69,9 @@ const AUTOCOMPLETE_MAP = {
|
|
|
55
69
|
'cc-name': 'name',
|
|
56
70
|
name: 'name',
|
|
57
71
|
email: 'email',
|
|
72
|
+
tel: 'phone',
|
|
73
|
+
'tel-national': 'phone',
|
|
74
|
+
'one-time-code': 'oneTimeCode',
|
|
58
75
|
'given-name': 'nameFirst',
|
|
59
76
|
'family-name': 'nameLast',
|
|
60
77
|
'address-line1': 'addressLine1',
|
|
@@ -151,6 +168,11 @@ function attrClassify(text, m) {
|
|
|
151
168
|
return 'expCombined';
|
|
152
169
|
if (emailRe.test(text))
|
|
153
170
|
return 'email';
|
|
171
|
+
// OTP comes after cvc (checked above) so card CVC still classifies as 'cvc'.
|
|
172
|
+
if (oneTimeCodeRe.test(text))
|
|
173
|
+
return 'oneTimeCode';
|
|
174
|
+
if (phoneRe.test(text))
|
|
175
|
+
return 'phone';
|
|
154
176
|
if (postalRe.test(text))
|
|
155
177
|
return 'postalCode';
|
|
156
178
|
if (countryRe.test(text))
|
|
@@ -163,6 +185,10 @@ function attrClassify(text, m) {
|
|
|
163
185
|
return 'addressLine2';
|
|
164
186
|
if (addr1Re.test(text))
|
|
165
187
|
return 'addressLine1';
|
|
188
|
+
// Last-resort bare "code" → OTP, only once every address role has been ruled
|
|
189
|
+
// out, so `postal_code`/`country_code`/etc. keep their own role above.
|
|
190
|
+
if (bareCodeRe.test(text))
|
|
191
|
+
return 'oneTimeCode';
|
|
166
192
|
if (nameFirstRe.test(text))
|
|
167
193
|
return 'nameFirst';
|
|
168
194
|
if (nameLastRe.test(text))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type EvidenceStepType = 'navigation' | 'dom-stable' | 'review' | 'approval' | 'adapter-selected' | 'reveal' | 'detect' | 'field-fill' | 'amount-fill' | 'credential-minted' | 'fill-complete' | 'psp-detected' | 'mandate-verdict' | 'submit' | 'challenge-hold' | 'outcome' | 'note';
|
|
1
|
+
export type EvidenceStepType = 'navigation' | 'dom-stable' | 'review' | 'approval' | 'adapter-selected' | 'reveal' | 'detect' | 'field-fill' | 'amount-fill' | 'contact-prefill' | 'credential-minted' | 'credential-skipped' | 'credential-expiry-check' | 'fill-complete' | 'psp-detected' | 'mandate-verdict' | 'submit' | 'challenge-hold' | 'outcome' | 'note';
|
|
2
2
|
export type EvidenceStep = {
|
|
3
3
|
ts: string;
|
|
4
4
|
type: EvidenceStepType;
|
|
@@ -7,6 +7,9 @@ export type EvidenceStep = {
|
|
|
7
7
|
export declare function maskPan(pan: string): string;
|
|
8
8
|
export declare function maskCvc(_cvc: string): string;
|
|
9
9
|
export declare function maskExpiry(): string;
|
|
10
|
+
export declare function maskOtp(): string;
|
|
11
|
+
export declare function hostOf(hostOrEmail: string): string;
|
|
12
|
+
export declare function verificationLinkAllowed(link: string, messageFrom: string): boolean;
|
|
10
13
|
export declare function redactContact(role: string, value: string): string;
|
|
11
14
|
export declare class EvidenceLog {
|
|
12
15
|
private steps;
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// the mandate verdict, the submit, and the final outcome, plus a compact
|
|
4
4
|
// accessibility snapshot of the end state.
|
|
5
5
|
//
|
|
6
|
-
// Money-adjacent hygiene rule: the full PAN, the cvc,
|
|
7
|
-
// NEVER enter the log. PANs are masked to last-4;
|
|
8
|
-
// entirely. maskPan/maskCvc/maskExpiry
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// Money-adjacent hygiene rule: the full PAN, the cvc, the card expiry, and any
|
|
7
|
+
// one-time verification code NEVER enter the log. PANs are masked to last-4;
|
|
8
|
+
// cvc, expiry, and OTP codes are redacted entirely. maskPan/maskCvc/maskExpiry/
|
|
9
|
+
// maskOtp are the only sanctioned way to put such a value anywhere near a log
|
|
10
|
+
// line. (receipt.ts redactPanLikeDigits only catches 12-19-digit PANs, so a
|
|
11
|
+
// 4-8-digit OTP would slip straight through — maskOtp at the fill site is the
|
|
12
|
+
// load-bearing guarantee it never reaches the evidence log.) Contact PII
|
|
13
|
+
// (cardholder name, email, address, ...) is redacted via redactContact: the
|
|
14
|
+
// log records THAT a field was filled, never the personal value.
|
|
12
15
|
// Mask a PAN to its last four digits. Non-digits are stripped for the count.
|
|
13
16
|
export function maskPan(pan) {
|
|
14
17
|
const digits = (pan || '').replace(/\D/g, '');
|
|
@@ -25,6 +28,48 @@ export function maskCvc(_cvc) {
|
|
|
25
28
|
export function maskExpiry() {
|
|
26
29
|
return 'redacted';
|
|
27
30
|
}
|
|
31
|
+
// A merchant one-time verification code is single-use secret material — it
|
|
32
|
+
// never enters the log, in any format. Used at the oneTimeCode fill site so
|
|
33
|
+
// the code is masked exactly as cvc/PAN are (see the header note above).
|
|
34
|
+
export function maskOtp() {
|
|
35
|
+
return 'redacted';
|
|
36
|
+
}
|
|
37
|
+
// The registrable domain (approx eTLD+1) of an email address or hostname, for
|
|
38
|
+
// the caller-side from-domain guard on verification links: before navigating a
|
|
39
|
+
// link parsed from an untrusted OTP email, require the link host to match the
|
|
40
|
+
// message sender's domain. extract.ts guarantees mechanical extraction, NOT
|
|
41
|
+
// navigation safety — this guard is the anti-injection backstop.
|
|
42
|
+
export function hostOf(hostOrEmail) {
|
|
43
|
+
return (hostOrEmail.split('@').pop() ?? '').trim().toLowerCase().replace(/\.$/, '');
|
|
44
|
+
}
|
|
45
|
+
// Strict bidirectional host-suffix match (exact / child / parent). Deliberately
|
|
46
|
+
// NOT approximate eTLD+1 — that collapses shared-tenant hosts (`x.myshopify.com`,
|
|
47
|
+
// `x.co.uk`) to a common suffix and lets a sibling tenant match; siblings never
|
|
48
|
+
// match here. Fails SAFE: an unrelated-but-legitimate sender is skipped, not
|
|
49
|
+
// trusted.
|
|
50
|
+
function hostsRelated(a, b) {
|
|
51
|
+
const x = hostOf(a);
|
|
52
|
+
const y = hostOf(b);
|
|
53
|
+
if (!x || !y)
|
|
54
|
+
return false;
|
|
55
|
+
return x === y || x.endsWith('.' + y) || y.endsWith('.' + x);
|
|
56
|
+
}
|
|
57
|
+
// Whether a verification LINK is safe to navigate: the sender must NOT be the
|
|
58
|
+
// unauthenticated inbound variant (AgentMail marks spoofable inbound with an
|
|
59
|
+
// `.unauthenticated` sub-label), and the link host must belong to the sender's
|
|
60
|
+
// domain. Returns false to SKIP a link that fails either check.
|
|
61
|
+
export function verificationLinkAllowed(link, messageFrom) {
|
|
62
|
+
if (/\.unauthenticated\b/i.test(messageFrom))
|
|
63
|
+
return false;
|
|
64
|
+
let linkHost;
|
|
65
|
+
try {
|
|
66
|
+
linkHost = new URL(link).hostname;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return hostsRelated(linkHost, messageFrom);
|
|
72
|
+
}
|
|
28
73
|
// Contact values (cardholder name, email, address, ...) are PII and never
|
|
29
74
|
// enter the log raw either. Email keeps its domain for debuggability;
|
|
30
75
|
// everything else becomes a bare presence marker.
|
|
@@ -2,11 +2,41 @@ import type { Browser, BrowserContext, Page } from 'playwright-core';
|
|
|
2
2
|
import { type FieldMap } from './detect.js';
|
|
3
3
|
import { type Mandate } from './mandate.js';
|
|
4
4
|
import type { Instrument } from './instrument.js';
|
|
5
|
-
import type { Contact } from './types.js';
|
|
5
|
+
import type { Contact, OtpResolver } from './types.js';
|
|
6
6
|
import { EvidenceLog } from './evidence.js';
|
|
7
7
|
import { type ObservedOutcome } from './outcome.js';
|
|
8
|
+
import { type WebBotAuthConfig } from './web-bot-auth.js';
|
|
9
|
+
export { minorFromDecimal, pageCurrency } from './amount.js';
|
|
8
10
|
export type CheckoutMode = 'dry-run' | 'submit';
|
|
9
|
-
export type
|
|
11
|
+
export type CheckoutRoute = 'guest-card';
|
|
12
|
+
export type CheckoutOutcome = 'reviewed-dry-run'
|
|
13
|
+
/** Historical receipt value from the credential-disclosing dry-run. */
|
|
14
|
+
| 'filled-dry-run' | 'partial-fill' | 'adapter-required' | 'confirmed' | 'declined' | 'action-required' | 'cancelled' | 'blocked-by-mandate'
|
|
15
|
+
/**
|
|
16
|
+
* KNOWN-NOT-CHARGED. Every `failed` path ends before the pay control was
|
|
17
|
+
* clicked (or before a credential existed at all), so a caller may retry it
|
|
18
|
+
* without risking a second charge.
|
|
19
|
+
*/
|
|
20
|
+
| 'failed'
|
|
21
|
+
/**
|
|
22
|
+
* SUBMITTED, OUTCOME UNKNOWN — the pay control WAS clicked and neither a
|
|
23
|
+
* confirmation nor a decline was observed before the deadline. The charge may
|
|
24
|
+
* have captured. NEVER retry this automatically.
|
|
25
|
+
*
|
|
26
|
+
* This case used to be reported as `failed`, and the difference is not
|
|
27
|
+
* academic: on 2026-08-17 a whop.com checkout returned `failed` here, the
|
|
28
|
+
* caller read that as "nothing happened" and re-ran the purchase, and the
|
|
29
|
+
* second run drew a second $5 against the owner's mandate. `outcome.ts`
|
|
30
|
+
* already treats an unrecognized post-submit page as the SAFE direction
|
|
31
|
+
* ('unknown' → operator verification); collapsing it into `failed` at the
|
|
32
|
+
* boundary is what threw that safety away. The distinction has to survive all
|
|
33
|
+
* the way to the value consumers branch on.
|
|
34
|
+
*/
|
|
35
|
+
| 'unverified';
|
|
36
|
+
/** Stable machine-readable cause for an expected terminal checkout result. */
|
|
37
|
+
export type CheckoutFailureCode = 'card-number-field-unavailable' | 'human-action-required' | 'mandate-blocked'
|
|
38
|
+
/** A detected, visible field we held a value for refused every fill attempt. */
|
|
39
|
+
| 'required-field-unfillable';
|
|
10
40
|
export type CredentialLifecycle = 'not-requested' | 'minted-not-exposed' | 'partially-exposed' | 'fully-filled';
|
|
11
41
|
export type CredentialTiming = {
|
|
12
42
|
approvedAt?: string;
|
|
@@ -22,26 +52,43 @@ export type CheckoutResult = {
|
|
|
22
52
|
requiresAdapter: string[];
|
|
23
53
|
credentialLifecycle: CredentialLifecycle;
|
|
24
54
|
credentialTiming: CredentialTiming;
|
|
55
|
+
failureCode?: CheckoutFailureCode;
|
|
25
56
|
detail?: string;
|
|
26
57
|
};
|
|
27
58
|
export type PrepareCheckoutOptions = {
|
|
28
59
|
url: string;
|
|
60
|
+
checkoutRoute: CheckoutRoute;
|
|
29
61
|
mandate: Mandate;
|
|
30
62
|
browser: Browser;
|
|
63
|
+
contact?: Contact;
|
|
31
64
|
amountMinor?: number;
|
|
32
65
|
currency?: string;
|
|
33
66
|
debugShotsDir?: string;
|
|
67
|
+
webBotAuth?: WebBotAuthConfig | null;
|
|
68
|
+
/**
|
|
69
|
+
* Process-internal UCP provenance. Public callers may relay only handoffId;
|
|
70
|
+
* this origin set is recovered by the CLI from its single-use registry.
|
|
71
|
+
*/
|
|
72
|
+
trustedMerchantIdentity?: Readonly<{
|
|
73
|
+
handoffId: string;
|
|
74
|
+
checkoutId: string;
|
|
75
|
+
allowedOrigins: readonly string[];
|
|
76
|
+
expiresAt: string;
|
|
77
|
+
}>;
|
|
34
78
|
};
|
|
35
79
|
export type RunCheckoutOptions = PrepareCheckoutOptions & {
|
|
36
80
|
instrument: Instrument;
|
|
37
81
|
contact: Contact;
|
|
38
82
|
mode: CheckoutMode;
|
|
39
83
|
outcomeDeadlineMs?: number;
|
|
84
|
+
resolveEmailOtp?: OtpResolver;
|
|
40
85
|
};
|
|
41
86
|
export type CheckoutReview = {
|
|
42
87
|
id: string;
|
|
43
88
|
url: string;
|
|
44
89
|
merchantHost: string;
|
|
90
|
+
/** Exact origin frozen for a trusted UCP checkout; omitted for generic flows. */
|
|
91
|
+
merchantOrigin?: string;
|
|
45
92
|
amountMinor: number;
|
|
46
93
|
currency: string;
|
|
47
94
|
mandateMaxAmountMinor: number;
|
|
@@ -49,6 +96,7 @@ export type CheckoutReview = {
|
|
|
49
96
|
submitTarget: string | null;
|
|
50
97
|
submitTargetFingerprint: Readonly<CheckoutSubmitTargetFingerprint> | null;
|
|
51
98
|
detectedRoles: readonly string[];
|
|
99
|
+
checkoutRoute: CheckoutRoute;
|
|
52
100
|
};
|
|
53
101
|
export type CheckoutSubmitTargetFingerprint = {
|
|
54
102
|
kind: 'submit-control' | 'text-button';
|
|
@@ -66,6 +114,7 @@ export type PreparedCheckout = {
|
|
|
66
114
|
readonly fields: Readonly<FieldMap>;
|
|
67
115
|
readonly evidence: EvidenceLog;
|
|
68
116
|
readonly requiresAdapter: readonly string[];
|
|
117
|
+
readonly checkoutRoute: CheckoutRoute;
|
|
69
118
|
};
|
|
70
119
|
export type PrepareCheckoutResult = {
|
|
71
120
|
status: 'ready';
|
|
@@ -86,9 +135,19 @@ export type SubmitApprovedCheckoutOptions = {
|
|
|
86
135
|
outcomeDeadlineMs?: number;
|
|
87
136
|
challengeHoldMs?: number;
|
|
88
137
|
onChallengeHold?: (signal: string | null) => void;
|
|
138
|
+
resolveEmailOtp?: OtpResolver;
|
|
139
|
+
};
|
|
140
|
+
export type SubmitCandidateMeta = {
|
|
141
|
+
label: string;
|
|
142
|
+
ariaHidden: boolean;
|
|
143
|
+
tabIndex: number | null;
|
|
144
|
+
visible: boolean;
|
|
145
|
+
area: number;
|
|
89
146
|
};
|
|
90
|
-
export declare function
|
|
91
|
-
|
|
147
|
+
export declare function preferredSubmitIndex(cands: readonly SubmitCandidateMeta[], opts?: {
|
|
148
|
+
allowDeferred?: boolean;
|
|
149
|
+
}): number;
|
|
150
|
+
export declare function snapshotOrigin(rawUrl: string): string;
|
|
92
151
|
export type PreparedCheckoutSession = {
|
|
93
152
|
checkout: PreparedCheckout;
|
|
94
153
|
context: BrowserContext;
|
|
@@ -131,6 +190,7 @@ export declare class InMemoryPreparedCheckoutStore implements PreparedCheckoutSe
|
|
|
131
190
|
private scheduleReaper;
|
|
132
191
|
private closeState;
|
|
133
192
|
}
|
|
193
|
+
export declare function trustedMerchantOriginRefusal(options: PrepareCheckoutOptions, pageUrl: string, expectedOrigin?: string): string | null;
|
|
134
194
|
export declare function reconcileHeldOutcome(original: ObservedOutcome, held: ObservedOutcome): ObservedOutcome;
|
|
135
195
|
export declare function debugShotMaskPlan(fields: FieldMap): {
|
|
136
196
|
skipReason: string | null;
|