@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.
Files changed (94) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +1028 -114
  3. package/dist/esm/src/cli.js +31 -13
  4. package/dist/esm/src/cli.js.map +1 -1
  5. package/dist/esm/src/commands/completion.js +36 -0
  6. package/dist/esm/src/commands/completion.js.map +1 -0
  7. package/dist/esm/src/commands/config.js +69 -0
  8. package/dist/esm/src/commands/config.js.map +1 -0
  9. package/dist/esm/src/commands/create.js +109 -30
  10. package/dist/esm/src/commands/create.js.map +1 -1
  11. package/dist/esm/src/commands/deactivate.js +21 -8
  12. package/dist/esm/src/commands/deactivate.js.map +1 -1
  13. package/dist/esm/src/commands/index.js +4 -0
  14. package/dist/esm/src/commands/index.js.map +1 -1
  15. package/dist/esm/src/commands/key.js +175 -0
  16. package/dist/esm/src/commands/key.js.map +1 -0
  17. package/dist/esm/src/commands/profile.js +63 -0
  18. package/dist/esm/src/commands/profile.js.map +1 -0
  19. package/dist/esm/src/commands/update.js +19 -9
  20. package/dist/esm/src/commands/update.js.map +1 -1
  21. package/dist/esm/src/config.js +119 -12
  22. package/dist/esm/src/config.js.map +1 -1
  23. package/dist/esm/src/keystore/atomic.js +64 -0
  24. package/dist/esm/src/keystore/atomic.js.map +1 -0
  25. package/dist/esm/src/keystore/envelope.js +123 -0
  26. package/dist/esm/src/keystore/envelope.js.map +1 -0
  27. package/dist/esm/src/keystore/error.js +16 -0
  28. package/dist/esm/src/keystore/error.js.map +1 -0
  29. package/dist/esm/src/keystore/file-backed-key-manager.js +78 -0
  30. package/dist/esm/src/keystore/file-backed-key-manager.js.map +1 -0
  31. package/dist/esm/src/keystore/file-key-store.js +184 -0
  32. package/dist/esm/src/keystore/file-key-store.js.map +1 -0
  33. package/dist/esm/src/keystore/passphrase.js +87 -0
  34. package/dist/esm/src/keystore/passphrase.js.map +1 -0
  35. package/dist/esm/src/keystore/paths.js +20 -0
  36. package/dist/esm/src/keystore/paths.js.map +1 -0
  37. package/dist/esm/src/keystore/resolve-key-ref.js +47 -0
  38. package/dist/esm/src/keystore/resolve-key-ref.js.map +1 -0
  39. package/dist/types/src/cli.d.ts +6 -2
  40. package/dist/types/src/cli.d.ts.map +1 -1
  41. package/dist/types/src/commands/completion.d.ts +5 -0
  42. package/dist/types/src/commands/completion.d.ts.map +1 -0
  43. package/dist/types/src/commands/config.d.ts +5 -0
  44. package/dist/types/src/commands/config.d.ts.map +1 -0
  45. package/dist/types/src/commands/create.d.ts +19 -1
  46. package/dist/types/src/commands/create.d.ts.map +1 -1
  47. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  48. package/dist/types/src/commands/index.d.ts +4 -0
  49. package/dist/types/src/commands/index.d.ts.map +1 -1
  50. package/dist/types/src/commands/key.d.ts +10 -0
  51. package/dist/types/src/commands/key.d.ts.map +1 -0
  52. package/dist/types/src/commands/profile.d.ts +5 -0
  53. package/dist/types/src/commands/profile.d.ts.map +1 -0
  54. package/dist/types/src/commands/update.d.ts.map +1 -1
  55. package/dist/types/src/config.d.ts +57 -5
  56. package/dist/types/src/config.d.ts.map +1 -1
  57. package/dist/types/src/keystore/atomic.d.ts +19 -0
  58. package/dist/types/src/keystore/atomic.d.ts.map +1 -0
  59. package/dist/types/src/keystore/envelope.d.ts +64 -0
  60. package/dist/types/src/keystore/envelope.d.ts.map +1 -0
  61. package/dist/types/src/keystore/error.d.ts +14 -0
  62. package/dist/types/src/keystore/error.d.ts.map +1 -0
  63. package/dist/types/src/keystore/file-backed-key-manager.d.ts +41 -0
  64. package/dist/types/src/keystore/file-backed-key-manager.d.ts.map +1 -0
  65. package/dist/types/src/keystore/file-key-store.d.ts +52 -0
  66. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -0
  67. package/dist/types/src/keystore/passphrase.d.ts +20 -0
  68. package/dist/types/src/keystore/passphrase.d.ts.map +1 -0
  69. package/dist/types/src/keystore/paths.d.ts +13 -0
  70. package/dist/types/src/keystore/paths.d.ts.map +1 -0
  71. package/dist/types/src/keystore/resolve-key-ref.d.ts +19 -0
  72. package/dist/types/src/keystore/resolve-key-ref.d.ts.map +1 -0
  73. package/dist/types/src/types.d.ts +93 -5
  74. package/dist/types/src/types.d.ts.map +1 -1
  75. package/package.json +9 -4
  76. package/src/cli.ts +37 -12
  77. package/src/commands/completion.ts +40 -0
  78. package/src/commands/config.ts +84 -0
  79. package/src/commands/create.ts +140 -52
  80. package/src/commands/deactivate.ts +25 -12
  81. package/src/commands/index.ts +4 -0
  82. package/src/commands/key.ts +193 -0
  83. package/src/commands/profile.ts +65 -0
  84. package/src/commands/update.ts +23 -13
  85. package/src/config.ts +165 -20
  86. package/src/keystore/atomic.ts +73 -0
  87. package/src/keystore/envelope.ts +172 -0
  88. package/src/keystore/error.ts +16 -0
  89. package/src/keystore/file-backed-key-manager.ts +99 -0
  90. package/src/keystore/file-key-store.ts +242 -0
  91. package/src/keystore/passphrase.ts +99 -0
  92. package/src/keystore/paths.ts +20 -0
  93. package/src/keystore/resolve-key-ref.ts +62 -0
  94. package/src/types.ts +31 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@did-btcr2/cli",
3
- "version": "0.10.3",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "CLI for interacting with did-btcr2-js, the JavaScript/TypeScript reference implementation of the did:btcr2 method. Exposes various parts of multiple packages in the did-btcr2-js monorepo.",
6
6
  "main": "./dist/cjs/index.js",
@@ -56,12 +56,17 @@
56
56
  "cli"
57
57
  ],
58
58
  "dependencies": {
59
+ "@noble/ciphers": "^2.1.1",
60
+ "@noble/hashes": "^2.0.1",
61
+ "@scure/base": "^1.2.6",
59
62
  "@web5/dids": "^1.2.0",
60
63
  "commander": "^13.1.0",
61
- "@did-btcr2/api": "^0.9.3",
64
+ "@did-btcr2/api": "^0.11.0",
62
65
  "@did-btcr2/common": "^9.1.0",
63
- "@did-btcr2/method": "^0.37.0",
64
- "@did-btcr2/cryptosuite": "^8.0.0"
66
+ "@did-btcr2/cryptosuite": "^8.0.0",
67
+ "@did-btcr2/key-manager": "^0.7.0",
68
+ "@did-btcr2/method": "^0.39.0",
69
+ "@did-btcr2/keypair": "^0.13.1"
65
70
  },
66
71
  "devDependencies": {
67
72
  "@eslint/js": "^9.21.0",
package/src/cli.ts CHANGED
@@ -1,12 +1,16 @@
1
+ import { DidMethodError } from '@did-btcr2/common';
1
2
  import { Command, CommanderError } from 'commander';
2
3
  import {
4
+ registerCompletionCommand,
5
+ registerConfigCommand,
3
6
  registerCreateCommand,
4
7
  registerDeactivateCommand,
8
+ registerKeyCommand,
9
+ registerProfileCommand,
5
10
  registerResolveCommand,
6
11
  registerUpdateCommand,
7
12
  } from './commands/index.js';
8
- import { defaultApiFactory, type ApiFactory } from './config.js';
9
- import { CLIError } from './error.js';
13
+ import { defaultApiFactory, keystoreApiFactory, type ApiFactory } from './config.js';
10
14
  import type { GlobalOptions } from './types.js';
11
15
  import { VERSION } from './version.js';
12
16
 
@@ -24,9 +28,16 @@ export class DidBtcr2Cli {
24
28
  * {@link defaultApiFactory} which uses public endpoints (mempool.space)
25
29
  * for known networks and localhost Polar for regtest.
26
30
  *
27
- * @param factory - Optional API factory. Defaults to {@link defaultApiFactory}.
31
+ * @param factory - Optional API factory for keystore-free commands (create,
32
+ * resolve). Defaults to {@link defaultApiFactory}.
33
+ * @param keystoreFactory - Optional keystore-aware API factory for commands
34
+ * that need a signing identity (key, update, deactivate). Defaults to
35
+ * {@link keystoreApiFactory}.
28
36
  */
29
- constructor(factory: ApiFactory = defaultApiFactory) {
37
+ constructor(
38
+ factory: ApiFactory = defaultApiFactory,
39
+ keystoreFactory: ApiFactory = keystoreApiFactory,
40
+ ) {
30
41
  this.program = new Command('btcr2')
31
42
  .version(`btcr2 ${VERSION}`, '-v, --version', 'Output the current version')
32
43
  .description('CLI tool for the did:btcr2 method')
@@ -39,14 +50,21 @@ export class DidBtcr2Cli {
39
50
  .option('--btc-rpc-url <url>', 'Override Bitcoin Core RPC endpoint')
40
51
  .option('--btc-rpc-user <user>', 'Bitcoin Core RPC username')
41
52
  .option('--btc-rpc-pass <pass>', 'Bitcoin Core RPC password')
42
- .option('--cas-gateway <url>', 'IPFS HTTP gateway for CAS reads');
53
+ .option('--cas-gateway <url>', 'IPFS HTTP gateway for CAS reads')
54
+ .option('--keystore <path>', 'Path to the keystore file (default: $XDG_DATA_HOME/btcr2/keystore.json)')
55
+ .option('--passphrase-file <path>', 'Read the keystore passphrase from a file (unattended use)')
56
+ .option('--signing-key <ref>', 'Key for update/deactivate signing: a URN, fingerprint prefix, or name');
43
57
 
44
58
  const globals = (): GlobalOptions => this.program.opts() as GlobalOptions;
45
59
 
46
- registerCreateCommand(this.program, factory, globals);
60
+ registerCreateCommand(this.program, factory, keystoreFactory, globals);
47
61
  registerResolveCommand(this.program, factory, globals);
48
- registerUpdateCommand(this.program, factory, globals);
49
- registerDeactivateCommand(this.program, factory, globals);
62
+ registerUpdateCommand(this.program, keystoreFactory, globals);
63
+ registerDeactivateCommand(this.program, keystoreFactory, globals);
64
+ registerKeyCommand(this.program, keystoreFactory, globals);
65
+ registerConfigCommand(this.program, globals);
66
+ registerProfileCommand(this.program, globals);
67
+ registerCompletionCommand(this.program, globals);
50
68
  }
51
69
 
52
70
  /**
@@ -60,7 +78,7 @@ export class DidBtcr2Cli {
60
78
  await this.program.parseAsync(normalized, { from: 'node' });
61
79
  if (!this.program.args.length) this.program.outputHelp();
62
80
  } catch (error: unknown) {
63
- handleError(error);
81
+ handleError(error, Boolean(this.program.opts().verbose));
64
82
  }
65
83
  }
66
84
  }
@@ -78,18 +96,25 @@ function normalizeArgv(argv: string[]): string[] {
78
96
 
79
97
  /**
80
98
  * Handles errors thrown during CLI execution.
99
+ *
100
+ * Known method errors ({@link DidMethodError} and its subclasses, including
101
+ * {@link CLIError} and the keystore errors) print only their message, never the
102
+ * stack or the structured `data` payload, so internal shapes are not leaked.
103
+ * The full error object and stack are shown only under `--verbose`.
104
+ *
81
105
  * @param {unknown} error - The error to handle.
106
+ * @param {boolean} verbose - Whether to print the full error object and stack.
82
107
  * @returns {void}
83
108
  */
84
- function handleError(error: unknown): void {
109
+ function handleError(error: unknown, verbose: boolean): void {
85
110
  if (
86
111
  error instanceof CommanderError &&
87
112
  (error.code === 'commander.helpDisplayed' || error.code === 'commander.help')
88
113
  ) {
89
114
  return;
90
115
  }
91
- if (error instanceof CLIError) {
92
- console.error(error.message);
116
+ if (error instanceof DidMethodError) {
117
+ console.error(verbose ? error : error.message);
93
118
  process.exitCode ??= 1;
94
119
  return;
95
120
  }
@@ -0,0 +1,40 @@
1
+ import type { Command } from 'commander';
2
+ import { CLIError } from '../error.js';
3
+ import type { GlobalOptions } from '../types.js';
4
+
5
+ const COMMANDS = 'create resolve read update deactivate delete key config profile completion';
6
+
7
+ /** Registers the `completion` command, which prints a shell completion script to stdout. */
8
+ export function registerCompletionCommand(program: Command, _globals: () => GlobalOptions): void {
9
+ program
10
+ .command('completion [shell]')
11
+ .description('Print a shell completion script (bash, zsh, or fish) to stdout.')
12
+ .action((shell = 'bash') => {
13
+ console.log(completionScript(shell));
14
+ });
15
+ }
16
+
17
+ /** Returns a completion script for the given shell. */
18
+ function completionScript(shell: string): string {
19
+ switch (shell) {
20
+ case 'bash':
21
+ return [
22
+ '# btcr2 bash completion. Install with: eval "$(btcr2 completion bash)"',
23
+ '_btcr2() { COMPREPLY=( $(compgen -W "' + COMMANDS + '" -- "${COMP_WORDS[COMP_CWORD]}") ); }',
24
+ 'complete -F _btcr2 btcr2',
25
+ ].join('\n');
26
+ case 'zsh':
27
+ return [
28
+ '# btcr2 zsh completion. Install with: eval "$(btcr2 completion zsh)"',
29
+ '_btcr2() { compadd ' + COMMANDS + ' }',
30
+ 'compdef _btcr2 btcr2',
31
+ ].join('\n');
32
+ case 'fish':
33
+ return [
34
+ '# btcr2 fish completion. Save to ~/.config/fish/completions/btcr2.fish',
35
+ 'complete -c btcr2 -f -a "' + COMMANDS + '"',
36
+ ].join('\n');
37
+ default:
38
+ throw new CLIError(`Unsupported shell "${shell}". Use bash, zsh, or fish.`, 'INVALID_ARGUMENT_ERROR', { shell });
39
+ }
40
+ }
@@ -0,0 +1,84 @@
1
+ import type { Command } from 'commander';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import {
5
+ CONFIG_SCHEMA_VERSION,
6
+ defaultConfigPath,
7
+ getConfigPath,
8
+ readConfigFile,
9
+ setConfigPath,
10
+ unsetConfigPath,
11
+ writeConfigFile,
12
+ } from '../config.js';
13
+ import { CLIError } from '../error.js';
14
+ import { ensureDir, writeFileAtomic } from '../keystore/atomic.js';
15
+ import { formatResult } from '../output.js';
16
+ import type { CommandResult, GlobalOptions } from '../types.js';
17
+ import { SUPPORTED_NETWORKS } from '../types.js';
18
+
19
+ /** Registers the `config` command group for reading and writing CLI configuration. */
20
+ export function registerConfigCommand(program: Command, globals: () => GlobalOptions): void {
21
+ const config = program.command('config').description('Read and write CLI configuration.');
22
+ const path = (): string => globals().config ?? defaultConfigPath();
23
+ const print = (result: CommandResult): void => console.log(formatResult(result, globals()));
24
+
25
+ config
26
+ .command('init')
27
+ .description('Create a default config file with one profile per network.')
28
+ .option('--force', 'Overwrite an existing config file.', false)
29
+ .action((options: { force?: boolean }) => {
30
+ const p = path();
31
+ if (existsSync(p) && !options.force) {
32
+ throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, 'INVALID_ARGUMENT_ERROR', { path: p });
33
+ }
34
+ const scaffold = {
35
+ schemaVersion : CONFIG_SCHEMA_VERSION,
36
+ defaults : { output: 'text' },
37
+ profiles : Object.fromEntries(SUPPORTED_NETWORKS.map(n => [ n, {} ])),
38
+ };
39
+ ensureDir(dirname(p), 0o700);
40
+ writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}\n`, 0o600);
41
+ print({ action: 'config-init', data: { path: p } });
42
+ });
43
+
44
+ config
45
+ .command('get [path]')
46
+ .description('Print a value at a dotted path, or the whole config.')
47
+ .action((dotted?: string) => {
48
+ const file = (readConfigFile(path()) ?? {}) as Record<string, unknown>;
49
+ print({ action: 'config-get', data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
50
+ });
51
+
52
+ config
53
+ .command('set <path> <value>')
54
+ .description('Set a value at a dotted path. The value is parsed as JSON when valid, else stored as a string.')
55
+ .action((dotted: string, value: string) => {
56
+ writeConfigFile(path(), raw => setConfigPath(raw, dotted, parseValue(value)));
57
+ print({ action: 'config-set', data: { path: dotted } });
58
+ });
59
+
60
+ config
61
+ .command('unset <path>')
62
+ .description('Delete a value at a dotted path.')
63
+ .action((dotted: string) => {
64
+ writeConfigFile(path(), raw => unsetConfigPath(raw, dotted));
65
+ print({ action: 'config-unset', data: { path: dotted } });
66
+ });
67
+
68
+ config
69
+ .command('list')
70
+ .alias('ls')
71
+ .description('Print the entire config file.')
72
+ .action(() => {
73
+ print({ action: 'config-list', data: readConfigFile(path()) ?? {} });
74
+ });
75
+ }
76
+
77
+ /** Parses a value as JSON when valid, otherwise treats it as a plain string. */
78
+ function parseValue(value: string): unknown {
79
+ try {
80
+ return JSON.parse(value);
81
+ } catch {
82
+ return value;
83
+ }
84
+ }
@@ -1,14 +1,12 @@
1
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
1
2
  import type { Command } from 'commander';
2
- import type { ApiFactory } from '../config.js';
3
+ import type { ApiFactory, ConnectionOverrides } from '../config.js';
4
+ import { resolveDefaultNetwork } from '../config.js';
3
5
  import { CLIError } from '../error.js';
6
+ import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
4
7
  import { formatResult } from '../output.js';
5
- import type {
6
- CreateCommandOptions,
7
- GlobalOptions,
8
- NetworkOption} from '../types.js';
9
- import {
10
- SUPPORTED_NETWORKS,
11
- } from '../types.js';
8
+ import type { CommandResult, GlobalOptions, NetworkOption } from '../types.js';
9
+ import { SUPPORTED_NETWORKS } from '../types.js';
12
10
 
13
11
  /** Expected byte length per identifier type: compressed secp256k1 = 33, SHA-256 hash = 32. */
14
12
  const EXPECTED_BYTES: Record<'k' | 'x', { length: number; label: string }> = {
@@ -16,74 +14,164 @@ const EXPECTED_BYTES: Record<'k' | 'x', { length: number; label: string }> = {
16
14
  x : { length: 32, label: 'SHA-256 hash (32 bytes)' },
17
15
  };
18
16
 
17
+ /**
18
+ * Registers the `create` command.
19
+ *
20
+ * A deterministic (`-t k`) identifier has three mutually-exclusive input modes,
21
+ * selected by which is present:
22
+ * - generate (neither `--bytes` nor `--signing-key`): mint a fresh key, persist
23
+ * it to the keystore, set it active, and print the identifier. Sealing the
24
+ * secret prompts for the keystore passphrase.
25
+ * - existing (`--signing-key <ref>`): use a stored key's public key as the
26
+ * genesis bytes. Reading a public key never decrypts, so this never prompts.
27
+ * - raw (`--bytes <hex>`): a 33-byte public key as hex. Offline, keystore-free.
28
+ *
29
+ * An external (`-t x`) identifier is raw-bytes-only: a 32-byte genesis-document
30
+ * hash via `--bytes`. Generation and `--signing-key` apply only to `-t k`.
31
+ *
32
+ * The keystore-free `factory` serves the raw-bytes path; the keystore-aware
33
+ * `keystoreFactory` serves the generate and existing-key paths.
34
+ */
19
35
  export function registerCreateCommand(
20
- program : Command,
21
- factory : ApiFactory,
22
- globals : () => GlobalOptions,
36
+ program : Command,
37
+ factory : ApiFactory,
38
+ keystoreFactory : ApiFactory,
39
+ globals : () => GlobalOptions,
23
40
  ): void {
24
41
  program
25
42
  .command('create')
26
43
  .description('Create an identifier and initial DID document')
27
- .requiredOption('-t, --type <type>', 'Identifier type <k|x>', 'k')
28
- .requiredOption(
44
+ .option('-t, --type <type>', 'Identifier type <k|x>', 'k')
45
+ .option(
29
46
  '-n, --network <network>',
30
- 'Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest>'
47
+ 'Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest> '
48
+ + '(default: config defaults.network, else regtest)'
31
49
  )
32
- .requiredOption(
50
+ .option(
33
51
  '-b, --bytes <bytes>',
34
- 'Genesis bytes as a hex string. ' +
35
- 'If type=k, MUST be secp256k1 public key. ' +
36
- 'If type=x, MUST be SHA-256 hash of a genesis document'
52
+ 'Genesis bytes as a hex string. '
53
+ + 'For type=k, a 33-byte secp256k1 public key (omit to generate a key). '
54
+ + 'For type=x, the 32-byte SHA-256 hash of a genesis document.'
37
55
  )
38
- .action(async (options: { type: string; network: string; bytes: string }) => {
39
- const parsed = validateCreateOptions(options);
40
- const api = factory();
41
- const type = parsed.type === 'k' ? 'deterministic' : 'external';
42
- const genesisBytes = Buffer.from(parsed.bytes, 'hex');
43
- const data = api.createDid(type, genesisBytes, { network: parsed.network });
44
- const result = { action: 'create' as const, data };
45
- console.log(formatResult(result, globals()));
56
+ .action(async (options: { type: string; network?: string; bytes?: string }) => {
57
+ const g = globals();
58
+ if (options.type !== 'k' && options.type !== 'x') {
59
+ throw new CLIError('Invalid type. Must be "k" or "x".', 'INVALID_ARGUMENT_ERROR', options);
60
+ }
61
+
62
+ const overrides = overridesFromGlobals(g);
63
+ const network = resolveNetwork(options.network, overrides);
64
+ const signingKey = g.signingKey;
65
+
66
+ /** Prints the result, plus a stderr provenance line in text mode. */
67
+ const print = (result: CommandResult, note?: string): void => {
68
+ console.log(formatResult(result, g));
69
+ if (note && g.output !== 'json') process.stderr.write(`${note}\n`);
70
+ };
71
+
72
+ // External: raw-bytes only.
73
+ if (options.type === 'x') {
74
+ if (signingKey) {
75
+ throw new CLIError(
76
+ '--signing-key applies only to deterministic identifiers (-t k).',
77
+ 'INVALID_ARGUMENT_ERROR',
78
+ );
79
+ }
80
+ if (options.bytes === undefined) {
81
+ throw new CLIError(
82
+ 'External identifiers (-t x) require --bytes <hex>, the 32-byte genesis document hash. '
83
+ + 'Key generation is only available for -t k.',
84
+ 'INVALID_ARGUMENT_ERROR',
85
+ );
86
+ }
87
+ const genesisBytes = parseGenesisBytes(options.bytes, 'x');
88
+ const did = factory().createDid('external', genesisBytes, { network });
89
+ print({ action: 'create', data: did });
90
+ return;
91
+ }
92
+
93
+ // Deterministic (KEY): three mutually-exclusive modes.
94
+ if (options.bytes !== undefined && signingKey) {
95
+ throw new CLIError(
96
+ 'Provide at most one of --bytes or --signing-key.',
97
+ 'INVALID_ARGUMENT_ERROR',
98
+ );
99
+ }
100
+
101
+ // Raw bytes: keystore-free, offline.
102
+ if (options.bytes !== undefined) {
103
+ const genesisBytes = parseGenesisBytes(options.bytes, 'k');
104
+ const did = factory().createDid('deterministic', genesisBytes, { network });
105
+ print({ action: 'create', data: did });
106
+ return;
107
+ }
108
+
109
+ // Existing key: read its public key from the keystore (no passphrase prompt).
110
+ if (signingKey) {
111
+ const api = keystoreFactory(undefined, overrides);
112
+ const keyId = resolveKeyRef(api.kms.kms, signingKey);
113
+ const publicKey = api.kms.getPublicKey(keyId);
114
+ const did = api.createDid('deterministic', publicKey, { network });
115
+ print(
116
+ { action: 'create', data: did, keyId, publicKey: bytesToHex(publicKey) },
117
+ `Using stored key ${keyId}.`,
118
+ );
119
+ return;
120
+ }
121
+
122
+ // Generate: mint a fresh key, persist it, and set it active (passphrase prompt).
123
+ const api = keystoreFactory(undefined, overrides);
124
+ const { did, keyId } = api.generateDid({ network, setActive: true });
125
+ const publicKey = bytesToHex(api.kms.getPublicKey(keyId));
126
+ print(
127
+ { action: 'create', data: did, keyId, publicKey },
128
+ `Generated and stored key ${keyId} (now the active key).`,
129
+ );
46
130
  });
47
131
  }
48
132
 
49
- function validateCreateOptions(
50
- options: { type: string; network: string; bytes: string }
51
- ): CreateCommandOptions {
52
- if (!['k', 'x'].includes(options.type)) {
53
- throw new CLIError(
54
- 'Invalid type. Must be "k" or "x".',
55
- 'INVALID_ARGUMENT_ERROR',
56
- options
57
- );
58
- }
59
- if (!SUPPORTED_NETWORKS.includes(options.network as NetworkOption)) {
133
+ /** Builds the keystore- and config-resolution overrides from the global flags. */
134
+ function overridesFromGlobals(g: GlobalOptions): ConnectionOverrides {
135
+ return {
136
+ config : g.config,
137
+ profile : g.profile,
138
+ keystore : g.keystore,
139
+ passphraseFile : g.passphraseFile,
140
+ };
141
+ }
142
+
143
+ /** Validates an explicit `--network`, or resolves the default from configuration. */
144
+ function resolveNetwork(explicit: string | undefined, overrides: ConnectionOverrides): NetworkOption {
145
+ if (!explicit) return resolveDefaultNetwork(overrides);
146
+ if (!SUPPORTED_NETWORKS.includes(explicit as NetworkOption)) {
60
147
  throw new CLIError(
61
148
  'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
62
149
  'INVALID_ARGUMENT_ERROR',
63
- options
150
+ { network: explicit },
64
151
  );
65
152
  }
153
+ return explicit as NetworkOption;
154
+ }
66
155
 
67
- const buf = Buffer.from(options.bytes, 'hex');
68
- if (buf.length === 0) {
156
+ /** Parses and length-checks hex genesis bytes for the given identifier type. */
157
+ function parseGenesisBytes(hex: string, type: 'k' | 'x'): Uint8Array {
158
+ const expected = EXPECTED_BYTES[type];
159
+ let bytes: Uint8Array;
160
+ try {
161
+ bytes = hexToBytes(hex.trim());
162
+ } catch {
69
163
  throw new CLIError(
70
- 'Invalid bytes. Must be a non-empty hex string.',
164
+ `Invalid bytes: not valid hex. Expected ${expected.label}.`,
71
165
  'INVALID_ARGUMENT_ERROR',
72
- options
166
+ { bytes: hex },
73
167
  );
74
168
  }
75
- const expected = EXPECTED_BYTES[options.type as 'k' | 'x'];
76
- if (buf.length !== expected.length) {
169
+ if (bytes.length !== expected.length) {
77
170
  throw new CLIError(
78
- `Invalid bytes length for type="${options.type}": expected ${expected.label}, got ${buf.length} bytes.`,
171
+ `Invalid bytes length for type="${type}": expected ${expected.label}, got ${bytes.length} bytes.`,
79
172
  'INVALID_ARGUMENT_ERROR',
80
- options
173
+ { bytes: hex },
81
174
  );
82
175
  }
83
-
84
- return {
85
- type : options.type as 'k' | 'x',
86
- network : options.network as NetworkOption,
87
- bytes : options.bytes,
88
- };
176
+ return bytes;
89
177
  }
@@ -1,6 +1,9 @@
1
+ import { KeyManagerSigner } from '@did-btcr2/key-manager';
1
2
  import type { Command } from 'commander';
2
3
  import { deriveNetwork, type ApiFactory } from '../config.js';
3
4
  import { CLIError } from '../error.js';
5
+ import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
6
+ import { formatResult } from '../output.js';
4
7
  import type { GlobalOptions, UpdateCommandOptions } from '../types.js';
5
8
 
6
9
  /** The JSON Patch that marks a DID document as permanently deactivated. */
@@ -39,6 +42,13 @@ export function registerDeactivateCommand(
39
42
  verificationMethodId : string;
40
43
  beaconId : unknown;
41
44
  }) => {
45
+ if (!/^\d+$/.test(options.sourceVersionId)) {
46
+ throw new CLIError(
47
+ '--source-version-id must be a non-negative integer.',
48
+ 'INVALID_ARGUMENT_ERROR',
49
+ { value: options.sourceVersionId },
50
+ );
51
+ }
42
52
  const parsed: UpdateCommandOptions = {
43
53
  sourceDocument : options.sourceDocument as UpdateCommandOptions['sourceDocument'],
44
54
  patches : DEACTIVATION_PATCH,
@@ -54,18 +64,21 @@ export function registerDeactivateCommand(
54
64
  options
55
65
  );
56
66
  }
57
- // CLI signing is not yet wired up; deactivate uses the same update path
58
- // and inherits the same gap. Drive the SDK directly with a `Signer` for now.
59
- // Variables above are kept so command parsing + validation still works.
60
- void deriveNetwork(did);
61
- void factory;
62
- void globals;
63
- void parsed;
64
- throw new CLIError(
65
- 'CLI signing is not yet implemented. Use @did-btcr2/api with a Signer directly.',
66
- 'NOT_IMPLEMENTED_ERROR',
67
- { command: 'deactivate' }
68
- );
67
+ // Deactivation is an update that applies the deactivation patch. The core
68
+ // method has no separate deactivate path, so this routes through update.
69
+ const network = deriveNetwork(did);
70
+ const api = factory(network, globals());
71
+ const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
72
+ const signer = new KeyManagerSigner(api.kms.kms, keyId);
73
+ const data = await api.btcr2.update({
74
+ sourceDocument : parsed.sourceDocument,
75
+ patches : parsed.patches,
76
+ sourceVersionId : parsed.sourceVersionId,
77
+ verificationMethodId : parsed.verificationMethodId,
78
+ beaconId : parsed.beaconId,
79
+ signer,
80
+ });
81
+ console.log(formatResult({ action: 'deactivate', data }, globals()));
69
82
  });
70
83
  }
71
84
 
@@ -2,3 +2,7 @@ export { registerCreateCommand } from './create.js';
2
2
  export { registerResolveCommand } from './resolve.js';
3
3
  export { registerUpdateCommand } from './update.js';
4
4
  export { registerDeactivateCommand } from './deactivate.js';
5
+ export { registerKeyCommand } from './key.js';
6
+ export { registerConfigCommand } from './config.js';
7
+ export { registerProfileCommand } from './profile.js';
8
+ export { registerCompletionCommand } from './completion.js';