@visa/cli 4.1.0-rc.66 → 4.1.0-rc.67
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.
- package/dist/checkout-engine/cli-engine.d.ts +14 -0
- package/dist/checkout-engine/cli-engine.js +45 -3
- package/dist/cli.js +322 -293
- package/dist/mcp-server/index.js +250 -225
- package/dist/skills/pair-visa-agent/SKILL.md +41 -20
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -21,6 +21,8 @@ export type CliReviewInput = {
|
|
|
21
21
|
amount: string;
|
|
22
22
|
currency: string;
|
|
23
23
|
credentialPath: string;
|
|
24
|
+
/** See {@link CardInstrumentSource}. */
|
|
25
|
+
cardTokenId?: string;
|
|
24
26
|
contact: Contact;
|
|
25
27
|
approvalBaseUrl: string;
|
|
26
28
|
merchantName?: string;
|
|
@@ -66,6 +68,8 @@ export type CliStartMandateInput = {
|
|
|
66
68
|
ceiling: string;
|
|
67
69
|
currency: string;
|
|
68
70
|
credentialPath: string;
|
|
71
|
+
/** See {@link CardInstrumentSource}. */
|
|
72
|
+
cardTokenId?: string;
|
|
69
73
|
contact: Contact;
|
|
70
74
|
approvalBaseUrl: string;
|
|
71
75
|
/**
|
|
@@ -90,6 +94,16 @@ export type CliMandateFacts = CardMandateFacts & {
|
|
|
90
94
|
* mandate-start now refuses before approval when no capability can register.
|
|
91
95
|
*/
|
|
92
96
|
registerFailed?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The server's refusal reason when {@link registerFailed} is true, verbatim.
|
|
99
|
+
* Carried out so the CLI can distinguish causes that need DIFFERENT operator
|
|
100
|
+
* actions — notably `token_mismatch`, which means the on-device grant record's
|
|
101
|
+
* cached token no longer matches the owner's live agentic token (they
|
|
102
|
+
* re-enrolled a card after the grant) and is fixed by re-running `grant-card`,
|
|
103
|
+
* not by retrying `mandate start`. Without the reason every failure reads as
|
|
104
|
+
* "the auth server was unreachable", which sends the operator in a loop.
|
|
105
|
+
*/
|
|
106
|
+
registerFailureReason?: string;
|
|
93
107
|
};
|
|
94
108
|
type Session = {
|
|
95
109
|
browser: Browser;
|
|
@@ -72,6 +72,33 @@ function classifyCardDrawVerdictFailure(err) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the card instrument for one flow. Fail-closed: an unusable legacy file
|
|
77
|
+
* with no grant token rethrows (never silently proceeds), and the ONLY thing the
|
|
78
|
+
* fallback contributes is a token id — a non-secret handle the server
|
|
79
|
+
* re-authorizes against the owner (`requireTokenOwnership`) and against the live
|
|
80
|
+
* grant on every draw. Nothing here authorizes anything.
|
|
81
|
+
*/
|
|
82
|
+
async function resolveCardInstrument(input) {
|
|
83
|
+
let credential = null;
|
|
84
|
+
let readError = null;
|
|
85
|
+
try {
|
|
86
|
+
credential = JSON.parse(await readFile(input.credentialPath, 'utf8'));
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
readError = err;
|
|
90
|
+
}
|
|
91
|
+
if (credential && typeof credential.tokenId === 'string' && credential.tokenId.trim()) {
|
|
92
|
+
return credential;
|
|
93
|
+
}
|
|
94
|
+
if (typeof input.cardTokenId === 'string' && input.cardTokenId.trim()) {
|
|
95
|
+
return { tokenId: input.cardTokenId, source: 'card-grant' };
|
|
96
|
+
}
|
|
97
|
+
throw new Error('no card instrument is available to this runtime: there is no usable credential at ' +
|
|
98
|
+
`${input.credentialPath} and no activated card:vic grant token was supplied. Run ` +
|
|
99
|
+
'`visa agent grant-card <agent-id> --ceiling <usd> --per-transaction <usd> --wait` to ' +
|
|
100
|
+
'attach one, or use a pre-provisioned VIC runtime.', readError instanceof Error ? { cause: readError } : undefined);
|
|
101
|
+
}
|
|
75
102
|
const RECEIPT_DIR = join(homedir(), '.visa-mcp', 'checkout-receipts');
|
|
76
103
|
// Must match the prepared-checkout store TTL so a session and its store entry
|
|
77
104
|
// expire together — an abandoned review can't leak the browser + state.
|
|
@@ -150,7 +177,9 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
150
177
|
url: 'https://retail-budget.visa/budget',
|
|
151
178
|
countryCode: 'US',
|
|
152
179
|
};
|
|
153
|
-
|
|
180
|
+
// Legacy credential file OR the activated card:vic grant's token — see
|
|
181
|
+
// resolveCardInstrument. A v2-paired runtime only ever has the latter.
|
|
182
|
+
const credential = await resolveCardInstrument(input);
|
|
154
183
|
// The passkey ceremony is scoped to the CEILING + merchant (not one
|
|
155
184
|
// charge) — that scope is the unproven part of the spike.
|
|
156
185
|
const ceilingTarget = {
|
|
@@ -206,6 +235,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
206
235
|
// intent ID. A later draw requires its PoP verdict; the budget token never
|
|
207
236
|
// falls back as payable authority.
|
|
208
237
|
let registerFailed = false;
|
|
238
|
+
let registerFailureReason;
|
|
209
239
|
if (registerCap && cardMandateRegister) {
|
|
210
240
|
const reg = await cardMandateRegister
|
|
211
241
|
.register({
|
|
@@ -222,6 +252,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
222
252
|
}));
|
|
223
253
|
if (!reg.ok) {
|
|
224
254
|
registerFailed = true;
|
|
255
|
+
registerFailureReason = reg.reason;
|
|
225
256
|
// Register failed: the server `card_mandate_spend` row was never
|
|
226
257
|
// created, so a delegated draw against this mandate would 404
|
|
227
258
|
// `no_mandate`. Mark it register-failed so findCovering() SKIPS it and
|
|
@@ -244,7 +275,12 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
244
275
|
`'mandate start'. mandateId=${facts.mandateId}\n`);
|
|
245
276
|
}
|
|
246
277
|
}
|
|
247
|
-
return {
|
|
278
|
+
return {
|
|
279
|
+
...facts,
|
|
280
|
+
merchantHost: new URL(merchant.url).hostname,
|
|
281
|
+
registerFailed,
|
|
282
|
+
...(registerFailureReason !== undefined ? { registerFailureReason } : {}),
|
|
283
|
+
};
|
|
248
284
|
},
|
|
249
285
|
async review(input) {
|
|
250
286
|
const amountMinor = decimalToMinor(input.amount);
|
|
@@ -391,7 +427,11 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
391
427
|
}
|
|
392
428
|
clearTimeout(session.cleanupTimer);
|
|
393
429
|
try {
|
|
394
|
-
|
|
430
|
+
// NO instrument read here. A tap-free mandate draw spends `covering
|
|
431
|
+
// .tokenId` (frozen into the mandate at start) and needs neither the
|
|
432
|
+
// legacy credential file nor a grant token; only the fresh-tap branch
|
|
433
|
+
// below needs one, and it resolves lazily so a grant-only device with an
|
|
434
|
+
// as-yet-unrefreshed token can still draw on a mandate it already holds.
|
|
395
435
|
const host = new URL(session.target.merchantUrl).hostname;
|
|
396
436
|
// Does an ACTIVE card mandate already cover this exact purchase? If so,
|
|
397
437
|
// draw against it TAP-FREE (no hosted passkey). Else fall back to today's
|
|
@@ -550,6 +590,8 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
550
590
|
else {
|
|
551
591
|
// --- 1:1 fresh-tap flow (unchanged) ---------------------------------
|
|
552
592
|
source = 'fresh-tap';
|
|
593
|
+
// Legacy credential file OR the activated card:vic grant's token.
|
|
594
|
+
const credential = await resolveCardInstrument(input);
|
|
553
595
|
const assurance = await runHostedApproval({
|
|
554
596
|
baseUrl: input.approvalBaseUrl,
|
|
555
597
|
tokenId: credential.tokenId,
|