@visa/cli 4.1.0-rc.297 → 4.1.0-rc.299

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 +29 -45
  2. package/dist/cli.js +556 -786
  3. package/dist/mcp-server/index.js +408 -622
  4. package/dist/merchant-ucp-mcp/index.js +6 -6
  5. package/dist/skills/pair-visa-agent/SKILL.md +175 -240
  6. package/native/bin/win32-arm64/visa-runtime-signer.exe +0 -0
  7. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  8. package/native/bin/win32-x64/visa-runtime-signer.exe +0 -0
  9. package/package.json +2 -4
  10. package/server.json +2 -2
  11. package/dist/checkout-engine/adapters/generic.d.ts +0 -88
  12. package/dist/checkout-engine/adapters/generic.js +0 -526
  13. package/dist/checkout-engine/adapters/index.d.ts +0 -10
  14. package/dist/checkout-engine/adapters/index.js +0 -24
  15. package/dist/checkout-engine/adapters/shopify.d.ts +0 -98
  16. package/dist/checkout-engine/adapters/shopify.js +0 -744
  17. package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
  18. package/dist/checkout-engine/adapters/stripe-like.js +0 -21
  19. package/dist/checkout-engine/amount.d.ts +0 -17
  20. package/dist/checkout-engine/amount.js +0 -72
  21. package/dist/checkout-engine/browser-launch.d.ts +0 -51
  22. package/dist/checkout-engine/browser-launch.js +0 -96
  23. package/dist/checkout-engine/browserbase-browser.d.ts +0 -24
  24. package/dist/checkout-engine/browserbase-browser.js +0 -186
  25. package/dist/checkout-engine/ceremony.d.ts +0 -64
  26. package/dist/checkout-engine/ceremony.js +0 -261
  27. package/dist/checkout-engine/cli-engine.d.ts +0 -417
  28. package/dist/checkout-engine/cli-engine.js +0 -1331
  29. package/dist/checkout-engine/confirmed-merchants.d.ts +0 -31
  30. package/dist/checkout-engine/confirmed-merchants.js +0 -165
  31. package/dist/checkout-engine/detect.d.ts +0 -61
  32. package/dist/checkout-engine/detect.js +0 -398
  33. package/dist/checkout-engine/evidence.d.ts +0 -25
  34. package/dist/checkout-engine/evidence.js +0 -104
  35. package/dist/checkout-engine/executor.d.ts +0 -262
  36. package/dist/checkout-engine/executor.js +0 -1837
  37. package/dist/checkout-engine/hosted-approval.d.ts +0 -195
  38. package/dist/checkout-engine/hosted-approval.js +0 -501
  39. package/dist/checkout-engine/index.d.ts +0 -12
  40. package/dist/checkout-engine/index.js +0 -13
  41. package/dist/checkout-engine/instrument.d.ts +0 -61
  42. package/dist/checkout-engine/instrument.js +0 -87
  43. package/dist/checkout-engine/known-merchants.d.ts +0 -10
  44. package/dist/checkout-engine/known-merchants.js +0 -38
  45. package/dist/checkout-engine/live-fill-approval.d.ts +0 -37
  46. package/dist/checkout-engine/live-fill-approval.js +0 -76
  47. package/dist/checkout-engine/mandate/card-mandate.d.ts +0 -121
  48. package/dist/checkout-engine/mandate/card-mandate.js +0 -226
  49. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +0 -175
  50. package/dist/checkout-engine/mandate/mandate-ledger.js +0 -425
  51. package/dist/checkout-engine/mandate.d.ts +0 -33
  52. package/dist/checkout-engine/mandate.js +0 -135
  53. package/dist/checkout-engine/outcome.d.ts +0 -30
  54. package/dist/checkout-engine/outcome.js +0 -225
  55. package/dist/checkout-engine/owner-only-file.d.ts +0 -19
  56. package/dist/checkout-engine/owner-only-file.js +0 -41
  57. package/dist/checkout-engine/package.json +0 -3
  58. package/dist/checkout-engine/receipt-dir.d.ts +0 -6
  59. package/dist/checkout-engine/receipt-dir.js +0 -8
  60. package/dist/checkout-engine/receipt.d.ts +0 -135
  61. package/dist/checkout-engine/receipt.js +0 -148
  62. package/dist/checkout-engine/shopify-primary-domain.d.ts +0 -25
  63. package/dist/checkout-engine/shopify-primary-domain.js +0 -96
  64. package/dist/checkout-engine/trace-handles.d.ts +0 -8
  65. package/dist/checkout-engine/trace-handles.js +0 -12
  66. package/dist/checkout-engine/types.d.ts +0 -52
  67. package/dist/checkout-engine/types.js +0 -2
  68. package/dist/checkout-engine/unresolved-charges.d.ts +0 -34
  69. package/dist/checkout-engine/unresolved-charges.js +0 -134
  70. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -155
  71. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -493
  72. package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -144
  73. package/dist/checkout-engine/vgs-live-instrument.js +0 -229
  74. package/dist/checkout-engine/vic-confirmation.d.ts +0 -52
  75. package/dist/checkout-engine/vic-confirmation.js +0 -45
  76. package/dist/checkout-engine/web-bot-auth.d.ts +0 -98
  77. package/dist/checkout-engine/web-bot-auth.js +0 -218
@@ -1,261 +0,0 @@
1
- import http from 'node:http';
2
- import https from 'node:https';
3
- import { exec } from 'node:child_process';
4
- // In-run device-binding ceremony — replaces the manual dev-harness tab and the
5
- // exported assurance file. After the operator types the approval phrase, the
6
- // runner serves a single-purpose loopback page, opens it in the operator's
7
- // normal browser (where their passkey, popup allowance, and cert exception
8
- // already live), and receives the ceremony's assuranceData back in-process.
9
- // The assurance is therefore seconds old at intent mint (the only
10
- // proven-reliable configuration — see the 2026-07-17 PENDING-intent runs) and
11
- // never touches disk.
12
- //
13
- // Security posture (same as the dev-harness, contained tighter):
14
- // - The server binds 127.0.0.1 ONLY. /api/token hands the page a short-lived
15
- // VGS bearer, so the server must never be reachable from another machine.
16
- // - assuranceData exists in process memory only; nothing is persisted.
17
- // - The page carries no card data and no service-account secret.
18
- /** ISO-4217 numeric codes for the consent screen — mirrors the dev-harness map. */
19
- export const CURRENCY_NUMERIC = {
20
- USD: '840',
21
- CAD: '124',
22
- EUR: '978',
23
- GBP: '826',
24
- AUD: '036',
25
- };
26
- export function currencyNumeric(code) {
27
- return CURRENCY_NUMERIC[code.toUpperCase()] ?? null;
28
- }
29
- /**
30
- * Build the runner-side PurchaseAssurance from a completed ceremony. Scope
31
- * fields come verbatim from the checkout target, so the instrument's
32
- * scope-alignment validation (#5709) is satisfied by construction; `now` is
33
- * injectable for tests.
34
- */
35
- export function assuranceFromCeremony(target, assuranceData, now = new Date()) {
36
- return {
37
- assuranceData,
38
- mintedAt: now.toISOString(),
39
- merchantHost: new URL(target.merchantUrl).hostname,
40
- transactionAmount: target.transactionAmount,
41
- transactionCurrencyCode: target.transactionCurrencyCode,
42
- };
43
- }
44
- /**
45
- * The single-purpose approval page. Pure string builder so tests can assert
46
- * the config embed is intact and escape-safe. The config is embedded as JSON
47
- * with `<` escaped, so a hostile merchant name cannot break out of the script
48
- * element.
49
- */
50
- export function ceremonyPageHtml(cfg) {
51
- const esc = (s) => s.replace(/[&<>"']/g, (ch) => {
52
- const map = {
53
- '&': '&amp;',
54
- '<': '&lt;',
55
- '>': '&gt;',
56
- '"': '&quot;',
57
- "'": '&#39;',
58
- };
59
- return map[ch];
60
- });
61
- const json = JSON.stringify(cfg).replace(/</g, '\\u003c');
62
- return `<!doctype html>
63
- <html>
64
- <head>
65
- <meta charset="utf-8" />
66
- <meta name="viewport" content="width=device-width, initial-scale=1" />
67
- <title>Approve purchase — Visa</title>
68
- <style>
69
- body { font: 15px/1.5 Menlo, 'SF Mono', monospace; max-width: 640px; margin: 40px auto; padding: 0 16px; color: #1a1a1a; background: #fff; }
70
- h1 { font-size: 16px; }
71
- .facts { border: 1px solid #e5e5e5; border-radius: 12px; padding: 14px; margin: 14px 0; background: #fafafa; }
72
- .facts b { font-size: 18px; }
73
- button { font: inherit; padding: 10px 16px; margin: 6px 6px 6px 0; border: 1px solid #111; background: #111; color: #fff; border-radius: 8px; cursor: pointer; }
74
- button:disabled { opacity: .4; cursor: default; }
75
- button.ghost { background: #fff; color: #111; }
76
- input { font: inherit; padding: 8px; border: 1px solid #d4d4d4; border-radius: 8px; }
77
- #iframeBox { border: 2px dashed #d4d4d4; border-radius: 12px; min-height: 90px; padding: 8px; margin: 12px 0; }
78
- #log { color: #666; font-size: 12.5px; white-space: pre-wrap; }
79
- #otpBox { display: none; margin: 10px 0; }
80
- .ok { color: #16a34a } .err { color: #be123c }
81
- </style>
82
- </head>
83
- <body>
84
- <h1>Approve this purchase with your passkey</h1>
85
- <div class="facts">${esc(cfg.merchantName)}<br /><b>${esc(cfg.currency)} ${esc(cfg.amount)}</b></div>
86
- <button id="go">Start approval</button>
87
- <button id="tap" disabled>Approve with passkey</button>
88
- <div id="otpBox"><span id="otpMethods"></span> <input id="otpCode" placeholder="code" style="width:120px" /> <button id="otpSubmit" class="ghost">Submit code</button></div>
89
- <div id="iframeBox"></div>
90
- <div id="log">The agent is waiting in the terminal. Complete the approval here, then return to it.</div>
91
- <script type="application/json" id="cfg">${json}</script>
92
- <script type="module">
93
- import { VgsAgenticAuth } from './vgs-agentic-auth.js'
94
- const cfg = JSON.parse(document.getElementById('cfg').textContent)
95
- const log = (m, c = '') => { const el = document.getElementById('log'); el.className = c; el.textContent = m }
96
- let session = null
97
- document.getElementById('go').onclick = async () => {
98
- const go = document.getElementById('go')
99
- go.disabled = true
100
- try {
101
- log('starting the Visa session…')
102
- const { access_token, error } = await (await fetch('/api/token')).json()
103
- if (error) throw new Error(error)
104
- const flow = new VgsAgenticAuth({
105
- tokenId: cfg.tokenId, environment: cfg.environment, consumerEmail: cfg.consumerEmail,
106
- accessToken: access_token, timeout: 180000,
107
- authenticationAmount: cfg.amount, currencyCode: cfg.currencyNumericCode, merchantName: cfg.merchantName,
108
- })
109
- const box = document.getElementById('iframeBox'); box.innerHTML = ''
110
- session = await flow.startSession(box)
111
- if (session.needsOtp) {
112
- const b = document.getElementById('otpBox'), d = document.getElementById('otpMethods')
113
- b.style.display = 'block'; d.innerHTML = ''
114
- log('your bank wants a one-time code first — pick a delivery method.')
115
- for (const m of session.otpMethods || []) {
116
- const btn = document.createElement('button'); btn.className = 'ghost'
117
- btn.textContent = 'Send via ' + (m.method || m.identifier)
118
- btn.onclick = async () => { try { await session.requestOtp(m); log('code sent — type it and submit.') } catch (e) { log('sending the code failed: ' + e.message, 'err') } }
119
- d.appendChild(btn)
120
- }
121
- } else {
122
- log('ready — click "Approve with passkey".', 'ok')
123
- document.getElementById('tap').disabled = false
124
- }
125
- } catch (e) {
126
- log('could not start: ' + (e && e.message || e) + ' — click "Start approval" to retry.', 'err')
127
- try { session && session.destroy && session.destroy() } catch {}
128
- session = null
129
- go.disabled = false
130
- }
131
- }
132
- document.getElementById('otpSubmit').onclick = async () => {
133
- try {
134
- await session.submitOtp(document.getElementById('otpCode').value.trim())
135
- document.getElementById('otpBox').style.display = 'none'
136
- log('code accepted — click "Approve with passkey".', 'ok')
137
- document.getElementById('tap').disabled = false
138
- } catch (e) { log('code rejected: ' + (e && e.message || e), 'err') }
139
- }
140
- document.getElementById('tap').onclick = async () => {
141
- const tap = document.getElementById('tap')
142
- tap.disabled = true
143
- try {
144
- log('waiting for your passkey…')
145
- const assuranceData = await session.authenticate()
146
- try { session.destroy && session.destroy() } catch {}
147
- const r = await (await fetch('/api/assurance', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assuranceData }) })).json()
148
- if (r.error) throw new Error(r.error)
149
- document.getElementById('go').disabled = true
150
- log('approved — return to the terminal. You can close this tab.', 'ok')
151
- } catch (e) {
152
- log('passkey failed: ' + (e && e.message || e) + ' — click "Start approval" to retry.', 'err')
153
- try { session && session.destroy && session.destroy() } catch {}
154
- session = null
155
- document.getElementById('go').disabled = false
156
- }
157
- }
158
- </script>
159
- </body>
160
- </html>
161
- `;
162
- }
163
- export const CEREMONY_DEFAULT_PORT = 4400;
164
- export const CEREMONY_DEFAULT_TIMEOUT_MS = 4 * 60 * 1000;
165
- /**
166
- * Serve the approval page on loopback, open it in the operator's browser, and
167
- * resolve with the fresh PurchaseAssurance once the passkey completes. Rejects
168
- * on timeout, port conflict, or server failure; the server is always closed.
169
- */
170
- export function runInteractiveCeremony(opts) {
171
- const port = opts.port ?? CEREMONY_DEFAULT_PORT;
172
- const timeoutMs = opts.timeoutMs ?? CEREMONY_DEFAULT_TIMEOUT_MS;
173
- const log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
174
- const openUrl = opts.openUrl ??
175
- ((url) => {
176
- // macOS `open`; on failure the operator still has the printed URL.
177
- exec(`open ${JSON.stringify(url)}`, () => { });
178
- });
179
- const html = ceremonyPageHtml(opts.page);
180
- return new Promise((resolve, reject) => {
181
- let settled = false;
182
- const handler = async (req, res) => {
183
- const send = (code, body, type = 'application/json') => {
184
- res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' });
185
- res.end(body);
186
- };
187
- try {
188
- const path = new URL(req.url ?? '/', 'http://localhost').pathname;
189
- if (path === '/')
190
- return send(200, html, 'text/html');
191
- if (path === '/vgs-agentic-auth.js')
192
- return send(200, opts.vendorSdkJs, 'text/javascript');
193
- if (path === '/api/token') {
194
- const access_token = await opts.mintAccessToken();
195
- return send(200, JSON.stringify({ access_token }));
196
- }
197
- if (path === '/api/assurance' && req.method === 'POST') {
198
- const chunks = [];
199
- for await (const chunk of req)
200
- chunks.push(chunk);
201
- let body = {};
202
- try {
203
- body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
204
- }
205
- catch {
206
- return send(400, JSON.stringify({ error: 'invalid JSON' }));
207
- }
208
- if (body.assuranceData == null) {
209
- return send(400, JSON.stringify({ error: 'assuranceData required' }));
210
- }
211
- // Flush { ok: true } BEFORE teardown: finish() drops every
212
- // connection, and destroying the in-flight socket first would show
213
- // the operator a failed approval while the runner proceeds — the
214
- // end() callback fires once the response has been handed to the OS.
215
- const assurance = assuranceFromCeremony(opts.target, body.assuranceData);
216
- res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
217
- res.end(JSON.stringify({ ok: true }), () => finish(null, assurance));
218
- return;
219
- }
220
- return send(404, JSON.stringify({ error: 'not found' }));
221
- }
222
- catch (err) {
223
- return send(500, JSON.stringify({ error: err.message }));
224
- }
225
- };
226
- const server = opts.tls ? https.createServer(opts.tls, handler) : http.createServer(handler);
227
- const timer = setTimeout(() => {
228
- finish(new Error(`no passkey approval within ${Math.round(timeoutMs / 1000)}s — ` +
229
- 'the checkout was not touched; rerun when ready'));
230
- }, timeoutMs);
231
- timer.unref();
232
- function finish(err, assurance) {
233
- if (settled)
234
- return;
235
- settled = true;
236
- clearTimeout(timer);
237
- server.closeAllConnections?.();
238
- server.close(() => {
239
- if (err)
240
- reject(err);
241
- else
242
- resolve(assurance);
243
- });
244
- }
245
- server.on('error', (err) => {
246
- finish(err.code === 'EADDRINUSE'
247
- ? new Error(`port ${port} is already in use (is the dev harness still running?) — ` +
248
- 'stop it and rerun')
249
- : err);
250
- });
251
- // Loopback ONLY: /api/token hands the page a live VGS bearer.
252
- server.listen(port, '127.0.0.1', () => {
253
- const address = server.address();
254
- const boundPort = typeof address === 'object' && address ? address.port : port;
255
- const scheme = opts.tls ? 'https' : 'http';
256
- const pageUrl = `${scheme}://localhost:${boundPort}`;
257
- log(`Visa approval page: ${pageUrl} — complete the passkey there.`);
258
- openUrl(pageUrl);
259
- });
260
- });
261
- }
@@ -1,417 +0,0 @@
1
- import { type Browser } from 'playwright-core';
2
- import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, type CheckoutMode, type CheckoutOutcome, type CheckoutFailureCode, type CheckoutResult, type PreparedCheckoutSessionStore } from './executor.js';
3
- import type { MandateRefusalCode } from './mandate.js';
4
- import { claimMandatePickup as realClaimMandatePickup, runHostedApproval as realRunHostedApproval } from './hosted-approval.js';
5
- import { type VgsCheckoutTarget } from './vgs-live-instrument.js';
6
- import { ServerIntentError, serverFetchCryptogram, serverPostConfirmation } from './vgs-gateway/server-mint-client.js';
7
- import { type CardMandateFacts } from './mandate/card-mandate.js';
8
- import { MandateLedger } from './mandate/mandate-ledger.js';
9
- import { writeReceipt as realWriteReceipt } from './receipt.js';
10
- import { reportVicOutcome as realReportVicOutcome, type VicConfirmationReport } from './vic-confirmation.js';
11
- import type { Contact, OtpResolver } from './types.js';
12
- /**
13
- * A card-mandate draw failed transiently (retryable) rather than definitively.
14
- * Gateway 5xx, "server cryptogram not completed / try again", and network
15
- * reset/timeout errors are transient: the mandate stays healthy and must NOT be
16
- * disabled. Walks the error's cause chain so a wrapped MandateDrawDeclinedError
17
- * is classified by its underlying gateway error. Exported for tests.
18
- */
19
- export declare function isTransientDrawFailure(err: unknown): boolean;
20
- /**
21
- * A verdict refusal may be wrapped by drawFromMandate after its local
22
- * reservation is released. Walk the cause chain so the original auth status +
23
- * reasons still decide whether the mandate is permanently disabled.
24
- */
25
- export declare function classifyCardDrawVerdictFailure(err: unknown): {
26
- transient: boolean;
27
- grantCapacity: boolean;
28
- reasons: string[];
29
- } | null;
30
- export type CliReviewInput = {
31
- url: string;
32
- checkoutRoute: 'guest-card';
33
- amount: string;
34
- currency: string;
35
- credentialPath: string;
36
- /** See {@link CardInstrumentSource}. */
37
- cardTokenId?: string;
38
- /** Exact request-key identity selected by the caller. */
39
- agentJkt?: string;
40
- /** Owner-facing selected-agent label for the compact local receipt. */
41
- agentName?: string;
42
- /** Safe display suffix derived from the selected card grant label. */
43
- cardLast4?: string;
44
- contact: Contact;
45
- approvalBaseUrl: string;
46
- /**
47
- * #8047: agent-authored purchase narrative captured at review time and
48
- * replayed into the card draw's Transaction Intent Journal at pay time.
49
- */
50
- intentNarrative?: CardIntentNarrative;
51
- merchantName?: string;
52
- merchantCountryCode?: string;
53
- trustedMerchantIdentity?: Readonly<{
54
- handoffId: string;
55
- checkoutId: string;
56
- allowedOrigins: readonly string[];
57
- expiresAt: string;
58
- }>;
59
- };
60
- export type CliReviewFacts = {
61
- reviewId: string;
62
- merchantHost: string;
63
- amountMinor: number;
64
- currency: string;
65
- submitTargetFingerprint: string;
66
- detectedRoles: string[];
67
- };
68
- /**
69
- * A checkout was inspected successfully but cannot be reviewed safely.
70
- *
71
- * The explicit fields survive the CLI's copied-engine boundary structurally,
72
- * so MCP callers do not have to parse the human-readable message.
73
- */
74
- export declare class CheckoutReviewRefusedError extends Error {
75
- readonly code = "CHECKOUT_REVIEW_REFUSED";
76
- readonly checkoutOutcome: CheckoutOutcome;
77
- readonly failureCode?: CheckoutFailureCode;
78
- /** Bounded mandate/trusted-identity reason (#8669); absent for other outcomes. */
79
- readonly refusalCode?: MandateRefusalCode;
80
- readonly requiresAdapter: string[];
81
- readonly detectedRoles: string[];
82
- readonly receiptWrite: ReceiptWriteObservation;
83
- readonly detail?: string;
84
- constructor(result: CheckoutResult, receiptWrite: ReceiptWriteObservation);
85
- }
86
- export type CliPayInput = CliReviewInput & {
87
- reviewId: string;
88
- submit: boolean;
89
- /**
90
- * Headless handoff for this payment attempt. The MCP layer uses it to return
91
- * the hosted passkey URL to a messaging surface while the engine continues
92
- * waiting in-process.
93
- */
94
- onApprovalUrl?: (url: string) => void;
95
- };
96
- export type CliReceiptFacts = {
97
- outcome: CheckoutOutcome;
98
- confirmationRef: string | null;
99
- receiptPath: string | null;
100
- detail: string | null;
101
- vicConfirmation: VicConfirmationReport | null;
102
- /**
103
- * Which credential path actually ran: `mandate` = tap-free draw against an
104
- * existing card mandate; `null` = none ran (a pre-flight refusal, e.g. no
105
- * prepared review or no covering mandate). Transparency, never magic — the
106
- * caller can always see whether a passkey was skipped.
107
- */
108
- source: 'mandate' | null;
109
- /** Remaining mandate budget (minor units) after a mandate draw; else null. */
110
- remainingMinor: number | null;
111
- /**
112
- * Immutable VIC/VGS intent used for server-side receipt correlation. This is
113
- * an opaque non-credential identifier; it never carries the mint/verdict
114
- * bearer that authorizes a cryptogram or confirmation. Present only after a
115
- * card confirmation target exists.
116
- */
117
- processorIntentId?: string | null;
118
- /** True only when the checkout engine recorded a credential-minted step. */
119
- credentialIssued: boolean;
120
- /** True only when at least one payment field was successfully filled. */
121
- credentialDisclosed: boolean;
122
- /** Privacy-safe local receipt persistence outcome for centralized correlation. */
123
- receiptWrite?: ReceiptWriteObservation;
124
- };
125
- type PayAttempt = {
126
- fingerprint: string;
127
- promise: Promise<CliReceiptFacts>;
128
- };
129
- export type CliStartMandateInput = {
130
- /** Optional local card-capability selector (legacy name or exact request-key JKT). */
131
- agentRef?: string;
132
- ceiling: string;
133
- currency: string;
134
- credentialPath: string;
135
- /** See {@link CardInstrumentSource}. */
136
- cardTokenId?: string;
137
- contact: Contact;
138
- approvalBaseUrl: string;
139
- /**
140
- * Per-purchase cap (decimal string, > 0 and <= ceiling). Registered with the
141
- * approval context so the operator reads it as a worst-case term, and carried
142
- * onto the budget mint token so it is enforced at draw time.
143
- */
144
- perTransaction?: string;
145
- /**
146
- * Agent-supplied one-liner shown on the approval page in a labeled
147
- * "written by the agent" block — provenance for the human, never trusted.
148
- */
149
- intent?: string;
150
- /**
151
- * Per-call relay of the hosted-approval URL, the moment it is known and
152
- * BEFORE the (up-to-timeout) wait. Takes precedence over the engine-level
153
- * `deps.onApprovalUrl`.
154
- *
155
- * The engine seam was deps-only, and the MCP surface builds its engine once
156
- * per process without it — so `start_card_mandate` produced an approval URL,
157
- * printed it to stderr, and blocked the agent's turn for minutes with no way
158
- * to tell the human what to open. A per-call hook lets one caller relay the
159
- * URL without every caller sharing one engine-wide callback.
160
- */
161
- onApprovalUrl?: (url: string) => void;
162
- };
163
- export type CliClaimMandateInput = {
164
- /** Optional local card-capability selector (legacy name or exact request-key JKT). */
165
- agentRef?: string;
166
- /** Single-use pickup code the owner handed over from the panel. */
167
- pickupCode: string;
168
- /**
169
- * The agent handoff this runtime just redeemed, when the pickup code rode
170
- * inside its claim code. A dialog-minted budget pins that handoff instead of
171
- * a key — there was no key yet — so this is what the pin is checked against.
172
- */
173
- expectedHandoffId?: string;
174
- credentialPath: string;
175
- /** See {@link CardInstrumentSource}. */
176
- cardTokenId?: string;
177
- approvalBaseUrl: string;
178
- };
179
- export type CliMandateFacts = CardMandateFacts & {
180
- merchantHost: string;
181
- /**
182
- * True when the mandate minted its ceiling intent but the #5942 register
183
- * handshake failed, so `findCovering` will SKIP it and no tap-free draw is
184
- * possible. The mandate exists but is not usable — the caller must surface
185
- * this (not report a plain success). Absent/false means registration succeeded;
186
- * mandate-start now refuses before approval when no capability can register.
187
- */
188
- registerFailed?: boolean;
189
- /**
190
- * The server's refusal reason when {@link registerFailed} is true, verbatim.
191
- * Carried out so the CLI can distinguish causes that need DIFFERENT operator
192
- * actions — notably `token_mismatch`, which means the on-device grant record's
193
- * cached token no longer matches the owner's live agentic token (they
194
- * re-enrolled a card after the grant) and is fixed by re-running `grant-card`,
195
- * not by retrying `mandate start`. Without the reason every failure reads as
196
- * "the auth server was unreachable", which sends the operator in a loop.
197
- */
198
- registerFailureReason?: string;
199
- };
200
- /**
201
- * The owner approved a budget but the intent bootstrap did not complete
202
- * (#8470). `resumable` means the same approval can be resumed without a second
203
- * passkey ceremony: the server's bootstrap state is at-most-once per signed
204
- * token, so a resume can only recover an intent that already exists or
205
- * dispatch once when nothing was ever dispatched — never create a sibling.
206
- */
207
- export type CardMandateActivationFacts = {
208
- phase: 'intent';
209
- /** `uncertain`: the provider may have created the intent. `not_created`: proven not. */
210
- outcome: 'uncertain' | 'not_created';
211
- resumable: boolean;
212
- status: number | null;
213
- errorCode: string | null;
214
- requestId: string | null;
215
- bootstrapState: string;
216
- /** Opaque, process-bound handle for {@link CliCheckoutEngine.resumeCardMandate}. */
217
- resumeToken?: string;
218
- /** When the approval's bootstrap credential stops being usable. */
219
- resumeExpiresAt?: string;
220
- };
221
- export declare class CardMandateActivationError extends Error {
222
- readonly facts: CardMandateActivationFacts;
223
- readonly code = "CARD_MANDATE_ACTIVATION_INCOMPLETE";
224
- constructor(message: string, facts: CardMandateActivationFacts);
225
- }
226
- export type CliResumeMandateInput = {
227
- resumeToken: string;
228
- };
229
- /**
230
- * Sort a budget-intent route failure into resume semantics. Exported for the
231
- * regression net; the truth table is the product contract of #8470.
232
- */
233
- export declare function classifyServerIntentFailure(err: ServerIntentError): {
234
- outcome: 'uncertain' | 'not_created';
235
- resumable: boolean;
236
- };
237
- type Session = {
238
- browser: Browser;
239
- /** Exact caller URL repeated at pay time; may contain a UCP capability. */
240
- requestUrl: string;
241
- checkoutRoute: 'guest-card';
242
- target: VgsCheckoutTarget;
243
- amountMinor: number;
244
- currency: string;
245
- contact: Contact;
246
- agentJkt?: string;
247
- /** #8047: narrative for this review's journal, carried review -> pay. */
248
- intentNarrative?: CardIntentNarrative;
249
- cleanupTimer: ReturnType<typeof setTimeout>;
250
- };
251
- /**
252
- * #8047 Transaction Intent Journal narrative — the agent-authored half of the
253
- * journal. Structural mirror of the CLI card client's `CardIntentNarrative`;
254
- * the engine only threads it through and never interprets it.
255
- */
256
- export interface CardIntentNarrative {
257
- userRequest: string;
258
- product: string;
259
- purpose?: string;
260
- quantity?: number;
261
- expectedResult?: string;
262
- recurring?: boolean;
263
- userIntentSalt?: string;
264
- }
265
- export interface CardDrawVerdictDraw {
266
- tokenId: string;
267
- amount: string;
268
- currency: string;
269
- merchantName: string;
270
- merchantUrl: string;
271
- merchantCountryCode: string;
272
- }
273
- export interface CardDrawVerdictCapability {
274
- /** Opaque to the engine — passed straight back to {@link CardDrawVerdictSeam.fetchVerdict}. */
275
- agentKey: unknown;
276
- /** Root-signed delegation for a short-lived device key. */
277
- runtimeCertificate?: string;
278
- agentJkt: string;
279
- /** Auth origin that minted the binding and hosts the /v4/card/draw* routes. */
280
- authBaseUrl: string;
281
- }
282
- export interface CardDrawVerdictSeam {
283
- loadCapability: (agentRef?: string) => CardDrawVerdictCapability | null;
284
- fetchVerdict: (input: {
285
- authBaseUrl: string;
286
- agentKey: unknown;
287
- runtimeCertificate?: string;
288
- mandateId: string;
289
- drawId: string;
290
- draw: CardDrawVerdictDraw;
291
- /** #8047: narrative the client turns into the draw's journal sidecar. */
292
- intentNarrative?: CardIntentNarrative;
293
- }) => Promise<{
294
- verdict: string;
295
- remainingCents: number;
296
- }>;
297
- }
298
- export interface CardMandateRegisterSeam {
299
- loadCapability: (agentRef?: string) => {
300
- agentKey: unknown;
301
- runtimeCertificate?: string;
302
- agentJkt: string;
303
- authBaseUrl: string;
304
- } | null;
305
- register: (input: {
306
- authBaseUrl: string;
307
- agentKey: unknown;
308
- runtimeCertificate?: string;
309
- mandateId: string;
310
- mintToken: string;
311
- ceiling: string;
312
- currency: string;
313
- }) => Promise<{
314
- ok: boolean;
315
- reason?: string;
316
- /**
317
- * The ceiling the server committed — `min(requested, the owner's live card
318
- * grant cap)`. Omitted by an older auth that does not report it, in which
319
- * case the requested ceiling stands.
320
- */
321
- approvedCeilingMinor?: number;
322
- }>;
323
- }
324
- export type CliEngineDeps = {
325
- launchBrowser?: () => Promise<Browser>;
326
- prepareCheckout?: typeof realPrepareCheckout;
327
- submitApprovedCheckout?: typeof realSubmitApprovedCheckout;
328
- runHostedApproval?: typeof realRunHostedApproval;
329
- /** Injectable pickup-code redeem for claimCardMandate — tests pass a fake. */
330
- claimMandatePickup?: typeof realClaimMandatePickup;
331
- /**
332
- * Relay the hosted-approval URL to the caller as DATA the moment it is known,
333
- * before the (up-to-timeout) poll wait. A headless agent surface wires this to
334
- * hand the URL to its operator; the raw CLI leaves it unset (the URL prints to
335
- * stderr and best-effort opens a browser).
336
- */
337
- onApprovalUrl?: (url: string) => void;
338
- reportVicOutcome?: typeof realReportVicOutcome;
339
- writeReceipt?: typeof realWriteReceipt;
340
- /**
341
- * Privacy-safe receipt persistence lifecycle signal. The event deliberately
342
- * excludes the receipt body, checkout URL, filesystem path, and raw error.
343
- * Observer failures are swallowed so logging can never alter checkout state.
344
- */
345
- onReceiptWrite?: (event: ReceiptWriteObservation) => void | Promise<void>;
346
- /** Injectable privacy-safe id source for terminal observations before reviewId exists. */
347
- createObservationId?: () => string;
348
- store?: PreparedCheckoutSessionStore;
349
- sessions?: Map<string, Session>;
350
- /** Exact-review singleflight registry; tests inject a fresh map for isolation. */
351
- payAttempts?: Map<string, PayAttempt>;
352
- ttlMs?: number;
353
- /** Owner-only card-mandate ledger — defaults to the ~/.visa-mcp singleton. */
354
- ledger?: MandateLedger;
355
- /** Injectable clock for mandate expiry decisions (tests pin it). */
356
- now?: () => Date;
357
- /**
358
- * Cryptogram transport for the TAP-FREE mandate draw — defaults to the real
359
- * server-side mint route. Injectable so a test can drive the draw-reject ->
360
- * markUnhonored path with no network.
361
- */
362
- serverFetchCryptogram?: typeof serverFetchCryptogram;
363
- /** Injectable confirmation transport; defaults to verify-web. */
364
- serverPostConfirmation?: typeof serverPostConfirmation;
365
- /**
366
- * #5923 delegated card-draw verdict seam (see {@link CardDrawVerdictSeam}).
367
- * Injected by the CLI when the runtime holds separately provisioned card
368
- * authority. When absent, a covering-mandate draw fails before cryptogram mint.
369
- */
370
- cardDrawVerdict?: CardDrawVerdictSeam;
371
- /**
372
- * #5942 delegated card-mandate register seam (see {@link CardMandateRegisterSeam}).
373
- * Required by mandate-start. When absent, the ceremony is refused before
374
- * passkey approval because a budget token cannot act as draw authority.
375
- */
376
- cardMandateRegister?: CardMandateRegisterSeam;
377
- /**
378
- * Canonical mailbox OTP reader supplied by the runtime. The checkout engine
379
- * owns no mailbox credential and never imports an email provider package.
380
- */
381
- resolveEmailOtp?: OtpResolver;
382
- };
383
- export type ReceiptWriteObservation = {
384
- event: 'checkout_receipt_write';
385
- status: 'written' | 'failed';
386
- mode: CheckoutMode;
387
- checkoutOutcome: CheckoutOutcome;
388
- merchantHost: string;
389
- observationId: string;
390
- failureCode: string | null;
391
- errorCode: string | null;
392
- panRedactions: number | null;
393
- };
394
- export declare function createCliCheckoutEngine(deps?: CliEngineDeps): {
395
- startCardMandate(input: CliStartMandateInput): Promise<CliMandateFacts>;
396
- resumeCardMandate(input: CliResumeMandateInput): Promise<CliMandateFacts>;
397
- claimCardMandate(input: CliClaimMandateInput): Promise<CliMandateFacts>;
398
- review(input: CliReviewInput): Promise<CliReviewFacts>;
399
- /**
400
- * Give up a prepared review without paying it (#7100).
401
- *
402
- * The retained browser is what lets a same-process MCP `review` → `pay`
403
- * draw against the checkout it already inspected. A TERMINAL review-only
404
- * run has no such second call — that process tells the operator to re-RUN
405
- * with `--submit`, and the new process cannot reach this one's in-memory
406
- * session. So the browser sat open for the full TTL, and because a live
407
- * browser connection keeps the event loop alive, a command that had already
408
- * succeeded looked hung until Ctrl-C.
409
- *
410
- * Idempotent and non-throwing: an unknown or already-released id is a
411
- * no-op, so it is safe on an error path that may not have prepared anything
412
- * and safe to call twice.
413
- */
414
- releaseReview(reviewId: string): Promise<void>;
415
- pay(input: CliPayInput): Promise<CliReceiptFacts>;
416
- };
417
- export {};