@visa/cli 4.1.0-rc.13 → 4.1.0-rc.131

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 (63) hide show
  1. package/README.md +188 -232
  2. package/dist/checkout-engine/adapters/generic.d.ts +4 -0
  3. package/dist/checkout-engine/adapters/generic.js +28 -13
  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 +31 -0
  7. package/dist/checkout-engine/adapters/shopify.js +423 -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 +207 -2
  11. package/dist/checkout-engine/cli-engine.js +677 -27
  12. package/dist/checkout-engine/detect.d.ts +1 -1
  13. package/dist/checkout-engine/detect.js +26 -0
  14. package/dist/checkout-engine/evidence.d.ts +4 -1
  15. package/dist/checkout-engine/evidence.js +51 -6
  16. package/dist/checkout-engine/executor.d.ts +34 -4
  17. package/dist/checkout-engine/executor.js +266 -115
  18. package/dist/checkout-engine/hosted-approval.d.ts +133 -8
  19. package/dist/checkout-engine/hosted-approval.js +400 -49
  20. package/dist/checkout-engine/index.d.ts +4 -1
  21. package/dist/checkout-engine/index.js +3 -0
  22. package/dist/checkout-engine/instrument.d.ts +7 -0
  23. package/dist/checkout-engine/instrument.js +4 -0
  24. package/dist/checkout-engine/live-fill-approval.d.ts +0 -20
  25. package/dist/checkout-engine/live-fill-approval.js +15 -51
  26. package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
  27. package/dist/checkout-engine/mandate/card-mandate.js +227 -0
  28. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +165 -0
  29. package/dist/checkout-engine/mandate/mandate-ledger.js +373 -0
  30. package/dist/checkout-engine/outcome.d.ts +2 -2
  31. package/dist/checkout-engine/outcome.js +36 -1
  32. package/dist/checkout-engine/owner-only-file.d.ts +9 -0
  33. package/dist/checkout-engine/owner-only-file.js +20 -1
  34. package/dist/checkout-engine/trace-handles.d.ts +8 -0
  35. package/dist/checkout-engine/trace-handles.js +12 -0
  36. package/dist/checkout-engine/types.d.ts +20 -2
  37. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +82 -0
  38. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +180 -0
  39. package/dist/checkout-engine/vgs-live-instrument.d.ts +38 -0
  40. package/dist/checkout-engine/vgs-live-instrument.js +52 -8
  41. package/dist/checkout-engine/vic-confirmation.js +2 -2
  42. package/dist/cli.js +580 -494
  43. package/dist/mcp-server/index.js +442 -176
  44. package/dist/skills/pair-visa-agent/RUNTIMES.md +92 -0
  45. package/dist/skills/pair-visa-agent/SKILL.md +467 -0
  46. package/dist/skills/pair-visa-agent/scripts/setup.mjs +48 -0
  47. package/install.ps1 +3 -41
  48. package/install.sh +4 -36
  49. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  50. package/package.json +16 -12
  51. package/server.json +3 -3
  52. package/dist/checkout-engine/inline-target.d.ts +0 -13
  53. package/dist/checkout-engine/inline-target.js +0 -37
  54. package/dist/checkout-engine/pay-args.d.ts +0 -14
  55. package/dist/checkout-engine/pay-args.js +0 -44
  56. package/dist/checkout-engine/pay.d.ts +0 -1
  57. package/dist/checkout-engine/pay.js +0 -13
  58. package/dist/checkout-engine/repo-env.d.ts +0 -11
  59. package/dist/checkout-engine/repo-env.js +0 -23
  60. package/dist/checkout-engine/run-live-fill.d.ts +0 -1
  61. package/dist/checkout-engine/run-live-fill.js +0 -443
  62. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  63. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -240
@@ -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, 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.
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,14 @@ 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
+ export { minorFromDecimal, pageCurrency } from './amount.js';
8
9
  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 CheckoutOutcome = 'reviewed-dry-run'
11
+ /** Historical receipt value from the credential-disclosing dry-run. */
12
+ | 'filled-dry-run' | 'partial-fill' | 'adapter-required' | 'confirmed' | 'declined' | 'action-required' | 'cancelled' | 'blocked-by-mandate' | 'failed';
10
13
  export type CredentialLifecycle = 'not-requested' | 'minted-not-exposed' | 'partially-exposed' | 'fully-filled';
11
14
  export type CredentialTiming = {
12
15
  approvedAt?: string;
@@ -28,6 +31,7 @@ export type PrepareCheckoutOptions = {
28
31
  url: string;
29
32
  mandate: Mandate;
30
33
  browser: Browser;
34
+ contact?: Contact;
31
35
  amountMinor?: number;
32
36
  currency?: string;
33
37
  debugShotsDir?: string;
@@ -37,6 +41,7 @@ export type RunCheckoutOptions = PrepareCheckoutOptions & {
37
41
  contact: Contact;
38
42
  mode: CheckoutMode;
39
43
  outcomeDeadlineMs?: number;
44
+ resolveEmailOtp?: OtpResolver;
40
45
  };
41
46
  export type CheckoutReview = {
42
47
  id: string;
@@ -86,9 +91,9 @@ export type SubmitApprovedCheckoutOptions = {
86
91
  outcomeDeadlineMs?: number;
87
92
  challengeHoldMs?: number;
88
93
  onChallengeHold?: (signal: string | null) => void;
94
+ resolveEmailOtp?: OtpResolver;
89
95
  };
90
- export declare function minorFromDecimal(text: string): number | null;
91
- export declare function pageCurrency(text: string): string | null;
96
+ export declare function snapshotOrigin(rawUrl: string): string;
92
97
  export type PreparedCheckoutSession = {
93
98
  checkout: PreparedCheckout;
94
99
  context: BrowserContext;
@@ -141,6 +146,31 @@ export declare function debugShotMaskPlan(fields: FieldMap): {
141
146
  }[];
142
147
  };
143
148
  export declare function isStripeLinkConsumerRequest(url: string): boolean;
149
+ export declare function suppressStripeLink(page: Page, evidence: EvidenceLog): Promise<void>;
150
+ export type LinkQuietResult = {
151
+ fired: boolean;
152
+ waitedMs: number;
153
+ };
154
+ /**
155
+ * Wait for the suppressed Stripe Link lookup to fire and settle BEFORE the
156
+ * submit click. Stripe debounces its consumer-session lookup ~300ms after the
157
+ * email input changes; our fill→click gap is single-digit ms, so the (aborted)
158
+ * lookup used to land INSIDE Stripe's in-flight submit chain and kill it
159
+ * silently — the click looked accepted but tokenization never ran and the page
160
+ * sat on the form until the outcome deadline (#5879: three identical live
161
+ * stalls at donate.stripe.com). Verified live A/B on that page: instant click →
162
+ * dead submit, no /v1/payment_methods; lookup settled first → tokenization and
163
+ * the confirm step both reached.
164
+ *
165
+ * If the lookup already fired, only the short settle applies (lets Stripe's
166
+ * abort handling unwind). If it never fires — non-Link page variants, no email
167
+ * field — the bound expires and the click proceeds as before.
168
+ */
169
+ export declare function waitForLinkLookupQuiet(page: Page, opts?: {
170
+ boundMs?: number;
171
+ settleMs?: number;
172
+ delay?: (ms: number) => Promise<void>;
173
+ }): Promise<LinkQuietResult>;
144
174
  export declare function prepareCheckout(opts: PrepareCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<PrepareCheckoutResult>;
145
175
  export declare function submitApprovedCheckout(reviewId: string, opts: SubmitApprovedCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
146
176
  export declare function cancelPreparedCheckout(reviewId: string, detail?: string, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;