@visa/cli 4.1.0-rc.3 → 4.1.0-rc.31

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 (70) hide show
  1. package/README.md +132 -242
  2. package/dist/checkout-engine/adapters/generic.d.ts +19 -0
  3. package/dist/checkout-engine/adapters/generic.js +201 -0
  4. package/dist/checkout-engine/adapters/index.d.ts +7 -0
  5. package/dist/checkout-engine/adapters/index.js +17 -0
  6. package/dist/checkout-engine/adapters/stripe-like.d.ts +10 -0
  7. package/dist/checkout-engine/adapters/stripe-like.js +21 -0
  8. package/dist/checkout-engine/browser-launch.d.ts +46 -0
  9. package/dist/checkout-engine/browser-launch.js +81 -0
  10. package/dist/checkout-engine/ceremony.d.ts +64 -0
  11. package/dist/checkout-engine/ceremony.js +261 -0
  12. package/dist/checkout-engine/cli-engine.d.ts +208 -0
  13. package/dist/checkout-engine/cli-engine.js +584 -0
  14. package/dist/checkout-engine/detect.d.ts +61 -0
  15. package/dist/checkout-engine/detect.js +392 -0
  16. package/dist/checkout-engine/evidence.d.ts +25 -0
  17. package/dist/checkout-engine/evidence.js +104 -0
  18. package/dist/checkout-engine/executor.d.ts +174 -0
  19. package/dist/checkout-engine/executor.js +1306 -0
  20. package/dist/checkout-engine/hosted-approval.d.ts +135 -0
  21. package/dist/checkout-engine/hosted-approval.js +311 -0
  22. package/dist/checkout-engine/index.d.ts +6 -0
  23. package/dist/checkout-engine/index.js +8 -0
  24. package/dist/checkout-engine/inline-target.d.ts +13 -0
  25. package/dist/checkout-engine/inline-target.js +37 -0
  26. package/dist/checkout-engine/instrument.d.ts +55 -0
  27. package/dist/checkout-engine/instrument.js +87 -0
  28. package/dist/checkout-engine/live-fill-approval.d.ts +43 -0
  29. package/dist/checkout-engine/live-fill-approval.js +90 -0
  30. package/dist/checkout-engine/mandate/card-mandate.d.ts +117 -0
  31. package/dist/checkout-engine/mandate/card-mandate.js +221 -0
  32. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +135 -0
  33. package/dist/checkout-engine/mandate/mandate-ledger.js +318 -0
  34. package/dist/checkout-engine/mandate.d.ts +25 -0
  35. package/dist/checkout-engine/mandate.js +100 -0
  36. package/dist/checkout-engine/outcome.d.ts +30 -0
  37. package/dist/checkout-engine/outcome.js +225 -0
  38. package/dist/checkout-engine/owner-only-file.d.ts +19 -0
  39. package/dist/checkout-engine/owner-only-file.js +41 -0
  40. package/dist/checkout-engine/package.json +3 -0
  41. package/dist/checkout-engine/pay-args.d.ts +14 -0
  42. package/dist/checkout-engine/pay-args.js +44 -0
  43. package/dist/checkout-engine/pay.d.ts +1 -0
  44. package/dist/checkout-engine/pay.js +13 -0
  45. package/dist/checkout-engine/receipt.d.ts +81 -0
  46. package/dist/checkout-engine/receipt.js +109 -0
  47. package/dist/checkout-engine/repo-env.d.ts +11 -0
  48. package/dist/checkout-engine/repo-env.js +23 -0
  49. package/dist/checkout-engine/run-live-fill.d.ts +1 -0
  50. package/dist/checkout-engine/run-live-fill.js +493 -0
  51. package/dist/checkout-engine/types.d.ts +39 -0
  52. package/dist/checkout-engine/types.js +2 -0
  53. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +74 -0
  54. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +248 -0
  55. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +82 -0
  56. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +178 -0
  57. package/dist/checkout-engine/vgs-live-instrument.d.ts +168 -0
  58. package/dist/checkout-engine/vgs-live-instrument.js +289 -0
  59. package/dist/checkout-engine/vic-confirmation.d.ts +34 -0
  60. package/dist/checkout-engine/vic-confirmation.js +39 -0
  61. package/dist/cli.js +327 -375
  62. package/dist/mcp-server/index.js +253 -163
  63. package/dist/skills/pair-visa-agent/RUNTIMES.md +79 -0
  64. package/dist/skills/pair-visa-agent/SKILL.md +403 -0
  65. package/dist/skills/pair-visa-agent/scripts/setup.mjs +48 -0
  66. package/install.ps1 +3 -41
  67. package/install.sh +3 -35
  68. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  69. package/package.json +9 -5
  70. package/server.json +3 -3
@@ -0,0 +1,261 @@
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
+ }
@@ -0,0 +1,208 @@
1
+ import { type Browser } from 'playwright-core';
2
+ import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, type PreparedCheckoutSessionStore } from './executor.js';
3
+ import { runHostedApproval as realRunHostedApproval } from './hosted-approval.js';
4
+ import { type VgsCheckoutTarget } from './vgs-live-instrument.js';
5
+ import { serverFetchCryptogram } from './vgs-gateway/server-mint-client.js';
6
+ import { type CardMandateFacts } from './mandate/card-mandate.js';
7
+ import { MandateLedger } from './mandate/mandate-ledger.js';
8
+ import { writeReceipt as realWriteReceipt } from './receipt.js';
9
+ import { reportVicOutcome as realReportVicOutcome, type VicConfirmationReport } from './vic-confirmation.js';
10
+ import type { Contact } from './types.js';
11
+ /**
12
+ * A card-mandate draw failed transiently (retryable) rather than definitively.
13
+ * Gateway 5xx, "server cryptogram not completed / try again", and network
14
+ * reset/timeout errors are transient: the mandate stays healthy and must NOT be
15
+ * disabled. Walks the error's cause chain so a wrapped MandateDrawDeclinedError
16
+ * is classified by its underlying gateway error. Exported for tests.
17
+ */
18
+ export declare function isTransientDrawFailure(err: unknown): boolean;
19
+ export type CliReviewInput = {
20
+ url: string;
21
+ amount: string;
22
+ currency: string;
23
+ credentialPath: string;
24
+ contact: Contact;
25
+ approvalBaseUrl: string;
26
+ merchantName?: string;
27
+ merchantCountryCode?: string;
28
+ };
29
+ export type CliReviewFacts = {
30
+ reviewId: string;
31
+ merchantHost: string;
32
+ amountMinor: number;
33
+ currency: string;
34
+ submitTargetFingerprint: string;
35
+ detectedRoles: string[];
36
+ };
37
+ export type CliPayInput = CliReviewInput & {
38
+ reviewId: string;
39
+ submit: boolean;
40
+ /**
41
+ * Headless handoff for this payment attempt. The MCP layer uses it to return
42
+ * the hosted passkey URL to a messaging surface while the engine continues
43
+ * waiting in-process.
44
+ */
45
+ onApprovalUrl?: (url: string) => void;
46
+ };
47
+ export type CliReceiptFacts = {
48
+ outcome: string;
49
+ confirmationRef: string | null;
50
+ receiptPath: string | null;
51
+ detail: string | null;
52
+ vicConfirmation: VicConfirmationReport | null;
53
+ /**
54
+ * Which credential path actually ran: `mandate` = tap-free draw against an
55
+ * existing card mandate; `fresh-tap` = today's 1:1 hosted-passkey mint; `null`
56
+ * = neither ran (a pre-flight refusal, e.g. no prepared review). Transparency,
57
+ * never magic — the caller can always see whether a passkey was skipped.
58
+ */
59
+ source: 'mandate' | 'fresh-tap' | null;
60
+ /** Remaining mandate budget (minor units) after a mandate draw; else null. */
61
+ remainingMinor: number | null;
62
+ };
63
+ export type CliStartMandateInput = {
64
+ /** The merchant URL for a merchant-scoped mandate. Optional (and ignored) when
65
+ * `anyMerchant` is set — a budget mandate is not tied to a merchant. */
66
+ url?: string;
67
+ ceiling: string;
68
+ currency: string;
69
+ credentialPath: string;
70
+ contact: Contact;
71
+ approvalBaseUrl: string;
72
+ merchantName?: string;
73
+ merchantCountryCode?: string;
74
+ /** ISO 8601 mandate expiry; defaults to now + 24h. */
75
+ expiresAt?: string;
76
+ /** Max draws the ceiling intent may fulfil. */
77
+ maxDraws?: number;
78
+ /**
79
+ * Per-purchase cap (decimal string, > 0 and <= ceiling). Registered with the
80
+ * approval context so the operator reads it as a worst-case term, and carried
81
+ * onto the budget mint token so it is enforced at draw time.
82
+ */
83
+ perTransaction?: string;
84
+ /**
85
+ * Agent-supplied one-liner shown on the approval page in a labeled
86
+ * "written by the agent" block — provenance for the human, never trusted.
87
+ */
88
+ intent?: string;
89
+ /**
90
+ * BUDGET mode: create a mandate spendable at ANY merchant (no merchant lock),
91
+ * bounded by the ceiling + per-transaction limit. The passkey approval shows
92
+ * "spend budget mandate" so the owner consents to the broader scope.
93
+ */
94
+ anyMerchant?: boolean;
95
+ };
96
+ export type CliMandateFacts = CardMandateFacts & {
97
+ merchantHost: string;
98
+ /**
99
+ * True when the mandate minted its ceiling intent but the #5942 register
100
+ * handshake failed, so `findCovering` will SKIP it and no tap-free draw is
101
+ * possible. The mandate exists but is not usable — the caller must surface
102
+ * this (not report a plain success). Absent/false → registered (or no
103
+ * delegated binding was present, so register was intentionally not attempted).
104
+ */
105
+ registerFailed?: boolean;
106
+ };
107
+ type Session = {
108
+ browser: Browser;
109
+ target: VgsCheckoutTarget;
110
+ amountMinor: number;
111
+ currency: string;
112
+ contact: Contact;
113
+ cleanupTimer: ReturnType<typeof setTimeout>;
114
+ };
115
+ export interface CardDrawVerdictDraw {
116
+ tokenId: string;
117
+ amount: string;
118
+ currency: string;
119
+ merchantName: string;
120
+ merchantUrl: string;
121
+ merchantCountryCode: string;
122
+ }
123
+ export interface CardDrawVerdictCapability {
124
+ /** Opaque to the engine — passed straight back to {@link CardDrawVerdictSeam.fetchVerdict}. */
125
+ agentKey: unknown;
126
+ agentJkt: string;
127
+ /** Auth origin that minted the binding and hosts the /v4/card/draw* routes. */
128
+ authBaseUrl: string;
129
+ }
130
+ export interface CardDrawVerdictSeam {
131
+ loadCapability: () => CardDrawVerdictCapability | null;
132
+ fetchVerdict: (input: {
133
+ authBaseUrl: string;
134
+ agentKey: unknown;
135
+ mandateId: string;
136
+ drawId: string;
137
+ draw: CardDrawVerdictDraw;
138
+ }) => Promise<{
139
+ verdict: string;
140
+ remainingCents: number;
141
+ }>;
142
+ }
143
+ export interface CardMandateRegisterSeam {
144
+ loadCapability: () => {
145
+ agentKey: unknown;
146
+ agentJkt: string;
147
+ authBaseUrl: string;
148
+ } | null;
149
+ register: (input: {
150
+ authBaseUrl: string;
151
+ agentKey: unknown;
152
+ mandateId: string;
153
+ mintToken: string;
154
+ ceiling: string;
155
+ currency: string;
156
+ }) => Promise<{
157
+ ok: boolean;
158
+ reason?: string;
159
+ }>;
160
+ }
161
+ export type CliEngineDeps = {
162
+ launchBrowser?: () => Promise<Browser>;
163
+ prepareCheckout?: typeof realPrepareCheckout;
164
+ submitApprovedCheckout?: typeof realSubmitApprovedCheckout;
165
+ runHostedApproval?: typeof realRunHostedApproval;
166
+ /**
167
+ * Relay the hosted-approval URL to the caller as DATA the moment it is known,
168
+ * before the (up-to-timeout) poll wait. A headless agent surface wires this to
169
+ * hand the URL to its operator; the raw CLI leaves it unset (the URL prints to
170
+ * stderr and best-effort opens a browser).
171
+ */
172
+ onApprovalUrl?: (url: string) => void;
173
+ reportVicOutcome?: typeof realReportVicOutcome;
174
+ writeReceipt?: typeof realWriteReceipt;
175
+ store?: PreparedCheckoutSessionStore;
176
+ sessions?: Map<string, Session>;
177
+ ttlMs?: number;
178
+ /** Owner-only card-mandate ledger — defaults to the ~/.visa-mcp singleton. */
179
+ ledger?: MandateLedger;
180
+ /** Injectable clock for mandate expiry decisions (tests pin it). */
181
+ now?: () => Date;
182
+ /**
183
+ * Cryptogram transport for the TAP-FREE mandate draw — defaults to the real
184
+ * server-side mint route. Injectable so a test can drive the draw-reject ->
185
+ * markUnhonored path with no network.
186
+ */
187
+ serverFetchCryptogram?: typeof serverFetchCryptogram;
188
+ /**
189
+ * #5923 delegated card-draw verdict seam (see {@link CardDrawVerdictSeam}).
190
+ * Injected by the CLI when the agent holds a mode='card' delegated binding;
191
+ * when absent the covering-mandate draw keeps using the bearer mint token (the
192
+ * shipped #5917 flow, unchanged).
193
+ */
194
+ cardDrawVerdict?: CardDrawVerdictSeam;
195
+ /**
196
+ * #5942 delegated card-mandate register seam (see {@link CardMandateRegisterSeam}).
197
+ * Injected by the CLI when the agent holds a mode='card' delegated binding;
198
+ * when absent mandate-start skips the register (best-effort — the bearer path
199
+ * still works, only the delegated draw needs the register row).
200
+ */
201
+ cardMandateRegister?: CardMandateRegisterSeam;
202
+ };
203
+ export declare function createCliCheckoutEngine(deps?: CliEngineDeps): {
204
+ startCardMandate(input: CliStartMandateInput): Promise<CliMandateFacts>;
205
+ review(input: CliReviewInput): Promise<CliReviewFacts>;
206
+ pay(input: CliPayInput): Promise<CliReceiptFacts>;
207
+ };
208
+ export {};