@did-btcr2/cli 0.10.3 → 0.12.0
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/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +1028 -114
- package/dist/esm/src/cli.js +31 -13
- package/dist/esm/src/cli.js.map +1 -1
- package/dist/esm/src/commands/completion.js +36 -0
- package/dist/esm/src/commands/completion.js.map +1 -0
- package/dist/esm/src/commands/config.js +69 -0
- package/dist/esm/src/commands/config.js.map +1 -0
- package/dist/esm/src/commands/create.js +109 -30
- package/dist/esm/src/commands/create.js.map +1 -1
- package/dist/esm/src/commands/deactivate.js +21 -8
- package/dist/esm/src/commands/deactivate.js.map +1 -1
- package/dist/esm/src/commands/index.js +4 -0
- package/dist/esm/src/commands/index.js.map +1 -1
- package/dist/esm/src/commands/key.js +175 -0
- package/dist/esm/src/commands/key.js.map +1 -0
- package/dist/esm/src/commands/profile.js +63 -0
- package/dist/esm/src/commands/profile.js.map +1 -0
- package/dist/esm/src/commands/update.js +19 -9
- package/dist/esm/src/commands/update.js.map +1 -1
- package/dist/esm/src/config.js +119 -12
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/keystore/atomic.js +64 -0
- package/dist/esm/src/keystore/atomic.js.map +1 -0
- package/dist/esm/src/keystore/envelope.js +123 -0
- package/dist/esm/src/keystore/envelope.js.map +1 -0
- package/dist/esm/src/keystore/error.js +16 -0
- package/dist/esm/src/keystore/error.js.map +1 -0
- package/dist/esm/src/keystore/file-backed-key-manager.js +78 -0
- package/dist/esm/src/keystore/file-backed-key-manager.js.map +1 -0
- package/dist/esm/src/keystore/file-key-store.js +184 -0
- package/dist/esm/src/keystore/file-key-store.js.map +1 -0
- package/dist/esm/src/keystore/passphrase.js +87 -0
- package/dist/esm/src/keystore/passphrase.js.map +1 -0
- package/dist/esm/src/keystore/paths.js +20 -0
- package/dist/esm/src/keystore/paths.js.map +1 -0
- package/dist/esm/src/keystore/resolve-key-ref.js +47 -0
- package/dist/esm/src/keystore/resolve-key-ref.js.map +1 -0
- package/dist/types/src/cli.d.ts +6 -2
- package/dist/types/src/cli.d.ts.map +1 -1
- package/dist/types/src/commands/completion.d.ts +5 -0
- package/dist/types/src/commands/completion.d.ts.map +1 -0
- package/dist/types/src/commands/config.d.ts +5 -0
- package/dist/types/src/commands/config.d.ts.map +1 -0
- package/dist/types/src/commands/create.d.ts +19 -1
- package/dist/types/src/commands/create.d.ts.map +1 -1
- package/dist/types/src/commands/deactivate.d.ts.map +1 -1
- package/dist/types/src/commands/index.d.ts +4 -0
- package/dist/types/src/commands/index.d.ts.map +1 -1
- package/dist/types/src/commands/key.d.ts +10 -0
- package/dist/types/src/commands/key.d.ts.map +1 -0
- package/dist/types/src/commands/profile.d.ts +5 -0
- package/dist/types/src/commands/profile.d.ts.map +1 -0
- package/dist/types/src/commands/update.d.ts.map +1 -1
- package/dist/types/src/config.d.ts +57 -5
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/keystore/atomic.d.ts +19 -0
- package/dist/types/src/keystore/atomic.d.ts.map +1 -0
- package/dist/types/src/keystore/envelope.d.ts +64 -0
- package/dist/types/src/keystore/envelope.d.ts.map +1 -0
- package/dist/types/src/keystore/error.d.ts +14 -0
- package/dist/types/src/keystore/error.d.ts.map +1 -0
- package/dist/types/src/keystore/file-backed-key-manager.d.ts +41 -0
- package/dist/types/src/keystore/file-backed-key-manager.d.ts.map +1 -0
- package/dist/types/src/keystore/file-key-store.d.ts +52 -0
- package/dist/types/src/keystore/file-key-store.d.ts.map +1 -0
- package/dist/types/src/keystore/passphrase.d.ts +20 -0
- package/dist/types/src/keystore/passphrase.d.ts.map +1 -0
- package/dist/types/src/keystore/paths.d.ts +13 -0
- package/dist/types/src/keystore/paths.d.ts.map +1 -0
- package/dist/types/src/keystore/resolve-key-ref.d.ts +19 -0
- package/dist/types/src/keystore/resolve-key-ref.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +93 -5
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +9 -4
- package/src/cli.ts +37 -12
- package/src/commands/completion.ts +40 -0
- package/src/commands/config.ts +84 -0
- package/src/commands/create.ts +140 -52
- package/src/commands/deactivate.ts +25 -12
- package/src/commands/index.ts +4 -0
- package/src/commands/key.ts +193 -0
- package/src/commands/profile.ts +65 -0
- package/src/commands/update.ts +23 -13
- package/src/config.ts +165 -20
- package/src/keystore/atomic.ts +73 -0
- package/src/keystore/envelope.ts +172 -0
- package/src/keystore/error.ts +16 -0
- package/src/keystore/file-backed-key-manager.ts +99 -0
- package/src/keystore/file-key-store.ts +242 -0
- package/src/keystore/passphrase.ts +99 -0
- package/src/keystore/paths.ts +20 -0
- package/src/keystore/resolve-key-ref.ts +62 -0
- package/src/types.ts +31 -18
package/dist/cjs/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CLIError: () => CLIError,
|
|
24
|
+
CONFIG_SCHEMA_VERSION: () => CONFIG_SCHEMA_VERSION,
|
|
24
25
|
DidBtcr2Cli: () => DidBtcr2Cli,
|
|
25
26
|
ENV_VARS: () => ENV_VARS,
|
|
26
27
|
SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
|
|
@@ -29,23 +30,43 @@ __export(index_exports, {
|
|
|
29
30
|
defaultConfigPath: () => defaultConfigPath,
|
|
30
31
|
deriveNetwork: () => deriveNetwork,
|
|
31
32
|
formatResult: () => formatResult,
|
|
33
|
+
getConfigPath: () => getConfigPath,
|
|
34
|
+
keystoreApiFactory: () => keystoreApiFactory,
|
|
32
35
|
profileToOverrides: () => profileToOverrides,
|
|
33
36
|
readConfigFile: () => readConfigFile,
|
|
34
37
|
readEnvOverrides: () => readEnvOverrides,
|
|
38
|
+
registerCompletionCommand: () => registerCompletionCommand,
|
|
39
|
+
registerConfigCommand: () => registerConfigCommand,
|
|
35
40
|
registerCreateCommand: () => registerCreateCommand,
|
|
36
41
|
registerDeactivateCommand: () => registerDeactivateCommand,
|
|
42
|
+
registerKeyCommand: () => registerKeyCommand,
|
|
43
|
+
registerProfileCommand: () => registerProfileCommand,
|
|
37
44
|
registerResolveCommand: () => registerResolveCommand,
|
|
38
|
-
registerUpdateCommand: () => registerUpdateCommand
|
|
45
|
+
registerUpdateCommand: () => registerUpdateCommand,
|
|
46
|
+
resolveDefaultNetwork: () => resolveDefaultNetwork,
|
|
47
|
+
setConfigPath: () => setConfigPath,
|
|
48
|
+
unsetConfigPath: () => unsetConfigPath,
|
|
49
|
+
writeConfigFile: () => writeConfigFile
|
|
39
50
|
});
|
|
40
51
|
module.exports = __toCommonJS(index_exports);
|
|
41
52
|
|
|
42
|
-
// ../../node_modules/.pnpm/tsup@8.5.
|
|
53
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_typescript@5.7.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
|
|
43
54
|
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
44
55
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
45
56
|
|
|
46
57
|
// src/cli.ts
|
|
58
|
+
var import_common3 = require("@did-btcr2/common");
|
|
47
59
|
var import_commander = require("commander");
|
|
48
60
|
|
|
61
|
+
// src/commands/create.ts
|
|
62
|
+
var import_utils2 = require("@noble/hashes/utils.js");
|
|
63
|
+
|
|
64
|
+
// src/config.ts
|
|
65
|
+
var import_api = require("@did-btcr2/api");
|
|
66
|
+
var import_node_fs4 = require("fs");
|
|
67
|
+
var import_node_os2 = require("os");
|
|
68
|
+
var import_node_path4 = require("path");
|
|
69
|
+
|
|
49
70
|
// src/error.ts
|
|
50
71
|
var import_common = require("@did-btcr2/common");
|
|
51
72
|
var CLIError = class extends import_common.DidMethodError {
|
|
@@ -54,13 +75,459 @@ var CLIError = class extends import_common.DidMethodError {
|
|
|
54
75
|
}
|
|
55
76
|
};
|
|
56
77
|
|
|
57
|
-
// src/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
// src/keystore/atomic.ts
|
|
79
|
+
var import_node_fs = require("fs");
|
|
80
|
+
var import_node_path = require("path");
|
|
81
|
+
|
|
82
|
+
// src/keystore/error.ts
|
|
83
|
+
var import_common2 = require("@did-btcr2/common");
|
|
84
|
+
var KeyStoreError = class extends import_common2.DidMethodError {
|
|
85
|
+
constructor(message, type = "KeyStoreError", data) {
|
|
86
|
+
super(message, { type, name: type, data });
|
|
61
87
|
}
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/keystore/atomic.ts
|
|
91
|
+
var isWindows = process.platform === "win32";
|
|
92
|
+
var permsWarned = false;
|
|
93
|
+
var tmpCounter = 0;
|
|
94
|
+
function ensureDir(dir, mode) {
|
|
95
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode });
|
|
96
|
+
if (!isWindows) {
|
|
97
|
+
try {
|
|
98
|
+
(0, import_node_fs.chmodSync)(dir, mode);
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writeFileAtomic(path, data, mode) {
|
|
104
|
+
const tmp = (0, import_node_path.join)((0, import_node_path.dirname)(path), `.${(0, import_node_path.basename)(path)}.${process.pid}.${tmpCounter++}.tmp`);
|
|
105
|
+
try {
|
|
106
|
+
(0, import_node_fs.writeFileSync)(tmp, data, { mode });
|
|
107
|
+
if (!isWindows) (0, import_node_fs.chmodSync)(tmp, mode);
|
|
108
|
+
(0, import_node_fs.renameSync)(tmp, path);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
try {
|
|
111
|
+
(0, import_node_fs.rmSync)(tmp, { force: true });
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
throw new KeyStoreError(
|
|
115
|
+
`Failed to write keystore at ${path}.`,
|
|
116
|
+
"ATOMIC_WRITE_ERROR",
|
|
117
|
+
{ path, cause: error instanceof Error ? error.message : String(error) }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function assertSecurePerms(path) {
|
|
122
|
+
if (isWindows) {
|
|
123
|
+
if (!permsWarned) {
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
"warning: file permissions are not enforced on Windows; protect the keystore directory manually.\n"
|
|
126
|
+
);
|
|
127
|
+
permsWarned = true;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const mode = (0, import_node_fs.statSync)(path).mode & 511;
|
|
132
|
+
if ((mode & 63) !== 0) {
|
|
133
|
+
throw new KeyStoreError(
|
|
134
|
+
`Keystore at ${path} has insecure permissions 0${mode.toString(8)}; expected 0600.`,
|
|
135
|
+
"KEYSTORE_PERMISSION_ERROR",
|
|
136
|
+
{ path, mode: `0${mode.toString(8)}` }
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/keystore/file-backed-key-manager.ts
|
|
142
|
+
var import_key_manager = require("@did-btcr2/key-manager");
|
|
143
|
+
|
|
144
|
+
// src/keystore/file-key-store.ts
|
|
145
|
+
var import_node_fs2 = require("fs");
|
|
146
|
+
var import_node_path3 = require("path");
|
|
147
|
+
var import_base2 = require("@scure/base");
|
|
148
|
+
|
|
149
|
+
// src/keystore/envelope.ts
|
|
150
|
+
var import_chacha = require("@noble/ciphers/chacha.js");
|
|
151
|
+
var import_argon2 = require("@noble/hashes/argon2.js");
|
|
152
|
+
var import_utils = require("@noble/hashes/utils.js");
|
|
153
|
+
var import_base = require("@scure/base");
|
|
154
|
+
var ENVELOPE_VERSION = 1;
|
|
155
|
+
var SALT_BYTES = 16;
|
|
156
|
+
var NONCE_BYTES = 24;
|
|
157
|
+
var KEY_BYTES = 32;
|
|
158
|
+
var DEFAULT_ARGON_PARAMS = { t: 3, m: 65536, p: 4, dkLen: KEY_BYTES };
|
|
159
|
+
function buildHeader(saltB64, params) {
|
|
160
|
+
return {
|
|
161
|
+
v: ENVELOPE_VERSION,
|
|
162
|
+
kdf: {
|
|
163
|
+
alg: "argon2id",
|
|
164
|
+
salt: saltB64,
|
|
165
|
+
t: params.t,
|
|
166
|
+
m: params.m,
|
|
167
|
+
p: params.p,
|
|
168
|
+
dkLen: params.dkLen
|
|
169
|
+
},
|
|
170
|
+
cipher: "xchacha20poly1305"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function headerAad(header) {
|
|
174
|
+
return (0, import_utils.utf8ToBytes)(JSON.stringify(header));
|
|
175
|
+
}
|
|
176
|
+
function deriveKey(passphrase, salt, params) {
|
|
177
|
+
const password = (0, import_utils.utf8ToBytes)(passphrase);
|
|
178
|
+
try {
|
|
179
|
+
return (0, import_argon2.argon2id)(password, salt, { t: params.t, m: params.m, p: params.p, dkLen: params.dkLen });
|
|
180
|
+
} finally {
|
|
181
|
+
password.fill(0);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function encryptSecret(secret, passphrase, params = DEFAULT_ARGON_PARAMS) {
|
|
185
|
+
if (secret.length === 0) {
|
|
186
|
+
throw new KeyStoreError("Cannot encrypt an empty secret.", "ENVELOPE_ENCRYPT_ERROR");
|
|
187
|
+
}
|
|
188
|
+
const salt = (0, import_utils.randomBytes)(SALT_BYTES);
|
|
189
|
+
const nonce = (0, import_utils.randomBytes)(NONCE_BYTES);
|
|
190
|
+
const header = buildHeader(import_base.base64urlnopad.encode(salt), params);
|
|
191
|
+
const key = deriveKey(passphrase, salt, params);
|
|
192
|
+
try {
|
|
193
|
+
const ciphertext = (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).encrypt(secret);
|
|
194
|
+
return {
|
|
195
|
+
...header,
|
|
196
|
+
nonce: import_base.base64urlnopad.encode(nonce),
|
|
197
|
+
ciphertext: import_base.base64urlnopad.encode(ciphertext)
|
|
198
|
+
};
|
|
199
|
+
} finally {
|
|
200
|
+
key.fill(0);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function decryptSecret(env, passphrase) {
|
|
204
|
+
if (env.v !== ENVELOPE_VERSION) {
|
|
205
|
+
throw new KeyStoreError(
|
|
206
|
+
`Unsupported keystore envelope version: ${String(env.v)}.`,
|
|
207
|
+
"ENVELOPE_VERSION_ERROR",
|
|
208
|
+
{ version: env.v }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (env.kdf?.alg !== "argon2id" || env.cipher !== "xchacha20poly1305") {
|
|
212
|
+
throw new KeyStoreError("Unsupported keystore envelope algorithm.", "ENVELOPE_VERSION_ERROR");
|
|
213
|
+
}
|
|
214
|
+
const params = { t: env.kdf.t, m: env.kdf.m, p: env.kdf.p, dkLen: env.kdf.dkLen };
|
|
215
|
+
const salt = import_base.base64urlnopad.decode(env.kdf.salt);
|
|
216
|
+
const nonce = import_base.base64urlnopad.decode(env.nonce);
|
|
217
|
+
const ciphertext = import_base.base64urlnopad.decode(env.ciphertext);
|
|
218
|
+
const header = buildHeader(env.kdf.salt, params);
|
|
219
|
+
const key = deriveKey(passphrase, salt, params);
|
|
220
|
+
try {
|
|
221
|
+
return (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).decrypt(ciphertext);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof KeyStoreError) throw error;
|
|
224
|
+
throw new KeyStoreError(
|
|
225
|
+
"Keystore decryption failed: wrong passphrase or corrupted keystore.",
|
|
226
|
+
"DECRYPT_ERROR"
|
|
227
|
+
);
|
|
228
|
+
} finally {
|
|
229
|
+
key.fill(0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/keystore/paths.ts
|
|
234
|
+
var import_node_os = require("os");
|
|
235
|
+
var import_node_path2 = require("path");
|
|
236
|
+
function defaultKeystorePath() {
|
|
237
|
+
const base = process.env.XDG_DATA_HOME ?? process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".local", "share");
|
|
238
|
+
return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/keystore/file-key-store.ts
|
|
242
|
+
var KEYSTORE_VERSION = 1;
|
|
243
|
+
var FileKeyStore = class {
|
|
244
|
+
#path;
|
|
245
|
+
#getPassphrase;
|
|
246
|
+
#argonParams;
|
|
247
|
+
#cache = /* @__PURE__ */ new Map();
|
|
248
|
+
#active;
|
|
249
|
+
constructor(options) {
|
|
250
|
+
this.#path = options.path ?? defaultKeystorePath();
|
|
251
|
+
this.#getPassphrase = options.getPassphrase;
|
|
252
|
+
this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
|
|
253
|
+
ensureDir((0, import_node_path3.dirname)(this.#path), 448);
|
|
254
|
+
this.#load();
|
|
255
|
+
}
|
|
256
|
+
#load() {
|
|
257
|
+
if (!(0, import_node_fs2.existsSync)(this.#path)) return;
|
|
258
|
+
assertSecurePerms(this.#path);
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = JSON.parse((0, import_node_fs2.readFileSync)(this.#path, "utf-8"));
|
|
262
|
+
} catch {
|
|
263
|
+
throw new KeyStoreError(
|
|
264
|
+
`Keystore at ${this.#path} is corrupt or unreadable.`,
|
|
265
|
+
"KEYSTORE_CORRUPT_ERROR",
|
|
266
|
+
{ path: this.#path }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (parsed.v !== KEYSTORE_VERSION) {
|
|
270
|
+
throw new KeyStoreError(
|
|
271
|
+
`Unsupported keystore version: ${String(parsed.v)}.`,
|
|
272
|
+
"KEYSTORE_VERSION_ERROR",
|
|
273
|
+
{ version: parsed.v }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
this.#active = parsed.active;
|
|
277
|
+
for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
|
|
278
|
+
let publicKey;
|
|
279
|
+
try {
|
|
280
|
+
if (typeof stored.publicKey !== "string") throw new Error("missing publicKey");
|
|
281
|
+
publicKey = import_base2.base64urlnopad.decode(stored.publicKey);
|
|
282
|
+
} catch {
|
|
283
|
+
throw new KeyStoreError(
|
|
284
|
+
`Keystore entry ${id} has a malformed public key.`,
|
|
285
|
+
"KEYSTORE_CORRUPT_ERROR",
|
|
286
|
+
{ path: this.#path, keyId: id }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (publicKey.length !== 33) {
|
|
290
|
+
throw new KeyStoreError(
|
|
291
|
+
`Keystore entry ${id} has a ${publicKey.length}-byte public key; expected 33.`,
|
|
292
|
+
"KEYSTORE_CORRUPT_ERROR",
|
|
293
|
+
{ path: this.#path, keyId: id }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
this.#cache.set(id, {
|
|
297
|
+
publicKey,
|
|
298
|
+
...stored.tags && { tags: stored.tags },
|
|
299
|
+
...stored.secret && { secret: stored.secret }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
#flush() {
|
|
304
|
+
const keys = {};
|
|
305
|
+
for (const [id, entry] of this.#cache) {
|
|
306
|
+
keys[id] = {
|
|
307
|
+
publicKey: import_base2.base64urlnopad.encode(entry.publicKey),
|
|
308
|
+
...entry.tags && { tags: entry.tags },
|
|
309
|
+
...entry.secret && { secret: entry.secret }
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const file = {
|
|
313
|
+
v: KEYSTORE_VERSION,
|
|
314
|
+
...this.#active && { active: this.#active },
|
|
315
|
+
keys
|
|
316
|
+
};
|
|
317
|
+
writeFileAtomic(this.#path, `${JSON.stringify(file, null, 2)}
|
|
318
|
+
`, 384);
|
|
319
|
+
}
|
|
320
|
+
get(id) {
|
|
321
|
+
const entry = this.#cache.get(id);
|
|
322
|
+
if (!entry) return void 0;
|
|
323
|
+
const result = {
|
|
324
|
+
publicKey: entry.publicKey,
|
|
325
|
+
...entry.tags && { tags: entry.tags }
|
|
326
|
+
};
|
|
327
|
+
if (entry.secret) {
|
|
328
|
+
const sealed = entry.secret;
|
|
329
|
+
Object.defineProperty(result, "secretKey", {
|
|
330
|
+
configurable: true,
|
|
331
|
+
enumerable: false,
|
|
332
|
+
get: () => {
|
|
333
|
+
entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
|
|
334
|
+
return entry.decrypted;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
has(id) {
|
|
341
|
+
return this.#cache.has(id);
|
|
342
|
+
}
|
|
343
|
+
set(id, value) {
|
|
344
|
+
const secret = value.secretKey ? encryptSecret(value.secretKey, this.#getPassphrase(), this.#argonParams) : void 0;
|
|
345
|
+
this.#cache.set(id, {
|
|
346
|
+
publicKey: value.publicKey,
|
|
347
|
+
...value.tags && { tags: value.tags },
|
|
348
|
+
...secret && { secret },
|
|
349
|
+
...value.secretKey && { decrypted: value.secretKey }
|
|
350
|
+
});
|
|
351
|
+
this.#flush();
|
|
352
|
+
}
|
|
353
|
+
delete(id) {
|
|
354
|
+
const existed = this.#cache.delete(id);
|
|
355
|
+
if (existed) {
|
|
356
|
+
if (this.#active === id) this.#active = void 0;
|
|
357
|
+
this.#flush();
|
|
358
|
+
}
|
|
359
|
+
return existed;
|
|
360
|
+
}
|
|
361
|
+
clear() {
|
|
362
|
+
this.#cache.clear();
|
|
363
|
+
this.#active = void 0;
|
|
364
|
+
this.#flush();
|
|
365
|
+
}
|
|
366
|
+
/** All stored values with secret keys omitted. Never decrypts, never prompts. */
|
|
367
|
+
list() {
|
|
368
|
+
return this.entries().map(([, value]) => value);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* All entries as id-value tuples with secret keys omitted. Never decrypts,
|
|
372
|
+
* never prompts: {@link FileKeyStore.get} is the only secret-materializing
|
|
373
|
+
* path, so callers that only need identifiers (such as `listKeys`) do not
|
|
374
|
+
* force a passphrase prompt. This deviates intentionally from the in-memory
|
|
375
|
+
* store, which returns stored values verbatim.
|
|
376
|
+
*/
|
|
377
|
+
entries() {
|
|
378
|
+
const out = [];
|
|
379
|
+
for (const [id, entry] of this.#cache) {
|
|
380
|
+
out.push([id, {
|
|
381
|
+
publicKey: entry.publicKey,
|
|
382
|
+
...entry.tags && { tags: entry.tags }
|
|
383
|
+
}]);
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
close() {
|
|
388
|
+
for (const entry of this.#cache.values()) {
|
|
389
|
+
entry.decrypted?.fill(0);
|
|
390
|
+
entry.decrypted = void 0;
|
|
391
|
+
}
|
|
392
|
+
this.#cache.clear();
|
|
393
|
+
}
|
|
394
|
+
/** The persisted active-key identifier, or undefined if none is set. */
|
|
395
|
+
getActive() {
|
|
396
|
+
return this.#active;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Persists the active-key pointer in the keystore file. Passing undefined
|
|
400
|
+
* clears it. Throws if the identifier is not a known key.
|
|
401
|
+
*/
|
|
402
|
+
setActive(id) {
|
|
403
|
+
if (id !== void 0 && !this.#cache.has(id)) {
|
|
404
|
+
throw new KeyStoreError(`Cannot set unknown key as active: ${id}.`, "KEY_NOT_FOUND_ERROR", { keyId: id });
|
|
405
|
+
}
|
|
406
|
+
this.#active = id;
|
|
407
|
+
this.#flush();
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/keystore/file-backed-key-manager.ts
|
|
412
|
+
var FileBackedKeyManager = class {
|
|
413
|
+
/** Capability probe: the local store supports exporting secret material. */
|
|
414
|
+
canExport = true;
|
|
415
|
+
#store;
|
|
416
|
+
#inner;
|
|
417
|
+
constructor(options) {
|
|
418
|
+
this.#store = new FileKeyStore(options);
|
|
419
|
+
this.#inner = new import_key_manager.LocalKeyManager(this.#store);
|
|
420
|
+
const active = this.#store.getActive();
|
|
421
|
+
if (active && this.#store.has(active)) this.#inner.setActiveKey(active);
|
|
422
|
+
}
|
|
423
|
+
get activeKeyId() {
|
|
424
|
+
return this.#inner.activeKeyId;
|
|
425
|
+
}
|
|
426
|
+
setActiveKey(id) {
|
|
427
|
+
this.#inner.setActiveKey(id);
|
|
428
|
+
this.#store.setActive(id);
|
|
429
|
+
}
|
|
430
|
+
importKey(keyPair, options) {
|
|
431
|
+
const id = this.#inner.importKey(keyPair, options);
|
|
432
|
+
if (options?.setActive) this.#store.setActive(id);
|
|
433
|
+
return id;
|
|
434
|
+
}
|
|
435
|
+
generateKey(options) {
|
|
436
|
+
const id = this.#inner.generateKey(options);
|
|
437
|
+
if (options?.setActive) this.#store.setActive(id);
|
|
438
|
+
return id;
|
|
439
|
+
}
|
|
440
|
+
removeKey(id, options) {
|
|
441
|
+
this.#inner.removeKey(id, options);
|
|
442
|
+
}
|
|
443
|
+
listKeys() {
|
|
444
|
+
return this.#inner.listKeys();
|
|
445
|
+
}
|
|
446
|
+
getPublicKey(id) {
|
|
447
|
+
return this.#inner.getPublicKey(id);
|
|
448
|
+
}
|
|
449
|
+
getEntry(id) {
|
|
450
|
+
return this.#inner.getEntry(id);
|
|
451
|
+
}
|
|
452
|
+
sign(data, id, options) {
|
|
453
|
+
return this.#inner.sign(data, id, options);
|
|
454
|
+
}
|
|
455
|
+
verify(signature, data, id, options) {
|
|
456
|
+
return this.#inner.verify(signature, data, id, options);
|
|
457
|
+
}
|
|
458
|
+
digest(data) {
|
|
459
|
+
return this.#inner.digest(data);
|
|
460
|
+
}
|
|
461
|
+
exportKey(id) {
|
|
462
|
+
return this.#inner.exportKey(id);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/keystore/passphrase.ts
|
|
467
|
+
var import_node_fs3 = require("fs");
|
|
468
|
+
var ENV_KEYSTORE_PASSPHRASE = "BTCR2_KEYSTORE_PASSPHRASE";
|
|
469
|
+
function acquirePassphrase(options = {}) {
|
|
470
|
+
const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
|
|
471
|
+
if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
|
|
472
|
+
if (options.passphraseFile) {
|
|
473
|
+
return assertNonEmpty((0, import_node_fs3.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
|
|
474
|
+
}
|
|
475
|
+
if (!process.stdin.isTTY) {
|
|
476
|
+
throw new KeyStoreError(
|
|
477
|
+
`No passphrase available. Set ${ENV_KEYSTORE_PASSPHRASE}, pass --passphrase-file, or run in a terminal.`,
|
|
478
|
+
"PASSPHRASE_REQUIRED_ERROR"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const passphrase = promptHidden(options.prompt ?? "Keystore passphrase: ");
|
|
482
|
+
if (options.confirm) {
|
|
483
|
+
const again = promptHidden("Confirm passphrase: ");
|
|
484
|
+
if (passphrase !== again) {
|
|
485
|
+
throw new KeyStoreError("Passphrases did not match.", "PASSPHRASE_MISMATCH_ERROR");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return assertNonEmpty(passphrase);
|
|
489
|
+
}
|
|
490
|
+
function assertNonEmpty(passphrase) {
|
|
491
|
+
if (passphrase.trim() === "") {
|
|
492
|
+
throw new KeyStoreError("A non-empty keystore passphrase is required.", "PASSPHRASE_REQUIRED_ERROR");
|
|
493
|
+
}
|
|
494
|
+
return passphrase;
|
|
495
|
+
}
|
|
496
|
+
function promptHidden(label) {
|
|
497
|
+
process.stderr.write(label);
|
|
498
|
+
const stdin = process.stdin;
|
|
499
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
500
|
+
stdin.setRawMode(true);
|
|
501
|
+
const byte = Buffer.alloc(1);
|
|
502
|
+
const bytes = [];
|
|
503
|
+
try {
|
|
504
|
+
for (; ; ) {
|
|
505
|
+
let read = 0;
|
|
506
|
+
try {
|
|
507
|
+
read = (0, import_node_fs3.readSync)(stdin.fd, byte, 0, 1, null);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const code = error.code;
|
|
510
|
+
if (code === "EAGAIN") continue;
|
|
511
|
+
if (code === "EOF") break;
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
if (read === 0) break;
|
|
515
|
+
const ch = byte[0];
|
|
516
|
+
if (ch === 10 || ch === 13) break;
|
|
517
|
+
if (ch === 3) {
|
|
518
|
+
throw new KeyStoreError("Passphrase entry aborted.", "PASSPHRASE_REQUIRED_ERROR");
|
|
519
|
+
}
|
|
520
|
+
if (ch === 127 || ch === 8) {
|
|
521
|
+
bytes.pop();
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
bytes.push(ch);
|
|
525
|
+
}
|
|
526
|
+
} finally {
|
|
527
|
+
stdin.setRawMode(wasRaw);
|
|
528
|
+
process.stderr.write("\n");
|
|
529
|
+
}
|
|
530
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
64
531
|
}
|
|
65
532
|
|
|
66
533
|
// src/types.ts
|
|
@@ -73,75 +540,44 @@ var SUPPORTED_NETWORKS = [
|
|
|
73
540
|
"regtest"
|
|
74
541
|
];
|
|
75
542
|
|
|
76
|
-
// src/
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
).requiredOption(
|
|
86
|
-
"-b, --bytes <bytes>",
|
|
87
|
-
"Genesis bytes as a hex string. If type=k, MUST be secp256k1 public key. If type=x, MUST be SHA-256 hash of a genesis document"
|
|
88
|
-
).action(async (options) => {
|
|
89
|
-
const parsed = validateCreateOptions(options);
|
|
90
|
-
const api = factory();
|
|
91
|
-
const type = parsed.type === "k" ? "deterministic" : "external";
|
|
92
|
-
const genesisBytes = Buffer.from(parsed.bytes, "hex");
|
|
93
|
-
const data = api.createDid(type, genesisBytes, { network: parsed.network });
|
|
94
|
-
const result = { action: "create", data };
|
|
95
|
-
console.log(formatResult(result, globals()));
|
|
96
|
-
});
|
|
543
|
+
// src/config.ts
|
|
544
|
+
var CONFIG_SCHEMA_VERSION = 1;
|
|
545
|
+
function writeConfigFile(path, mutate) {
|
|
546
|
+
const raw = readConfigFile(path) ?? {};
|
|
547
|
+
mutate(raw);
|
|
548
|
+
raw.schemaVersion = CONFIG_SCHEMA_VERSION;
|
|
549
|
+
ensureDir((0, import_node_path4.dirname)(path), 448);
|
|
550
|
+
writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
|
|
551
|
+
`, 384);
|
|
97
552
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const buf = Buffer.from(options.bytes, "hex");
|
|
114
|
-
if (buf.length === 0) {
|
|
115
|
-
throw new CLIError(
|
|
116
|
-
"Invalid bytes. Must be a non-empty hex string.",
|
|
117
|
-
"INVALID_ARGUMENT_ERROR",
|
|
118
|
-
options
|
|
119
|
-
);
|
|
553
|
+
function getConfigPath(config, path) {
|
|
554
|
+
return path.split(".").reduce(
|
|
555
|
+
(node, key) => node?.[key],
|
|
556
|
+
config
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
function setConfigPath(config, path, value) {
|
|
560
|
+
const keys = path.split(".");
|
|
561
|
+
const last = keys.pop();
|
|
562
|
+
if (!last) throw new CLIError("Config path must be non-empty.", "INVALID_ARGUMENT_ERROR");
|
|
563
|
+
let node = config;
|
|
564
|
+
for (const key of keys) {
|
|
565
|
+
if (typeof node[key] !== "object" || node[key] === null) node[key] = {};
|
|
566
|
+
node = node[key];
|
|
120
567
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
568
|
+
node[last] = value;
|
|
569
|
+
}
|
|
570
|
+
function unsetConfigPath(config, path) {
|
|
571
|
+
const keys = path.split(".");
|
|
572
|
+
const last = keys.pop();
|
|
573
|
+
if (!last) return;
|
|
574
|
+
let node = config;
|
|
575
|
+
for (const key of keys) {
|
|
576
|
+
node = node?.[key];
|
|
577
|
+
if (!node) return;
|
|
128
578
|
}
|
|
129
|
-
|
|
130
|
-
type: options.type,
|
|
131
|
-
network: options.network,
|
|
132
|
-
bytes: options.bytes
|
|
133
|
-
};
|
|
579
|
+
delete node[last];
|
|
134
580
|
}
|
|
135
|
-
|
|
136
|
-
// src/commands/resolve.ts
|
|
137
|
-
var import_api2 = require("@did-btcr2/api");
|
|
138
|
-
var import_promises = require("fs/promises");
|
|
139
|
-
|
|
140
|
-
// src/config.ts
|
|
141
|
-
var import_api = require("@did-btcr2/api");
|
|
142
|
-
var import_node_fs = require("fs");
|
|
143
|
-
var import_node_os = require("os");
|
|
144
|
-
var import_node_path = require("path");
|
|
145
581
|
var ENV_VARS = {
|
|
146
582
|
BTC_REST: "BTCR2_BTC_REST",
|
|
147
583
|
BTC_RPC_URL: "BTCR2_BTC_RPC_URL",
|
|
@@ -160,12 +596,12 @@ function readEnvOverrides() {
|
|
|
160
596
|
};
|
|
161
597
|
}
|
|
162
598
|
function defaultConfigPath() {
|
|
163
|
-
const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0,
|
|
164
|
-
return (0,
|
|
599
|
+
const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
|
|
600
|
+
return (0, import_node_path4.join)(base, "btcr2", "config.json");
|
|
165
601
|
}
|
|
166
602
|
function readConfigFile(path) {
|
|
167
603
|
try {
|
|
168
|
-
const content = (0,
|
|
604
|
+
const content = (0, import_node_fs4.readFileSync)(path, "utf-8");
|
|
169
605
|
return JSON.parse(content);
|
|
170
606
|
} catch {
|
|
171
607
|
return void 0;
|
|
@@ -182,11 +618,22 @@ function profileToOverrides(config, profileName) {
|
|
|
182
618
|
casGateway: profile.cas?.gateway
|
|
183
619
|
};
|
|
184
620
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
621
|
+
function resolveDefaultNetwork(overrides) {
|
|
622
|
+
const configPath = overrides?.config ?? defaultConfigPath();
|
|
623
|
+
const file = readConfigFile(configPath);
|
|
624
|
+
const explicit = file?.defaults?.network;
|
|
625
|
+
if (explicit && SUPPORTED_NETWORKS.includes(explicit)) return explicit;
|
|
626
|
+
const profile = overrides?.profile ?? file?.defaults?.profile;
|
|
627
|
+
if (profile && SUPPORTED_NETWORKS.includes(profile)) {
|
|
628
|
+
return profile;
|
|
629
|
+
}
|
|
630
|
+
return "regtest";
|
|
631
|
+
}
|
|
632
|
+
function resolveConnectionConfig(network, overrides) {
|
|
633
|
+
if (!network) return {};
|
|
187
634
|
const configPath = overrides?.config ?? defaultConfigPath();
|
|
188
|
-
const profileName = overrides?.profile ?? network;
|
|
189
635
|
const file = readConfigFile(configPath);
|
|
636
|
+
const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
|
|
190
637
|
const fileOverrides = file ? profileToOverrides(file, profileName) : {};
|
|
191
638
|
const env = readEnvOverrides();
|
|
192
639
|
const merged = {
|
|
@@ -208,7 +655,22 @@ function defaultApiFactory(network, overrides) {
|
|
|
208
655
|
};
|
|
209
656
|
}
|
|
210
657
|
const cas = merged.casGateway ? { gateway: merged.casGateway } : void 0;
|
|
211
|
-
return
|
|
658
|
+
return { btc, ...cas && { cas } };
|
|
659
|
+
}
|
|
660
|
+
function defaultApiFactory(network, overrides) {
|
|
661
|
+
return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
|
|
662
|
+
}
|
|
663
|
+
function buildKeystoreKms(overrides) {
|
|
664
|
+
return new FileBackedKeyManager({
|
|
665
|
+
path: overrides?.keystore ?? defaultKeystorePath(),
|
|
666
|
+
getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function keystoreApiFactory(network, overrides) {
|
|
670
|
+
return (0, import_api.createApi)({
|
|
671
|
+
...resolveConnectionConfig(network, overrides),
|
|
672
|
+
kms: buildKeystoreKms(overrides)
|
|
673
|
+
});
|
|
212
674
|
}
|
|
213
675
|
function deriveNetwork(did) {
|
|
214
676
|
const { network } = import_api.Identifier.decode(did);
|
|
@@ -222,7 +684,172 @@ function deriveNetwork(did) {
|
|
|
222
684
|
return network;
|
|
223
685
|
}
|
|
224
686
|
|
|
687
|
+
// src/keystore/resolve-key-ref.ts
|
|
688
|
+
function fingerprintOf(id) {
|
|
689
|
+
return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
|
|
690
|
+
}
|
|
691
|
+
function resolveKeyRef(kms, ref) {
|
|
692
|
+
if (!ref) {
|
|
693
|
+
if (!kms.activeKeyId) {
|
|
694
|
+
throw new CLIError(
|
|
695
|
+
"No key specified and no active key is set. Use --key <ref> or set one with `btcr2 key use <ref>`.",
|
|
696
|
+
"INVALID_ARGUMENT_ERROR"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return kms.activeKeyId;
|
|
700
|
+
}
|
|
701
|
+
const ids = kms.listKeys();
|
|
702
|
+
if (ids.includes(ref)) return ref;
|
|
703
|
+
const prefix = ref.toLowerCase();
|
|
704
|
+
const byPrefix = ids.filter((id) => fingerprintOf(id)?.startsWith(prefix));
|
|
705
|
+
if (byPrefix.length === 1) return byPrefix[0];
|
|
706
|
+
if (byPrefix.length > 1) {
|
|
707
|
+
throw new CLIError(
|
|
708
|
+
`Ambiguous key reference "${ref}" matches ${byPrefix.length} keys by fingerprint.`,
|
|
709
|
+
"KEY_REF_AMBIGUOUS_ERROR",
|
|
710
|
+
{ ref }
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const byName = ids.filter((id) => kms.getEntry(id).tags?.name === ref);
|
|
714
|
+
if (byName.length === 1) return byName[0];
|
|
715
|
+
if (byName.length > 1) {
|
|
716
|
+
throw new CLIError(
|
|
717
|
+
`Ambiguous key name "${ref}" matches ${byName.length} keys.`,
|
|
718
|
+
"KEY_REF_AMBIGUOUS_ERROR",
|
|
719
|
+
{ ref }
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
throw new CLIError(`No key matches reference "${ref}".`, "KEY_NOT_FOUND_ERROR", { ref });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/output.ts
|
|
726
|
+
function formatResult(result, options) {
|
|
727
|
+
if (options.output === "json") {
|
|
728
|
+
return JSON.stringify(result, null, 2);
|
|
729
|
+
}
|
|
730
|
+
const { data } = result;
|
|
731
|
+
return typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/commands/create.ts
|
|
735
|
+
var EXPECTED_BYTES = {
|
|
736
|
+
k: { length: 33, label: "secp256k1 compressed public key (33 bytes)" },
|
|
737
|
+
x: { length: 32, label: "SHA-256 hash (32 bytes)" }
|
|
738
|
+
};
|
|
739
|
+
function registerCreateCommand(program, factory, keystoreFactory, globals) {
|
|
740
|
+
program.command("create").description("Create an identifier and initial DID document").option("-t, --type <type>", "Identifier type <k|x>", "k").option(
|
|
741
|
+
"-n, --network <network>",
|
|
742
|
+
"Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest> (default: config defaults.network, else regtest)"
|
|
743
|
+
).option(
|
|
744
|
+
"-b, --bytes <bytes>",
|
|
745
|
+
"Genesis bytes as a hex string. For type=k, a 33-byte secp256k1 public key (omit to generate a key). For type=x, the 32-byte SHA-256 hash of a genesis document."
|
|
746
|
+
).action(async (options) => {
|
|
747
|
+
const g = globals();
|
|
748
|
+
if (options.type !== "k" && options.type !== "x") {
|
|
749
|
+
throw new CLIError('Invalid type. Must be "k" or "x".', "INVALID_ARGUMENT_ERROR", options);
|
|
750
|
+
}
|
|
751
|
+
const overrides = overridesFromGlobals(g);
|
|
752
|
+
const network = resolveNetwork(options.network, overrides);
|
|
753
|
+
const signingKey = g.signingKey;
|
|
754
|
+
const print = (result, note) => {
|
|
755
|
+
console.log(formatResult(result, g));
|
|
756
|
+
if (note && g.output !== "json") process.stderr.write(`${note}
|
|
757
|
+
`);
|
|
758
|
+
};
|
|
759
|
+
if (options.type === "x") {
|
|
760
|
+
if (signingKey) {
|
|
761
|
+
throw new CLIError(
|
|
762
|
+
"--signing-key applies only to deterministic identifiers (-t k).",
|
|
763
|
+
"INVALID_ARGUMENT_ERROR"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (options.bytes === void 0) {
|
|
767
|
+
throw new CLIError(
|
|
768
|
+
"External identifiers (-t x) require --bytes <hex>, the 32-byte genesis document hash. Key generation is only available for -t k.",
|
|
769
|
+
"INVALID_ARGUMENT_ERROR"
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const genesisBytes = parseGenesisBytes(options.bytes, "x");
|
|
773
|
+
const did2 = factory().createDid("external", genesisBytes, { network });
|
|
774
|
+
print({ action: "create", data: did2 });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (options.bytes !== void 0 && signingKey) {
|
|
778
|
+
throw new CLIError(
|
|
779
|
+
"Provide at most one of --bytes or --signing-key.",
|
|
780
|
+
"INVALID_ARGUMENT_ERROR"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (options.bytes !== void 0) {
|
|
784
|
+
const genesisBytes = parseGenesisBytes(options.bytes, "k");
|
|
785
|
+
const did2 = factory().createDid("deterministic", genesisBytes, { network });
|
|
786
|
+
print({ action: "create", data: did2 });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (signingKey) {
|
|
790
|
+
const api2 = keystoreFactory(void 0, overrides);
|
|
791
|
+
const keyId2 = resolveKeyRef(api2.kms.kms, signingKey);
|
|
792
|
+
const publicKey2 = api2.kms.getPublicKey(keyId2);
|
|
793
|
+
const did2 = api2.createDid("deterministic", publicKey2, { network });
|
|
794
|
+
print(
|
|
795
|
+
{ action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils2.bytesToHex)(publicKey2) },
|
|
796
|
+
`Using stored key ${keyId2}.`
|
|
797
|
+
);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const api = keystoreFactory(void 0, overrides);
|
|
801
|
+
const { did, keyId } = api.generateDid({ network, setActive: true });
|
|
802
|
+
const publicKey = (0, import_utils2.bytesToHex)(api.kms.getPublicKey(keyId));
|
|
803
|
+
print(
|
|
804
|
+
{ action: "create", data: did, keyId, publicKey },
|
|
805
|
+
`Generated and stored key ${keyId} (now the active key).`
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function overridesFromGlobals(g) {
|
|
810
|
+
return {
|
|
811
|
+
config: g.config,
|
|
812
|
+
profile: g.profile,
|
|
813
|
+
keystore: g.keystore,
|
|
814
|
+
passphraseFile: g.passphraseFile
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function resolveNetwork(explicit, overrides) {
|
|
818
|
+
if (!explicit) return resolveDefaultNetwork(overrides);
|
|
819
|
+
if (!SUPPORTED_NETWORKS.includes(explicit)) {
|
|
820
|
+
throw new CLIError(
|
|
821
|
+
'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
|
|
822
|
+
"INVALID_ARGUMENT_ERROR",
|
|
823
|
+
{ network: explicit }
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return explicit;
|
|
827
|
+
}
|
|
828
|
+
function parseGenesisBytes(hex, type) {
|
|
829
|
+
const expected = EXPECTED_BYTES[type];
|
|
830
|
+
let bytes;
|
|
831
|
+
try {
|
|
832
|
+
bytes = (0, import_utils2.hexToBytes)(hex.trim());
|
|
833
|
+
} catch {
|
|
834
|
+
throw new CLIError(
|
|
835
|
+
`Invalid bytes: not valid hex. Expected ${expected.label}.`,
|
|
836
|
+
"INVALID_ARGUMENT_ERROR",
|
|
837
|
+
{ bytes: hex }
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (bytes.length !== expected.length) {
|
|
841
|
+
throw new CLIError(
|
|
842
|
+
`Invalid bytes length for type="${type}": expected ${expected.label}, got ${bytes.length} bytes.`,
|
|
843
|
+
"INVALID_ARGUMENT_ERROR",
|
|
844
|
+
{ bytes: hex }
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return bytes;
|
|
848
|
+
}
|
|
849
|
+
|
|
225
850
|
// src/commands/resolve.ts
|
|
851
|
+
var import_api2 = require("@did-btcr2/api");
|
|
852
|
+
var import_promises = require("fs/promises");
|
|
226
853
|
function registerResolveCommand(program, factory, globals) {
|
|
227
854
|
program.command("resolve").alias("read").description("Resolve the DID document of the identifier.").requiredOption("-i, --identifier <identifier>", "did:btcr2 identifier").option("-r, --resolution-options <json>", "JSON string containing resolution options").option("-p, --resolution-options-path <path>", "Path to a JSON file containing resolution options").action(async (options) => {
|
|
228
855
|
const parsed = await validateResolveOptions(options);
|
|
@@ -262,6 +889,7 @@ async function validateResolveOptions(options) {
|
|
|
262
889
|
}
|
|
263
890
|
|
|
264
891
|
// src/commands/update.ts
|
|
892
|
+
var import_key_manager2 = require("@did-btcr2/key-manager");
|
|
265
893
|
function registerUpdateCommand(program, factory, globals) {
|
|
266
894
|
program.command("update").description("Update a did:btcr2 document.").requiredOption(
|
|
267
895
|
"-s, --source-document <json>",
|
|
@@ -282,6 +910,13 @@ function registerUpdateCommand(program, factory, globals) {
|
|
|
282
910
|
"Beacon ID as a JSON string",
|
|
283
911
|
parseJsonArg("--beacon-id")
|
|
284
912
|
).action(async (options) => {
|
|
913
|
+
if (!/^\d+$/.test(options.sourceVersionId)) {
|
|
914
|
+
throw new CLIError(
|
|
915
|
+
"--source-version-id must be a non-negative integer.",
|
|
916
|
+
"INVALID_ARGUMENT_ERROR",
|
|
917
|
+
{ value: options.sourceVersionId }
|
|
918
|
+
);
|
|
919
|
+
}
|
|
285
920
|
const parsed = {
|
|
286
921
|
sourceDocument: options.sourceDocument,
|
|
287
922
|
patches: options.patches,
|
|
@@ -297,15 +932,19 @@ function registerUpdateCommand(program, factory, globals) {
|
|
|
297
932
|
options
|
|
298
933
|
);
|
|
299
934
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
935
|
+
const network = deriveNetwork(did);
|
|
936
|
+
const api = factory(network, globals());
|
|
937
|
+
const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
|
|
938
|
+
const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
|
|
939
|
+
const data = await api.btcr2.update({
|
|
940
|
+
sourceDocument: parsed.sourceDocument,
|
|
941
|
+
patches: parsed.patches,
|
|
942
|
+
sourceVersionId: parsed.sourceVersionId,
|
|
943
|
+
verificationMethodId: parsed.verificationMethodId,
|
|
944
|
+
beaconId: parsed.beaconId,
|
|
945
|
+
signer
|
|
946
|
+
});
|
|
947
|
+
console.log(formatResult({ action: "update", data }, globals()));
|
|
309
948
|
});
|
|
310
949
|
}
|
|
311
950
|
function parseJsonArg(flagName) {
|
|
@@ -323,6 +962,7 @@ function parseJsonArg(flagName) {
|
|
|
323
962
|
}
|
|
324
963
|
|
|
325
964
|
// src/commands/deactivate.ts
|
|
965
|
+
var import_key_manager3 = require("@did-btcr2/key-manager");
|
|
326
966
|
var DEACTIVATION_PATCH = [{ op: "add", path: "/deactivated", value: true }];
|
|
327
967
|
function registerDeactivateCommand(program, factory, globals) {
|
|
328
968
|
program.command("deactivate").alias("delete").description("Deactivate the did:btcr2 identifier permanently. This is irreversible.").requiredOption(
|
|
@@ -340,6 +980,13 @@ function registerDeactivateCommand(program, factory, globals) {
|
|
|
340
980
|
"Beacon ID as a JSON string",
|
|
341
981
|
parseJsonArg2("--beacon-id")
|
|
342
982
|
).action(async (options) => {
|
|
983
|
+
if (!/^\d+$/.test(options.sourceVersionId)) {
|
|
984
|
+
throw new CLIError(
|
|
985
|
+
"--source-version-id must be a non-negative integer.",
|
|
986
|
+
"INVALID_ARGUMENT_ERROR",
|
|
987
|
+
{ value: options.sourceVersionId }
|
|
988
|
+
);
|
|
989
|
+
}
|
|
343
990
|
const parsed = {
|
|
344
991
|
sourceDocument: options.sourceDocument,
|
|
345
992
|
patches: DEACTIVATION_PATCH,
|
|
@@ -355,15 +1002,19 @@ function registerDeactivateCommand(program, factory, globals) {
|
|
|
355
1002
|
options
|
|
356
1003
|
);
|
|
357
1004
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1005
|
+
const network = deriveNetwork(did);
|
|
1006
|
+
const api = factory(network, globals());
|
|
1007
|
+
const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
|
|
1008
|
+
const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
|
|
1009
|
+
const data = await api.btcr2.update({
|
|
1010
|
+
sourceDocument: parsed.sourceDocument,
|
|
1011
|
+
patches: parsed.patches,
|
|
1012
|
+
sourceVersionId: parsed.sourceVersionId,
|
|
1013
|
+
verificationMethodId: parsed.verificationMethodId,
|
|
1014
|
+
beaconId: parsed.beaconId,
|
|
1015
|
+
signer
|
|
1016
|
+
});
|
|
1017
|
+
console.log(formatResult({ action: "deactivate", data }, globals()));
|
|
367
1018
|
});
|
|
368
1019
|
}
|
|
369
1020
|
function parseJsonArg2(flagName) {
|
|
@@ -380,19 +1031,263 @@ function parseJsonArg2(flagName) {
|
|
|
380
1031
|
};
|
|
381
1032
|
}
|
|
382
1033
|
|
|
1034
|
+
// src/commands/key.ts
|
|
1035
|
+
var import_keypair = require("@did-btcr2/keypair");
|
|
1036
|
+
var import_utils3 = require("@noble/hashes/utils.js");
|
|
1037
|
+
var import_node_fs5 = require("fs");
|
|
1038
|
+
function registerKeyCommand(program, factory, globals) {
|
|
1039
|
+
const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
|
|
1040
|
+
const print = (result) => console.log(formatResult(result, globals()));
|
|
1041
|
+
key.command("generate").description("Generate a new keypair and store it.").option("--name <name>", "A human-friendly name, stored as a tag and usable as a key reference.").option("--set-active", "Make this the active key.", false).action((options) => {
|
|
1042
|
+
const api = factory(void 0, globals());
|
|
1043
|
+
assertNameAvailable(api.kms.kms, options.name);
|
|
1044
|
+
const setActive = options.setActive ?? false;
|
|
1045
|
+
const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
|
|
1046
|
+
print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
|
|
1047
|
+
});
|
|
1048
|
+
key.command("list").alias("ls").description("List stored keys.").action(() => {
|
|
1049
|
+
const kms = factory(void 0, globals()).kms.kms;
|
|
1050
|
+
const active = kms.activeKeyId;
|
|
1051
|
+
const data = kms.listKeys().map((id) => {
|
|
1052
|
+
const entry = kms.getEntry(id);
|
|
1053
|
+
return {
|
|
1054
|
+
keyId: id,
|
|
1055
|
+
fingerprint: id.split(":").pop() ?? id,
|
|
1056
|
+
...entry.tags?.name && { name: entry.tags.name },
|
|
1057
|
+
active: id === active
|
|
1058
|
+
};
|
|
1059
|
+
});
|
|
1060
|
+
print({ action: "key-list", data });
|
|
1061
|
+
});
|
|
1062
|
+
key.command("show <ref>").description("Show a key's public material and tags. Never prints the secret.").action((ref) => {
|
|
1063
|
+
const kms = factory(void 0, globals()).kms.kms;
|
|
1064
|
+
const id = resolveKeyRef(kms, ref);
|
|
1065
|
+
const entry = kms.getEntry(id);
|
|
1066
|
+
print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
|
|
1067
|
+
});
|
|
1068
|
+
key.command("import").description("Import a key: a secret from a hex file, or a public key as watch-only.").option("--secret-file <path>", "Path to a file containing a 32-byte secret key as hex.").option("--public <hex>", "A 33-byte compressed public key as hex (imported watch-only).").option("--name <name>", "A human-friendly name, stored as a tag.").option("--set-active", "Make this the active key.", false).action((options) => {
|
|
1069
|
+
if (Boolean(options.secretFile) === Boolean(options.public)) {
|
|
1070
|
+
throw new CLIError("Provide exactly one of --secret-file or --public.", "INVALID_ARGUMENT_ERROR");
|
|
1071
|
+
}
|
|
1072
|
+
const api = factory(void 0, globals());
|
|
1073
|
+
assertNameAvailable(api.kms.kms, options.name);
|
|
1074
|
+
const keyPair = options.secretFile ? new import_keypair.SchnorrKeyPair({ secretKey: readHexFile(options.secretFile, 32, "--secret-file") }) : new import_keypair.SchnorrKeyPair({ publicKey: parseHex(options.public ?? "", 33, "--public") });
|
|
1075
|
+
const setActive = options.setActive ?? false;
|
|
1076
|
+
const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
|
|
1077
|
+
print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
|
|
1078
|
+
});
|
|
1079
|
+
key.command("export <ref>").description("Export a key. Public material by default; --secret writes the secret to a file.").option("--secret", "Export the secret key. Requires --out.", false).option("--out <path>", "Write the exported secret to this file (created 0600).").action((ref, options) => {
|
|
1080
|
+
const api = factory(void 0, globals());
|
|
1081
|
+
const id = resolveKeyRef(api.kms.kms, ref);
|
|
1082
|
+
if (!options.secret) {
|
|
1083
|
+
print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)) } });
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (!options.out) {
|
|
1087
|
+
throw new CLIError("Exporting a secret requires --out <file> so it is not written to the terminal.", "INVALID_ARGUMENT_ERROR");
|
|
1088
|
+
}
|
|
1089
|
+
const keyPair = api.kms.export(id);
|
|
1090
|
+
if (!keyPair.hasSecretKey) {
|
|
1091
|
+
throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
|
|
1092
|
+
}
|
|
1093
|
+
process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
|
|
1094
|
+
writeSecretFile(options.out, (0, import_utils3.bytesToHex)(keyPair.secretKey.bytes));
|
|
1095
|
+
print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
|
|
1096
|
+
});
|
|
1097
|
+
key.command("delete <ref>").alias("rm").description("Delete a key from the keystore.").option("--force", "Delete even if it is the active key.", false).action((ref, options) => {
|
|
1098
|
+
const api = factory(void 0, globals());
|
|
1099
|
+
const id = resolveKeyRef(api.kms.kms, ref);
|
|
1100
|
+
api.kms.removeKey(id, { force: options.force ?? false });
|
|
1101
|
+
print({ action: "key-delete", data: { keyId: id, deleted: true } });
|
|
1102
|
+
});
|
|
1103
|
+
key.command("use <ref>").description("Set the active key, persisted across invocations.").action((ref) => {
|
|
1104
|
+
const api = factory(void 0, globals());
|
|
1105
|
+
const id = resolveKeyRef(api.kms.kms, ref);
|
|
1106
|
+
api.kms.setActive(id);
|
|
1107
|
+
print({ action: "key-use", data: { keyId: id, active: true } });
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function assertNameAvailable(kms, name) {
|
|
1111
|
+
if (!name) return;
|
|
1112
|
+
if (kms.listKeys().some((id) => kms.getEntry(id).tags?.name === name)) {
|
|
1113
|
+
throw new CLIError(`A key named "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function parseHex(hex, expectedBytes, label) {
|
|
1117
|
+
let bytes;
|
|
1118
|
+
try {
|
|
1119
|
+
bytes = (0, import_utils3.hexToBytes)(hex.trim());
|
|
1120
|
+
} catch {
|
|
1121
|
+
throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
|
|
1122
|
+
}
|
|
1123
|
+
if (bytes.length !== expectedBytes) {
|
|
1124
|
+
throw new CLIError(
|
|
1125
|
+
`${label} must be ${expectedBytes} bytes (${expectedBytes * 2} hex chars), got ${bytes.length}.`,
|
|
1126
|
+
"INVALID_ARGUMENT_ERROR",
|
|
1127
|
+
{ label }
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return bytes;
|
|
1131
|
+
}
|
|
1132
|
+
function readHexFile(path, expectedBytes, label) {
|
|
1133
|
+
let content;
|
|
1134
|
+
try {
|
|
1135
|
+
content = (0, import_node_fs5.readFileSync)(path, "utf-8");
|
|
1136
|
+
} catch {
|
|
1137
|
+
throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
|
|
1138
|
+
}
|
|
1139
|
+
return parseHex(content, expectedBytes, label);
|
|
1140
|
+
}
|
|
1141
|
+
function writeSecretFile(path, contents) {
|
|
1142
|
+
let fd;
|
|
1143
|
+
try {
|
|
1144
|
+
fd = (0, import_node_fs5.openSync)(path, "wx", 384);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (error.code === "EEXIST") {
|
|
1147
|
+
throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
|
|
1148
|
+
}
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
(0, import_node_fs5.writeFileSync)(fd, contents);
|
|
1153
|
+
} finally {
|
|
1154
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/commands/config.ts
|
|
1159
|
+
var import_node_fs6 = require("fs");
|
|
1160
|
+
var import_node_path5 = require("path");
|
|
1161
|
+
function registerConfigCommand(program, globals) {
|
|
1162
|
+
const config = program.command("config").description("Read and write CLI configuration.");
|
|
1163
|
+
const path = () => globals().config ?? defaultConfigPath();
|
|
1164
|
+
const print = (result) => console.log(formatResult(result, globals()));
|
|
1165
|
+
config.command("init").description("Create a default config file with one profile per network.").option("--force", "Overwrite an existing config file.", false).action((options) => {
|
|
1166
|
+
const p = path();
|
|
1167
|
+
if ((0, import_node_fs6.existsSync)(p) && !options.force) {
|
|
1168
|
+
throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
|
|
1169
|
+
}
|
|
1170
|
+
const scaffold = {
|
|
1171
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
1172
|
+
defaults: { output: "text" },
|
|
1173
|
+
profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
|
|
1174
|
+
};
|
|
1175
|
+
ensureDir((0, import_node_path5.dirname)(p), 448);
|
|
1176
|
+
writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}
|
|
1177
|
+
`, 384);
|
|
1178
|
+
print({ action: "config-init", data: { path: p } });
|
|
1179
|
+
});
|
|
1180
|
+
config.command("get [path]").description("Print a value at a dotted path, or the whole config.").action((dotted) => {
|
|
1181
|
+
const file = readConfigFile(path()) ?? {};
|
|
1182
|
+
print({ action: "config-get", data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
|
|
1183
|
+
});
|
|
1184
|
+
config.command("set <path> <value>").description("Set a value at a dotted path. The value is parsed as JSON when valid, else stored as a string.").action((dotted, value) => {
|
|
1185
|
+
writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parseValue(value)));
|
|
1186
|
+
print({ action: "config-set", data: { path: dotted } });
|
|
1187
|
+
});
|
|
1188
|
+
config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
|
|
1189
|
+
writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
|
|
1190
|
+
print({ action: "config-unset", data: { path: dotted } });
|
|
1191
|
+
});
|
|
1192
|
+
config.command("list").alias("ls").description("Print the entire config file.").action(() => {
|
|
1193
|
+
print({ action: "config-list", data: readConfigFile(path()) ?? {} });
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
function parseValue(value) {
|
|
1197
|
+
try {
|
|
1198
|
+
return JSON.parse(value);
|
|
1199
|
+
} catch {
|
|
1200
|
+
return value;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/commands/profile.ts
|
|
1205
|
+
function registerProfileCommand(program, globals) {
|
|
1206
|
+
const profile = program.command("profile").description("Manage configuration profiles.");
|
|
1207
|
+
const path = () => globals().config ?? defaultConfigPath();
|
|
1208
|
+
const print = (result) => console.log(formatResult(result, globals()));
|
|
1209
|
+
profile.command("add <name>").description("Add an empty profile.").action((name) => {
|
|
1210
|
+
writeConfigFile(path(), (raw) => {
|
|
1211
|
+
if (raw.profiles === void 0 || raw.profiles === null) raw.profiles = {};
|
|
1212
|
+
const profiles = raw.profiles;
|
|
1213
|
+
if (profiles[name]) throw new CLIError(`Profile "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
|
|
1214
|
+
profiles[name] = {};
|
|
1215
|
+
});
|
|
1216
|
+
print({ action: "profile-add", data: { profile: name } });
|
|
1217
|
+
});
|
|
1218
|
+
profile.command("use <name>").description("Set the active profile (writes defaults.profile).").action((name) => {
|
|
1219
|
+
writeConfigFile(path(), (raw) => {
|
|
1220
|
+
if (raw.defaults === void 0 || raw.defaults === null) raw.defaults = {};
|
|
1221
|
+
raw.defaults.profile = name;
|
|
1222
|
+
});
|
|
1223
|
+
print({ action: "profile-use", data: { profile: name } });
|
|
1224
|
+
});
|
|
1225
|
+
profile.command("show [name]").description("Show a profile (defaults to the active profile).").action((name) => {
|
|
1226
|
+
const file = readConfigFile(path()) ?? {};
|
|
1227
|
+
const target = name ?? file.defaults?.profile;
|
|
1228
|
+
if (!target) {
|
|
1229
|
+
throw new CLIError("No profile specified and no active profile is set.", "INVALID_ARGUMENT_ERROR");
|
|
1230
|
+
}
|
|
1231
|
+
const data = file.profiles?.[target];
|
|
1232
|
+
if (!data) {
|
|
1233
|
+
throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
|
|
1234
|
+
}
|
|
1235
|
+
print({ action: "profile-show", data: { profile: target, ...data } });
|
|
1236
|
+
});
|
|
1237
|
+
profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
|
|
1238
|
+
writeConfigFile(path(), (raw) => {
|
|
1239
|
+
const profiles = raw.profiles;
|
|
1240
|
+
if (!profiles?.[name]) throw new CLIError(`Profile "${name}" not found.`, "INVALID_ARGUMENT_ERROR", { name });
|
|
1241
|
+
delete profiles[name];
|
|
1242
|
+
});
|
|
1243
|
+
print({ action: "profile-remove", data: { profile: name } });
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/commands/completion.ts
|
|
1248
|
+
var COMMANDS = "create resolve read update deactivate delete key config profile completion";
|
|
1249
|
+
function registerCompletionCommand(program, _globals) {
|
|
1250
|
+
program.command("completion [shell]").description("Print a shell completion script (bash, zsh, or fish) to stdout.").action((shell = "bash") => {
|
|
1251
|
+
console.log(completionScript(shell));
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function completionScript(shell) {
|
|
1255
|
+
switch (shell) {
|
|
1256
|
+
case "bash":
|
|
1257
|
+
return [
|
|
1258
|
+
'# btcr2 bash completion. Install with: eval "$(btcr2 completion bash)"',
|
|
1259
|
+
'_btcr2() { COMPREPLY=( $(compgen -W "' + COMMANDS + '" -- "${COMP_WORDS[COMP_CWORD]}") ); }',
|
|
1260
|
+
"complete -F _btcr2 btcr2"
|
|
1261
|
+
].join("\n");
|
|
1262
|
+
case "zsh":
|
|
1263
|
+
return [
|
|
1264
|
+
'# btcr2 zsh completion. Install with: eval "$(btcr2 completion zsh)"',
|
|
1265
|
+
"_btcr2() { compadd " + COMMANDS + " }",
|
|
1266
|
+
"compdef _btcr2 btcr2"
|
|
1267
|
+
].join("\n");
|
|
1268
|
+
case "fish":
|
|
1269
|
+
return [
|
|
1270
|
+
"# btcr2 fish completion. Save to ~/.config/fish/completions/btcr2.fish",
|
|
1271
|
+
'complete -c btcr2 -f -a "' + COMMANDS + '"'
|
|
1272
|
+
].join("\n");
|
|
1273
|
+
default:
|
|
1274
|
+
throw new CLIError(`Unsupported shell "${shell}". Use bash, zsh, or fish.`, "INVALID_ARGUMENT_ERROR", { shell });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
383
1278
|
// src/version.ts
|
|
384
|
-
var
|
|
385
|
-
var
|
|
1279
|
+
var import_node_fs7 = require("fs");
|
|
1280
|
+
var import_node_path6 = require("path");
|
|
386
1281
|
var import_node_url = require("url");
|
|
387
1282
|
function readVersion() {
|
|
388
|
-
let dir = (0,
|
|
1283
|
+
let dir = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
389
1284
|
for (let i = 0; i < 5; i++) {
|
|
390
1285
|
try {
|
|
391
|
-
const pkg = JSON.parse((0,
|
|
1286
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(dir, "package.json"), "utf-8"));
|
|
392
1287
|
if (pkg.name === "@did-btcr2/cli") return pkg.version;
|
|
393
1288
|
} catch {
|
|
394
1289
|
}
|
|
395
|
-
dir = (0,
|
|
1290
|
+
dir = (0, import_node_path6.dirname)(dir);
|
|
396
1291
|
}
|
|
397
1292
|
return "0.0.0";
|
|
398
1293
|
}
|
|
@@ -409,15 +1304,23 @@ var DidBtcr2Cli = class {
|
|
|
409
1304
|
* {@link defaultApiFactory} which uses public endpoints (mempool.space)
|
|
410
1305
|
* for known networks and localhost Polar for regtest.
|
|
411
1306
|
*
|
|
412
|
-
* @param factory - Optional API factory
|
|
1307
|
+
* @param factory - Optional API factory for keystore-free commands (create,
|
|
1308
|
+
* resolve). Defaults to {@link defaultApiFactory}.
|
|
1309
|
+
* @param keystoreFactory - Optional keystore-aware API factory for commands
|
|
1310
|
+
* that need a signing identity (key, update, deactivate). Defaults to
|
|
1311
|
+
* {@link keystoreApiFactory}.
|
|
413
1312
|
*/
|
|
414
|
-
constructor(factory = defaultApiFactory) {
|
|
415
|
-
this.program = new import_commander.Command("btcr2").version(`btcr2 ${VERSION}`, "-v, --version", "Output the current version").description("CLI tool for the did:btcr2 method").option("-o, --output <format>", "Output format <json|text>", "text").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("-c, --config <path>", "Path to config file (default: $XDG_CONFIG_HOME/btcr2/config.json)").option("--profile <name>", "Config profile name (default: auto-detected from network)").option("--btc-rest <url>", "Override Bitcoin REST endpoint (Esplora API)").option("--btc-rpc-url <url>", "Override Bitcoin Core RPC endpoint").option("--btc-rpc-user <user>", "Bitcoin Core RPC username").option("--btc-rpc-pass <pass>", "Bitcoin Core RPC password").option("--cas-gateway <url>", "IPFS HTTP gateway for CAS reads");
|
|
1313
|
+
constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
|
|
1314
|
+
this.program = new import_commander.Command("btcr2").version(`btcr2 ${VERSION}`, "-v, --version", "Output the current version").description("CLI tool for the did:btcr2 method").option("-o, --output <format>", "Output format <json|text>", "text").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("-c, --config <path>", "Path to config file (default: $XDG_CONFIG_HOME/btcr2/config.json)").option("--profile <name>", "Config profile name (default: auto-detected from network)").option("--btc-rest <url>", "Override Bitcoin REST endpoint (Esplora API)").option("--btc-rpc-url <url>", "Override Bitcoin Core RPC endpoint").option("--btc-rpc-user <user>", "Bitcoin Core RPC username").option("--btc-rpc-pass <pass>", "Bitcoin Core RPC password").option("--cas-gateway <url>", "IPFS HTTP gateway for CAS reads").option("--keystore <path>", "Path to the keystore file (default: $XDG_DATA_HOME/btcr2/keystore.json)").option("--passphrase-file <path>", "Read the keystore passphrase from a file (unattended use)").option("--signing-key <ref>", "Key for update/deactivate signing: a URN, fingerprint prefix, or name");
|
|
416
1315
|
const globals = () => this.program.opts();
|
|
417
|
-
registerCreateCommand(this.program, factory, globals);
|
|
1316
|
+
registerCreateCommand(this.program, factory, keystoreFactory, globals);
|
|
418
1317
|
registerResolveCommand(this.program, factory, globals);
|
|
419
|
-
registerUpdateCommand(this.program,
|
|
420
|
-
registerDeactivateCommand(this.program,
|
|
1318
|
+
registerUpdateCommand(this.program, keystoreFactory, globals);
|
|
1319
|
+
registerDeactivateCommand(this.program, keystoreFactory, globals);
|
|
1320
|
+
registerKeyCommand(this.program, keystoreFactory, globals);
|
|
1321
|
+
registerConfigCommand(this.program, globals);
|
|
1322
|
+
registerProfileCommand(this.program, globals);
|
|
1323
|
+
registerCompletionCommand(this.program, globals);
|
|
421
1324
|
}
|
|
422
1325
|
/**
|
|
423
1326
|
* Runs the CLI with the provided argv or process.argv.
|
|
@@ -430,7 +1333,7 @@ var DidBtcr2Cli = class {
|
|
|
430
1333
|
await this.program.parseAsync(normalized, { from: "node" });
|
|
431
1334
|
if (!this.program.args.length) this.program.outputHelp();
|
|
432
1335
|
} catch (error) {
|
|
433
|
-
handleError(error);
|
|
1336
|
+
handleError(error, Boolean(this.program.opts().verbose));
|
|
434
1337
|
}
|
|
435
1338
|
}
|
|
436
1339
|
};
|
|
@@ -439,12 +1342,12 @@ function normalizeArgv(argv) {
|
|
|
439
1342
|
if (argv.length === 1) return ["node", argv[0]];
|
|
440
1343
|
return ["node", "btcr2"];
|
|
441
1344
|
}
|
|
442
|
-
function handleError(error) {
|
|
1345
|
+
function handleError(error, verbose) {
|
|
443
1346
|
if (error instanceof import_commander.CommanderError && (error.code === "commander.helpDisplayed" || error.code === "commander.help")) {
|
|
444
1347
|
return;
|
|
445
1348
|
}
|
|
446
|
-
if (error instanceof
|
|
447
|
-
console.error(error.message);
|
|
1349
|
+
if (error instanceof import_common3.DidMethodError) {
|
|
1350
|
+
console.error(verbose ? error : error.message);
|
|
448
1351
|
process.exitCode ??= 1;
|
|
449
1352
|
return;
|
|
450
1353
|
}
|
|
@@ -454,6 +1357,7 @@ function handleError(error) {
|
|
|
454
1357
|
// Annotate the CommonJS export names for ESM import in node:
|
|
455
1358
|
0 && (module.exports = {
|
|
456
1359
|
CLIError,
|
|
1360
|
+
CONFIG_SCHEMA_VERSION,
|
|
457
1361
|
DidBtcr2Cli,
|
|
458
1362
|
ENV_VARS,
|
|
459
1363
|
SUPPORTED_NETWORKS,
|
|
@@ -462,11 +1366,21 @@ function handleError(error) {
|
|
|
462
1366
|
defaultConfigPath,
|
|
463
1367
|
deriveNetwork,
|
|
464
1368
|
formatResult,
|
|
1369
|
+
getConfigPath,
|
|
1370
|
+
keystoreApiFactory,
|
|
465
1371
|
profileToOverrides,
|
|
466
1372
|
readConfigFile,
|
|
467
1373
|
readEnvOverrides,
|
|
1374
|
+
registerCompletionCommand,
|
|
1375
|
+
registerConfigCommand,
|
|
468
1376
|
registerCreateCommand,
|
|
469
1377
|
registerDeactivateCommand,
|
|
1378
|
+
registerKeyCommand,
|
|
1379
|
+
registerProfileCommand,
|
|
470
1380
|
registerResolveCommand,
|
|
471
|
-
registerUpdateCommand
|
|
1381
|
+
registerUpdateCommand,
|
|
1382
|
+
resolveDefaultNetwork,
|
|
1383
|
+
setConfigPath,
|
|
1384
|
+
unsetConfigPath,
|
|
1385
|
+
writeConfigFile
|
|
472
1386
|
});
|