@visa/cli 4.1.0-rc.18 → 4.1.0-rc.181

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 (77) hide show
  1. package/README.md +189 -232
  2. package/dist/checkout-engine/adapters/generic.d.ts +67 -0
  3. package/dist/checkout-engine/adapters/generic.js +233 -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 +62 -4
  19. package/dist/checkout-engine/executor.js +402 -122
  20. package/dist/checkout-engine/hosted-approval.d.ts +124 -7
  21. package/dist/checkout-engine/hosted-approval.js +381 -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 +5 -20
  29. package/dist/checkout-engine/live-fill-approval.js +20 -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 +43 -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/unresolved-charges.d.ts +34 -0
  46. package/dist/checkout-engine/unresolved-charges.js +125 -0
  47. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +53 -1
  48. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +78 -10
  49. package/dist/checkout-engine/vgs-live-instrument.d.ts +38 -35
  50. package/dist/checkout-engine/vgs-live-instrument.js +51 -74
  51. package/dist/checkout-engine/vic-confirmation.d.ts +18 -0
  52. package/dist/checkout-engine/vic-confirmation.js +9 -3
  53. package/dist/checkout-engine/web-bot-auth.d.ts +92 -0
  54. package/dist/checkout-engine/web-bot-auth.js +159 -0
  55. package/dist/cli.js +703 -494
  56. package/dist/mcp-apps/ucp-checkout.html +280 -0
  57. package/dist/mcp-server/index.js +564 -174
  58. package/dist/skills/pair-visa-agent/RUNTIMES.md +93 -0
  59. package/dist/skills/pair-visa-agent/SKILL.md +463 -221
  60. package/dist/subway-direct.mjs +1 -0
  61. package/install.ps1 +3 -41
  62. package/install.sh +4 -36
  63. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  64. package/package.json +18 -14
  65. package/server.json +3 -3
  66. package/dist/checkout-engine/inline-target.d.ts +0 -13
  67. package/dist/checkout-engine/inline-target.js +0 -37
  68. package/dist/checkout-engine/pay-args.d.ts +0 -14
  69. package/dist/checkout-engine/pay-args.js +0 -44
  70. package/dist/checkout-engine/pay.d.ts +0 -1
  71. package/dist/checkout-engine/pay.js +0 -13
  72. package/dist/checkout-engine/repo-env.d.ts +0 -11
  73. package/dist/checkout-engine/repo-env.js +0 -23
  74. package/dist/checkout-engine/run-live-fill.d.ts +0 -1
  75. package/dist/checkout-engine/run-live-fill.js +0 -443
  76. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  77. 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');
@@ -73,6 +74,51 @@ export function scrubFillErrorMessage(message, value) {
73
74
  scrubbed = scrubbed.split(value).join('<redacted>');
74
75
  return scrubbed;
75
76
  }
77
+ /**
78
+ * A fill failure in a few words, for a message a human reads.
79
+ *
80
+ * Playwright's error is a multi-line call log — useful in the evidence file,
81
+ * unreadable in a refusal message and in the receipt an operator opens a week
82
+ * later. The refusal names WHICH fields refused; without this it never says
83
+ * WHY, so diagnosing a merchant we cannot drive means either reproducing it or
84
+ * reading someone's evidence JSON. Each cause maps to a different fix:
85
+ *
86
+ * not editable — the input exists but is readonly/disabled at fill time
87
+ * (a custom widget owning the value, or a not-yet-ready
88
+ * form). Typing will not help; the field needs an adapter
89
+ * or a longer wait.
90
+ * not a text field — a non-input element pretending to be one. Needs an
91
+ * adapter that drives the widget.
92
+ * not visible /
93
+ * detached — a re-render race. The reveal loop is the lever.
94
+ *
95
+ * Input is already scrubbed by scrubFillErrorMessage; this only ever shortens.
96
+ */
97
+ export function summarizeFillFailure(error) {
98
+ if (!error)
99
+ return 'no reason recorded';
100
+ if (/not an? <input>|not.*\[contenteditable\]/i.test(error))
101
+ return 'not a text field';
102
+ if (/element is not visible/i.test(error))
103
+ return 'not visible';
104
+ if (/not attached to the DOM|detached/i.test(error))
105
+ return 'detached from the page';
106
+ if (/element is not enabled/i.test(error))
107
+ return 'disabled';
108
+ if (/not editable/i.test(error)) {
109
+ const timeout = /Timeout (\d+)ms exceeded/i.exec(error);
110
+ return timeout
111
+ ? `not editable within ${Math.round(Number(timeout[1]) / 1000)}s`
112
+ : 'not editable';
113
+ }
114
+ if (/Timeout (\d+)ms exceeded/i.test(error)) {
115
+ const timeout = /Timeout (\d+)ms exceeded/i.exec(error);
116
+ return `timed out after ${Math.round(Number(timeout[1]) / 1000)}s`;
117
+ }
118
+ // Unrecognized: the first line, bounded. Better a clipped real message than a
119
+ // confident wrong summary.
120
+ return error.split('\n')[0].slice(0, 120);
121
+ }
76
122
  const DEFAULT_FILL_TIMEOUT_MS = 5000;
77
123
  async function fillOne(page, role, entry, value, displayValue, fillTimeoutMs) {
78
124
  const base = {
@@ -97,13 +143,24 @@ async function fillOne(page, role, entry, value, displayValue, fillTimeoutMs) {
97
143
  return { ...base, ok: false, error: scrubFillErrorMessage(err.message, value) };
98
144
  }
99
145
  }
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 = {}) {
146
+ /**
147
+ * The contact record and the page rarely agree on name shape: the record may
148
+ * carry fullName while the page wants first/last inputs, or vice versa. Derive
149
+ * the missing shape so either page can be filled from either record.
150
+ */
151
+ export function contactNameShapes(contact, cardholderName) {
152
+ const fullName = contact.fullName ??
153
+ (contact.firstName && contact.lastName
154
+ ? `${contact.firstName} ${contact.lastName}`
155
+ : cardholderName);
156
+ const first = contact.firstName ?? (fullName?.split(/\s+/)[0] || undefined);
157
+ const last = contact.lastName ?? (fullName?.split(/\s+/).slice(1).join(' ') || undefined);
158
+ return { fullName, first, last };
159
+ }
160
+ async function fillFields(page, fields, credential, contact, opts = {}) {
103
161
  const fillTimeoutMs = opts.fillTimeoutMs ?? DEFAULT_FILL_TIMEOUT_MS;
104
162
  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(' ') ?? '';
163
+ const { fullName, first, last } = contactNameShapes(contact, credential?.cardholderName);
107
164
  // Order matters a little: contact/name before card is harmless, but we fill
108
165
  // card fields explicitly per role so order is not load-bearing.
109
166
  const jobs = [];
@@ -120,26 +177,31 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
120
177
  run: async () => fillOne(page, role, entry, value(), display(), fillTimeoutMs),
121
178
  });
122
179
  };
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));
180
+ if (credential) {
181
+ add('number', fields.number, () => credential.pan, () => maskPan(credential.pan));
182
+ add('cvc', fields.cvc, () => credential.cvc, () => maskCvc(credential.cvc));
183
+ }
184
+ if (fullName)
185
+ add('name', fields.name, () => fullName, () => redactContact('name', fullName));
186
+ if (first)
187
+ add('nameFirst', fields.nameFirst, () => first, () => redactContact('nameFirst', first));
188
+ if (last)
189
+ add('nameLast', fields.nameLast, () => last, () => redactContact('nameLast', last));
128
190
  // Expiry display values are always redacted: the expiry is part of the
129
191
  // keyable credential (DPAN + expiry + DAVV) and never enters the log.
130
- if (fields.expCombined) {
192
+ if (credential && fields.expCombined) {
131
193
  const e = fields.expCombined;
132
194
  const v = expCombinedValue(e, credential.expMonth, credential.expYear);
133
195
  add('expCombined', e, () => v, () => maskExpiry());
134
196
  }
135
- if (fields.expMonth) {
197
+ if (credential && fields.expMonth) {
136
198
  const e = fields.expMonth;
137
199
  const value = e.tag === 'select'
138
200
  ? (monthOptionValue(e.options ?? [], credential.expMonth) ?? pad2(credential.expMonth))
139
201
  : pad2(credential.expMonth);
140
202
  add('expMonth', e, () => value, () => maskExpiry());
141
203
  }
142
- if (fields.expYear) {
204
+ if (credential && fields.expYear) {
143
205
  const e = fields.expYear;
144
206
  let value;
145
207
  if (e.tag === 'select') {
@@ -157,6 +219,8 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
157
219
  // the evidence log is built to be persistable.
158
220
  if (contact.email)
159
221
  add('email', fields.email, () => contact.email, () => redactContact('email', contact.email));
222
+ if (contact.phone)
223
+ add('phone', fields.phone, () => contact.phone, () => redactContact('phone', contact.phone));
160
224
  if (contact.addressLine1)
161
225
  add('addressLine1', fields.addressLine1, () => contact.addressLine1, () => redactContact('addressLine1', contact.addressLine1));
162
226
  if (contact.addressLine2)
@@ -185,6 +249,154 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
185
249
  }
186
250
  return filled;
187
251
  }
252
+ // Credential-free contact prefill for merchants that must calculate shipping,
253
+ // tax, and the final total before a human reviews the payment.
254
+ export async function fillContactFieldMap(page, fields, contact, opts = {}) {
255
+ return fillFields(page, fields, null, contact, opts);
256
+ }
257
+ // The shared post-approval fill routine used by every adapter.
258
+ export async function fillFieldMap(page, fields, credential, contact, opts = {}) {
259
+ return fillFields(page, fields, credential, contact, opts);
260
+ }
261
+ /** Option text that identifies a card-paying choice, most specific first. */
262
+ const CARD_OPTION_PATTERNS = [
263
+ /^\s*visa\s*$/i,
264
+ /credit\s*card|card\s*payment/i,
265
+ /^\s*(mastercard|master\s*card)\s*$/i,
266
+ /\bcard\b/i,
267
+ ];
268
+ /**
269
+ * Attribute selectors for a card-number input that survive a panel re-render,
270
+ * tried in order. The originally detected locator is tried first so a page that
271
+ * does NOT re-render keeps its higher-confidence match.
272
+ */
273
+ const CARD_NUMBER_FALLBACK_SELECTORS = [
274
+ 'input[autocomplete="cc-number"]',
275
+ 'input[name*="creditcardnumber" i]',
276
+ 'input[name*="cardnumber" i]',
277
+ 'input[id*="cardnumber" i]',
278
+ 'input[name*="cc-number" i]',
279
+ ];
280
+ /** First selector that resolves to a visible input, or null if none do. */
281
+ async function firstVisibleCardNumberLocator(page, detectedLocator, timeoutMs) {
282
+ for (const selector of [detectedLocator, ...CARD_NUMBER_FALLBACK_SELECTORS]) {
283
+ try {
284
+ await page
285
+ .locator(selector)
286
+ .first()
287
+ .waitFor({ state: 'visible', timeout: Math.max(500, Math.floor(timeoutMs / 3)) });
288
+ return selector;
289
+ }
290
+ catch {
291
+ continue;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ /** Card-credential roles that share the panel a payment select unfolds. */
297
+ const CARD_GROUP_ROLES = ['number', 'cvc', 'expCombined', 'expMonth', 'expYear'];
298
+ /**
299
+ * Re-detect and adopt fresh entries for every card field after the panel is
300
+ * unfolded. Injected for tests; the executor's own detector is used in
301
+ * production.
302
+ */
303
+ export async function refreshCardGroupFromPage(page, fields, detect = detectFields) {
304
+ let fresh;
305
+ try {
306
+ fresh = (await detect(page)).fields;
307
+ }
308
+ catch {
309
+ return [];
310
+ }
311
+ const adopted = [];
312
+ for (const role of CARD_GROUP_ROLES) {
313
+ const next = fresh[role];
314
+ if (!next || next.visible === false)
315
+ continue;
316
+ const current = fields[role];
317
+ // Only ever replace an entry we could not have filled anyway. A field that
318
+ // is already visible was detected against the live DOM and keeps its
319
+ // higher-confidence match.
320
+ if (current && current.visible !== false)
321
+ continue;
322
+ fields[role] = next;
323
+ adopted.push(role);
324
+ }
325
+ return adopted;
326
+ }
327
+ /**
328
+ * Reveal card fields that a checkout keeps collapsed until a payment method is
329
+ * chosen.
330
+ *
331
+ * `fillFields` skips any entry with `visible === false`, so a card-number input
332
+ * sitting inside a folded panel is never even attempted — the generic adapter
333
+ * then reports `ok: false` ("fill incomplete") without having typed anything.
334
+ * That is the correct default: filling an invisible input is how a credential
335
+ * gets typed into the wrong place. But a payment-method `<select>` guarding the
336
+ * card panel is common enough to be worth handling, and the recovery is a
337
+ * single deterministic interaction rather than a guess.
338
+ *
339
+ * We only ever SELECT a card option — never a wallet, bank transfer, or
340
+ * anything else — and we only act when the card field is already detected but
341
+ * hidden. If nothing changes, the caller proceeds exactly as before and still
342
+ * fails closed.
343
+ *
344
+ * Mutates `fields.number.visible` on success so the subsequent fill attempts
345
+ * the field it just revealed.
346
+ */
347
+ export async function revealCollapsedCardSection(page, fields, opts = {}) {
348
+ const number = fields.number;
349
+ if (!number || number.visible !== false)
350
+ return { revealed: false, via: null };
351
+ const timeoutMs = opts.timeoutMs ?? 5_000;
352
+ const selects = page.locator('select');
353
+ const count = await selects.count().catch(() => 0);
354
+ for (let i = 0; i < Math.min(count, 12); i++) {
355
+ const select = selects.nth(i);
356
+ // Read option labels through the locator API rather than page.evaluate.
357
+ // A bundled build rewrites the function passed to evaluate() and the
358
+ // injected helper is not defined in page scope, so it throws at runtime —
359
+ // silently, once a catch treats it as "this select didn't match". Staying
360
+ // on the locator API keeps this working in source and bundled alike.
361
+ let labels;
362
+ try {
363
+ labels = await select.locator('option').allTextContents();
364
+ }
365
+ catch {
366
+ continue;
367
+ }
368
+ for (const pattern of CARD_OPTION_PATTERNS) {
369
+ const label = labels.map((l) => l.trim()).find((l) => l && pattern.test(l));
370
+ if (!label)
371
+ continue;
372
+ try {
373
+ await select.selectOption({ label }, { timeout: timeoutMs });
374
+ }
375
+ catch {
376
+ continue;
377
+ }
378
+ // Re-acquire the field instead of waiting on the detected locator.
379
+ // Unfolding the panel typically re-renders it, and the detector's
380
+ // synthetic `data-ca-id` attribute does not survive that — waiting on the
381
+ // old locator times out even though the field is now on screen and
382
+ // fillable. Stable attribute selectors survive the re-render.
383
+ const revealedLocator = await firstVisibleCardNumberLocator(page, number.locator, timeoutMs);
384
+ if (!revealedLocator)
385
+ continue;
386
+ number.locator = revealedLocator;
387
+ number.visible = true;
388
+ // The number is not alone in that panel: cvc and expiry were re-rendered
389
+ // with it and still carry stale, invisible entries. Filling only the
390
+ // number would trade "adapter fill incomplete" for "credential fill
391
+ // incomplete: missing cvc, expiry" — still a failed purchase, still after
392
+ // a credential was minted. Re-detect and adopt fresh entries for the
393
+ // whole card group.
394
+ await refreshCardGroupFromPage(page, fields);
395
+ return { revealed: true, via: label };
396
+ }
397
+ }
398
+ return { revealed: false, via: null };
399
+ }
188
400
  export class GenericAdapter {
189
401
  name = 'generic';
190
402
  matches(_detected) {
@@ -192,10 +404,17 @@ export class GenericAdapter {
192
404
  return true;
193
405
  }
194
406
  async fill(page, fields, credential, contact) {
407
+ const reveal = await revealCollapsedCardSection(page, fields);
195
408
  const filled = await fillFieldMap(page, fields, credential, contact);
409
+ const ok = filled.some((f) => f.role === 'number' && f.ok);
196
410
  return {
197
- ok: filled.some((f) => f.role === 'number' && f.ok),
411
+ ok,
198
412
  filled,
413
+ ...(ok || !reveal.revealed
414
+ ? {}
415
+ : {
416
+ detail: `revealed the card section via "${reveal.via}" but the number field still did not fill`,
417
+ }),
199
418
  };
200
419
  }
201
420
  }
@@ -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 {};