@vultisig/cli 2.8.1 → 2.8.2
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/CHANGELOG.md +34 -0
- package/dist/index.js +115 -50
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @vultisig/cli
|
|
2
2
|
|
|
3
|
+
## 2.8.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#869](https://github.com/vultisig/vultisig-sdk/pull/869) [`2623ca0`](https://github.com/vultisig/vultisig-sdk/commit/2623ca0dd04c5ca3c65126b6008a93c1d7559793) Thanks [@neavra](https://github.com/neavra)! - Harden agent conversation auth across every path. A revoked-but-unexpired
|
|
8
|
+
cached token now recovers uniformly: a new `withAuthRetry` helper does
|
|
9
|
+
clear→reauth→retry-once and wraps the fresh-conversation `createConversation`
|
|
10
|
+
(previously unguarded — it hard-threw `Authentication failed`), the resume
|
|
11
|
+
`getConversation`, the send-message stream, and the `agent sessions
|
|
12
|
+
list`/`delete` commands.
|
|
13
|
+
|
|
14
|
+
A `--session-id` resume whose 401 survives the single retry — or that fails for
|
|
15
|
+
any other reason — now falls back to a fresh conversation instead of throwing
|
|
16
|
+
uncaught, and emits a typed, non-fatal `SESSION_NOT_FOUND` signal carrying the
|
|
17
|
+
new conversation id so a headless caller knows prior context was dropped. In
|
|
18
|
+
`agent ask`, that initialize-time signal is now preserved into the first turn's
|
|
19
|
+
result envelope (it was previously cleared at turn start, so a stale-session
|
|
20
|
+
`ask --json` returned a false-success envelope instead of the promised non-zero
|
|
21
|
+
`SESSION_NOT_FOUND`).
|
|
22
|
+
|
|
23
|
+
Models the backend's `refresh_token`/`access_token` in `AuthTokenResponse` and
|
|
24
|
+
persists the refresh token in the token cache (0o600) for a future
|
|
25
|
+
`POST /auth/refresh` exchange; the retry path re-auths via MPC re-sign today.
|
|
26
|
+
The retry preserves a previously cached refresh token when the re-auth response
|
|
27
|
+
omits one, and the cache directory is chmod'd to 0o700 on every write (not only
|
|
28
|
+
on create) so a pre-existing `~/.vultisig` can't retain looser perms on upgrade.
|
|
29
|
+
|
|
30
|
+
Adds the first `auth.ts` unit tests (EIP-191 hash cross-checked against viem,
|
|
31
|
+
DER→65-byte formatting, signing-retry classification) plus session
|
|
32
|
+
auth-retry/fallback coverage.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [[`0dc1620`](https://github.com/vultisig/vultisig-sdk/commit/0dc16206bedcdde8832a068b15383565a6b98896)]:
|
|
35
|
+
- @vultisig/sdk@2.8.2
|
|
36
|
+
|
|
3
37
|
## 2.8.1
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -8074,6 +8074,7 @@ var AgentErrorCode = /* @__PURE__ */ ((AgentErrorCode3) => {
|
|
|
8074
8074
|
AgentErrorCode3["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
|
|
8075
8075
|
AgentErrorCode3["SIGNING_FAILED"] = "SIGNING_FAILED";
|
|
8076
8076
|
AgentErrorCode3["SESSION_NOT_INITIALIZED"] = "SESSION_NOT_INITIALIZED";
|
|
8077
|
+
AgentErrorCode3["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
|
|
8077
8078
|
AgentErrorCode3["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
8078
8079
|
return AgentErrorCode3;
|
|
8079
8080
|
})(AgentErrorCode || {});
|
|
@@ -8185,6 +8186,11 @@ var AskInterface = class {
|
|
|
8185
8186
|
transactions = [];
|
|
8186
8187
|
cards = [];
|
|
8187
8188
|
error;
|
|
8189
|
+
// Whether ask() has run at least once. initialize() drives getCallbacks()
|
|
8190
|
+
// BEFORE the first ask() — a stale --session fallback fires
|
|
8191
|
+
// onError(SESSION_NOT_FOUND) there. ask() must NOT clear that initialize-time
|
|
8192
|
+
// error on its first turn, or the result envelope reports false success.
|
|
8193
|
+
hasAsked = false;
|
|
8188
8194
|
constructor(session, verbose = false, autoApprove = false) {
|
|
8189
8195
|
this.session = session;
|
|
8190
8196
|
this.verbose = verbose;
|
|
@@ -8269,7 +8275,10 @@ var AskInterface = class {
|
|
|
8269
8275
|
this.toolCalls = [];
|
|
8270
8276
|
this.transactions = [];
|
|
8271
8277
|
this.cards = [];
|
|
8272
|
-
this.
|
|
8278
|
+
if (this.hasAsked) {
|
|
8279
|
+
this.error = void 0;
|
|
8280
|
+
}
|
|
8281
|
+
this.hasAsked = true;
|
|
8273
8282
|
const callbacks = this.getCallbacks();
|
|
8274
8283
|
await this.session.sendMessage(message, callbacks);
|
|
8275
8284
|
return this.partialResult();
|
|
@@ -8624,7 +8633,13 @@ async function authenticateVault(client, vault, password, maxAttempts = 3) {
|
|
|
8624
8633
|
});
|
|
8625
8634
|
return {
|
|
8626
8635
|
token: authResponse.token,
|
|
8627
|
-
expiresAt: authResponse.expires_at
|
|
8636
|
+
expiresAt: authResponse.expires_at,
|
|
8637
|
+
// Captured + persisted by the session token cache. The backend exposes
|
|
8638
|
+
// POST /auth/refresh to exchange it for a fresh access token without a
|
|
8639
|
+
// new MPC round; wiring that exchange is a future enhancement — today
|
|
8640
|
+
// the CLI re-auths via a full MPC re-sign (authenticateVault), which is
|
|
8641
|
+
// always available and avoids depending on refresh-token rotation.
|
|
8642
|
+
refreshToken: authResponse.refresh_token
|
|
8628
8643
|
};
|
|
8629
8644
|
} catch (err) {
|
|
8630
8645
|
lastError = err;
|
|
@@ -12072,7 +12087,7 @@ var PipeInterface = class {
|
|
|
12072
12087
|
};
|
|
12073
12088
|
|
|
12074
12089
|
// src/agent/session.ts
|
|
12075
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12090
|
+
import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12076
12091
|
import { homedir as homedir2 } from "node:os";
|
|
12077
12092
|
import { join as join2 } from "node:path";
|
|
12078
12093
|
import { MemoryStorage, PushNotificationService } from "@vultisig/sdk";
|
|
@@ -12144,7 +12159,7 @@ var AgentSession = class {
|
|
|
12144
12159
|
} else {
|
|
12145
12160
|
const auth = await authenticateVault(this.client, this.vault, this.config.password);
|
|
12146
12161
|
this.client.setAuthToken(auth.token);
|
|
12147
|
-
saveCachedToken(this.publicKey, auth.token, auth.expiresAt);
|
|
12162
|
+
saveCachedToken(this.publicKey, auth.token, auth.expiresAt, auth.refreshToken);
|
|
12148
12163
|
}
|
|
12149
12164
|
} catch (err) {
|
|
12150
12165
|
throw new Error(`Authentication failed: ${err.message}`);
|
|
@@ -12152,25 +12167,20 @@ var AgentSession = class {
|
|
|
12152
12167
|
if (this.config.sessionId) {
|
|
12153
12168
|
this.conversationId = this.config.sessionId;
|
|
12154
12169
|
try {
|
|
12155
|
-
const conv = await this.client.getConversation(this.conversationId, this.publicKey);
|
|
12170
|
+
const conv = await this.withAuthRetry(() => this.client.getConversation(this.conversationId, this.publicKey));
|
|
12156
12171
|
this.historyMessages = conv.messages || [];
|
|
12157
12172
|
} catch (err) {
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
this.conversationId = null;
|
|
12167
|
-
this.historyMessages = [];
|
|
12168
|
-
const conv = await this.client.createConversation(this.publicKey);
|
|
12169
|
-
this.conversationId = conv.id;
|
|
12170
|
-
}
|
|
12173
|
+
this.conversationId = null;
|
|
12174
|
+
this.historyMessages = [];
|
|
12175
|
+
const conv = await this.withAuthRetry(() => this.client.createConversation(this.publicKey));
|
|
12176
|
+
this.conversationId = conv.id;
|
|
12177
|
+
ui.onError(
|
|
12178
|
+
`Session ${this.config.sessionId} could not be resumed (${err?.message ?? "unknown error"}); started a new conversation ${conv.id}`,
|
|
12179
|
+
"SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */
|
|
12180
|
+
);
|
|
12171
12181
|
}
|
|
12172
12182
|
} else {
|
|
12173
|
-
const conv = await this.client.createConversation(this.publicKey);
|
|
12183
|
+
const conv = await this.withAuthRetry(() => this.client.createConversation(this.publicKey));
|
|
12174
12184
|
this.conversationId = conv.id;
|
|
12175
12185
|
}
|
|
12176
12186
|
this.cachedContext = this.config.viaAgent || this.config.askMode ? await buildMinimalContext(this.vault) : await buildMessageContext(this.vault);
|
|
@@ -12204,6 +12214,40 @@ var AgentSession = class {
|
|
|
12204
12214
|
}
|
|
12205
12215
|
}
|
|
12206
12216
|
}
|
|
12217
|
+
/**
|
|
12218
|
+
* Run an authenticated backend request and, on a 401/403, do a single
|
|
12219
|
+
* clear → re-auth → retry. This is the ONE chokepoint every conversation
|
|
12220
|
+
* request shares (resume fetch, fresh-convo create, error-fallback create,
|
|
12221
|
+
* and the send-message stream) so a revoked-but-unexpired cached token
|
|
12222
|
+
* recovers identically everywhere instead of throwing on some paths.
|
|
12223
|
+
*
|
|
12224
|
+
* The retry replays the EXACT same `request` closure, which matters for the
|
|
12225
|
+
* send-message path: the replayed body must carry the same content +
|
|
12226
|
+
* recent_actions or the LLM re-emits tool calls (runaway loop). Re-auth is a
|
|
12227
|
+
* full MPC re-sign via authenticateVault — the backend also exposes
|
|
12228
|
+
* POST /auth/refresh, but exchanging the refresh token is a future
|
|
12229
|
+
* enhancement (see auth.ts); the re-sign is always available.
|
|
12230
|
+
*
|
|
12231
|
+
* `onReauth` (optional) fires the instant a re-auth is committed to — BEFORE
|
|
12232
|
+
* authenticateVault runs — so a caller in a retry loop (recoverDisconnectedTurn)
|
|
12233
|
+
* can record that its single re-auth has been spent even if the MPC re-sign
|
|
12234
|
+
* itself then throws. Without this hook a re-auth that fails with a non-auth
|
|
12235
|
+
* error would let the caller re-enter and re-sign on every iteration.
|
|
12236
|
+
*/
|
|
12237
|
+
async withAuthRetry(request, onReauth) {
|
|
12238
|
+
try {
|
|
12239
|
+
return await request();
|
|
12240
|
+
} catch (err) {
|
|
12241
|
+
if (!isAuthError(err)) throw err;
|
|
12242
|
+
onReauth?.();
|
|
12243
|
+
const previousRefreshToken = readTokenStore()[this.publicKey]?.refreshToken;
|
|
12244
|
+
clearCachedToken(this.publicKey);
|
|
12245
|
+
const auth = await authenticateVault(this.client, this.vault, this.config.password);
|
|
12246
|
+
this.client.setAuthToken(auth.token);
|
|
12247
|
+
saveCachedToken(this.publicKey, auth.token, auth.expiresAt, auth.refreshToken ?? previousRefreshToken);
|
|
12248
|
+
return await request();
|
|
12249
|
+
}
|
|
12250
|
+
}
|
|
12207
12251
|
getConversationId() {
|
|
12208
12252
|
return this.conversationId;
|
|
12209
12253
|
}
|
|
@@ -12328,31 +12372,15 @@ var AgentSession = class {
|
|
|
12328
12372
|
}
|
|
12329
12373
|
};
|
|
12330
12374
|
let streamResult;
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
this.abortController?.signal
|
|
12339
|
-
);
|
|
12340
|
-
break;
|
|
12341
|
-
} catch (err) {
|
|
12342
|
-
const isAuthErr = err.message?.includes("401") || err.message?.includes("403");
|
|
12343
|
-
if (isAuthErr && !authRetried) {
|
|
12344
|
-
authRetried = true;
|
|
12345
|
-
clearCachedToken(this.publicKey);
|
|
12346
|
-
const auth = await authenticateVault(this.client, this.vault, this.config.password);
|
|
12347
|
-
this.client.setAuthToken(auth.token);
|
|
12348
|
-
saveCachedToken(this.publicKey, auth.token, auth.expiresAt);
|
|
12349
|
-
continue;
|
|
12350
|
-
}
|
|
12351
|
-
if (flushedThisCall.length > 0) {
|
|
12352
|
-
this.pendingToolResults = [...flushedThisCall, ...this.pendingToolResults];
|
|
12353
|
-
}
|
|
12354
|
-
throw err;
|
|
12375
|
+
try {
|
|
12376
|
+
streamResult = await this.withAuthRetry(
|
|
12377
|
+
() => this.client.sendMessageStream(this.conversationId, request, callbacks, this.abortController?.signal)
|
|
12378
|
+
);
|
|
12379
|
+
} catch (err) {
|
|
12380
|
+
if (flushedThisCall.length > 0) {
|
|
12381
|
+
this.pendingToolResults = [...flushedThisCall, ...this.pendingToolResults];
|
|
12355
12382
|
}
|
|
12383
|
+
throw err;
|
|
12356
12384
|
}
|
|
12357
12385
|
if (pendingDispatches.length > 0) {
|
|
12358
12386
|
await Promise.all(pendingDispatches);
|
|
@@ -12415,10 +12443,16 @@ var AgentSession = class {
|
|
|
12415
12443
|
const since = serverAnchor ?? new Date(Date.now() - 2e3).toISOString();
|
|
12416
12444
|
const replaySignableCards = serverAnchor !== null;
|
|
12417
12445
|
let cursor;
|
|
12446
|
+
let authRecovered = false;
|
|
12418
12447
|
for (let attempt = 0; attempt < this.recoveryMaxPolls; attempt++) {
|
|
12419
12448
|
let resp;
|
|
12420
12449
|
try {
|
|
12421
|
-
resp = await this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since })
|
|
12450
|
+
resp = authRecovered ? await this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since }) : await this.withAuthRetry(
|
|
12451
|
+
() => this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since }),
|
|
12452
|
+
() => {
|
|
12453
|
+
authRecovered = true;
|
|
12454
|
+
}
|
|
12455
|
+
);
|
|
12422
12456
|
} catch (err) {
|
|
12423
12457
|
if (this.config.verbose) {
|
|
12424
12458
|
process.stderr.write(`[session] recovery poll ${attempt + 1} failed: ${err?.message ?? err}
|
|
@@ -12708,6 +12742,10 @@ function serverNowToIso(serverNow) {
|
|
|
12708
12742
|
function hasTxReadyPart(parts) {
|
|
12709
12743
|
return !!parts?.some((p) => p.type === "data-tx_ready" && !!p.data);
|
|
12710
12744
|
}
|
|
12745
|
+
function isAuthError(err) {
|
|
12746
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
12747
|
+
return /\b(401|403)\b/.test(msg);
|
|
12748
|
+
}
|
|
12711
12749
|
function getTokenCachePath() {
|
|
12712
12750
|
const dir = process.env.VULTISIG_CONFIG_DIR ?? join2(homedir2(), ".vultisig");
|
|
12713
12751
|
return join2(dir, "agent-tokens.json");
|
|
@@ -12724,8 +12762,16 @@ function readTokenStore() {
|
|
|
12724
12762
|
function writeTokenStore(store) {
|
|
12725
12763
|
const path4 = getTokenCachePath();
|
|
12726
12764
|
const dir = join2(path4, "..");
|
|
12727
|
-
if (!existsSync(dir)) mkdirSync2(dir, { recursive: true });
|
|
12765
|
+
if (!existsSync(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
12766
|
+
try {
|
|
12767
|
+
chmodSync(dir, 448);
|
|
12768
|
+
} catch {
|
|
12769
|
+
}
|
|
12728
12770
|
writeFileSync2(path4, JSON.stringify(store, null, 2), { mode: 384 });
|
|
12771
|
+
try {
|
|
12772
|
+
chmodSync(path4, 384);
|
|
12773
|
+
} catch {
|
|
12774
|
+
}
|
|
12729
12775
|
}
|
|
12730
12776
|
function loadCachedToken(publicKey) {
|
|
12731
12777
|
const store = readTokenStore();
|
|
@@ -12743,9 +12789,13 @@ function loadCachedToken(publicKey) {
|
|
|
12743
12789
|
}
|
|
12744
12790
|
return entry.token;
|
|
12745
12791
|
}
|
|
12746
|
-
function saveCachedToken(publicKey, token, expiresAt) {
|
|
12792
|
+
function saveCachedToken(publicKey, token, expiresAt, refreshToken) {
|
|
12747
12793
|
const store = readTokenStore();
|
|
12748
|
-
store[publicKey] = {
|
|
12794
|
+
store[publicKey] = {
|
|
12795
|
+
token,
|
|
12796
|
+
expiresAt,
|
|
12797
|
+
refreshToken: refreshToken ?? store[publicKey]?.refreshToken
|
|
12798
|
+
};
|
|
12749
12799
|
try {
|
|
12750
12800
|
writeTokenStore(store);
|
|
12751
12801
|
} catch {
|
|
@@ -13293,7 +13343,12 @@ async function executeAgentSessionsList(ctx2, options) {
|
|
|
13293
13343
|
let totalCount = 0;
|
|
13294
13344
|
let skip = 0;
|
|
13295
13345
|
while (true) {
|
|
13296
|
-
const page = await
|
|
13346
|
+
const page = await withClientAuthRetry(
|
|
13347
|
+
client,
|
|
13348
|
+
vault,
|
|
13349
|
+
options.password,
|
|
13350
|
+
() => client.listConversations(publicKey, skip, PAGE_SIZE)
|
|
13351
|
+
);
|
|
13297
13352
|
totalCount = page.total_count;
|
|
13298
13353
|
allConversations.push(...page.conversations);
|
|
13299
13354
|
if (allConversations.length >= totalCount || page.conversations.length < PAGE_SIZE) break;
|
|
@@ -13335,7 +13390,7 @@ async function executeAgentSessionsDelete(ctx2, sessionId, options) {
|
|
|
13335
13390
|
const backendUrl = options.backendUrl || process.env.VULTISIG_AGENT_URL || "https://abe.vultisig.com";
|
|
13336
13391
|
const client = await createAuthenticatedClient(backendUrl, vault, options.password);
|
|
13337
13392
|
const publicKey = vault.publicKeys.ecdsa;
|
|
13338
|
-
await client.deleteConversation(sessionId, publicKey);
|
|
13393
|
+
await withClientAuthRetry(client, vault, options.password, () => client.deleteConversation(sessionId, publicKey));
|
|
13339
13394
|
if (isJsonOutput()) {
|
|
13340
13395
|
outputJson({ deleted: sessionId });
|
|
13341
13396
|
return;
|
|
@@ -13348,6 +13403,16 @@ async function createAuthenticatedClient(backendUrl, vault, password) {
|
|
|
13348
13403
|
client.setAuthToken(auth.token);
|
|
13349
13404
|
return client;
|
|
13350
13405
|
}
|
|
13406
|
+
async function withClientAuthRetry(client, vault, password, request) {
|
|
13407
|
+
try {
|
|
13408
|
+
return await request();
|
|
13409
|
+
} catch (err) {
|
|
13410
|
+
if (!isAuthError(err)) throw err;
|
|
13411
|
+
const auth = await authenticateVault(client, vault, password);
|
|
13412
|
+
client.setAuthToken(auth.token);
|
|
13413
|
+
return await request();
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
13351
13416
|
function formatDate(iso) {
|
|
13352
13417
|
try {
|
|
13353
13418
|
const d = new Date(iso);
|
|
@@ -13366,7 +13431,7 @@ var cachedVersion = null;
|
|
|
13366
13431
|
function getVersion() {
|
|
13367
13432
|
if (cachedVersion) return cachedVersion;
|
|
13368
13433
|
if (true) {
|
|
13369
|
-
cachedVersion = "2.8.
|
|
13434
|
+
cachedVersion = "2.8.2";
|
|
13370
13435
|
return cachedVersion;
|
|
13371
13436
|
}
|
|
13372
13437
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vultisig/cli",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "The self-custody MPC wallet CLI for AI coding agents (Claude Code, Cursor, OpenCode). Natural-language agent mode, 36+ chains, DKLS23 threshold signatures. Seedless.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@vultisig/client-shared": "^0.2.16",
|
|
77
77
|
"@vultisig/core-chain": "^2.17.8",
|
|
78
78
|
"@vultisig/rujira": "^41.0.0",
|
|
79
|
-
"@vultisig/sdk": "^2.8.
|
|
79
|
+
"@vultisig/sdk": "^2.8.2",
|
|
80
80
|
"chalk": "^5.6.2",
|
|
81
81
|
"cli-table3": "^0.6.5",
|
|
82
82
|
"commander": "^15.0.0",
|