@visa/cli 4.1.0-rc.24 → 4.1.0-rc.3
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/dist/cli.js +295 -364
- package/dist/mcp-server/index.js +148 -148
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +3 -5
- package/server.json +2 -2
- package/dist/checkout-engine/adapters/generic.d.ts +0 -19
- package/dist/checkout-engine/adapters/generic.js +0 -201
- package/dist/checkout-engine/adapters/index.d.ts +0 -7
- package/dist/checkout-engine/adapters/index.js +0 -17
- package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
- package/dist/checkout-engine/adapters/stripe-like.js +0 -21
- package/dist/checkout-engine/browser-launch.d.ts +0 -46
- package/dist/checkout-engine/browser-launch.js +0 -81
- package/dist/checkout-engine/ceremony.d.ts +0 -64
- package/dist/checkout-engine/ceremony.js +0 -261
- package/dist/checkout-engine/cli-engine.d.ts +0 -60
- package/dist/checkout-engine/cli-engine.js +0 -242
- package/dist/checkout-engine/detect.d.ts +0 -61
- package/dist/checkout-engine/detect.js +0 -372
- package/dist/checkout-engine/evidence.d.ts +0 -22
- package/dist/checkout-engine/evidence.js +0 -59
- package/dist/checkout-engine/executor.d.ts +0 -172
- package/dist/checkout-engine/executor.js +0 -1233
- package/dist/checkout-engine/hosted-approval.d.ts +0 -78
- package/dist/checkout-engine/hosted-approval.js +0 -171
- package/dist/checkout-engine/index.d.ts +0 -3
- package/dist/checkout-engine/index.js +0 -5
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/instrument.d.ts +0 -54
- package/dist/checkout-engine/instrument.js +0 -83
- package/dist/checkout-engine/live-fill-approval.d.ts +0 -52
- package/dist/checkout-engine/live-fill-approval.js +0 -107
- package/dist/checkout-engine/mandate.d.ts +0 -25
- package/dist/checkout-engine/mandate.js +0 -100
- package/dist/checkout-engine/outcome.d.ts +0 -30
- package/dist/checkout-engine/outcome.js +0 -190
- package/dist/checkout-engine/owner-only-file.d.ts +0 -10
- package/dist/checkout-engine/owner-only-file.js +0 -22
- package/dist/checkout-engine/package.json +0 -3
- 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/receipt.d.ts +0 -81
- package/dist/checkout-engine/receipt.js +0 -109
- 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/types.d.ts +0 -26
- package/dist/checkout-engine/types.js +0 -2
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -49
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -150
- package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -141
- package/dist/checkout-engine/vgs-live-instrument.js +0 -252
- package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
- package/dist/checkout-engine/vic-confirmation.js +0 -39
- package/dist/skills/pair-visa-agent/RUNTIMES.md +0 -79
- package/dist/skills/pair-visa-agent/SKILL.md +0 -329
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +0 -48
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
// Layered field detection. This is the success-rate core of the spike: given a
|
|
2
|
-
// live page, work out which element is the card number, which is the expiry,
|
|
3
|
-
// the cvc, the cardholder name, and the contact/shipping fields, with a
|
|
4
|
-
// confidence and a provenance for each. Higher layers (explicit autocomplete
|
|
5
|
-
// attributes) beat lower ones (raw type inference). Every candidate is kept so
|
|
6
|
-
// the evidence log can show WHY a field was chosen.
|
|
7
|
-
//
|
|
8
|
-
// Layers, highest confidence first:
|
|
9
|
-
// 1.0 autocomplete attributes (cc-number, cc-exp, cc-csc, ...)
|
|
10
|
-
// 0.7 attribute regex bank over name/id/placeholder/aria-label + <select> shape
|
|
11
|
-
// 0.6 associated <label> text
|
|
12
|
-
// 0.5 PSP iframe fingerprints (Stripe/Braintree/Adyen) -> detect + report
|
|
13
|
-
// 0.4 type inference (numeric inputmode/pattern + maxlength)
|
|
14
|
-
export const ALL_ROLES = [
|
|
15
|
-
'number',
|
|
16
|
-
'expMonth',
|
|
17
|
-
'expYear',
|
|
18
|
-
'expCombined',
|
|
19
|
-
'cvc',
|
|
20
|
-
'name',
|
|
21
|
-
'nameFirst',
|
|
22
|
-
'nameLast',
|
|
23
|
-
'email',
|
|
24
|
-
'addressLine1',
|
|
25
|
-
'addressLine2',
|
|
26
|
-
'city',
|
|
27
|
-
'state',
|
|
28
|
-
'postalCode',
|
|
29
|
-
'country',
|
|
30
|
-
];
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Regex bank (layer 2 / 3)
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
const numberRe = /(card.?number|cardnumber|cardnum|card_num|ccnum|cc.?number|(^|[^a-z])pan([^a-z]|$)|kartennummer|numero.?(de.?)?(carte|tarjeta))/;
|
|
35
|
-
const cvcRe = /(cvv|cvc|csc|cvn|security.?code|card.?code|card.?verif|pruef|prüf)/;
|
|
36
|
-
const expMonthRe = /(exp.?month|expmonth|exp_mm|(^|[^a-z])mm([^a-z]|$)|(^|[^a-z])month([^a-z]|$)|monat|mois|(^|[^a-z])mes([^a-z]|$))/;
|
|
37
|
-
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
|
-
const expCombinedRe = /(exp(iry|iration)?(.?date)?|mm.?\/.?yy|mm.?jj|valid.?thru|ablauf|caducidad)/;
|
|
39
|
-
const emailRe = /(e-?mail|correo|courriel)/;
|
|
40
|
-
const postalRe = /(zip|postal|postcode|(^|[^a-z])plz([^a-z]|$)|(^|[^a-z])cep([^a-z]|$)|codigo.?postal)/;
|
|
41
|
-
const countryRe = /(country|(^|[^a-z])land([^a-z]|$)|(^|[^a-z])pais)/;
|
|
42
|
-
const stateRe = /((^|[^a-z])state([^a-z]|$)|province|region|bundesland|provincia)/;
|
|
43
|
-
const cityRe = /((^|[^a-z])city([^a-z]|$)|(^|[^a-z])town([^a-z]|$)|(^|[^a-z])ort([^a-z]|$)|ciudad|ville)/;
|
|
44
|
-
const addr2Re = /(address.?2|addr.?2|line.?2|(^|[^a-z])apt|apartment|suite|(^|[^a-z])unit|zusatz)/;
|
|
45
|
-
const addr1Re = /(address|street|(^|[^a-z])addr|strasse|stra.?e|adresse|direccion|(^|[^a-z])rue)/;
|
|
46
|
-
const nameFirstRe = /(first.?name|(^|[^a-z])fname|given.?name|vorname|nombre|pr.?nom)/;
|
|
47
|
-
const nameLastRe = /(last.?name|(^|[^a-z])lname|surname|family.?name|nachname|apellido)/;
|
|
48
|
-
const nameRe = /(card.?holder|name.?on.?card|cardname|ccname|karteninhaber|titular|holder|(^|[^a-z])name([^a-z]|$)|(^|[^a-z])nom([^a-z]|$))/;
|
|
49
|
-
const AUTOCOMPLETE_MAP = {
|
|
50
|
-
'cc-number': 'number',
|
|
51
|
-
'cc-exp': 'expCombined',
|
|
52
|
-
'cc-exp-month': 'expMonth',
|
|
53
|
-
'cc-exp-year': 'expYear',
|
|
54
|
-
'cc-csc': 'cvc',
|
|
55
|
-
'cc-name': 'name',
|
|
56
|
-
name: 'name',
|
|
57
|
-
email: 'email',
|
|
58
|
-
'given-name': 'nameFirst',
|
|
59
|
-
'family-name': 'nameLast',
|
|
60
|
-
'address-line1': 'addressLine1',
|
|
61
|
-
'address-line2': 'addressLine2',
|
|
62
|
-
'address-level2': 'city',
|
|
63
|
-
'address-level1': 'state',
|
|
64
|
-
'postal-code': 'postalCode',
|
|
65
|
-
country: 'country',
|
|
66
|
-
'country-name': 'country',
|
|
67
|
-
};
|
|
68
|
-
// Real PSP iframe fingerprints. A match on any of these hosts is a real,
|
|
69
|
-
// cross-origin PSP surface: for M0 we DETECT and report it (requiresAdapter)
|
|
70
|
-
// but do not attempt to fill it. Same-origin test iframes never match these.
|
|
71
|
-
const PSP_FINGERPRINTS = [
|
|
72
|
-
{ name: 'stripe', re: /js\.stripe\.com|m\.stripe\.network/ },
|
|
73
|
-
{ name: 'braintree', re: /braintree(gateway|-api)?\.com|assets\.braintreegateway\.com/ },
|
|
74
|
-
{ name: 'adyen', re: /checkoutshopper|adyen\.com/ },
|
|
75
|
-
];
|
|
76
|
-
function fingerprintPsp(src) {
|
|
77
|
-
for (const f of PSP_FINGERPRINTS)
|
|
78
|
-
if (f.re.test(src))
|
|
79
|
-
return f.name;
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// Classifier (pure — unit-testable without a browser)
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
function isMonthOptions(options) {
|
|
86
|
-
const vals = options.map((o) => o.value).filter((v) => v !== '');
|
|
87
|
-
const texts = options.map((o) => o.text.toLowerCase()).join(' ');
|
|
88
|
-
if (/jan|feb|mar|month/.test(texts) && vals.length >= 11)
|
|
89
|
-
return true;
|
|
90
|
-
if (vals.length >= 12 && vals.length <= 13) {
|
|
91
|
-
const nums = vals.filter((v) => /^\d{1,2}$/.test(v)).map((v) => Number(v));
|
|
92
|
-
if (nums.length >= 12 && nums.every((n) => n >= 1 && n <= 12))
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
function isYearOptions(options) {
|
|
98
|
-
const vals = options.map((o) => o.value).filter((v) => v !== '');
|
|
99
|
-
if (vals.length < 4 || vals.length > 30)
|
|
100
|
-
return false;
|
|
101
|
-
const four = vals.filter((v) => /^\d{4}$/.test(v)).map(Number);
|
|
102
|
-
if (four.length >= 4 && four.some((y) => y >= 2020 && y <= 2100))
|
|
103
|
-
return true;
|
|
104
|
-
const two = vals.filter((v) => /^\d{2}$/.test(v)).map(Number);
|
|
105
|
-
if (two.length === vals.length && two.some((n) => n > 12))
|
|
106
|
-
return true;
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
function looksLikeCountry(options) {
|
|
110
|
-
const texts = options.map((o) => o.text.toLowerCase()).join(' ');
|
|
111
|
-
if (/united states|canada|united kingdom|australia|germany/.test(texts))
|
|
112
|
-
return true;
|
|
113
|
-
const codes = options.map((o) => o.value).filter((v) => /^[A-Za-z]{2}$/.test(v));
|
|
114
|
-
return codes.length >= 5;
|
|
115
|
-
}
|
|
116
|
-
function attrClassify(text, m) {
|
|
117
|
-
if (m.tag === 'select') {
|
|
118
|
-
if (expMonthRe.test(text))
|
|
119
|
-
return 'expMonth';
|
|
120
|
-
if (expYearRe.test(text))
|
|
121
|
-
return 'expYear';
|
|
122
|
-
if (countryRe.test(text))
|
|
123
|
-
return 'country';
|
|
124
|
-
if (stateRe.test(text))
|
|
125
|
-
return 'state';
|
|
126
|
-
if (isMonthOptions(m.options))
|
|
127
|
-
return 'expMonth';
|
|
128
|
-
if (isYearOptions(m.options))
|
|
129
|
-
return 'expYear';
|
|
130
|
-
if (looksLikeCountry(m.options))
|
|
131
|
-
return 'country';
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
if (numberRe.test(text))
|
|
135
|
-
return 'number';
|
|
136
|
-
if (cvcRe.test(text))
|
|
137
|
-
return 'cvc';
|
|
138
|
-
// Expiry: a combined MM/YY field must not be mistaken for a month-only one.
|
|
139
|
-
// Explicit combined markers, or the presence of BOTH month and year tokens,
|
|
140
|
-
// win over a single-part match.
|
|
141
|
-
const explicitCombined = /(mm\s*[/\-]?\s*yy|mm.?jj|valid.?thru|ablauf|caducidad)/.test(text);
|
|
142
|
-
const hasMonth = expMonthRe.test(text);
|
|
143
|
-
const hasYear = expYearRe.test(text);
|
|
144
|
-
if (explicitCombined || (hasMonth && hasYear))
|
|
145
|
-
return 'expCombined';
|
|
146
|
-
if (hasMonth && !hasYear)
|
|
147
|
-
return 'expMonth';
|
|
148
|
-
if (hasYear && !hasMonth)
|
|
149
|
-
return 'expYear';
|
|
150
|
-
if (expCombinedRe.test(text))
|
|
151
|
-
return 'expCombined';
|
|
152
|
-
if (emailRe.test(text))
|
|
153
|
-
return 'email';
|
|
154
|
-
if (postalRe.test(text))
|
|
155
|
-
return 'postalCode';
|
|
156
|
-
if (countryRe.test(text))
|
|
157
|
-
return 'country';
|
|
158
|
-
if (stateRe.test(text))
|
|
159
|
-
return 'state';
|
|
160
|
-
if (cityRe.test(text))
|
|
161
|
-
return 'city';
|
|
162
|
-
if (addr2Re.test(text))
|
|
163
|
-
return 'addressLine2';
|
|
164
|
-
if (addr1Re.test(text))
|
|
165
|
-
return 'addressLine1';
|
|
166
|
-
if (nameFirstRe.test(text))
|
|
167
|
-
return 'nameFirst';
|
|
168
|
-
if (nameLastRe.test(text))
|
|
169
|
-
return 'nameLast';
|
|
170
|
-
if (nameRe.test(text))
|
|
171
|
-
return 'name';
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
function fromAutocomplete(ac) {
|
|
175
|
-
if (!ac)
|
|
176
|
-
return null;
|
|
177
|
-
for (const tok of ac.split(/\s+/)) {
|
|
178
|
-
const role = AUTOCOMPLETE_MAP[tok];
|
|
179
|
-
if (role)
|
|
180
|
-
return { role, confidence: 1.0, source: 'autocomplete' };
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
function typeInfer(m) {
|
|
185
|
-
if (m.tag !== 'input')
|
|
186
|
-
return null;
|
|
187
|
-
const ml = Number.parseInt(m.maxlength || '0', 10);
|
|
188
|
-
const numeric = m.inputmode === 'numeric' ||
|
|
189
|
-
/\\d|\[0-9\]/.test(m.pattern) ||
|
|
190
|
-
m.type === 'tel' ||
|
|
191
|
-
m.type === 'number';
|
|
192
|
-
if (!numeric)
|
|
193
|
-
return null;
|
|
194
|
-
if (ml >= 15 && ml <= 19)
|
|
195
|
-
return 'number';
|
|
196
|
-
if (ml >= 3 && ml <= 4)
|
|
197
|
-
return 'cvc';
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
// Returns every layer match for an element. Exported for unit tests.
|
|
201
|
-
export function classifyCandidate(m) {
|
|
202
|
-
const ac = fromAutocomplete(m.autocomplete);
|
|
203
|
-
if (ac)
|
|
204
|
-
return [ac];
|
|
205
|
-
const out = [];
|
|
206
|
-
const text = [m.name, m.id, m.placeholder, m.ariaLabel].join(' ').toLowerCase();
|
|
207
|
-
const attrRole = attrClassify(text, m);
|
|
208
|
-
if (attrRole)
|
|
209
|
-
out.push({ role: attrRole, confidence: 0.7, source: 'attr-heuristic' });
|
|
210
|
-
const labelRole = attrClassify(m.labelText, m);
|
|
211
|
-
if (labelRole)
|
|
212
|
-
out.push({ role: labelRole, confidence: 0.6, source: 'label-text' });
|
|
213
|
-
const inf = typeInfer(m);
|
|
214
|
-
if (inf)
|
|
215
|
-
out.push({ role: inf, confidence: 0.4, source: 'type-inference' });
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
function bestMatch(matches) {
|
|
219
|
-
let best = null;
|
|
220
|
-
for (const mm of matches)
|
|
221
|
-
if (!best || mm.confidence > best.confidence)
|
|
222
|
-
best = mm;
|
|
223
|
-
return best;
|
|
224
|
-
}
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// DOM extraction (one evaluate pass per frame; stamps a stable data-ca-id)
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
async function extractCandidateMeta(root) {
|
|
229
|
-
return root.evaluate(() => {
|
|
230
|
-
const SKIP = new Set([
|
|
231
|
-
'hidden',
|
|
232
|
-
'submit',
|
|
233
|
-
'button',
|
|
234
|
-
'reset',
|
|
235
|
-
'checkbox',
|
|
236
|
-
'radio',
|
|
237
|
-
'image',
|
|
238
|
-
'file',
|
|
239
|
-
]);
|
|
240
|
-
const els = Array.from(document.querySelectorAll('input, select, textarea'));
|
|
241
|
-
const out = [];
|
|
242
|
-
let i = 0;
|
|
243
|
-
for (const el of els) {
|
|
244
|
-
const tag = el.tagName.toLowerCase();
|
|
245
|
-
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
246
|
-
if (tag === 'input' && SKIP.has(type))
|
|
247
|
-
continue;
|
|
248
|
-
const idx = i++;
|
|
249
|
-
el.setAttribute('data-ca-id', String(idx));
|
|
250
|
-
let labelText = '';
|
|
251
|
-
const anyEl = el;
|
|
252
|
-
if (anyEl.labels)
|
|
253
|
-
labelText += Array.from(anyEl.labels)
|
|
254
|
-
.map((l) => l.textContent || '')
|
|
255
|
-
.join(' ');
|
|
256
|
-
const alb = el.getAttribute('aria-labelledby');
|
|
257
|
-
if (alb) {
|
|
258
|
-
labelText +=
|
|
259
|
-
' ' +
|
|
260
|
-
alb
|
|
261
|
-
.split(/\s+/)
|
|
262
|
-
.map((id) => {
|
|
263
|
-
const n = document.getElementById(id);
|
|
264
|
-
return n ? n.textContent || '' : '';
|
|
265
|
-
})
|
|
266
|
-
.join(' ');
|
|
267
|
-
}
|
|
268
|
-
const wrap = el.closest('label');
|
|
269
|
-
if (wrap)
|
|
270
|
-
labelText += ' ' + (wrap.textContent || '');
|
|
271
|
-
const style = getComputedStyle(el);
|
|
272
|
-
const visible = el.offsetParent !== null &&
|
|
273
|
-
style.visibility !== 'hidden' &&
|
|
274
|
-
style.display !== 'none';
|
|
275
|
-
const options = tag === 'select'
|
|
276
|
-
? Array.from(el.options).map((o) => ({
|
|
277
|
-
value: o.value,
|
|
278
|
-
text: (o.textContent || '').trim(),
|
|
279
|
-
}))
|
|
280
|
-
: [];
|
|
281
|
-
out.push({
|
|
282
|
-
idx,
|
|
283
|
-
tag,
|
|
284
|
-
type,
|
|
285
|
-
name: el.getAttribute('name') || '',
|
|
286
|
-
id: el.id || '',
|
|
287
|
-
placeholder: el.getAttribute('placeholder') || '',
|
|
288
|
-
ariaLabel: el.getAttribute('aria-label') || '',
|
|
289
|
-
autocomplete: (el.getAttribute('autocomplete') || '').toLowerCase(),
|
|
290
|
-
maxlength: el.getAttribute('maxlength') || '',
|
|
291
|
-
inputmode: (el.getAttribute('inputmode') || '').toLowerCase(),
|
|
292
|
-
pattern: el.getAttribute('pattern') || '',
|
|
293
|
-
labelText: labelText.trim().toLowerCase(),
|
|
294
|
-
options,
|
|
295
|
-
visible,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
return out;
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
function toCandidate(m, frame) {
|
|
302
|
-
const matches = classifyCandidate(m);
|
|
303
|
-
const best = bestMatch(matches);
|
|
304
|
-
return {
|
|
305
|
-
role: best?.role ?? null,
|
|
306
|
-
confidence: best?.confidence ?? 0,
|
|
307
|
-
source: best?.source ?? null,
|
|
308
|
-
matches,
|
|
309
|
-
frame,
|
|
310
|
-
meta: m,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
function buildFieldMap(candidates) {
|
|
314
|
-
const map = {};
|
|
315
|
-
for (const c of candidates) {
|
|
316
|
-
if (!c.role || !c.source)
|
|
317
|
-
continue;
|
|
318
|
-
const existing = map[c.role];
|
|
319
|
-
const better = !existing ||
|
|
320
|
-
c.confidence > existing.confidence ||
|
|
321
|
-
(c.confidence === existing.confidence && c.meta.visible && !existing.visible);
|
|
322
|
-
if (!better)
|
|
323
|
-
continue;
|
|
324
|
-
map[c.role] = {
|
|
325
|
-
locator: `[data-ca-id="${c.meta.idx}"]`,
|
|
326
|
-
confidence: c.confidence,
|
|
327
|
-
// The true provenance is kept even for in-frame candidates — the frame
|
|
328
|
-
// is carried separately in `frame`. 'iframe-psp' means a PSP
|
|
329
|
-
// fingerprint match, never "this field happened to be in a frame".
|
|
330
|
-
source: c.source,
|
|
331
|
-
frame: c.frame,
|
|
332
|
-
tag: c.meta.tag,
|
|
333
|
-
inputType: c.meta.type,
|
|
334
|
-
visible: c.meta.visible,
|
|
335
|
-
options: c.meta.options.length ? c.meta.options : undefined,
|
|
336
|
-
maxlength: c.meta.maxlength ? Number.parseInt(c.meta.maxlength, 10) : undefined,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
return map;
|
|
340
|
-
}
|
|
341
|
-
export async function detectFields(page) {
|
|
342
|
-
const candidates = [];
|
|
343
|
-
const psps = [];
|
|
344
|
-
const mainMeta = await extractCandidateMeta(page);
|
|
345
|
-
for (const m of mainMeta)
|
|
346
|
-
candidates.push(toCandidate(m, undefined));
|
|
347
|
-
const frameEls = await page.$$('iframe');
|
|
348
|
-
for (let k = 0; k < frameEls.length; k++) {
|
|
349
|
-
const el = frameEls[k];
|
|
350
|
-
const src = (await el.getAttribute('src')) || '';
|
|
351
|
-
const frameSelector = `iframe[data-ca-frame="${k}"]`;
|
|
352
|
-
await el.evaluate((node, idx) => node.setAttribute('data-ca-frame', String(idx)), k);
|
|
353
|
-
const psp = fingerprintPsp(src);
|
|
354
|
-
if (psp) {
|
|
355
|
-
// Real, cross-origin PSP surface: detect + report, do not fill in M0.
|
|
356
|
-
psps.push({ psp, requiresAdapter: psp, frameSelector });
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
const frame = await el.contentFrame();
|
|
360
|
-
if (!frame)
|
|
361
|
-
continue;
|
|
362
|
-
try {
|
|
363
|
-
const meta = await extractCandidateMeta(frame);
|
|
364
|
-
for (const m of meta)
|
|
365
|
-
candidates.push(toCandidate(m, frameSelector));
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
// Frame not accessible; skip.
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { fields: buildFieldMap(candidates), candidates, psps };
|
|
372
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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';
|
|
2
|
-
export type EvidenceStep = {
|
|
3
|
-
ts: string;
|
|
4
|
-
type: EvidenceStepType;
|
|
5
|
-
data: Record<string, unknown>;
|
|
6
|
-
};
|
|
7
|
-
export declare function maskPan(pan: string): string;
|
|
8
|
-
export declare function maskCvc(_cvc: string): string;
|
|
9
|
-
export declare function maskExpiry(): string;
|
|
10
|
-
export declare function redactContact(role: string, value: string): string;
|
|
11
|
-
export declare class EvidenceLog {
|
|
12
|
-
private steps;
|
|
13
|
-
private snapshotSummary;
|
|
14
|
-
step(type: EvidenceStepType, data?: Record<string, unknown>): void;
|
|
15
|
-
setSnapshotSummary(summary: string): void;
|
|
16
|
-
getSteps(): readonly EvidenceStep[];
|
|
17
|
-
toJSONL(): string;
|
|
18
|
-
toObject(): {
|
|
19
|
-
steps: EvidenceStep[];
|
|
20
|
-
snapshotSummary: string | null;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// Evidence log. A timestamped JSONL trail of everything the executor did:
|
|
2
|
-
// navigation, adapter choice, each field fill (with confidence + source),
|
|
3
|
-
// the mandate verdict, the submit, and the final outcome, plus a compact
|
|
4
|
-
// accessibility snapshot of the end state.
|
|
5
|
-
//
|
|
6
|
-
// Money-adjacent hygiene rule: the full PAN, the cvc, and the card expiry
|
|
7
|
-
// NEVER enter the log. PANs are masked to last-4; cvc and expiry are redacted
|
|
8
|
-
// entirely. maskPan/maskCvc/maskExpiry are the only sanctioned way to put a
|
|
9
|
-
// card value anywhere near a log line. Contact PII (cardholder name, email,
|
|
10
|
-
// address, ...) is redacted via redactContact: the log records THAT a field
|
|
11
|
-
// was filled, never the personal value.
|
|
12
|
-
// Mask a PAN to its last four digits. Non-digits are stripped for the count.
|
|
13
|
-
export function maskPan(pan) {
|
|
14
|
-
const digits = (pan || '').replace(/\D/g, '');
|
|
15
|
-
if (digits.length <= 4)
|
|
16
|
-
return '****';
|
|
17
|
-
return `**** **** **** ${digits.slice(-4)}`;
|
|
18
|
-
}
|
|
19
|
-
// The cvc is never displayed, even masked by length.
|
|
20
|
-
export function maskCvc(_cvc) {
|
|
21
|
-
return 'redacted';
|
|
22
|
-
}
|
|
23
|
-
// The expiry is credential material too (one of the three keyable components
|
|
24
|
-
// alongside the DPAN and the DAVV) — it never enters the log, in any format.
|
|
25
|
-
export function maskExpiry() {
|
|
26
|
-
return 'redacted';
|
|
27
|
-
}
|
|
28
|
-
// Contact values (cardholder name, email, address, ...) are PII and never
|
|
29
|
-
// enter the log raw either. Email keeps its domain for debuggability;
|
|
30
|
-
// everything else becomes a bare presence marker.
|
|
31
|
-
export function redactContact(role, value) {
|
|
32
|
-
if (!value)
|
|
33
|
-
return '';
|
|
34
|
-
if (role === 'email') {
|
|
35
|
-
const at = value.lastIndexOf('@');
|
|
36
|
-
return at > 0 ? `<redacted>@${value.slice(at + 1)}` : '<redacted>';
|
|
37
|
-
}
|
|
38
|
-
return '<redacted>';
|
|
39
|
-
}
|
|
40
|
-
export class EvidenceLog {
|
|
41
|
-
steps = [];
|
|
42
|
-
snapshotSummary = null;
|
|
43
|
-
step(type, data = {}) {
|
|
44
|
-
this.steps.push({ ts: new Date().toISOString(), type, data });
|
|
45
|
-
}
|
|
46
|
-
setSnapshotSummary(summary) {
|
|
47
|
-
this.snapshotSummary = summary;
|
|
48
|
-
}
|
|
49
|
-
getSteps() {
|
|
50
|
-
return this.steps;
|
|
51
|
-
}
|
|
52
|
-
// Newline-delimited JSON. One object per line.
|
|
53
|
-
toJSONL() {
|
|
54
|
-
return this.steps.map((s) => JSON.stringify(s)).join('\n');
|
|
55
|
-
}
|
|
56
|
-
toObject() {
|
|
57
|
-
return { steps: this.steps, snapshotSummary: this.snapshotSummary };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import type { Browser, BrowserContext, Page } from 'playwright-core';
|
|
2
|
-
import { type FieldMap } from './detect.js';
|
|
3
|
-
import { type Mandate } from './mandate.js';
|
|
4
|
-
import type { Instrument } from './instrument.js';
|
|
5
|
-
import type { Contact } from './types.js';
|
|
6
|
-
import { EvidenceLog } from './evidence.js';
|
|
7
|
-
import { type ObservedOutcome } from './outcome.js';
|
|
8
|
-
export type CheckoutMode = 'dry-run' | 'submit';
|
|
9
|
-
export type CheckoutOutcome = 'filled-dry-run' | 'partial-fill' | 'adapter-required' | 'confirmed' | 'declined' | 'action-required' | 'cancelled' | 'blocked-by-mandate' | 'failed';
|
|
10
|
-
export type CredentialLifecycle = 'not-requested' | 'minted-not-exposed' | 'partially-exposed' | 'fully-filled';
|
|
11
|
-
export type CredentialTiming = {
|
|
12
|
-
approvedAt?: string;
|
|
13
|
-
credentialMintedAt?: string;
|
|
14
|
-
credentialExpiresAt?: string;
|
|
15
|
-
fillCompletedAt?: string;
|
|
16
|
-
};
|
|
17
|
-
export type CheckoutResult = {
|
|
18
|
-
outcome: CheckoutOutcome;
|
|
19
|
-
fields: FieldMap;
|
|
20
|
-
evidence: EvidenceLog;
|
|
21
|
-
confirmationRef?: string;
|
|
22
|
-
requiresAdapter: string[];
|
|
23
|
-
credentialLifecycle: CredentialLifecycle;
|
|
24
|
-
credentialTiming: CredentialTiming;
|
|
25
|
-
detail?: string;
|
|
26
|
-
};
|
|
27
|
-
export type PrepareCheckoutOptions = {
|
|
28
|
-
url: string;
|
|
29
|
-
mandate: Mandate;
|
|
30
|
-
browser: Browser;
|
|
31
|
-
amountMinor?: number;
|
|
32
|
-
currency?: string;
|
|
33
|
-
debugShotsDir?: string;
|
|
34
|
-
};
|
|
35
|
-
export type RunCheckoutOptions = PrepareCheckoutOptions & {
|
|
36
|
-
instrument: Instrument;
|
|
37
|
-
contact: Contact;
|
|
38
|
-
mode: CheckoutMode;
|
|
39
|
-
outcomeDeadlineMs?: number;
|
|
40
|
-
};
|
|
41
|
-
export type CheckoutReview = {
|
|
42
|
-
id: string;
|
|
43
|
-
url: string;
|
|
44
|
-
merchantHost: string;
|
|
45
|
-
amountMinor: number;
|
|
46
|
-
currency: string;
|
|
47
|
-
mandateMaxAmountMinor: number;
|
|
48
|
-
mandateExpiresAt: string;
|
|
49
|
-
submitTarget: string | null;
|
|
50
|
-
submitTargetFingerprint: Readonly<CheckoutSubmitTargetFingerprint> | null;
|
|
51
|
-
detectedRoles: readonly string[];
|
|
52
|
-
};
|
|
53
|
-
export type CheckoutSubmitTargetFingerprint = {
|
|
54
|
-
kind: 'submit-control' | 'text-button';
|
|
55
|
-
label: string;
|
|
56
|
-
elementTag: string;
|
|
57
|
-
elementId: string | null;
|
|
58
|
-
elementName: string | null;
|
|
59
|
-
elementType: string | null;
|
|
60
|
-
formAction: string | null;
|
|
61
|
-
formMethod: string | null;
|
|
62
|
-
formTarget: string | null;
|
|
63
|
-
};
|
|
64
|
-
export type PreparedCheckout = {
|
|
65
|
-
readonly review: Readonly<CheckoutReview>;
|
|
66
|
-
readonly fields: Readonly<FieldMap>;
|
|
67
|
-
readonly evidence: EvidenceLog;
|
|
68
|
-
readonly requiresAdapter: readonly string[];
|
|
69
|
-
};
|
|
70
|
-
export type PrepareCheckoutResult = {
|
|
71
|
-
status: 'ready';
|
|
72
|
-
checkout: PreparedCheckout;
|
|
73
|
-
} | {
|
|
74
|
-
status: 'finished';
|
|
75
|
-
result: CheckoutResult;
|
|
76
|
-
};
|
|
77
|
-
export type CheckoutApproval = {
|
|
78
|
-
approved: true;
|
|
79
|
-
reviewId: string;
|
|
80
|
-
};
|
|
81
|
-
export type SubmitApprovedCheckoutOptions = {
|
|
82
|
-
approval: CheckoutApproval;
|
|
83
|
-
instrument: Instrument;
|
|
84
|
-
contact: Contact;
|
|
85
|
-
mode: CheckoutMode;
|
|
86
|
-
outcomeDeadlineMs?: number;
|
|
87
|
-
challengeHoldMs?: number;
|
|
88
|
-
onChallengeHold?: (signal: string | null) => void;
|
|
89
|
-
};
|
|
90
|
-
export declare function minorFromDecimal(text: string): number | null;
|
|
91
|
-
export declare function pageCurrency(text: string): string | null;
|
|
92
|
-
export type PreparedCheckoutSession = {
|
|
93
|
-
checkout: PreparedCheckout;
|
|
94
|
-
context: BrowserContext;
|
|
95
|
-
page: Page;
|
|
96
|
-
options: PrepareCheckoutOptions;
|
|
97
|
-
evidence: EvidenceLog;
|
|
98
|
-
fields: FieldMap;
|
|
99
|
-
requiresAdapter: Set<string>;
|
|
100
|
-
};
|
|
101
|
-
export type PreparedCheckoutSessionStore = {
|
|
102
|
-
put: (reviewId: string, state: PreparedCheckoutSession) => void;
|
|
103
|
-
take: (reviewId: string) => PreparedCheckoutSession | undefined;
|
|
104
|
-
peek: (reviewId: string) => PreparedCheckoutSession | undefined;
|
|
105
|
-
pendingCleanupReviewIds: () => readonly string[];
|
|
106
|
-
reapExpired: (nowMs?: number) => Promise<number>;
|
|
107
|
-
dispose: () => Promise<void>;
|
|
108
|
-
};
|
|
109
|
-
export type InMemoryPreparedCheckoutStoreOptions = {
|
|
110
|
-
ttlMs?: number;
|
|
111
|
-
cleanupRetryMs?: number;
|
|
112
|
-
now?: () => number;
|
|
113
|
-
};
|
|
114
|
-
export declare const DEFAULT_PREPARED_CHECKOUT_TTL_MS: number;
|
|
115
|
-
export declare const DEFAULT_PREPARED_CHECKOUT_CLEANUP_RETRY_MS = 5000;
|
|
116
|
-
export declare class InMemoryPreparedCheckoutStore implements PreparedCheckoutSessionStore {
|
|
117
|
-
private readonly entries;
|
|
118
|
-
private readonly cleanupPending;
|
|
119
|
-
private readonly ttlMs;
|
|
120
|
-
private readonly cleanupRetryMs;
|
|
121
|
-
private readonly now;
|
|
122
|
-
private reaper;
|
|
123
|
-
constructor(options?: InMemoryPreparedCheckoutStoreOptions);
|
|
124
|
-
put(reviewId: string, state: PreparedCheckoutSession): void;
|
|
125
|
-
take(reviewId: string): PreparedCheckoutSession | undefined;
|
|
126
|
-
peek(reviewId: string): PreparedCheckoutSession | undefined;
|
|
127
|
-
pendingCleanupReviewIds(): readonly string[];
|
|
128
|
-
reapExpired(nowMs?: number): Promise<number>;
|
|
129
|
-
dispose(): Promise<void>;
|
|
130
|
-
private queueCleanup;
|
|
131
|
-
private scheduleReaper;
|
|
132
|
-
private closeState;
|
|
133
|
-
}
|
|
134
|
-
export declare function reconcileHeldOutcome(original: ObservedOutcome, held: ObservedOutcome): ObservedOutcome;
|
|
135
|
-
export declare function debugShotMaskPlan(fields: FieldMap): {
|
|
136
|
-
skipReason: string | null;
|
|
137
|
-
maskLocators: string[];
|
|
138
|
-
maskFrames: {
|
|
139
|
-
frame: string;
|
|
140
|
-
locator: string;
|
|
141
|
-
}[];
|
|
142
|
-
};
|
|
143
|
-
export declare function isStripeLinkConsumerRequest(url: string): boolean;
|
|
144
|
-
export declare function suppressStripeLink(page: Page, evidence: EvidenceLog): Promise<void>;
|
|
145
|
-
export type LinkQuietResult = {
|
|
146
|
-
fired: boolean;
|
|
147
|
-
waitedMs: number;
|
|
148
|
-
};
|
|
149
|
-
/**
|
|
150
|
-
* Wait for the suppressed Stripe Link lookup to fire and settle BEFORE the
|
|
151
|
-
* submit click. Stripe debounces its consumer-session lookup ~300ms after the
|
|
152
|
-
* email input changes; our fill→click gap is single-digit ms, so the (aborted)
|
|
153
|
-
* lookup used to land INSIDE Stripe's in-flight submit chain and kill it
|
|
154
|
-
* silently — the click looked accepted but tokenization never ran and the page
|
|
155
|
-
* sat on the form until the outcome deadline (#5879: three identical live
|
|
156
|
-
* stalls at donate.stripe.com). Verified live A/B on that page: instant click →
|
|
157
|
-
* dead submit, no /v1/payment_methods; lookup settled first → tokenization and
|
|
158
|
-
* the confirm step both reached.
|
|
159
|
-
*
|
|
160
|
-
* If the lookup already fired, only the short settle applies (lets Stripe's
|
|
161
|
-
* abort handling unwind). If it never fires — non-Link page variants, no email
|
|
162
|
-
* field — the bound expires and the click proceeds as before.
|
|
163
|
-
*/
|
|
164
|
-
export declare function waitForLinkLookupQuiet(page: Page, opts?: {
|
|
165
|
-
boundMs?: number;
|
|
166
|
-
settleMs?: number;
|
|
167
|
-
delay?: (ms: number) => Promise<void>;
|
|
168
|
-
}): Promise<LinkQuietResult>;
|
|
169
|
-
export declare function prepareCheckout(opts: PrepareCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<PrepareCheckoutResult>;
|
|
170
|
-
export declare function submitApprovedCheckout(reviewId: string, opts: SubmitApprovedCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
|
|
171
|
-
export declare function cancelPreparedCheckout(reviewId: string, detail?: string, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
|
|
172
|
-
export declare function runCheckout(opts: RunCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
|