@did-btcr2/cli 0.22.0 → 0.23.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 (73) hide show
  1. package/README.md +17 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +394 -78
  4. package/dist/esm/src/cli.js +2 -1
  5. package/dist/esm/src/cli.js.map +1 -1
  6. package/dist/esm/src/commands/completion.js +1 -1
  7. package/dist/esm/src/commands/completion.js.map +1 -1
  8. package/dist/esm/src/commands/create.js +33 -41
  9. package/dist/esm/src/commands/create.js.map +1 -1
  10. package/dist/esm/src/commands/genesis.js +119 -0
  11. package/dist/esm/src/commands/genesis.js.map +1 -0
  12. package/dist/esm/src/commands/identifier.js +3 -17
  13. package/dist/esm/src/commands/identifier.js.map +1 -1
  14. package/dist/esm/src/commands/index.js +1 -0
  15. package/dist/esm/src/commands/index.js.map +1 -1
  16. package/dist/esm/src/commands/resolve.js +15 -1
  17. package/dist/esm/src/commands/resolve.js.map +1 -1
  18. package/dist/esm/src/commands/write.js +10 -7
  19. package/dist/esm/src/commands/write.js.map +1 -1
  20. package/dist/esm/src/genesis-document-file.js +26 -0
  21. package/dist/esm/src/genesis-document-file.js.map +1 -0
  22. package/dist/esm/src/genesis-spec.js +93 -0
  23. package/dist/esm/src/genesis-spec.js.map +1 -0
  24. package/dist/esm/src/genesis-wizard.js +132 -0
  25. package/dist/esm/src/genesis-wizard.js.map +1 -0
  26. package/dist/esm/src/hints.js +14 -3
  27. package/dist/esm/src/hints.js.map +1 -1
  28. package/dist/esm/src/network-option.js +39 -0
  29. package/dist/esm/src/network-option.js.map +1 -0
  30. package/dist/esm/src/resolution-options.js +19 -2
  31. package/dist/esm/src/resolution-options.js.map +1 -1
  32. package/dist/types/src/cli.d.ts.map +1 -1
  33. package/dist/types/src/commands/create.d.ts +5 -4
  34. package/dist/types/src/commands/create.d.ts.map +1 -1
  35. package/dist/types/src/commands/genesis.d.ts +20 -0
  36. package/dist/types/src/commands/genesis.d.ts.map +1 -0
  37. package/dist/types/src/commands/identifier.d.ts.map +1 -1
  38. package/dist/types/src/commands/index.d.ts +1 -0
  39. package/dist/types/src/commands/index.d.ts.map +1 -1
  40. package/dist/types/src/commands/resolve.d.ts +7 -0
  41. package/dist/types/src/commands/resolve.d.ts.map +1 -1
  42. package/dist/types/src/commands/write.d.ts +2 -1
  43. package/dist/types/src/commands/write.d.ts.map +1 -1
  44. package/dist/types/src/genesis-document-file.d.ts +11 -0
  45. package/dist/types/src/genesis-document-file.d.ts.map +1 -0
  46. package/dist/types/src/genesis-spec.d.ts +70 -0
  47. package/dist/types/src/genesis-spec.d.ts.map +1 -0
  48. package/dist/types/src/genesis-wizard.d.ts +30 -0
  49. package/dist/types/src/genesis-wizard.d.ts.map +1 -0
  50. package/dist/types/src/hints.d.ts +7 -0
  51. package/dist/types/src/hints.d.ts.map +1 -1
  52. package/dist/types/src/network-option.d.ts +15 -0
  53. package/dist/types/src/network-option.d.ts.map +1 -0
  54. package/dist/types/src/resolution-options.d.ts +6 -2
  55. package/dist/types/src/resolution-options.d.ts.map +1 -1
  56. package/dist/types/src/types.d.ts +18 -1
  57. package/dist/types/src/types.d.ts.map +1 -1
  58. package/package.json +5 -5
  59. package/src/cli.ts +2 -0
  60. package/src/commands/completion.ts +1 -1
  61. package/src/commands/create.ts +39 -55
  62. package/src/commands/genesis.ts +149 -0
  63. package/src/commands/identifier.ts +3 -25
  64. package/src/commands/index.ts +1 -0
  65. package/src/commands/resolve.ts +20 -1
  66. package/src/commands/write.ts +10 -7
  67. package/src/genesis-document-file.ts +35 -0
  68. package/src/genesis-spec.ts +149 -0
  69. package/src/genesis-wizard.ts +163 -0
  70. package/src/hints.ts +13 -2
  71. package/src/network-option.ts +50 -0
  72. package/src/resolution-options.ts +21 -2
  73. package/src/types.ts +17 -2
@@ -8,10 +8,13 @@ import {
8
8
  resolveSigningKeyRef,
9
9
  type ApiFactory,
10
10
  } from '../config.js';
11
+ import { Identifier } from '@did-btcr2/api';
11
12
  import { CLIError } from '../error.js';
13
+ import { GENESIS_DOCUMENT_HELP } from '../genesis-document-file.js';
12
14
  import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
13
- import { MIN_CONF_HELP, parseMinConf, readResolutionOptions, type ResolutionOptionFlags } from '../resolution-options.js';
15
+ import { hasResolutionFlags, MIN_CONF_HELP, parseMinConf, readResolutionOptions, type ResolutionOptionFlags } from '../resolution-options.js';
14
16
  import type { GlobalOptions, NetworkOption, UpdateCommandOptions } from '../types.js';
17
+ import { assertGenesisDocumentApplies } from './resolve.js';
15
18
 
16
19
  /** The parsed flags that `update` and `deactivate` share. */
17
20
  export type WriteFlags = ResolutionOptionFlags & {
@@ -62,6 +65,7 @@ export function registerWriteOptions(command: Command): Command {
62
65
  'Path to a JSON file with resolution options, for the resolution of the source document',
63
66
  )
64
67
  .option('--min-conf <n>', MIN_CONF_HELP, parseMinConf)
68
+ .option('--genesis-document <path>', `${GENESIS_DOCUMENT_HELP}, for the resolution of the source document`)
65
69
  .option(
66
70
  '--publish-to-cas <mode>',
67
71
  'Publish update artifacts to a writable CAS before broadcast: auto|always|never. '
@@ -93,7 +97,8 @@ export function registerWriteOptions(command: Command): Command {
93
97
  * 3. The resolution flags come only without the source pair. The api ignores
94
98
  * `resolutionOptions` when the pair is supplied (ADR 098). A silent ignore
95
99
  * of `--min-conf` would mislead.
96
- * 4. A mainnet write is refused with an unencrypted dev keystore (ADR 080).
100
+ * 4. `--genesis-document` comes only with an external (x) identifier.
101
+ * 5. A mainnet write is refused with an unencrypted dev keystore (ADR 080).
97
102
  */
98
103
  export async function prepareWrite(
99
104
  options : WriteFlags,
@@ -112,17 +117,15 @@ export async function prepareWrite(
112
117
  { did },
113
118
  );
114
119
  }
115
- const hasResolutionFlags = options.resolutionOptions !== undefined
116
- || options.resolutionOptionsPath !== undefined
117
- || options.minConf !== undefined;
118
- if (hasDocument && hasResolutionFlags) {
120
+ if (hasDocument && hasResolutionFlags(options)) {
119
121
  throw new CLIError(
120
- '--resolution-options, --resolution-options-path, and --min-conf apply only when '
122
+ '--resolution-options, --resolution-options-path, --min-conf, and --genesis-document apply only when '
121
123
  + '--source-document and --source-version-id are omitted. A supplied source pair skips resolution.',
122
124
  'INVALID_ARGUMENT_ERROR',
123
125
  { did },
124
126
  );
125
127
  }
128
+ assertGenesisDocumentApplies(options, Identifier.decode(did).hrp);
126
129
  assertKeystoreAllowedForNetwork(network, g);
127
130
  const resolutionOptions = await readResolutionOptions(options);
128
131
  const api = factory(network, g);
@@ -0,0 +1,35 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { CLIError } from './error.js';
3
+
4
+ /** The help text of `--genesis-document`. `resolve`, `update`, and `deactivate` share it. */
5
+ export const GENESIS_DOCUMENT_HELP =
6
+ 'Path to the JSON genesis document of an external identifier (x). '
7
+ + 'Fills sidecar.genesisDocument of the resolution options';
8
+
9
+ /**
10
+ * Reads and parses a genesis document file. The content must be a JSON
11
+ * object. The shape of the document is checked by the api, not here.
12
+ * @param path The path of the JSON file.
13
+ * @returns The parsed object.
14
+ * @throws {CLIError} If the file is unreadable, not JSON, or not an object.
15
+ */
16
+ export async function readGenesisDocumentFile(path: string): Promise<object> {
17
+ let parsed: unknown;
18
+ try {
19
+ parsed = JSON.parse(await readFile(path, 'utf-8'));
20
+ } catch {
21
+ throw new CLIError(
22
+ 'Invalid genesis document path. Must be a valid path to a JSON file.',
23
+ 'INVALID_ARGUMENT_ERROR',
24
+ { genesisDocument: path },
25
+ );
26
+ }
27
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
28
+ throw new CLIError(
29
+ 'Invalid genesis document. The file must contain a JSON object.',
30
+ 'INVALID_ARGUMENT_ERROR',
31
+ { genesisDocument: path },
32
+ );
33
+ }
34
+ return parsed;
35
+ }
@@ -0,0 +1,149 @@
1
+ import type {
2
+ BeaconAddressType,
3
+ BeaconType,
4
+ GenesisBeaconSpec,
5
+ GenesisDocumentSpec,
6
+ GenesisVerificationMethodSpec,
7
+ VerificationRelationship
8
+ } from '@did-btcr2/api';
9
+ import { hexToBytes } from '@noble/hashes/utils.js';
10
+ import type { DidService } from '@web5/dids';
11
+ import { CLIError } from './error.js';
12
+ import type { NetworkOption } from './types.js';
13
+
14
+ /**
15
+ * One verification method of a genesis spec file. `key` is a keystore
16
+ * reference (URN, name, or fingerprint prefix); `publicKey` is a 33-byte
17
+ * compressed public key as hex. Exactly one of the two.
18
+ */
19
+ export interface GenesisSpecVerificationMethod {
20
+ key? : string;
21
+ publicKey? : string;
22
+ relationships? : string[];
23
+ fragment? : string;
24
+ }
25
+
26
+ /**
27
+ * One beacon of a genesis spec file. `key` or `publicKey` derives the beacon
28
+ * address from a key; `address` uses a Bitcoin address as given. At most one
29
+ * of the three; the api requires one.
30
+ */
31
+ export interface GenesisSpecBeacon {
32
+ type : string;
33
+ key? : string;
34
+ publicKey? : string;
35
+ addressType? : string;
36
+ address? : string;
37
+ fragment? : string;
38
+ }
39
+
40
+ /** One other service of a genesis spec file. */
41
+ export interface GenesisSpecService {
42
+ id? : string;
43
+ type : string;
44
+ serviceEndpoint : unknown;
45
+ }
46
+
47
+ /**
48
+ * The genesis spec file that `btcr2 genesis build --spec <path>` reads, and
49
+ * that the wizard collects. The network comes from `-n` or the configuration,
50
+ * not from the file.
51
+ */
52
+ export interface GenesisSpecFile {
53
+ verificationMethods : GenesisSpecVerificationMethod[];
54
+ beacons? : GenesisSpecBeacon[];
55
+ services? : GenesisSpecService[];
56
+ }
57
+
58
+ /** Resolves a keystore key reference to the 33-byte public key. */
59
+ export type ResolvePublicKey = (ref: string) => Uint8Array;
60
+
61
+ /**
62
+ * Checks the shape of a parsed genesis spec file. The values (relationship
63
+ * names, beacon types, addresses) are checked by the api when it builds the
64
+ * document; this function checks only what the api cannot see: that each
65
+ * verification method names exactly one key source, and that each beacon
66
+ * names at most one.
67
+ * @param value The parsed JSON value.
68
+ * @param source The file path, for the error data.
69
+ * @returns The spec.
70
+ * @throws {CLIError} If the shape is not valid.
71
+ */
72
+ export function parseGenesisSpec(value: unknown, source: string): GenesisSpecFile {
73
+ const fail = (message: string): never => {
74
+ throw new CLIError(`Invalid genesis spec: ${message}`, 'INVALID_ARGUMENT_ERROR', { spec: source });
75
+ };
76
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
77
+ return fail('the file must contain a JSON object.');
78
+ }
79
+ const spec = value as Record<string, unknown>;
80
+ if (!Array.isArray(spec.verificationMethods) || spec.verificationMethods.length === 0) {
81
+ return fail('"verificationMethods" must be a non-empty array.');
82
+ }
83
+ spec.verificationMethods.forEach((vm, index) => {
84
+ if (vm === null || typeof vm !== 'object') return fail(`verificationMethods[${index}] must be an object.`);
85
+ const hasKey = typeof vm.key === 'string' && vm.key.length > 0;
86
+ const hasPublicKey = typeof vm.publicKey === 'string' && vm.publicKey.length > 0;
87
+ if (hasKey === hasPublicKey) {
88
+ return fail(`verificationMethods[${index}] needs exactly one of "key" (a keystore reference) or "publicKey" (hex).`);
89
+ }
90
+ });
91
+ if (spec.beacons !== undefined) {
92
+ if (!Array.isArray(spec.beacons)) return fail('"beacons" must be an array.');
93
+ spec.beacons.forEach((beacon, index) => {
94
+ if (beacon === null || typeof beacon !== 'object') return fail(`beacons[${index}] must be an object.`);
95
+ const sources = ['key', 'publicKey', 'address'].filter(name => beacon[name] !== undefined);
96
+ if (sources.length > 1) {
97
+ return fail(`beacons[${index}] names ${sources.join(' and ')}. Give only one of "key", "publicKey", or "address".`);
98
+ }
99
+ });
100
+ }
101
+ if (spec.services !== undefined && !Array.isArray(spec.services)) {
102
+ return fail('"services" must be an array.');
103
+ }
104
+ return spec as unknown as GenesisSpecFile;
105
+ }
106
+
107
+ /**
108
+ * Converts a genesis spec file to the api spec: resolves each keystore
109
+ * reference to its public key, parses each hex public key, and passes the
110
+ * other values through for the api to check.
111
+ * @param spec The spec file.
112
+ * @param network The network of the beacon addresses.
113
+ * @param resolvePublicKey Resolves a keystore reference to the public key.
114
+ * @returns The api spec.
115
+ * @throws {CLIError} If a hex public key is not hex.
116
+ */
117
+ export function toApiSpec(spec: GenesisSpecFile, network: NetworkOption, resolvePublicKey: ResolvePublicKey): GenesisDocumentSpec {
118
+ const keyBytes = (entry: { key?: string; publicKey?: string }, label: string): Uint8Array =>
119
+ entry.key !== undefined ? resolvePublicKey(entry.key) : parseHexPublicKey(entry.publicKey as string, label);
120
+ const verificationMethods: GenesisVerificationMethodSpec[] = spec.verificationMethods.map((vm, index) => ({
121
+ publicKey : keyBytes(vm, `verificationMethods[${index}].publicKey`),
122
+ relationships : vm.relationships as VerificationRelationship[] | undefined,
123
+ fragment : vm.fragment,
124
+ }));
125
+ const beacons: GenesisBeaconSpec[] | undefined = spec.beacons?.map((beacon, index) => ({
126
+ type : beacon.type as BeaconType,
127
+ publicKey : beacon.key !== undefined || beacon.publicKey !== undefined
128
+ ? keyBytes(beacon, `beacons[${index}].publicKey`)
129
+ : undefined,
130
+ addressType : beacon.addressType as BeaconAddressType | undefined,
131
+ address : beacon.address,
132
+ fragment : beacon.fragment,
133
+ }));
134
+ return {
135
+ network,
136
+ verificationMethods,
137
+ beacons,
138
+ services : spec.services as DidService[] | undefined,
139
+ };
140
+ }
141
+
142
+ /** Parses a hex public key. The length and the curve check belong to the api. */
143
+ export function parseHexPublicKey(hex: string, label: string): Uint8Array {
144
+ try {
145
+ return hexToBytes(hex.trim());
146
+ } catch {
147
+ throw new CLIError(`Invalid ${label}: not valid hex.`, 'INVALID_ARGUMENT_ERROR', { publicKey: hex });
148
+ }
149
+ }
@@ -0,0 +1,163 @@
1
+ import { BEACON_ADDRESS_TYPES, BEACON_TYPES, DEFAULT_BEACON_ADDRESS_TYPE, VERIFICATION_RELATIONSHIPS } from '@did-btcr2/api';
2
+ import type { GenesisSpecBeacon, GenesisSpecFile, GenesisSpecService, GenesisSpecVerificationMethod } from './genesis-spec.js';
3
+
4
+ /** Asks one question and returns the answer line. */
5
+ export type Ask = (question: string) => Promise<string>;
6
+
7
+ /** One keystore key as the wizard lists it. */
8
+ export interface WizardKey {
9
+ keyId : string;
10
+ fingerprint : string;
11
+ name? : string;
12
+ active : boolean;
13
+ }
14
+
15
+ /** What the wizard needs from the command: output, the key list, and key resolution. */
16
+ export interface WizardContext {
17
+ /** Prints one line to the operator. */
18
+ say: (line: string) => void;
19
+ /** The keys of the keystore, for the list and the default. */
20
+ keys: WizardKey[];
21
+ /** Resolves a key reference to its id, or throws with a message. */
22
+ resolveKey: (ref: string) => string;
23
+ }
24
+
25
+ /** A 33-byte compressed public key as hex. */
26
+ const PUBLIC_KEY_HEX = /^0[23][0-9a-fA-F]{64}$/;
27
+
28
+ /**
29
+ * Collects a genesis spec through questions: the verification methods with
30
+ * their relationships, the beacons, and the other services. The answers are
31
+ * not checked against the network here; the api checks them when it builds
32
+ * the document, and the command reports the failure.
33
+ * @param ask Asks one question.
34
+ * @param ctx The output, the key list, and the key resolver.
35
+ * @returns The spec.
36
+ */
37
+ export async function collectGenesisSpec(ask: Ask, ctx: WizardContext): Promise<GenesisSpecFile> {
38
+ const verificationMethods = await collectVerificationMethods(ask, ctx);
39
+ const beacons = await collectBeacons(ask, ctx, verificationMethods[0]);
40
+ const services = await collectServices(ask, ctx);
41
+ return { verificationMethods, beacons, ...(services.length > 0 && { services }) };
42
+ }
43
+
44
+ /** Asks for the verification methods. The first one defaults to the active key. */
45
+ async function collectVerificationMethods(ask: Ask, ctx: WizardContext): Promise<GenesisSpecVerificationMethod[]> {
46
+ if (ctx.keys.length === 0) {
47
+ ctx.say('The keystore has no keys. Enter each key as a 33-byte compressed public key in hex.');
48
+ } else {
49
+ ctx.say('Keys in the keystore:');
50
+ for (const key of ctx.keys) {
51
+ ctx.say(` ${key.fingerprint}${key.name ? ` ${key.name}` : ''}${key.active ? ' (active)' : ''}`);
52
+ }
53
+ }
54
+ const active = ctx.keys.find(key => key.active);
55
+ const methods: GenesisSpecVerificationMethod[] = [];
56
+ for (let n = 1; ; n++) {
57
+ const defaultRef = n === 1 && active ? (active.name ?? active.fingerprint) : undefined;
58
+ const source = await askKeySource(ask, ctx, `Verification method ${n}: key reference or public key hex`, defaultRef);
59
+ const relationships = await askRelationships(ask, ctx, n);
60
+ methods.push({ ...source, ...(relationships && { relationships }) });
61
+ if (!(await askYesNo(ask, 'Add another verification method?'))) break;
62
+ }
63
+ return methods;
64
+ }
65
+
66
+ /** Asks for the beacons. The first one defaults to a Singleton beacon on the first key. */
67
+ async function collectBeacons(ask: Ask, ctx: WizardContext, firstMethod: GenesisSpecVerificationMethod): Promise<GenesisSpecBeacon[]> {
68
+ const beacons: GenesisSpecBeacon[] = [];
69
+ for (let n = 1; ; n++) {
70
+ const type = await askChoice(ask, ctx, `Beacon ${n} type`, BEACON_TYPES, 'SingletonBeacon');
71
+ const address = (await ask(`Beacon ${n} address (blank: derive the address from the key of method 1): `)).trim();
72
+ if (address.length === 0) {
73
+ const addressType = await askChoice(ask, ctx, `Beacon ${n} address type`, BEACON_ADDRESS_TYPES, DEFAULT_BEACON_ADDRESS_TYPE);
74
+ const keySource = firstMethod.key !== undefined ? { key: firstMethod.key } : { publicKey: firstMethod.publicKey };
75
+ beacons.push({ type, ...keySource, addressType });
76
+ } else {
77
+ beacons.push({ type, address });
78
+ }
79
+ if (!(await askYesNo(ask, 'Add another beacon?'))) break;
80
+ }
81
+ return beacons;
82
+ }
83
+
84
+ /** Asks for the other services, if any. */
85
+ async function collectServices(ask: Ask, ctx: WizardContext): Promise<GenesisSpecService[]> {
86
+ const services: GenesisSpecService[] = [];
87
+ while (await askYesNo(ask, 'Add a service?')) {
88
+ const n = services.length + 1;
89
+ const fragment = (await ask(`Service ${n} id fragment (blank: service-<position>): `)).trim();
90
+ const type = await askRequired(ask, ctx, `Service ${n} type`);
91
+ const serviceEndpoint = await askRequired(ask, ctx, `Service ${n} endpoint`);
92
+ services.push({ ...(fragment.length > 0 && { id: `#${fragment.replace(/^#/, '')}` }), type, serviceEndpoint });
93
+ }
94
+ return services;
95
+ }
96
+
97
+ /** Asks for a key reference or a public key hex, until the answer resolves. */
98
+ async function askKeySource(
99
+ ask : Ask,
100
+ ctx : WizardContext,
101
+ question : string,
102
+ defaultRef : string | undefined,
103
+ ): Promise<{ key: string } | { publicKey: string }> {
104
+ for (;;) {
105
+ const raw = (await ask(`${question}${defaultRef ? ` [${defaultRef}]` : ''}: `)).trim();
106
+ const answer = raw.length > 0 ? raw : defaultRef;
107
+ if (answer === undefined) {
108
+ ctx.say('A key is required.');
109
+ continue;
110
+ }
111
+ if (PUBLIC_KEY_HEX.test(answer)) return { publicKey: answer.toLowerCase() };
112
+ try {
113
+ ctx.resolveKey(answer);
114
+ return { key: answer };
115
+ } catch (error) {
116
+ ctx.say((error as Error).message);
117
+ }
118
+ }
119
+ }
120
+
121
+ /** Asks for the relationships of a method. Blank means all four (the api default). */
122
+ async function askRelationships(ask: Ask, ctx: WizardContext, n: number): Promise<string[] | undefined> {
123
+ const names = VERIFICATION_RELATIONSHIPS.join(', ');
124
+ for (;;) {
125
+ const raw = (await ask(`Relationships of method ${n} (comma-separated: ${names}) [all]: `)).trim();
126
+ if (raw.length === 0) return undefined;
127
+ const chosen = raw.split(',').map(s => s.trim()).filter(s => s.length > 0);
128
+ const unknown = chosen.filter(name => !(VERIFICATION_RELATIONSHIPS as readonly string[]).includes(name));
129
+ if (unknown.length === 0) return chosen;
130
+ ctx.say(`Unknown relationship: ${unknown.join(', ')}. Expected one of ${names}.`);
131
+ }
132
+ }
133
+
134
+ /** Asks for one value of a list, with a default. */
135
+ async function askChoice<T extends string>(
136
+ ask : Ask,
137
+ ctx : WizardContext,
138
+ question : string,
139
+ choices : readonly T[],
140
+ defaultValue : T,
141
+ ): Promise<T> {
142
+ for (;;) {
143
+ const raw = (await ask(`${question} (${choices.join(', ')}) [${defaultValue}]: `)).trim();
144
+ if (raw.length === 0) return defaultValue;
145
+ if ((choices as readonly string[]).includes(raw)) return raw as T;
146
+ ctx.say(`Unknown value "${raw}". Expected one of ${choices.join(', ')}.`);
147
+ }
148
+ }
149
+
150
+ /** Asks for a non-empty value. */
151
+ async function askRequired(ask: Ask, ctx: WizardContext, question: string): Promise<string> {
152
+ for (;;) {
153
+ const raw = (await ask(`${question}: `)).trim();
154
+ if (raw.length > 0) return raw;
155
+ ctx.say('A value is required.');
156
+ }
157
+ }
158
+
159
+ /** Asks a yes/no question. Blank means no. */
160
+ async function askYesNo(ask: Ask, question: string): Promise<boolean> {
161
+ const raw = (await ask(`${question} [y/N]: `)).trim();
162
+ return /^y(es)?$/i.test(raw);
163
+ }
package/src/hints.ts CHANGED
@@ -20,8 +20,6 @@ import type { GlobalOptions, NetworkOption } from './types.js';
20
20
  */
21
21
  export function printCreateFundingHint(g: GlobalOptions, network: NetworkOption, did: string): void {
22
22
  if (g.quiet || g.output === 'json') return;
23
- const faucet = faucetUrl(network);
24
- if (!faucet) return;
25
23
  let beaconAddress: string;
26
24
  try {
27
25
  const { serviceEndpoint } = BeaconUtils.createBeaconService(did, 'p2wpkh', 'SingletonBeacon');
@@ -29,6 +27,19 @@ export function printCreateFundingHint(g: GlobalOptions, network: NetworkOption,
29
27
  } catch {
30
28
  return;
31
29
  }
30
+ printBeaconFundingHint(g, network, beaconAddress);
31
+ }
32
+
33
+ /**
34
+ * Prints a funding hint for a beacon address on a network with a public
35
+ * faucet: the address next to the faucet and explorer links. A no-op on a
36
+ * network without a faucet (regtest/mainnet). `create -t x --document` and
37
+ * `genesis build` use it for the first beacon of the genesis document.
38
+ */
39
+ export function printBeaconFundingHint(g: GlobalOptions, network: NetworkOption, beaconAddress: string): void {
40
+ if (g.quiet || g.output === 'json') return;
41
+ const faucet = faucetUrl(network);
42
+ if (!faucet) return;
32
43
  const explorer = explorerAddressUrl(network, beaconAddress);
33
44
  const lines = [
34
45
  'Fund the initial beacon to anchor updates:',
@@ -0,0 +1,50 @@
1
+ import type { ConnectionOverrides } from './config.js';
2
+ import { profileNetworkMismatch, resolveDefaultNetwork } from './config.js';
3
+ import { CLIError } from './error.js';
4
+ import type { GlobalOptions, NetworkOption } from './types.js';
5
+ import { SUPPORTED_NETWORKS } from './types.js';
6
+
7
+ /** The help text of `-n, --network` on the offline creation commands. */
8
+ export const NETWORK_OPTION_HELP =
9
+ 'Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest> '
10
+ + '(default: config defaults.network, else regtest)';
11
+
12
+ /** Builds the keystore- and config-resolution overrides from the global flags. */
13
+ export function overridesFromGlobals(g: GlobalOptions): ConnectionOverrides {
14
+ return {
15
+ home : g.home,
16
+ config : g.config,
17
+ profile : g.profile,
18
+ keystore : g.keystore,
19
+ passphraseFile : g.passphraseFile,
20
+ };
21
+ }
22
+
23
+ /** Validates an explicit `--network`, or resolves the default from configuration. */
24
+ export function resolveNetworkOption(explicit: string | undefined, overrides: ConnectionOverrides): NetworkOption {
25
+ if (!explicit) return resolveDefaultNetwork(overrides);
26
+ if (!SUPPORTED_NETWORKS.includes(explicit as NetworkOption)) {
27
+ throw new CLIError(
28
+ 'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
29
+ 'INVALID_ARGUMENT_ERROR',
30
+ { network: explicit },
31
+ );
32
+ }
33
+ return explicit as NetworkOption;
34
+ }
35
+
36
+ /**
37
+ * Warns on stderr, and never blocks, when the network of a new identifier
38
+ * disagrees with the network that the active profile declares. A `production`
39
+ * profile that holds mainnet endpoints must not mint a regtest DID in silence.
40
+ */
41
+ export function warnProfileNetworkMismatch(g: GlobalOptions, network: NetworkOption, overrides: ConnectionOverrides): void {
42
+ const mismatch = profileNetworkMismatch(network, overrides);
43
+ if (mismatch && !g.quiet) {
44
+ process.stderr.write(
45
+ `Warning: creating a "${network}" identifier while the active profile `
46
+ + `"${mismatch.profile}" declares network "${mismatch.declared}". The `
47
+ + 'identifier\'s network and the profile\'s endpoints may not match.\n'
48
+ );
49
+ }
50
+ }
@@ -2,14 +2,24 @@ import { DEFAULT_MIN_CONF } from '@did-btcr2/api';
2
2
  import type { ResolutionOptions } from '@did-btcr2/method';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { CLIError } from './error.js';
5
+ import { readGenesisDocumentFile } from './genesis-document-file.js';
5
6
 
6
7
  /** The flags that select the resolution options of a command. */
7
8
  export type ResolutionOptionFlags = {
8
9
  resolutionOptions? : string;
9
10
  resolutionOptionsPath? : string;
10
11
  minConf? : number;
12
+ genesisDocument? : string;
11
13
  };
12
14
 
15
+ /** Whether any resolution flag is set. */
16
+ export function hasResolutionFlags(flags: ResolutionOptionFlags): boolean {
17
+ return flags.resolutionOptions !== undefined
18
+ || flags.resolutionOptionsPath !== undefined
19
+ || flags.minConf !== undefined
20
+ || flags.genesisDocument !== undefined;
21
+ }
22
+
13
23
  /** The help text of `--min-conf`. `resolve`, `update`, and `deactivate` share it. */
14
24
  export const MIN_CONF_HELP =
15
25
  'Minimum block confirmations a beacon signal needs before resolution applies it '
@@ -17,8 +27,9 @@ export const MIN_CONF_HELP =
17
27
 
18
28
  /**
19
29
  * Builds the resolution options from the flags. The inline JSON wins over
20
- * the file. The `--min-conf` flag wins over a `minConf` inside the JSON.
21
- * Returns `undefined` if no flag is set.
30
+ * the file. The `--min-conf` flag wins over a `minConf` inside the JSON. The
31
+ * `--genesis-document` file wins over a `sidecar.genesisDocument` inside the
32
+ * JSON. Returns `undefined` if no flag is set.
22
33
  */
23
34
  export async function readResolutionOptions(flags: ResolutionOptionFlags): Promise<ResolutionOptions | undefined> {
24
35
  let resolutionOptions: ResolutionOptions | undefined;
@@ -48,6 +59,14 @@ export async function readResolutionOptions(flags: ResolutionOptionFlags): Promi
48
59
  if (flags.minConf !== undefined) {
49
60
  resolutionOptions = { ...(resolutionOptions ?? {}), minConf: flags.minConf };
50
61
  }
62
+ // The file wins over a sidecar.genesisDocument inside the JSON options.
63
+ if (flags.genesisDocument !== undefined) {
64
+ const genesisDocument = await readGenesisDocumentFile(flags.genesisDocument);
65
+ resolutionOptions = {
66
+ ...(resolutionOptions ?? {}),
67
+ sidecar : { ...(resolutionOptions?.sidecar ?? {}), genesisDocument },
68
+ };
69
+ }
51
70
  return resolutionOptions;
52
71
  }
53
72
 
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BroadcastOptions, DidUpdateResult, IdentifierReport, PublishToCasMode, Signer } from '@did-btcr2/api';
1
+ import type { BeaconInfo, BroadcastOptions, DidUpdateResult, IdentifierReport, PublishToCasMode, Signer } from '@did-btcr2/api';
2
2
  import type { Btcr2DidDocument, ResolutionOptions } from '@did-btcr2/method';
3
3
  import type { DidResolutionResult } from '@web5/dids';
4
4
  import type { DoctorReport, EffectiveConfig } from './config.js';
@@ -69,13 +69,28 @@ export interface IdentifierDecodeData {
69
69
  initialDocument? : Btcr2DidDocument;
70
70
  }
71
71
 
72
+ /**
73
+ * The data that `genesis build` prints: the external identifier, its
74
+ * network, its genesis bytes as hex, the path of the written genesis
75
+ * document, and the beacons of the initial DID document with their
76
+ * addresses to fund.
77
+ */
78
+ export interface GenesisBuildData {
79
+ did : string;
80
+ network : NetworkOption;
81
+ genesisBytes : string;
82
+ path : string;
83
+ beacons : BeaconInfo[];
84
+ }
85
+
72
86
  export type CommandResult =
73
- | { action: 'create'; data: string; keyId?: string; publicKey?: string }
87
+ | { action: 'create'; data: string; keyId?: string; publicKey?: string; genesisBytes?: string }
74
88
  | { action: 'resolve'; data: DidResolutionResult }
75
89
  | { action: 'update'; data: DidUpdateResult }
76
90
  | { action: 'deactivate'; data: DidUpdateResult }
77
91
  | { action: 'identifier-decode'; data: IdentifierDecodeData }
78
92
  | { action: 'identifier-validate'; data: IdentifierReport }
93
+ | { action: 'genesis-build'; data: GenesisBuildData }
79
94
  | { action: 'key-generate'; data: { keyId: string; publicKey: string; active: boolean } }
80
95
  | { action: 'key-list'; data: Array<{ keyId: string; fingerprint: string; name?: string; active: boolean }> }
81
96
  | { action: 'key-show'; data: { keyId: string; publicKey: string; tags?: Record<string, string> } }