@candledottv/cli 0.8.0 → 0.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/dist/index.js +207 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18761,6 +18761,24 @@ function isLoopbackHost2(hostname) {
|
|
|
18761
18761
|
return true;
|
|
18762
18762
|
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
18763
18763
|
}
|
|
18764
|
+
function isPrivateHost2(hostname) {
|
|
18765
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
18766
|
+
if (!host.includes(".") && !host.includes(":"))
|
|
18767
|
+
return true;
|
|
18768
|
+
if (/\.(local|internal|home\.arpa)$/.test(host))
|
|
18769
|
+
return true;
|
|
18770
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
18771
|
+
return true;
|
|
18772
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
18773
|
+
return true;
|
|
18774
|
+
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
18775
|
+
return true;
|
|
18776
|
+
if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
18777
|
+
return true;
|
|
18778
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
18779
|
+
return true;
|
|
18780
|
+
return /^fe[89ab][0-9a-f]:/.test(host);
|
|
18781
|
+
}
|
|
18764
18782
|
function assertTransportSecurity(apiUrl, env) {
|
|
18765
18783
|
let parsed;
|
|
18766
18784
|
try {
|
|
@@ -18775,9 +18793,9 @@ function assertTransportSecurity(apiUrl, env) {
|
|
|
18775
18793
|
}
|
|
18776
18794
|
if (isLoopbackHost2(parsed.hostname))
|
|
18777
18795
|
return;
|
|
18778
|
-
if (env.CANDLE_ALLOW_INSECURE_HTTP?.trim())
|
|
18796
|
+
if (env.CANDLE_ALLOW_INSECURE_HTTP?.trim() && isPrivateHost2(parsed.hostname))
|
|
18779
18797
|
return;
|
|
18780
|
-
throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint.");
|
|
18798
|
+
throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + (isPrivateHost2(parsed.hostname) ? "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint." : "URL. CANDLE_ALLOW_INSECURE_HTTP does not apply here: it covers private networks only, " + "and this is a public address."));
|
|
18781
18799
|
}
|
|
18782
18800
|
function resolveConfig(env = process.env) {
|
|
18783
18801
|
const apiUrl = env.CANDLE_API_URL?.trim() || DEFAULT_API_URL2;
|
|
@@ -19527,7 +19545,7 @@ var init_tools = __esm(() => {
|
|
|
19527
19545
|
});
|
|
19528
19546
|
|
|
19529
19547
|
// ../mcp/src/version.ts
|
|
19530
|
-
var SERVER_VERSION = "0.
|
|
19548
|
+
var SERVER_VERSION = "0.6.2";
|
|
19531
19549
|
|
|
19532
19550
|
// ../mcp/src/server.ts
|
|
19533
19551
|
var exports_server = {};
|
|
@@ -19578,6 +19596,24 @@ function isLoopbackHost(hostname) {
|
|
|
19578
19596
|
return true;
|
|
19579
19597
|
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
19580
19598
|
}
|
|
19599
|
+
function isPrivateHost(hostname) {
|
|
19600
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
19601
|
+
if (!host.includes(".") && !host.includes(":"))
|
|
19602
|
+
return true;
|
|
19603
|
+
if (/\.(local|internal|home\.arpa)$/.test(host))
|
|
19604
|
+
return true;
|
|
19605
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
19606
|
+
return true;
|
|
19607
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
19608
|
+
return true;
|
|
19609
|
+
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
19610
|
+
return true;
|
|
19611
|
+
if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
19612
|
+
return true;
|
|
19613
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
19614
|
+
return true;
|
|
19615
|
+
return /^fe[89ab][0-9a-f]:/.test(host);
|
|
19616
|
+
}
|
|
19581
19617
|
function insecureApiUrlFault(url, env = process.env) {
|
|
19582
19618
|
let parsed;
|
|
19583
19619
|
try {
|
|
@@ -19589,9 +19625,9 @@ function insecureApiUrlFault(url, env = process.env) {
|
|
|
19589
19625
|
return;
|
|
19590
19626
|
if (isLoopbackHost(parsed.hostname))
|
|
19591
19627
|
return;
|
|
19592
|
-
if (env[ALLOW_INSECURE_HTTP_ENV]?.trim())
|
|
19628
|
+
if (env[ALLOW_INSECURE_HTTP_ENV]?.trim() && isPrivateHost(parsed.hostname))
|
|
19593
19629
|
return;
|
|
19594
|
-
return `Refusing to send credentials in the clear to ${parsed.origin}. Use https
|
|
19630
|
+
return `Refusing to send credentials in the clear to ${parsed.origin}. Use https://` + (isPrivateHost(parsed.hostname) ? `, or set ${ALLOW_INSECURE_HTTP_ENV}=1 if this really is a trusted local endpoint.` : `. ${ALLOW_INSECURE_HTTP_ENV} does not apply here: it covers private networks only, and this is a public address.`);
|
|
19595
19631
|
}
|
|
19596
19632
|
function resolveApiUrl(configuredApiUrl, env = process.env) {
|
|
19597
19633
|
const fromEnv = env.CANDLE_API_URL?.trim();
|
|
@@ -19889,7 +19925,7 @@ function resolveProfileName(config, opts) {
|
|
|
19889
19925
|
if (requested !== undefined) {
|
|
19890
19926
|
if (!isValidProfileName(requested))
|
|
19891
19927
|
return { ok: false, message: `Invalid profile name: ${requested}` };
|
|
19892
|
-
if (!(requested
|
|
19928
|
+
if (!Object.hasOwn(profiles, requested)) {
|
|
19893
19929
|
return {
|
|
19894
19930
|
ok: false,
|
|
19895
19931
|
message: `No profile named "${requested}".${names.length ? `
|
|
@@ -19899,7 +19935,7 @@ ${listForHumans(profiles, config.activeProfile)}` : " Run: candle auth login --p
|
|
|
19899
19935
|
}
|
|
19900
19936
|
return { ok: true, name: requested };
|
|
19901
19937
|
}
|
|
19902
|
-
if (config.activeProfile && config.activeProfile
|
|
19938
|
+
if (config.activeProfile && Object.hasOwn(profiles, config.activeProfile))
|
|
19903
19939
|
return { ok: true, name: config.activeProfile };
|
|
19904
19940
|
if (names.length === 0)
|
|
19905
19941
|
return { ok: true, name: undefined };
|
|
@@ -19919,7 +19955,7 @@ function resolveProfileNameForLogin(config, opts) {
|
|
|
19919
19955
|
return { ok: false, message: `Invalid profile name: ${requested}` };
|
|
19920
19956
|
return { ok: true, name: requested };
|
|
19921
19957
|
}
|
|
19922
|
-
if (config.activeProfile && config.activeProfile
|
|
19958
|
+
if (config.activeProfile && Object.hasOwn(profiles, config.activeProfile))
|
|
19923
19959
|
return { ok: true, name: config.activeProfile };
|
|
19924
19960
|
const names = Object.keys(profiles);
|
|
19925
19961
|
return { ok: true, name: names.length === 1 ? names[0] : undefined };
|
|
@@ -20016,6 +20052,41 @@ function profileTable(config, now) {
|
|
|
20016
20052
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
20017
20053
|
import { homedir } from "node:os";
|
|
20018
20054
|
import { dirname, join } from "node:path";
|
|
20055
|
+
|
|
20056
|
+
// src/file-lock.ts
|
|
20057
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
20058
|
+
var STALE_MS = 30000;
|
|
20059
|
+
var RETRY_MS = 25;
|
|
20060
|
+
var TIMEOUT_MS = 1e4;
|
|
20061
|
+
async function withFileLock(target, fn) {
|
|
20062
|
+
const lockPath = `${target}.lock`;
|
|
20063
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
20064
|
+
for (;; ) {
|
|
20065
|
+
try {
|
|
20066
|
+
await (await open(lockPath, "wx")).close();
|
|
20067
|
+
break;
|
|
20068
|
+
} catch (error) {
|
|
20069
|
+
if (error.code !== "EEXIST")
|
|
20070
|
+
throw error;
|
|
20071
|
+
const age = await stat(lockPath).then((s) => Date.now() - s.mtimeMs).catch(() => 0);
|
|
20072
|
+
if (age > STALE_MS) {
|
|
20073
|
+
await rm(lockPath, { force: true });
|
|
20074
|
+
continue;
|
|
20075
|
+
}
|
|
20076
|
+
if (Date.now() > deadline) {
|
|
20077
|
+
throw new Error(`Timed out waiting for ${lockPath}. Another candle process is writing; if none is running, delete that file.`);
|
|
20078
|
+
}
|
|
20079
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
|
|
20080
|
+
}
|
|
20081
|
+
}
|
|
20082
|
+
try {
|
|
20083
|
+
return await fn();
|
|
20084
|
+
} finally {
|
|
20085
|
+
await rm(lockPath, { force: true });
|
|
20086
|
+
}
|
|
20087
|
+
}
|
|
20088
|
+
|
|
20089
|
+
// src/secret-store.ts
|
|
20019
20090
|
var SECRET_REFS = {
|
|
20020
20091
|
deviceToken: "device_token",
|
|
20021
20092
|
apiKey: "api_key"
|
|
@@ -20073,26 +20144,30 @@ class EncryptedFileSecretStore {
|
|
|
20073
20144
|
}
|
|
20074
20145
|
async set(ref, value) {
|
|
20075
20146
|
const passphrase = await this.resolvePassphrase();
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20147
|
+
return withFileLock(this.path, async () => {
|
|
20148
|
+
const contents = await this.readContents();
|
|
20149
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH_BYTES));
|
|
20150
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
|
|
20151
|
+
const key = await deriveKey(passphrase, salt, this.iterations);
|
|
20152
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(value));
|
|
20153
|
+
contents[ref] = {
|
|
20154
|
+
salt: toBase64(salt),
|
|
20155
|
+
iv: toBase64(iv),
|
|
20156
|
+
ciphertext: toBase64(new Uint8Array(ciphertext)),
|
|
20157
|
+
iterations: this.iterations
|
|
20158
|
+
};
|
|
20159
|
+
await this.writeContents(contents);
|
|
20160
|
+
});
|
|
20088
20161
|
}
|
|
20089
20162
|
async delete(ref) {
|
|
20090
20163
|
await this.resolvePassphrase();
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
|
|
20094
|
-
|
|
20095
|
-
|
|
20164
|
+
return withFileLock(this.path, async () => {
|
|
20165
|
+
const contents = await this.readContents();
|
|
20166
|
+
if (!Object.hasOwn(contents, ref))
|
|
20167
|
+
return;
|
|
20168
|
+
delete contents[ref];
|
|
20169
|
+
await this.writeContents(contents);
|
|
20170
|
+
});
|
|
20096
20171
|
}
|
|
20097
20172
|
async resolvePassphrase() {
|
|
20098
20173
|
if (this.cachedPassphrase !== undefined)
|
|
@@ -20128,7 +20203,7 @@ class EncryptedFileSecretStore {
|
|
|
20128
20203
|
const dir = dirname(this.path);
|
|
20129
20204
|
await mkdir(dir, { recursive: true });
|
|
20130
20205
|
await chmod(dir, 448);
|
|
20131
|
-
const tmpPath = `${this.path}.tmp`;
|
|
20206
|
+
const tmpPath = `${this.path}.${crypto.randomUUID()}.tmp`;
|
|
20132
20207
|
await writeFile(tmpPath, JSON.stringify(contents, null, 2), { encoding: "utf8", mode: 384 });
|
|
20133
20208
|
await chmod(tmpPath, 384);
|
|
20134
20209
|
await rename(tmpPath, this.path);
|
|
@@ -20183,7 +20258,7 @@ async function resolveApiKey(deps, profile) {
|
|
|
20183
20258
|
}
|
|
20184
20259
|
|
|
20185
20260
|
// src/version.ts
|
|
20186
|
-
var CLI_VERSION = "0.8.
|
|
20261
|
+
var CLI_VERSION = "0.8.2";
|
|
20187
20262
|
|
|
20188
20263
|
// src/commands/auth.ts
|
|
20189
20264
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -20865,21 +20940,25 @@ async function keysCreate(args, ctx) {
|
|
|
20865
20940
|
}
|
|
20866
20941
|
const body = result.body;
|
|
20867
20942
|
const apiKeyRef = ctx.profile ? profileSecretRef(ctx.profile, "apiKey") : SECRET_REFS.apiKey;
|
|
20868
|
-
const existingKey = await deps.store.get(apiKeyRef);
|
|
20869
20943
|
let stored = false;
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
if (
|
|
20873
|
-
await deps.
|
|
20874
|
-
|
|
20875
|
-
|
|
20944
|
+
let storeError;
|
|
20945
|
+
try {
|
|
20946
|
+
if (!await deps.store.get(apiKeyRef)) {
|
|
20947
|
+
await deps.store.set(apiKeyRef, body.key);
|
|
20948
|
+
if (ctx.profile) {
|
|
20949
|
+
await deps.updateProfile(ctx.profile, { keyPrefix: body.keyPrefix, scopes: body.scopes });
|
|
20950
|
+
} else {
|
|
20951
|
+
await deps.writeConfig({ keyPrefix: body.keyPrefix, scopes: body.scopes });
|
|
20952
|
+
}
|
|
20953
|
+
stored = true;
|
|
20876
20954
|
}
|
|
20877
|
-
|
|
20955
|
+
} catch (error) {
|
|
20956
|
+
storeError = error instanceof Error ? error.message : String(error);
|
|
20878
20957
|
}
|
|
20879
20958
|
if (json) {
|
|
20880
|
-
deps.stdout.write(`${JSON.stringify({ ...body, stored })}
|
|
20959
|
+
deps.stdout.write(`${JSON.stringify({ ...body, stored, ...storeError ? { storeError } : {} })}
|
|
20881
20960
|
`);
|
|
20882
|
-
return 0;
|
|
20961
|
+
return storeError ? 1 : 0;
|
|
20883
20962
|
}
|
|
20884
20963
|
deps.stdout.write(`API key: ${body.key}
|
|
20885
20964
|
`);
|
|
@@ -20889,14 +20968,22 @@ async function keysCreate(args, ctx) {
|
|
|
20889
20968
|
`);
|
|
20890
20969
|
deps.stdout.write(`Scopes: ${formatScopesForSummary(body.scopes)}
|
|
20891
20970
|
`);
|
|
20971
|
+
if (storeError !== undefined) {
|
|
20972
|
+
deps.stderr.write(`
|
|
20973
|
+
WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError}
|
|
20974
|
+
` + "It is live on your account. Save it now, or revoke it with: candle keys revoke " + `${body.keyPrefix}
|
|
20975
|
+
`);
|
|
20976
|
+
}
|
|
20892
20977
|
if (!requestedScopes) {
|
|
20893
20978
|
deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
|
|
20894
20979
|
`);
|
|
20895
20980
|
}
|
|
20896
|
-
|
|
20981
|
+
if (storeError === undefined) {
|
|
20982
|
+
deps.stdout.write(stored ? `Stored in the ${deps.backend} backend as the CLI's working key.
|
|
20897
20983
|
` : `Not stored: the CLI already manages a different working key. This key belongs to whichever agent it was minted for.
|
|
20898
20984
|
`);
|
|
20899
|
-
|
|
20985
|
+
}
|
|
20986
|
+
return storeError === undefined ? 0 : 1;
|
|
20900
20987
|
}
|
|
20901
20988
|
async function keysRevoke(args, ctx) {
|
|
20902
20989
|
const { deps, apiUrl, json } = ctx;
|
|
@@ -21149,7 +21236,7 @@ async function profileAdd(args, ctx) {
|
|
|
21149
21236
|
return 2;
|
|
21150
21237
|
}
|
|
21151
21238
|
const config = await deps.readConfig();
|
|
21152
|
-
if (config.profiles
|
|
21239
|
+
if (config.profiles !== undefined && Object.hasOwn(config.profiles, name)) {
|
|
21153
21240
|
writeLocalFailure(deps, {
|
|
21154
21241
|
code: "PROFILE_EXISTS",
|
|
21155
21242
|
message: `Profile "${name}" already exists.`,
|
|
@@ -21181,7 +21268,7 @@ async function profileUse(args, ctx) {
|
|
|
21181
21268
|
return 2;
|
|
21182
21269
|
}
|
|
21183
21270
|
const config = await deps.readConfig();
|
|
21184
|
-
const profile = config.profiles
|
|
21271
|
+
const profile = config.profiles !== undefined && Object.hasOwn(config.profiles, name) ? config.profiles[name] : undefined;
|
|
21185
21272
|
if (!profile) {
|
|
21186
21273
|
const names = Object.keys(config.profiles ?? {}).join(", ") || "(none)";
|
|
21187
21274
|
writeLocalFailure(deps, {
|
|
@@ -24768,11 +24855,16 @@ function buildCipherSuite() {
|
|
|
24768
24855
|
function parseSolanaSecret(input) {
|
|
24769
24856
|
const trimmed = input.trim();
|
|
24770
24857
|
if (!trimmed.startsWith("[")) {
|
|
24858
|
+
let decoded;
|
|
24771
24859
|
try {
|
|
24772
|
-
|
|
24860
|
+
decoded = base58.decode(trimmed);
|
|
24773
24861
|
} catch {
|
|
24774
24862
|
throw new Error("Invalid Solana private key: expected a base58 string or an id.json byte array.");
|
|
24775
24863
|
}
|
|
24864
|
+
if (decoded.length !== 64) {
|
|
24865
|
+
throw new Error(`Invalid Solana private key: expected 64 bytes, got ${decoded.length}. A 32-byte value is the ` + "SEED, not the keypair; export the full secret key, which is what solana-keygen writes to id.json.");
|
|
24866
|
+
}
|
|
24867
|
+
return decoded;
|
|
24776
24868
|
}
|
|
24777
24869
|
const parsed = (() => {
|
|
24778
24870
|
try {
|
|
@@ -24790,8 +24882,8 @@ function parseSolanaSecret(input) {
|
|
|
24790
24882
|
function decodeWalletPrivateKey(chain2, privateKey) {
|
|
24791
24883
|
if (chain2 === "evm") {
|
|
24792
24884
|
const hex2 = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
|
|
24793
|
-
if (hex2.length
|
|
24794
|
-
throw new Error(
|
|
24885
|
+
if (hex2.length !== 64 || !/^[0-9a-fA-F]+$/.test(hex2)) {
|
|
24886
|
+
throw new Error("Invalid EVM private key: expected 32 bytes as 64 hex characters " + `(optionally "0x"-prefixed), got ${hex2.length} characters`);
|
|
24795
24887
|
}
|
|
24796
24888
|
return Uint8Array.from(Buffer.from(hex2, "hex"));
|
|
24797
24889
|
}
|
|
@@ -25187,22 +25279,32 @@ async function walletsImport(args, ctx) {
|
|
|
25187
25279
|
}
|
|
25188
25280
|
async function verifyImportLanded(args) {
|
|
25189
25281
|
const { deps } = args.ctx;
|
|
25190
|
-
|
|
25191
|
-
|
|
25192
|
-
|
|
25193
|
-
|
|
25194
|
-
|
|
25195
|
-
|
|
25196
|
-
|
|
25197
|
-
|
|
25198
|
-
|
|
25199
|
-
|
|
25200
|
-
|
|
25201
|
-
|
|
25202
|
-
|
|
25203
|
-
|
|
25204
|
-
|
|
25205
|
-
|
|
25282
|
+
let cursor = null;
|
|
25283
|
+
let account;
|
|
25284
|
+
for (let pageCount = 0;pageCount < 25; pageCount++) {
|
|
25285
|
+
const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
|
|
25286
|
+
const listed = await apiRequest(`/api/v1/agent/wallets${query}`, {
|
|
25287
|
+
method: "GET",
|
|
25288
|
+
auth: "key",
|
|
25289
|
+
credentials: { apiKey: args.apiKey },
|
|
25290
|
+
apiUrl: args.apiUrl,
|
|
25291
|
+
fetch: deps.fetch,
|
|
25292
|
+
env: deps.env
|
|
25293
|
+
});
|
|
25294
|
+
if (!listed.ok)
|
|
25295
|
+
return { status: "unchecked", ...account !== undefined ? { account } : {} };
|
|
25296
|
+
const body = listed.body;
|
|
25297
|
+
if (!Array.isArray(body.page))
|
|
25298
|
+
return { status: "unchecked", ...account !== undefined ? { account } : {} };
|
|
25299
|
+
account ??= body.page.find((row) => typeof row.userAddress === "string")?.userAddress;
|
|
25300
|
+
if (body.page.some((row) => row._id === args.id)) {
|
|
25301
|
+
return { status: "verified", ...account !== undefined ? { account } : {} };
|
|
25302
|
+
}
|
|
25303
|
+
if (body.isDone !== false || typeof body.continueCursor !== "string")
|
|
25304
|
+
break;
|
|
25305
|
+
cursor = body.continueCursor;
|
|
25306
|
+
}
|
|
25307
|
+
return { status: "missing", ...account !== undefined ? { account } : {} };
|
|
25206
25308
|
}
|
|
25207
25309
|
async function walletsRevoke(args, ctx) {
|
|
25208
25310
|
const { deps, apiUrl, json } = ctx;
|
|
@@ -25315,7 +25417,7 @@ async function writeKeystoreFile(path, contents) {
|
|
|
25315
25417
|
const dir = dirname3(path);
|
|
25316
25418
|
await mkdir2(dir, { recursive: true });
|
|
25317
25419
|
await chmod2(dir, 448);
|
|
25318
|
-
const tmpPath = `${path}.tmp`;
|
|
25420
|
+
const tmpPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
25319
25421
|
await writeFile2(tmpPath, contents, { encoding: "utf8", mode: 384 });
|
|
25320
25422
|
await chmod2(tmpPath, 384);
|
|
25321
25423
|
await rename2(tmpPath, path);
|
|
@@ -27919,7 +28021,16 @@ async function walletsGenerate(args, ctx) {
|
|
|
27919
28021
|
let existingRaw = null;
|
|
27920
28022
|
try {
|
|
27921
28023
|
existingRaw = await deps.readFile(keystorePath);
|
|
27922
|
-
} catch {
|
|
28024
|
+
} catch (error) {
|
|
28025
|
+
const code = error?.code;
|
|
28026
|
+
if (code !== undefined && code !== "ENOENT") {
|
|
28027
|
+
writeLocalFailure(deps, {
|
|
28028
|
+
code: "KEYSTORE_UNREADABLE",
|
|
28029
|
+
message: `Could not read ${keystorePath}: ${error instanceof Error ? error.message : error}`,
|
|
28030
|
+
suggestion: "Refusing to continue: a keystore may exist at that path, and overwriting it would destroy the only copy of its keys."
|
|
28031
|
+
}, json);
|
|
28032
|
+
return 1;
|
|
28033
|
+
}
|
|
27923
28034
|
existingRaw = null;
|
|
27924
28035
|
}
|
|
27925
28036
|
if (existingRaw !== null && !resuming) {
|
|
@@ -28045,14 +28156,25 @@ Sealed to ${keystorePath}
|
|
|
28045
28156
|
if (alreadyExists) {
|
|
28046
28157
|
const found = await lookupByAddress(entry.address, apiKey, ctx);
|
|
28047
28158
|
if (found) {
|
|
28159
|
+
const promoted = await promoteStagedSigner(deps, entry, found._id);
|
|
28048
28160
|
entry.imported = true;
|
|
28049
|
-
entry.
|
|
28161
|
+
entry.linkedWalletId = found._id;
|
|
28050
28162
|
entry.importedAt = new Date().toISOString();
|
|
28051
28163
|
await persist(store, keystorePath);
|
|
28052
|
-
if (
|
|
28053
|
-
|
|
28164
|
+
if (promoted.ok) {
|
|
28165
|
+
if (!json) {
|
|
28166
|
+
deps.stdout.write(` [${entry.index}] ${entry.address} already imported, reconciled as ${found._id}
|
|
28054
28167
|
`);
|
|
28055
|
-
|
|
28168
|
+
}
|
|
28169
|
+
continue;
|
|
28170
|
+
}
|
|
28171
|
+
failures++;
|
|
28172
|
+
writeLocalFailure(deps, {
|
|
28173
|
+
code: "SIGNER_MISSING",
|
|
28174
|
+
message: `Wallet ${entry.address} exists as ${found._id}, but ${promoted.message}.`,
|
|
28175
|
+
suggestion: `Revoke it (candle wallets revoke ${found._id}) and run: candle wallets generate --resume`
|
|
28176
|
+
}, json);
|
|
28177
|
+
break;
|
|
28056
28178
|
}
|
|
28057
28179
|
}
|
|
28058
28180
|
failures++;
|
|
@@ -28068,6 +28190,7 @@ Sealed to ${keystorePath}
|
|
|
28068
28190
|
break;
|
|
28069
28191
|
}
|
|
28070
28192
|
entry.imported = true;
|
|
28193
|
+
entry.linkedWalletId = flow.submitted.id;
|
|
28071
28194
|
entry.privyWalletId = flow.submitted.privyWalletId;
|
|
28072
28195
|
entry.importedAt = new Date().toISOString();
|
|
28073
28196
|
await persist(store, keystorePath);
|
|
@@ -28106,6 +28229,22 @@ ${imported}/${store.entries.length} imported.
|
|
|
28106
28229
|
`);
|
|
28107
28230
|
return 0;
|
|
28108
28231
|
}
|
|
28232
|
+
async function promoteStagedSigner(deps, entry, linkedWalletId) {
|
|
28233
|
+
const committedRef = walletSignerRef(linkedWalletId);
|
|
28234
|
+
if (await deps.store.get(committedRef) !== null)
|
|
28235
|
+
return { ok: true };
|
|
28236
|
+
const pendingRef = importPendingSignerRef(entry.chain, entry.address);
|
|
28237
|
+
const staged = await deps.store.get(pendingRef);
|
|
28238
|
+
if (staged === null) {
|
|
28239
|
+
return {
|
|
28240
|
+
ok: false,
|
|
28241
|
+
message: `its signer is on neither "${committedRef}" nor "${pendingRef}" in the ${deps.backend} store`
|
|
28242
|
+
};
|
|
28243
|
+
}
|
|
28244
|
+
await deps.store.set(committedRef, staged);
|
|
28245
|
+
await deps.store.delete(pendingRef).catch(() => {});
|
|
28246
|
+
return { ok: true };
|
|
28247
|
+
}
|
|
28109
28248
|
async function persist(store, path) {
|
|
28110
28249
|
await writeKeystoreFile(path, await serializeKeystore(store.entries, store.key, store.salt, store.iterations));
|
|
28111
28250
|
}
|
|
@@ -28124,7 +28263,7 @@ async function lookupByAddress(address, apiKey, ctx) {
|
|
|
28124
28263
|
}
|
|
28125
28264
|
|
|
28126
28265
|
// src/config.ts
|
|
28127
|
-
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
28266
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
28128
28267
|
import { homedir as homedir4 } from "node:os";
|
|
28129
28268
|
import { join as join6 } from "node:path";
|
|
28130
28269
|
function configDir2() {
|
|
@@ -28159,7 +28298,7 @@ async function updateProfile(name, patch) {
|
|
|
28159
28298
|
}
|
|
28160
28299
|
async function clearConfig() {
|
|
28161
28300
|
try {
|
|
28162
|
-
await
|
|
28301
|
+
await rm2(configFilePath());
|
|
28163
28302
|
} catch (err) {
|
|
28164
28303
|
if (err.code !== "ENOENT")
|
|
28165
28304
|
throw err;
|
package/package.json
CHANGED