@did-btcr2/cli 0.3.0 → 0.4.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 (95) hide show
  1. package/LICENSE +373 -1
  2. package/dist/cjs/cli.js +56 -264
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/commands/create.js +49 -0
  5. package/dist/cjs/commands/create.js.map +1 -0
  6. package/dist/cjs/commands/deactivate.js +41 -0
  7. package/dist/cjs/commands/deactivate.js.map +1 -0
  8. package/dist/cjs/commands/index.js +5 -0
  9. package/dist/cjs/commands/index.js.map +1 -0
  10. package/dist/cjs/commands/resolve.js +59 -0
  11. package/dist/cjs/commands/resolve.js.map +1 -0
  12. package/dist/cjs/commands/update.js +39 -0
  13. package/dist/cjs/commands/update.js.map +1 -0
  14. package/dist/cjs/error.js +1 -1
  15. package/dist/cjs/error.js.map +1 -1
  16. package/dist/cjs/index.js +4 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/output.js +16 -0
  19. package/dist/cjs/output.js.map +1 -0
  20. package/dist/cjs/types.js +4 -0
  21. package/dist/cjs/types.js.map +1 -0
  22. package/dist/cjs/version.js +22 -0
  23. package/dist/cjs/version.js.map +1 -0
  24. package/dist/esm/src/cli.js +56 -264
  25. package/dist/esm/src/cli.js.map +1 -1
  26. package/dist/esm/src/commands/create.js +49 -0
  27. package/dist/esm/src/commands/create.js.map +1 -0
  28. package/dist/esm/src/commands/deactivate.js +41 -0
  29. package/dist/esm/src/commands/deactivate.js.map +1 -0
  30. package/dist/esm/src/commands/index.js +5 -0
  31. package/dist/esm/src/commands/index.js.map +1 -0
  32. package/dist/esm/src/commands/resolve.js +59 -0
  33. package/dist/esm/src/commands/resolve.js.map +1 -0
  34. package/dist/esm/src/commands/update.js +39 -0
  35. package/dist/esm/src/commands/update.js.map +1 -0
  36. package/dist/esm/src/error.js +1 -1
  37. package/dist/esm/src/error.js.map +1 -1
  38. package/dist/esm/src/index.js +4 -1
  39. package/dist/esm/src/index.js.map +1 -1
  40. package/dist/esm/src/output.js +16 -0
  41. package/dist/esm/src/output.js.map +1 -0
  42. package/dist/esm/src/types.js +4 -0
  43. package/dist/esm/src/types.js.map +1 -0
  44. package/dist/esm/src/version.js +22 -0
  45. package/dist/esm/src/version.js.map +1 -0
  46. package/dist/types/cli.d.ts +7 -98
  47. package/dist/types/commands/create.d.ts +3 -0
  48. package/dist/types/commands/deactivate.d.ts +3 -0
  49. package/dist/types/commands/index.d.ts +4 -0
  50. package/dist/types/commands/resolve.d.ts +3 -0
  51. package/dist/types/commands/update.d.ts +3 -0
  52. package/dist/types/index.d.ts +4 -1
  53. package/dist/types/output.d.ts +10 -0
  54. package/dist/types/src/cli.d.ts +7 -98
  55. package/dist/types/src/cli.d.ts.map +1 -1
  56. package/dist/types/src/commands/create.d.ts +4 -0
  57. package/dist/types/src/commands/create.d.ts.map +1 -0
  58. package/dist/types/src/commands/deactivate.d.ts +4 -0
  59. package/dist/types/src/commands/deactivate.d.ts.map +1 -0
  60. package/dist/types/src/commands/index.d.ts +5 -0
  61. package/dist/types/src/commands/index.d.ts.map +1 -0
  62. package/dist/types/src/commands/resolve.d.ts +4 -0
  63. package/dist/types/src/commands/resolve.d.ts.map +1 -0
  64. package/dist/types/src/commands/update.d.ts +4 -0
  65. package/dist/types/src/commands/update.d.ts.map +1 -0
  66. package/dist/types/src/index.d.ts +4 -1
  67. package/dist/types/src/index.d.ts.map +1 -1
  68. package/dist/types/src/output.d.ts +11 -0
  69. package/dist/types/src/output.d.ts.map +1 -0
  70. package/dist/types/src/types.d.ts +54 -0
  71. package/dist/types/src/types.d.ts.map +1 -0
  72. package/dist/types/src/version.d.ts +2 -0
  73. package/dist/types/src/version.d.ts.map +1 -0
  74. package/dist/types/types.d.ts +53 -0
  75. package/dist/types/version.d.ts +1 -0
  76. package/package.json +6 -7
  77. package/src/cli.ts +67 -347
  78. package/src/commands/create.ts +88 -0
  79. package/src/commands/deactivate.ts +71 -0
  80. package/src/commands/index.ts +4 -0
  81. package/src/commands/resolve.ts +89 -0
  82. package/src/commands/update.ts +73 -0
  83. package/src/error.ts +2 -2
  84. package/src/index.ts +4 -1
  85. package/src/output.ts +17 -0
  86. package/src/types.ts +61 -0
  87. package/src/version.ts +21 -0
  88. package/dist/cjs/command.js +0 -35
  89. package/dist/cjs/command.js.map +0 -1
  90. package/dist/esm/src/command.js +0 -35
  91. package/dist/esm/src/command.js.map +0 -1
  92. package/dist/types/command.d.ts +0 -55
  93. package/dist/types/src/command.d.ts +0 -56
  94. package/dist/types/src/command.d.ts.map +0 -1
  95. package/src/command.ts +0 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@did-btcr2/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -58,9 +58,9 @@
58
58
  "dependencies": {
59
59
  "@web5/dids": "^1.2.0",
60
60
  "commander": "^13.1.0",
61
- "@did-btcr2/common": "3.1.0",
62
- "@did-btcr2/method": "0.20.0",
63
- "@did-btcr2/cryptosuite": "5.0.0"
61
+ "@did-btcr2/common": "5.0.0",
62
+ "@did-btcr2/cryptosuite": "6.0.0",
63
+ "@did-btcr2/method": "0.23.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@eslint/js": "^9.21.0",
@@ -95,7 +95,7 @@
95
95
  "wipe": "pnpm clean && pnpm clean:deps",
96
96
  "reinstall": "pnpm install --force",
97
97
  "build": "pnpm clean:build && pnpm build:esm && pnpm build:cjs",
98
- "build:esm": "rimraf dist/esm dist/types && pnpm tsc -p tsconfig.json",
98
+ "build:esm": "rimraf dist/esm dist/types && pnpm tsc -p tsconfig.json && chmod +x dist/esm/bin/btcr2.js",
99
99
  "build:cjs": "rimraf dist/cjs && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./dist/cjs/package.json",
100
100
  "build:tests": "pnpm clean:tests && pnpm tsc -p tests/tsconfig.json",
101
101
  "build:all": "pnpm build && pnpm build:tests",
@@ -103,7 +103,6 @@
103
103
  "lint:fix": "eslint . --fix",
104
104
  "test": "pnpm c8 mocha",
105
105
  "build:test": "pnpm build && pnpm build:tests && pnpm c8 mocha",
106
- "build:lint": "pnpm build && pnpm build:tests && pnpm lint:fix",
107
- "prepublish": "pnpm build"
106
+ "build:lint": "pnpm build && pnpm build:tests && pnpm lint:fix"
108
107
  }
109
108
  }
package/src/cli.ts CHANGED
@@ -1,374 +1,94 @@
1
- import { Identifier } from '@did-btcr2/method';
2
1
  import { Command, CommanderError } from 'commander';
3
- import { readFile } from 'fs/promises';
4
- import Btcr2Command, {
5
- CommandRequest,
6
- CommandResult,
7
- CreateCommandOptions,
8
- NetworkOption,
9
- ResolveCommandOptions,
10
- UpdateCommandOptions,
11
- } from './command.js';
2
+ import { DidBtcr2 } from '@did-btcr2/method';
3
+ import {
4
+ registerCreateCommand,
5
+ registerDeactivateCommand,
6
+ registerResolveCommand,
7
+ registerUpdateCommand,
8
+ } from './commands/index.js';
12
9
  import { CLIError } from './error.js';
10
+ import { GlobalOptions, MethodOperations } from './types.js';
11
+ import { VERSION } from './version.js';
13
12
 
14
- const SUPPORTED_NETWORKS: NetworkOption[] = ['bitcoin', 'testnet3', 'testnet4', 'signet', 'mutinynet', 'regtest'];
13
+ /** Default MethodOperations delegating to DidBtcr2 static methods. */
14
+ const defaultOps: MethodOperations = {
15
+ create : (genesisBytes, options) => DidBtcr2.create(genesisBytes, options),
16
+ resolve : (identifier, options) => DidBtcr2.resolve(identifier, options),
17
+ update : (params) => DidBtcr2.update(params),
18
+ };
15
19
 
16
20
  /**
17
21
  * CLI tool for the did:btcr2 method.
18
- * @type {DidBtcr2Cli}
19
- * @class DidBtcr2Cli
20
22
  */
21
23
  export class DidBtcr2Cli {
22
24
  public readonly program: Command;
23
-
24
- constructor() {
25
- this.program = new Command('btcr2')
26
- .version('btcr2 0.1.0', '-v, --version', 'Output the current version')
27
- .description('CLI tool for the did:btcr2 method');
28
-
29
- this.configureCommands();
30
- }
25
+ private readonly ops: MethodOperations;
31
26
 
32
27
  /**
33
- * Configures the CLI commands.
34
- * @returns {void}
28
+ * Initializes the CLI with optional custom MethodOperations.
29
+ * @param {MethodOperations} ops - Custom operations for create, resolve, and update.
35
30
  */
36
- private configureCommands(): void {
37
- // Create command
38
- this.program
39
- .command('create')
40
- .description('Create an identifier and initial DID document')
41
- .requiredOption('-t, --type <type>', 'Identifier type <k|x>', 'k')
42
- .requiredOption(
43
- '-n, --network <network>',
44
- 'Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest>'
45
- )
46
- .requiredOption(
47
- '-b, --bytes <bytes>',
48
- 'The genesis bytes used to create a DID and DID document as a hex string. ' +
49
- 'If type=k, MUST be secp256k1 public key. ' +
50
- 'If type=x, MUST be SHA-256 hash of a genesis document'
51
- )
52
- .action(async (options: { type: string; network: string; bytes: string }) => {
53
- const parsedOptions = this.parseCreateOptions(options);
54
- const result = await this.invokeCommand({ options: parsedOptions, action: 'create', command: new Btcr2Command() });
55
- this.printResult(result);
56
- });
57
-
58
- // Resolve command
59
- this.program
60
- .command('resolve')
61
- .alias('read')
62
- .description('Resolve the DID document of the identifier.')
63
- .requiredOption('-i, --identifier <identifier>', 'did:btcr2 identifier')
64
- .option('-r, --resolutionOptions <resolutionOptions>', 'JSON string containing resolution options')
65
- .option('-p, --resolutionOptionsPath <resolutionOptionsPath>', 'Path to a JSON file containing resolution options')
66
- .action(async (options: { identifier: string; resolutionOptions?: string; resolutionOptionsPath?: string }) => {
67
- const parsedOptions = await this.parseResolveOptions(options);
68
- const result = await this.invokeCommand({ options: parsedOptions, action: 'resolve', command: new Btcr2Command() });
69
- this.printResult(result);
70
- });
71
-
72
- // Update command
73
- this.program
74
- .command('update')
75
- .description('Update a did:btcr2 document.')
76
- .requiredOption('-i, --identifier <identifier>', 'did:btcr2 identifier')
77
- .requiredOption('-s, --sourceDocument <sourceDocument>', 'Source DID document as JSON string')
78
- .requiredOption('-v, --sourceVersionId <sourceVersionId>', 'Source version ID as a number')
79
- .requiredOption('-p, --patch <patch>', 'JSON Patch operations as a JSON string array')
80
- .requiredOption('-m, --verificationMethodId <verificationMethodId>', 'Did document verification method ID as a string')
81
- .requiredOption('-b, --beaconIds <beaconIds>', 'Beacon IDs as a JSON string array')
82
- .action(async (options: {
83
- sourceDocument: string;
84
- sourceVersionId: number;
85
- patches: string;
86
- verificationMethodId: string;
87
- beaconId: string;
88
- }) => {
89
- const parsedOptions = this.parseUpdateOptions(options);
90
- const result = await this.invokeCommand({ options: parsedOptions, action: 'update', command: new Btcr2Command() });
91
- this.printResult(result);
92
- });
93
-
94
- // Deactivate command
95
- this.program
96
- .command('deactivate')
97
- .alias('delete')
98
- .description('Deactivate the did:btcr2 identifier permanently.')
99
- .action(async () => {
100
- const result = await this.invokeCommand({ options: {}, action: 'deactivate', command: new Btcr2Command() });
101
- this.printResult(result);
102
- });
103
- }
104
-
105
- /**
106
- * Invokes a command with the provided request.
107
- * @param {object} request The command request
108
- * @param {any} request.options The command options
109
- * @param {CommandRequest['action']} request.action The command action
110
- * @param {Btcr2Command} request.command The command instance
111
- * @returns {Promise<CommandResult>} The command result
112
- */
113
- private async invokeCommand(request: {
114
- options: any;
115
- action: CommandRequest['action'];
116
- command: Btcr2Command;
117
- }): Promise<CommandResult> {
118
- return await request.command.execute({ action: request.action, options: request.options } as CommandRequest);
31
+ constructor(ops: MethodOperations = defaultOps) {
32
+ this.ops = ops;
33
+ this.program = new Command('btcr2')
34
+ .version(`btcr2 ${VERSION}`, '-v, --version', 'Output the current version')
35
+ .description('CLI tool for the did:btcr2 method')
36
+ .option('-o, --output <format>', 'Output format <json|text>', 'text')
37
+ .option('--verbose', 'Verbose output', false)
38
+ .option('--quiet', 'Suppress non-essential output', false);
39
+
40
+ const globals = (): GlobalOptions => this.program.opts() as GlobalOptions;
41
+
42
+ registerCreateCommand(this.program, this.ops, globals);
43
+ registerResolveCommand(this.program, this.ops, globals);
44
+ registerUpdateCommand(this.program, this.ops, globals);
45
+ registerDeactivateCommand(this.program, this.ops, globals);
119
46
  }
120
47
 
121
48
  /**
122
49
  * Runs the CLI with the provided argv or process.argv.
123
- * @param {string[]} [argv] The argv array to use. Defaults to process.argv.
50
+ * @param {string[]} [argv] - Optional array of command-line arguments.
51
+ * @returns {Promise<void>} - Resolves when execution is complete.
124
52
  */
125
- public async run(argv?: string[]) {
53
+ public async run(argv?: string[]): Promise<void> {
126
54
  try {
127
- const normalized = this.normalizeArgv(argv ?? process.argv);
55
+ const normalized = normalizeArgv(argv ?? process.argv);
128
56
  await this.program.parseAsync(normalized, { from: 'node' });
129
57
  if (!this.program.args.length) this.program.outputHelp();
130
- } catch (error: any) {
131
- this.handleError(error);
132
- }
133
- }
134
-
135
- /**
136
- * Normalizes argv to ensure it has at least two elements.
137
- * @param {string[]} argv The argv array to normalize
138
- * @returns {string[]} Normalized argv array
139
- */
140
- private normalizeArgv(argv: string[]): string[] {
141
- if (argv.length >= 2) return argv;
142
- if (argv.length === 1) return ['node', argv[0]];
143
- return ['node', 'btcr2'];
144
- }
145
-
146
- /**
147
- * Handles errors thrown during CLI execution.
148
- * @param {unknown} error The error to handle
149
- * @returns {void}
150
- */
151
- private handleError(error: unknown): void {
152
- if (error instanceof CommanderError && (error.code === 'commander.helpDisplayed' || error.code === 'commander.help')) {
153
- return;
154
- }
155
- if (error instanceof CLIError) {
156
- console.error(error.message);
157
- process.exitCode ??= 1;
158
- return;
159
- }
160
- console.error(error);
161
- process.exitCode ??= 1;
162
- }
163
-
164
- /**
165
- * Parses create command options and throws CLIError on invalid input.
166
- * @param {object} options The create command options
167
- * @param {string} options.type The identifier type
168
- * @param {string} options.network The bitcoin network
169
- * @param {string} options.bytes The genesis bytes as a hex string
170
- * @returns {CreateCommandOptions} The parsed create command options
171
- */
172
- private parseCreateOptions(options: { type: string; network: string; bytes: string; }): CreateCommandOptions {
173
- if (!['k', 'x'].includes(options.type)) {
174
- throw new CLIError(
175
- 'Invalid type. Must be "k" or "x".',
176
- 'INVALID_ARGUMENT_ERROR',
177
- options
178
- );
179
- }
180
- if (!this.isNetworkValid(options.network)) {
181
- throw new CLIError(
182
- 'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
183
- 'INVALID_ARGUMENT_ERROR',
184
- options
185
- );
186
- }
187
- if (Buffer.from(options.bytes, 'hex').length === 0) {
188
- throw new CLIError(
189
- 'Invalid bytes. Must be a non-empty hex string.',
190
- 'INVALID_ARGUMENT_ERROR',
191
- options
192
- );
193
- }
194
- return {
195
- type : options.type as CreateCommandOptions['type'],
196
- network : options.network as NetworkOption,
197
- bytes : options.bytes,
198
- };
199
- }
200
-
201
- /**
202
- * Parses resolve command options and throws CLIError on invalid input.
203
- * @param {object} options The resolve command options
204
- * @param {string} options.identifier The did:btcr2 identifier
205
- * @param {string} [options.resolutionOptions] JSON string of resolution options
206
- * @param {string} [options.resolutionOptionsPath] Path to a JSON file of resolution options
207
- * @returns {Promise<ResolveCommandOptions>} The parsed resolve command options
208
- */
209
- private async parseResolveOptions(options: {
210
- identifier: string;
211
- resolutionOptions?: string;
212
- resolutionOptionsPath?: string;
213
- }): Promise<ResolveCommandOptions> {
214
- this.validateIdentifier(options.identifier, options);
215
- const resolutionOptions = await this.parseResolutionOptions(options);
216
- return {
217
- identifier : options.identifier,
218
- ...(resolutionOptions && { options: resolutionOptions }),
219
- };
220
- }
221
-
222
- /**
223
- * Parses update command options and throws CLIError on invalid input.
224
- * @param {object} options The update command options
225
- * @param {string} options.sourceDocument The source DID document as a JSON string
226
- * @param {number} options.sourceVersionId The source version ID as a number
227
- * @param {string} options.patches The JSON Patch operations as a JSON string array
228
- * @param {string} options.verificationMethodId The DID document verification method ID as a string
229
- * @param {string} options.beaconId The beacon IDs as a JSON string array
230
- * @returns {UpdateCommandOptions} The parsed update command options
231
- */
232
- private parseUpdateOptions(options: {
233
- sourceDocument: string;
234
- sourceVersionId: number;
235
- patches: string;
236
- verificationMethodId: string;
237
- beaconId: string;
238
- }): UpdateCommandOptions {
239
- const sourceDocument = this.parseJsonOption<UpdateCommandOptions['sourceDocument']>(
240
- options.sourceDocument,
241
- 'Invalid options. Must be a valid JSON string.',
242
- options
243
- );
244
- const patches = this.parseJsonOption<UpdateCommandOptions['patches']>(
245
- options.patches,
246
- 'Invalid options. Must be a valid JSON string.',
247
- options
248
- );
249
- const beaconId = this.parseJsonOption<UpdateCommandOptions['beaconId']>(
250
- options.beaconId,
251
- 'Invalid options. Must be a valid JSON string.',
252
- options
253
- );
254
-
255
- return {
256
- sourceDocument,
257
- sourceVersionId : Number(options.sourceVersionId),
258
- patches,
259
- verificationMethodId : options.verificationMethodId,
260
- beaconId,
261
- } as UpdateCommandOptions;
262
- }
263
-
264
- /**
265
- * Parses a JSON option and throws a CLIError on failure.
266
- * @param {string} value JSON string to parse
267
- * @param {string} errorMessage Error message to use on failure
268
- * @param {Record<string, any>} data Additional data to include in the error
269
- * @returns {T} Parsed JSON object
270
- */
271
- private parseJsonOption<T>(value: string, errorMessage: string, data: Record<string, any>): T {
272
- try {
273
- return JSON.parse(value) as T;
274
- } catch {
275
- throw new CLIError(
276
- errorMessage,
277
- 'INVALID_ARGUMENT_ERROR',
278
- data
279
- );
280
- }
281
- }
282
-
283
- /**
284
- * Parses resolution options from JSON string or file path.
285
- * @param {object} options The options containing resolution options
286
- * @param {string} [options.resolutionOptions] JSON string of resolution options
287
- * @param {string} [options.resolutionOptionsPath] Path to a JSON file of resolution options
288
- * @returns {Promise<any>} The parsed resolution options
289
- */
290
- private async parseResolutionOptions(options: { resolutionOptions?: string; resolutionOptionsPath?: string; }): Promise<any> {
291
- if (options.resolutionOptions) {
292
- return this.parseJsonOption(options.resolutionOptions, 'Invalid options. Must be a valid JSON string.', options);
293
- }
294
- if (options.resolutionOptionsPath) {
295
- try {
296
- const data = await readFile(options.resolutionOptionsPath, 'utf-8');
297
- return JSON.parse(data);
298
- } catch {
299
- throw new CLIError(
300
- 'Invalid options path. Must be a valid path to a JSON file.',
301
- 'INVALID_ARGUMENT_ERROR',
302
- options
303
- );
304
- }
305
- }
306
- return undefined;
307
- }
308
-
309
- /**
310
- * Validates the did:btcr2 identifier format.
311
- * @param {string} identifier The identifier to validate
312
- * @param {Record<string, any>} data Additional data to include in the error
313
- * @returns {void}
314
- */
315
- private validateIdentifier(identifier: string, data: Record<string, any>): void {
316
- try {
317
- Identifier.decode(identifier);
318
- } catch {
319
- throw new CLIError(
320
- 'Invalid identifier. Must be a valid did:btcr2 identifier.',
321
- 'INVALID_ARGUMENT_ERROR',
322
- data
323
- );
58
+ } catch (error: unknown) {
59
+ handleError(error);
324
60
  }
325
61
  }
62
+ }
326
63
 
327
- /**
328
- * Validates if the provided network is supported.
329
- * @param {string} network The network to validate
330
- * @returns {boolean} True if the network is valid
331
- */
332
- private isNetworkValid(network: string): network is NetworkOption {
333
- return SUPPORTED_NETWORKS.includes(network as NetworkOption);
334
- }
64
+ /**
65
+ * Normalizes argv to ensure it has at least two elements.
66
+ * @param {string[]} argv - The original argv array.
67
+ * @returns {string[]} - The normalized argv array.
68
+ */
69
+ function normalizeArgv(argv: string[]): string[] {
70
+ if (argv.length >= 2) return argv;
71
+ if (argv.length === 1) return ['node', argv[0]];
72
+ return ['node', 'btcr2'];
73
+ }
335
74
 
336
- /**
337
- * Prints the command result to the console.
338
- * @param {CommandResult} result The command result to print
339
- * @returns {void}
340
- */
341
- private printResult(result: CommandResult): void {
342
- switch (result.action) {
343
- case 'create':
344
- this.log(result.did);
345
- break;
346
- case 'resolve':
347
- case 'read':
348
- this.log(result.resolution);
349
- break;
350
- case 'update':
351
- this.log(result.signed);
352
- break;
353
- case 'deactivate':
354
- case 'delete':
355
- this.log(result.message);
356
- break;
357
- default:
358
- this.log(result);
359
- }
75
+ /**
76
+ * Handles errors thrown during CLI execution.
77
+ * @param {unknown} error - The error to handle.
78
+ * @returns {void}
79
+ */
80
+ function handleError(error: unknown): void {
81
+ if (
82
+ error instanceof CommanderError &&
83
+ (error.code === 'commander.helpDisplayed' || error.code === 'commander.help')
84
+ ) {
85
+ return;
360
86
  }
361
-
362
- /**
363
- * Logs a payload to the console, formatting objects as JSON.
364
- * @param {unknown} payload The payload to log
365
- * @returns {void}
366
- */
367
- private log(payload: unknown): void {
368
- if (typeof payload === 'string') {
369
- console.log(payload);
370
- return;
371
- }
372
- console.log(JSON.stringify(payload, null, 2));
87
+ if (error instanceof CLIError) {
88
+ console.error(error.message);
89
+ process.exitCode ??= 1;
90
+ return;
373
91
  }
92
+ console.error(error);
93
+ process.exitCode ??= 1;
374
94
  }
@@ -0,0 +1,88 @@
1
+ import { IdentifierTypes } from '@did-btcr2/common';
2
+ import { Command } from 'commander';
3
+ import { CLIError } from '../error.js';
4
+ import { formatResult } from '../output.js';
5
+ import {
6
+ CreateCommandOptions,
7
+ GlobalOptions,
8
+ MethodOperations,
9
+ NetworkOption,
10
+ SUPPORTED_NETWORKS,
11
+ } from '../types.js';
12
+
13
+ /** Expected byte length per identifier type: compressed secp256k1 = 33, SHA-256 hash = 32. */
14
+ const EXPECTED_BYTES: Record<'k' | 'x', { length: number; label: string }> = {
15
+ k : { length: 33, label: 'secp256k1 compressed public key (33 bytes)' },
16
+ x : { length: 32, label: 'SHA-256 hash (32 bytes)' },
17
+ };
18
+
19
+ export function registerCreateCommand(
20
+ program : Command,
21
+ ops : MethodOperations,
22
+ globals : () => GlobalOptions,
23
+ ): void {
24
+ program
25
+ .command('create')
26
+ .description('Create an identifier and initial DID document')
27
+ .requiredOption('-t, --type <type>', 'Identifier type <k|x>', 'k')
28
+ .requiredOption(
29
+ '-n, --network <network>',
30
+ 'Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest>'
31
+ )
32
+ .requiredOption(
33
+ '-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'
37
+ )
38
+ .action(async (options: { type: string; network: string; bytes: string }) => {
39
+ const parsed = validateCreateOptions(options);
40
+ const idType = parsed.type === 'k' ? IdentifierTypes.KEY : IdentifierTypes.EXTERNAL;
41
+ const genesisBytes = Buffer.from(parsed.bytes, 'hex');
42
+ const data = ops.create(genesisBytes, { idType, network: parsed.network });
43
+ const result = { action: 'create' as const, data };
44
+ console.log(formatResult(result, globals()));
45
+ });
46
+ }
47
+
48
+ function validateCreateOptions(
49
+ options: { type: string; network: string; bytes: string }
50
+ ): CreateCommandOptions {
51
+ if (!['k', 'x'].includes(options.type)) {
52
+ throw new CLIError(
53
+ 'Invalid type. Must be "k" or "x".',
54
+ 'INVALID_ARGUMENT_ERROR',
55
+ options
56
+ );
57
+ }
58
+ if (!SUPPORTED_NETWORKS.includes(options.network as NetworkOption)) {
59
+ throw new CLIError(
60
+ 'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
61
+ 'INVALID_ARGUMENT_ERROR',
62
+ options
63
+ );
64
+ }
65
+
66
+ const buf = Buffer.from(options.bytes, 'hex');
67
+ if (buf.length === 0) {
68
+ throw new CLIError(
69
+ 'Invalid bytes. Must be a non-empty hex string.',
70
+ 'INVALID_ARGUMENT_ERROR',
71
+ options
72
+ );
73
+ }
74
+ const expected = EXPECTED_BYTES[options.type as 'k' | 'x'];
75
+ if (buf.length !== expected.length) {
76
+ throw new CLIError(
77
+ `Invalid bytes length for type="${options.type}": expected ${expected.label}, got ${buf.length} bytes.`,
78
+ 'INVALID_ARGUMENT_ERROR',
79
+ options
80
+ );
81
+ }
82
+
83
+ return {
84
+ type : options.type as 'k' | 'x',
85
+ network : options.network as NetworkOption,
86
+ bytes : options.bytes,
87
+ };
88
+ }
@@ -0,0 +1,71 @@
1
+ import { Command } from 'commander';
2
+ import { CLIError } from '../error.js';
3
+ import { formatResult } from '../output.js';
4
+ import { GlobalOptions, MethodOperations, UpdateCommandOptions } from '../types.js';
5
+
6
+ /** The JSON Patch that marks a DID document as permanently deactivated. */
7
+ const DEACTIVATION_PATCH = [{ op: 'add' as const, path: '/deactivated', value: true }];
8
+
9
+ export function registerDeactivateCommand(
10
+ program : Command,
11
+ ops : MethodOperations,
12
+ globals : () => GlobalOptions,
13
+ ): void {
14
+ program
15
+ .command('deactivate')
16
+ .alias('delete')
17
+ .description('Deactivate the did:btcr2 identifier permanently. This is irreversible.')
18
+ .requiredOption(
19
+ '-s, --source-document <json>',
20
+ 'Current DID document as JSON string',
21
+ parseJsonArg('--source-document'),
22
+ )
23
+ .requiredOption(
24
+ '--source-version-id <number>',
25
+ 'Current version ID of the DID document'
26
+ )
27
+ .requiredOption(
28
+ '-m, --verification-method-id <id>',
29
+ 'DID document verification method ID used to sign the deactivation'
30
+ )
31
+ .requiredOption(
32
+ '-b, --beacon-id <json>',
33
+ 'Beacon ID as a JSON string',
34
+ parseJsonArg('--beacon-id'),
35
+ )
36
+ .action(async (options: {
37
+ sourceDocument : unknown;
38
+ sourceVersionId : string;
39
+ verificationMethodId : string;
40
+ beaconId : unknown;
41
+ }) => {
42
+ const parsed: UpdateCommandOptions = {
43
+ sourceDocument : options.sourceDocument as UpdateCommandOptions['sourceDocument'],
44
+ patches : DEACTIVATION_PATCH,
45
+ sourceVersionId : Number(options.sourceVersionId),
46
+ verificationMethodId : options.verificationMethodId,
47
+ beaconId : options.beaconId as UpdateCommandOptions['beaconId'],
48
+ };
49
+ const data = await ops.update(parsed);
50
+ const result = { action: 'deactivate' as const, data };
51
+ console.log(formatResult(result, globals()));
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Returns a commander argParser that validates JSON.
57
+ * Errors at parse time with a clear flag reference.
58
+ */
59
+ function parseJsonArg(flagName: string): (value: string) => unknown {
60
+ return (value: string): unknown => {
61
+ try {
62
+ return JSON.parse(value);
63
+ } catch {
64
+ throw new CLIError(
65
+ `Invalid JSON for ${flagName}. Must be a valid JSON string.`,
66
+ 'INVALID_ARGUMENT_ERROR',
67
+ { flagName, value }
68
+ );
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,4 @@
1
+ export { registerCreateCommand } from './create.js';
2
+ export { registerResolveCommand } from './resolve.js';
3
+ export { registerUpdateCommand } from './update.js';
4
+ export { registerDeactivateCommand } from './deactivate.js';