@visa/cli 4.1.0-rc.16 → 4.1.0-rc.161

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.
Files changed (76) hide show
  1. package/README.md +189 -232
  2. package/dist/checkout-engine/adapters/generic.d.ts +46 -0
  3. package/dist/checkout-engine/adapters/generic.js +188 -14
  4. package/dist/checkout-engine/adapters/index.d.ts +4 -1
  5. package/dist/checkout-engine/adapters/index.js +10 -3
  6. package/dist/checkout-engine/adapters/shopify.d.ts +55 -0
  7. package/dist/checkout-engine/adapters/shopify.js +514 -0
  8. package/dist/checkout-engine/amount.d.ts +15 -0
  9. package/dist/checkout-engine/amount.js +72 -0
  10. package/dist/checkout-engine/cli-engine.d.ts +259 -4
  11. package/dist/checkout-engine/cli-engine.js +797 -43
  12. package/dist/checkout-engine/confirmed-merchants.d.ts +31 -0
  13. package/dist/checkout-engine/confirmed-merchants.js +165 -0
  14. package/dist/checkout-engine/detect.d.ts +1 -1
  15. package/dist/checkout-engine/detect.js +26 -0
  16. package/dist/checkout-engine/evidence.d.ts +4 -1
  17. package/dist/checkout-engine/evidence.js +51 -6
  18. package/dist/checkout-engine/executor.d.ts +39 -4
  19. package/dist/checkout-engine/executor.js +323 -117
  20. package/dist/checkout-engine/hosted-approval.d.ts +124 -7
  21. package/dist/checkout-engine/hosted-approval.js +379 -54
  22. package/dist/checkout-engine/index.d.ts +8 -2
  23. package/dist/checkout-engine/index.js +7 -1
  24. package/dist/checkout-engine/instrument.d.ts +7 -0
  25. package/dist/checkout-engine/instrument.js +4 -0
  26. package/dist/checkout-engine/known-merchants.d.ts +10 -0
  27. package/dist/checkout-engine/known-merchants.js +38 -0
  28. package/dist/checkout-engine/live-fill-approval.d.ts +0 -20
  29. package/dist/checkout-engine/live-fill-approval.js +15 -51
  30. package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
  31. package/dist/checkout-engine/mandate/card-mandate.js +227 -0
  32. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +178 -0
  33. package/dist/checkout-engine/mandate/mandate-ledger.js +395 -0
  34. package/dist/checkout-engine/outcome.d.ts +2 -2
  35. package/dist/checkout-engine/outcome.js +36 -1
  36. package/dist/checkout-engine/owner-only-file.d.ts +9 -0
  37. package/dist/checkout-engine/owner-only-file.js +20 -1
  38. package/dist/checkout-engine/receipt-dir.d.ts +6 -0
  39. package/dist/checkout-engine/receipt-dir.js +8 -0
  40. package/dist/checkout-engine/receipt.d.ts +42 -2
  41. package/dist/checkout-engine/receipt.js +30 -14
  42. package/dist/checkout-engine/trace-handles.d.ts +8 -0
  43. package/dist/checkout-engine/trace-handles.js +12 -0
  44. package/dist/checkout-engine/types.d.ts +20 -2
  45. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +53 -1
  46. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +78 -10
  47. package/dist/checkout-engine/vgs-live-instrument.d.ts +38 -35
  48. package/dist/checkout-engine/vgs-live-instrument.js +51 -74
  49. package/dist/checkout-engine/vic-confirmation.d.ts +18 -0
  50. package/dist/checkout-engine/vic-confirmation.js +9 -3
  51. package/dist/checkout-engine/web-bot-auth.d.ts +92 -0
  52. package/dist/checkout-engine/web-bot-auth.js +159 -0
  53. package/dist/cli.js +663 -492
  54. package/dist/mcp-apps/ucp-checkout.html +280 -0
  55. package/dist/mcp-server/index.js +514 -174
  56. package/dist/skills/pair-visa-agent/RUNTIMES.md +92 -0
  57. package/dist/skills/pair-visa-agent/SKILL.md +526 -0
  58. package/dist/skills/pair-visa-agent/scripts/setup.mjs +48 -0
  59. package/dist/subway-direct.mjs +1 -0
  60. package/install.ps1 +3 -41
  61. package/install.sh +4 -36
  62. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  63. package/package.json +18 -14
  64. package/server.json +3 -3
  65. package/dist/checkout-engine/inline-target.d.ts +0 -13
  66. package/dist/checkout-engine/inline-target.js +0 -37
  67. package/dist/checkout-engine/pay-args.d.ts +0 -14
  68. package/dist/checkout-engine/pay-args.js +0 -44
  69. package/dist/checkout-engine/pay.d.ts +0 -1
  70. package/dist/checkout-engine/pay.js +0 -13
  71. package/dist/checkout-engine/repo-env.d.ts +0 -11
  72. package/dist/checkout-engine/repo-env.js +0 -23
  73. package/dist/checkout-engine/run-live-fill.d.ts +0 -1
  74. package/dist/checkout-engine/run-live-fill.js +0 -443
  75. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  76. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
@@ -2,6 +2,7 @@
2
2
  // <select> dropdowns, handles split vs combined expiry, two- vs four-digit
3
3
  // years, and split first/last name. It is the fallback that should beat any
4
4
  // well-behaved guest checkout on its own.
5
+ import { detectFields } from '../detect.js';
5
6
  import { maskCvc, maskExpiry, maskPan, redactContact } from '../evidence.js';
6
7
  function pad2(n) {
7
8
  return String(n).padStart(2, '0');
@@ -97,13 +98,24 @@ async function fillOne(page, role, entry, value, displayValue, fillTimeoutMs) {
97
98
  return { ...base, ok: false, error: scrubFillErrorMessage(err.message, value) };
98
99
  }
99
100
  }
100
- // The shared fill routine used by every adapter. Takes an already-detected
101
- // FieldMap so the executor controls when detection runs.
102
- export async function fillFieldMap(page, fields, credential, contact, opts = {}) {
101
+ /**
102
+ * The contact record and the page rarely agree on name shape: the record may
103
+ * carry fullName while the page wants first/last inputs, or vice versa. Derive
104
+ * the missing shape so either page can be filled from either record.
105
+ */
106
+ export function contactNameShapes(contact, cardholderName) {
107
+ const fullName = contact.fullName ??
108
+ (contact.firstName && contact.lastName
109
+ ? `${contact.firstName} ${contact.lastName}`
110
+ : cardholderName);
111
+ const first = contact.firstName ?? (fullName?.split(/\s+/)[0] || undefined);
112
+ const last = contact.lastName ?? (fullName?.split(/\s+/).slice(1).join(' ') || undefined);
113
+ return { fullName, first, last };
114
+ }
115
+ async function fillFields(page, fields, credential, contact, opts = {}) {
103
116
  const fillTimeoutMs = opts.fillTimeoutMs ?? DEFAULT_FILL_TIMEOUT_MS;
104
117
  const filled = [];
105
- const first = contact.firstName ?? credential.cardholderName.split(/\s+/)[0] ?? credential.cardholderName;
106
- const last = contact.lastName ?? credential.cardholderName.split(/\s+/).slice(1).join(' ') ?? '';
118
+ const { fullName, first, last } = contactNameShapes(contact, credential?.cardholderName);
107
119
  // Order matters a little: contact/name before card is harmless, but we fill
108
120
  // card fields explicitly per role so order is not load-bearing.
109
121
  const jobs = [];
@@ -120,26 +132,31 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
120
132
  run: async () => fillOne(page, role, entry, value(), display(), fillTimeoutMs),
121
133
  });
122
134
  };
123
- add('number', fields.number, () => credential.pan, () => maskPan(credential.pan));
124
- add('cvc', fields.cvc, () => credential.cvc, () => maskCvc(credential.cvc));
125
- add('name', fields.name, () => credential.cardholderName, () => redactContact('name', credential.cardholderName));
126
- add('nameFirst', fields.nameFirst, () => first, () => redactContact('nameFirst', first));
127
- add('nameLast', fields.nameLast, () => last, () => redactContact('nameLast', last));
135
+ if (credential) {
136
+ add('number', fields.number, () => credential.pan, () => maskPan(credential.pan));
137
+ add('cvc', fields.cvc, () => credential.cvc, () => maskCvc(credential.cvc));
138
+ }
139
+ if (fullName)
140
+ add('name', fields.name, () => fullName, () => redactContact('name', fullName));
141
+ if (first)
142
+ add('nameFirst', fields.nameFirst, () => first, () => redactContact('nameFirst', first));
143
+ if (last)
144
+ add('nameLast', fields.nameLast, () => last, () => redactContact('nameLast', last));
128
145
  // Expiry display values are always redacted: the expiry is part of the
129
146
  // keyable credential (DPAN + expiry + DAVV) and never enters the log.
130
- if (fields.expCombined) {
147
+ if (credential && fields.expCombined) {
131
148
  const e = fields.expCombined;
132
149
  const v = expCombinedValue(e, credential.expMonth, credential.expYear);
133
150
  add('expCombined', e, () => v, () => maskExpiry());
134
151
  }
135
- if (fields.expMonth) {
152
+ if (credential && fields.expMonth) {
136
153
  const e = fields.expMonth;
137
154
  const value = e.tag === 'select'
138
155
  ? (monthOptionValue(e.options ?? [], credential.expMonth) ?? pad2(credential.expMonth))
139
156
  : pad2(credential.expMonth);
140
157
  add('expMonth', e, () => value, () => maskExpiry());
141
158
  }
142
- if (fields.expYear) {
159
+ if (credential && fields.expYear) {
143
160
  const e = fields.expYear;
144
161
  let value;
145
162
  if (e.tag === 'select') {
@@ -157,6 +174,8 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
157
174
  // the evidence log is built to be persistable.
158
175
  if (contact.email)
159
176
  add('email', fields.email, () => contact.email, () => redactContact('email', contact.email));
177
+ if (contact.phone)
178
+ add('phone', fields.phone, () => contact.phone, () => redactContact('phone', contact.phone));
160
179
  if (contact.addressLine1)
161
180
  add('addressLine1', fields.addressLine1, () => contact.addressLine1, () => redactContact('addressLine1', contact.addressLine1));
162
181
  if (contact.addressLine2)
@@ -185,6 +204,154 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
185
204
  }
186
205
  return filled;
187
206
  }
207
+ // Credential-free contact prefill for merchants that must calculate shipping,
208
+ // tax, and the final total before a human reviews the payment.
209
+ export async function fillContactFieldMap(page, fields, contact, opts = {}) {
210
+ return fillFields(page, fields, null, contact, opts);
211
+ }
212
+ // The shared post-approval fill routine used by every adapter.
213
+ export async function fillFieldMap(page, fields, credential, contact, opts = {}) {
214
+ return fillFields(page, fields, credential, contact, opts);
215
+ }
216
+ /** Option text that identifies a card-paying choice, most specific first. */
217
+ const CARD_OPTION_PATTERNS = [
218
+ /^\s*visa\s*$/i,
219
+ /credit\s*card|card\s*payment/i,
220
+ /^\s*(mastercard|master\s*card)\s*$/i,
221
+ /\bcard\b/i,
222
+ ];
223
+ /**
224
+ * Attribute selectors for a card-number input that survive a panel re-render,
225
+ * tried in order. The originally detected locator is tried first so a page that
226
+ * does NOT re-render keeps its higher-confidence match.
227
+ */
228
+ const CARD_NUMBER_FALLBACK_SELECTORS = [
229
+ 'input[autocomplete="cc-number"]',
230
+ 'input[name*="creditcardnumber" i]',
231
+ 'input[name*="cardnumber" i]',
232
+ 'input[id*="cardnumber" i]',
233
+ 'input[name*="cc-number" i]',
234
+ ];
235
+ /** First selector that resolves to a visible input, or null if none do. */
236
+ async function firstVisibleCardNumberLocator(page, detectedLocator, timeoutMs) {
237
+ for (const selector of [detectedLocator, ...CARD_NUMBER_FALLBACK_SELECTORS]) {
238
+ try {
239
+ await page
240
+ .locator(selector)
241
+ .first()
242
+ .waitFor({ state: 'visible', timeout: Math.max(500, Math.floor(timeoutMs / 3)) });
243
+ return selector;
244
+ }
245
+ catch {
246
+ continue;
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+ /** Card-credential roles that share the panel a payment select unfolds. */
252
+ const CARD_GROUP_ROLES = ['number', 'cvc', 'expCombined', 'expMonth', 'expYear'];
253
+ /**
254
+ * Re-detect and adopt fresh entries for every card field after the panel is
255
+ * unfolded. Injected for tests; the executor's own detector is used in
256
+ * production.
257
+ */
258
+ export async function refreshCardGroupFromPage(page, fields, detect = detectFields) {
259
+ let fresh;
260
+ try {
261
+ fresh = (await detect(page)).fields;
262
+ }
263
+ catch {
264
+ return [];
265
+ }
266
+ const adopted = [];
267
+ for (const role of CARD_GROUP_ROLES) {
268
+ const next = fresh[role];
269
+ if (!next || next.visible === false)
270
+ continue;
271
+ const current = fields[role];
272
+ // Only ever replace an entry we could not have filled anyway. A field that
273
+ // is already visible was detected against the live DOM and keeps its
274
+ // higher-confidence match.
275
+ if (current && current.visible !== false)
276
+ continue;
277
+ fields[role] = next;
278
+ adopted.push(role);
279
+ }
280
+ return adopted;
281
+ }
282
+ /**
283
+ * Reveal card fields that a checkout keeps collapsed until a payment method is
284
+ * chosen.
285
+ *
286
+ * `fillFields` skips any entry with `visible === false`, so a card-number input
287
+ * sitting inside a folded panel is never even attempted — the generic adapter
288
+ * then reports `ok: false` ("fill incomplete") without having typed anything.
289
+ * That is the correct default: filling an invisible input is how a credential
290
+ * gets typed into the wrong place. But a payment-method `<select>` guarding the
291
+ * card panel is common enough to be worth handling, and the recovery is a
292
+ * single deterministic interaction rather than a guess.
293
+ *
294
+ * We only ever SELECT a card option — never a wallet, bank transfer, or
295
+ * anything else — and we only act when the card field is already detected but
296
+ * hidden. If nothing changes, the caller proceeds exactly as before and still
297
+ * fails closed.
298
+ *
299
+ * Mutates `fields.number.visible` on success so the subsequent fill attempts
300
+ * the field it just revealed.
301
+ */
302
+ export async function revealCollapsedCardSection(page, fields, opts = {}) {
303
+ const number = fields.number;
304
+ if (!number || number.visible !== false)
305
+ return { revealed: false, via: null };
306
+ const timeoutMs = opts.timeoutMs ?? 5_000;
307
+ const selects = page.locator('select');
308
+ const count = await selects.count().catch(() => 0);
309
+ for (let i = 0; i < Math.min(count, 12); i++) {
310
+ const select = selects.nth(i);
311
+ // Read option labels through the locator API rather than page.evaluate.
312
+ // A bundled build rewrites the function passed to evaluate() and the
313
+ // injected helper is not defined in page scope, so it throws at runtime —
314
+ // silently, once a catch treats it as "this select didn't match". Staying
315
+ // on the locator API keeps this working in source and bundled alike.
316
+ let labels;
317
+ try {
318
+ labels = await select.locator('option').allTextContents();
319
+ }
320
+ catch {
321
+ continue;
322
+ }
323
+ for (const pattern of CARD_OPTION_PATTERNS) {
324
+ const label = labels.map((l) => l.trim()).find((l) => l && pattern.test(l));
325
+ if (!label)
326
+ continue;
327
+ try {
328
+ await select.selectOption({ label }, { timeout: timeoutMs });
329
+ }
330
+ catch {
331
+ continue;
332
+ }
333
+ // Re-acquire the field instead of waiting on the detected locator.
334
+ // Unfolding the panel typically re-renders it, and the detector's
335
+ // synthetic `data-ca-id` attribute does not survive that — waiting on the
336
+ // old locator times out even though the field is now on screen and
337
+ // fillable. Stable attribute selectors survive the re-render.
338
+ const revealedLocator = await firstVisibleCardNumberLocator(page, number.locator, timeoutMs);
339
+ if (!revealedLocator)
340
+ continue;
341
+ number.locator = revealedLocator;
342
+ number.visible = true;
343
+ // The number is not alone in that panel: cvc and expiry were re-rendered
344
+ // with it and still carry stale, invisible entries. Filling only the
345
+ // number would trade "adapter fill incomplete" for "credential fill
346
+ // incomplete: missing cvc, expiry" — still a failed purchase, still after
347
+ // a credential was minted. Re-detect and adopt fresh entries for the
348
+ // whole card group.
349
+ await refreshCardGroupFromPage(page, fields);
350
+ return { revealed: true, via: label };
351
+ }
352
+ }
353
+ return { revealed: false, via: null };
354
+ }
188
355
  export class GenericAdapter {
189
356
  name = 'generic';
190
357
  matches(_detected) {
@@ -192,10 +359,17 @@ export class GenericAdapter {
192
359
  return true;
193
360
  }
194
361
  async fill(page, fields, credential, contact) {
362
+ const reveal = await revealCollapsedCardSection(page, fields);
195
363
  const filled = await fillFieldMap(page, fields, credential, contact);
364
+ const ok = filled.some((f) => f.role === 'number' && f.ok);
196
365
  return {
197
- ok: filled.some((f) => f.role === 'number' && f.ok),
366
+ ok,
198
367
  filled,
368
+ ...(ok || !reveal.revealed
369
+ ? {}
370
+ : {
371
+ detail: `revealed the card section via "${reveal.via}" but the number field still did not fill`,
372
+ }),
199
373
  };
200
374
  }
201
375
  }
@@ -2,6 +2,9 @@ import type { DetectResult } from '../detect.js';
2
2
  import type { CheckoutAdapter } from './generic.js';
3
3
  export type { CheckoutAdapter } from './generic.js';
4
4
  export { GenericAdapter } from './generic.js';
5
+ export { ShopifyAdapter } from './shopify.js';
5
6
  export { StripeLikeAdapter } from './stripe-like.js';
6
7
  export { fillFieldMap, resolveLocator } from './generic.js';
7
- export declare function selectAdapter(detected: DetectResult): CheckoutAdapter;
8
+ export declare function selectAdapter(detected: DetectResult, pageKind?: {
9
+ shopify?: boolean;
10
+ }): CheckoutAdapter;
@@ -2,14 +2,21 @@
2
2
  // the universal fallback and always matches last. Selection is a pure
3
3
  // function of an already-run detection — adapters never re-detect.
4
4
  import { GenericAdapter } from './generic.js';
5
+ import { ShopifyAdapter } from './shopify.js';
5
6
  import { StripeLikeAdapter } from './stripe-like.js';
6
7
  export { GenericAdapter } from './generic.js';
8
+ export { ShopifyAdapter } from './shopify.js';
7
9
  export { StripeLikeAdapter } from './stripe-like.js';
8
10
  export { fillFieldMap, resolveLocator } from './generic.js';
9
- const SPECIFIC = [new StripeLikeAdapter()];
10
11
  const FALLBACK = new GenericAdapter();
11
- export function selectAdapter(detected) {
12
- for (const a of SPECIFIC) {
12
+ export function selectAdapter(detected, pageKind = {}) {
13
+ // Shopify owns the outer checkout form and can still delegate card fields to
14
+ // an iframe, so it gets first crack and reuses fillFieldMap for both.
15
+ const shopify = new ShopifyAdapter(detected);
16
+ if (pageKind.shopify === true)
17
+ return shopify;
18
+ const specific = [shopify, new StripeLikeAdapter()];
19
+ for (const a of specific) {
13
20
  if (a.matches(detected))
14
21
  return a;
15
22
  }
@@ -0,0 +1,55 @@
1
+ import type { Page } from 'playwright-core';
2
+ import { type PageAmountRead } from '../amount.js';
3
+ import { type DetectResult, type FieldMap } from '../detect.js';
4
+ import type { CardCredential } from '../instrument.js';
5
+ import type { Contact, FillResult, FilledField } from '../types.js';
6
+ import { type CheckoutAdapter } from './generic.js';
7
+ type ShopifySummary = {
8
+ subtotalMinor: number | null;
9
+ shippingMinor: number | null;
10
+ taxMinor: number | null;
11
+ discountMinor: number;
12
+ totalMinor: number | null;
13
+ currency: string | null;
14
+ verified: boolean;
15
+ };
16
+ export declare function parseShopifySummary(text: string): ShopifySummary;
17
+ export declare function readShopifyAmount(page: Page, requireVerified: boolean): Promise<PageAmountRead>;
18
+ export declare function readStableShopifyAmount(page: Page, timeoutMs?: number): Promise<PageAmountRead>;
19
+ export declare function isShopifyCheckoutPage(page: Page): Promise<boolean>;
20
+ export declare function detectShopifyChallenge(page: Page): Promise<{
21
+ signal: string;
22
+ } | null>;
23
+ /**
24
+ * The en-US variant of a localized Shopify checkout URL, or null when it is
25
+ * already English (or not locale-suffixed). Shopify renders the checkout in
26
+ * the URL's trailing locale segment, and amount reconciliation reads the
27
+ * order summary by its ENGLISH labels — a store whose primary market is not
28
+ * English serves /checkouts/cn/<token>/<locale> and the total never parses
29
+ * (observed live 2026-08-16: /es-us rendered "Precio total" and the review
30
+ * refused fail-closed on a good checkout). The locale segment is
31
+ * presentation-only: swapping it keeps the same checkout session and token.
32
+ */
33
+ export declare function shopifyEnglishCheckoutUrl(current: string): string | null;
34
+ export declare function missingContactRoles(filled: FilledField[], expected: string[]): string[];
35
+ /**
36
+ * The contact surface to prefill. Shipping fields when the checkout has them —
37
+ * but a digital-goods (no-shipping) Shopify checkout renders exactly one
38
+ * address block and marks every field autocomplete="billing ..." (observed
39
+ * live 2026-08-16: all 18 candidates billing-classified, so the shipping map
40
+ * came back empty and the prefill reported every role missing). That billing
41
+ * block IS the primary contact surface, under its base roles.
42
+ */
43
+ export declare function contactPrefillFieldMap(detected: DetectResult): {
44
+ fields: FieldMap;
45
+ surface: 'shipping' | 'billing-only';
46
+ };
47
+ export declare class ShopifyAdapter implements CheckoutAdapter {
48
+ private readonly detected;
49
+ name: string;
50
+ constructor(detected: DetectResult);
51
+ matches(detected: DetectResult): boolean;
52
+ prepareContact(page: Page, contact: Contact): Promise<FillResult>;
53
+ fill(page: Page, _fields: FieldMap, credential: CardCredential, _contact: Contact): Promise<FillResult>;
54
+ }
55
+ export {};