@did-btcr2/cli 0.20.0 → 0.21.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/README.md +31 -26
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +126 -178
- package/dist/esm/src/commands/deactivate.js +11 -88
- package/dist/esm/src/commands/deactivate.js.map +1 -1
- package/dist/esm/src/commands/resolve.js +4 -37
- package/dist/esm/src/commands/resolve.js.map +1 -1
- package/dist/esm/src/commands/update.js +15 -85
- package/dist/esm/src/commands/update.js.map +1 -1
- package/dist/esm/src/commands/write.js +119 -0
- package/dist/esm/src/commands/write.js.map +1 -0
- package/dist/esm/src/resolution-options.js +47 -0
- package/dist/esm/src/resolution-options.js.map +1 -0
- package/dist/esm/src/types.js.map +1 -1
- package/dist/types/src/commands/deactivate.d.ts +1 -1
- package/dist/types/src/commands/deactivate.d.ts.map +1 -1
- package/dist/types/src/commands/resolve.d.ts.map +1 -1
- package/dist/types/src/commands/update.d.ts +1 -1
- package/dist/types/src/commands/update.d.ts.map +1 -1
- package/dist/types/src/commands/write.d.ts +47 -0
- package/dist/types/src/commands/write.d.ts.map +1 -0
- package/dist/types/src/resolution-options.d.ts +21 -0
- package/dist/types/src/resolution-options.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +17 -7
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/commands/deactivate.ts +14 -142
- package/src/commands/resolve.ts +8 -64
- package/src/commands/update.ts +19 -139
- package/src/commands/write.ts +198 -0
- package/src/resolution-options.ts +67 -0
- package/src/types.ts +17 -7
|
@@ -1,156 +1,28 @@
|
|
|
1
|
-
import type { PublishToCasMode } from '@did-btcr2/api';
|
|
2
|
-
import { KeyManagerSigner } from '@did-btcr2/key-manager';
|
|
3
1
|
import type { Command } from 'commander';
|
|
4
|
-
import {
|
|
5
|
-
import { CLIError } from '../error.js';
|
|
2
|
+
import type { ApiFactory } from '../config.js';
|
|
6
3
|
import { printWatchHint } from '../hints.js';
|
|
7
|
-
import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
|
|
8
4
|
import { formatResult } from '../output.js';
|
|
9
|
-
import type { GlobalOptions
|
|
10
|
-
|
|
11
|
-
/** The JSON Patch that marks a DID document as permanently deactivated. */
|
|
12
|
-
const DEACTIVATION_PATCH = [{ op: 'add' as const, path: '/deactivated', value: true }];
|
|
5
|
+
import type { GlobalOptions } from '../types.js';
|
|
6
|
+
import { prepareWrite, registerWriteOptions, type WriteFlags } from './write.js';
|
|
13
7
|
|
|
14
8
|
export function registerDeactivateCommand(
|
|
15
9
|
program : Command,
|
|
16
10
|
factory : ApiFactory,
|
|
17
11
|
globals : () => GlobalOptions,
|
|
18
12
|
): void {
|
|
19
|
-
program
|
|
13
|
+
const command = program
|
|
20
14
|
.command('deactivate')
|
|
21
15
|
.alias('delete')
|
|
22
16
|
.description('Deactivate the did:btcr2 identifier permanently. This is irreversible.')
|
|
23
|
-
.requiredOption(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'-m, --verification-method-id <id>',
|
|
34
|
-
'DID document verification method ID used to sign the deactivation'
|
|
35
|
-
)
|
|
36
|
-
.requiredOption(
|
|
37
|
-
'-b, --beacon-id <json>',
|
|
38
|
-
'Beacon ID as a JSON string',
|
|
39
|
-
parseJsonArg('--beacon-id'),
|
|
40
|
-
)
|
|
41
|
-
.option(
|
|
42
|
-
'--publish-to-cas <mode>',
|
|
43
|
-
'Publish update artifacts to a writable CAS before broadcast: auto|always|never. '
|
|
44
|
-
+ 'CAS publication is optional; the default distributes the returned artifacts via sidecar.',
|
|
45
|
-
parsePublishToCasMode,
|
|
46
|
-
'never',
|
|
47
|
-
)
|
|
48
|
-
.option(
|
|
49
|
-
'--fee-rate <satsPerVByte>',
|
|
50
|
-
'Fee rate in sats/vByte for the beacon transaction (default: 5). '
|
|
51
|
-
+ 'Raise it under congestion so the transaction confirms.',
|
|
52
|
-
)
|
|
53
|
-
.option(
|
|
54
|
-
'--change-address <address>',
|
|
55
|
-
'Send transaction change to this address instead of the beacon address, '
|
|
56
|
-
+ 'so a DID\'s announcements are not linked on-chain (ADR 044).',
|
|
57
|
-
)
|
|
58
|
-
.action(async (options: {
|
|
59
|
-
sourceDocument : unknown;
|
|
60
|
-
sourceVersionId : string;
|
|
61
|
-
verificationMethodId : string;
|
|
62
|
-
beaconId : unknown;
|
|
63
|
-
publishToCas : PublishToCasMode;
|
|
64
|
-
feeRate? : string;
|
|
65
|
-
changeAddress? : string;
|
|
66
|
-
}) => {
|
|
67
|
-
if (!/^\d+$/.test(options.sourceVersionId)) {
|
|
68
|
-
throw new CLIError(
|
|
69
|
-
'--source-version-id must be a non-negative integer.',
|
|
70
|
-
'INVALID_ARGUMENT_ERROR',
|
|
71
|
-
{ value: options.sourceVersionId },
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
const parsed: UpdateCommandOptions = {
|
|
75
|
-
sourceDocument : options.sourceDocument as UpdateCommandOptions['sourceDocument'],
|
|
76
|
-
patches : DEACTIVATION_PATCH,
|
|
77
|
-
sourceVersionId : Number(options.sourceVersionId),
|
|
78
|
-
verificationMethodId : options.verificationMethodId,
|
|
79
|
-
beaconId : options.beaconId as UpdateCommandOptions['beaconId'],
|
|
80
|
-
};
|
|
81
|
-
const did = parsed.sourceDocument?.id;
|
|
82
|
-
if (!did) {
|
|
83
|
-
throw new CLIError(
|
|
84
|
-
'Source document must contain an "id" field.',
|
|
85
|
-
'INVALID_ARGUMENT_ERROR',
|
|
86
|
-
options
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
// Deactivation is an update that applies the deactivation patch. The core
|
|
90
|
-
// method has no separate deactivate path, so this routes through update.
|
|
91
|
-
const network = deriveNetwork(did);
|
|
92
|
-
// Refuse to sign a mainnet deactivation with an unencrypted dev keystore (ADR 080).
|
|
93
|
-
assertKeystoreAllowedForNetwork(network, globals());
|
|
94
|
-
const api = factory(network, globals());
|
|
95
|
-
const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
|
|
96
|
-
const signer = new KeyManagerSigner(api.kms.kms, keyId);
|
|
97
|
-
// Resolve fee-rate/change-address through the flag -> env -> profile chain
|
|
98
|
-
// into beacon broadcast options. Undefined when neither is set, so the
|
|
99
|
-
// SDK defaults (5 sat/vB, change back to the beacon address) still apply.
|
|
100
|
-
const broadcastOptions = resolveBroadcastOptions(network, globals(), {
|
|
101
|
-
feeRate : options.feeRate,
|
|
102
|
-
changeAddress : options.changeAddress,
|
|
103
|
-
});
|
|
104
|
-
// CAS publication is optional and never required: every beacon update can
|
|
105
|
-
// be completed and shared via sidecar alone. It is opt-in and defaults to
|
|
106
|
-
// 'never'; pass --publish-to-cas auto|always to publish the signed update
|
|
107
|
-
// (and, for CAS beacons, the announcement) to a writable CAS configured
|
|
108
|
-
// via --cas-rpc-url. The returned artifacts (txid, announcement, proof)
|
|
109
|
-
// are always printed for sidecar distribution regardless.
|
|
110
|
-
const data = await api.btcr2.update({
|
|
111
|
-
sourceDocument : parsed.sourceDocument,
|
|
112
|
-
patches : parsed.patches,
|
|
113
|
-
sourceVersionId : parsed.sourceVersionId,
|
|
114
|
-
verificationMethodId : parsed.verificationMethodId,
|
|
115
|
-
beaconId : parsed.beaconId,
|
|
116
|
-
signer,
|
|
117
|
-
publishToCas : options.publishToCas,
|
|
118
|
-
...(broadcastOptions ? { broadcastOptions } : {}),
|
|
119
|
-
});
|
|
120
|
-
console.log(formatResult({ action: 'deactivate', data }, globals()));
|
|
121
|
-
printWatchHint(globals(), network, data.txid);
|
|
17
|
+
.requiredOption('-i, --identifier <identifier>', 'did:btcr2 identifier to deactivate');
|
|
18
|
+
registerWriteOptions(command)
|
|
19
|
+
.action(async (options: WriteFlags) => {
|
|
20
|
+
const g = globals();
|
|
21
|
+
const { network, api, params } = await prepareWrite(options, factory, g);
|
|
22
|
+
// The api supplies the deactivation patch (ADR 094) and refuses a
|
|
23
|
+
// document that is deactivated already (ADR 100).
|
|
24
|
+
const data = await api.deactivateDid(params);
|
|
25
|
+
console.log(formatResult({ action: 'deactivate', data }, g));
|
|
26
|
+
printWatchHint(g, network, data.txid);
|
|
122
27
|
});
|
|
123
28
|
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Commander argParser for `--publish-to-cas`. Validates the value is one of the
|
|
127
|
-
* three {@link PublishToCasMode} policies, erroring at parse time otherwise.
|
|
128
|
-
*/
|
|
129
|
-
function parsePublishToCasMode(value: string): PublishToCasMode {
|
|
130
|
-
if (value !== 'auto' && value !== 'always' && value !== 'never') {
|
|
131
|
-
throw new CLIError(
|
|
132
|
-
'--publish-to-cas must be one of "auto", "always", or "never".',
|
|
133
|
-
'INVALID_ARGUMENT_ERROR',
|
|
134
|
-
{ value },
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Returns a commander argParser that validates JSON.
|
|
142
|
-
* Errors at parse time with a clear flag reference.
|
|
143
|
-
*/
|
|
144
|
-
function parseJsonArg(flagName: string): (value: string) => unknown {
|
|
145
|
-
return (value: string): unknown => {
|
|
146
|
-
try {
|
|
147
|
-
return JSON.parse(value);
|
|
148
|
-
} catch {
|
|
149
|
-
throw new CLIError(
|
|
150
|
-
`Invalid JSON for ${flagName}. Must be a valid JSON string.`,
|
|
151
|
-
'INVALID_ARGUMENT_ERROR',
|
|
152
|
-
{ flagName, value }
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
}
|
package/src/commands/resolve.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Identifier } from '@did-btcr2/api';
|
|
2
2
|
import type { Command } from 'commander';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import { deriveNetwork, type ApiFactory } from '../config.js';
|
|
5
|
-
import { CLIError } from '../error.js';
|
|
6
4
|
import { formatResult } from '../output.js';
|
|
5
|
+
import { MIN_CONF_HELP, parseMinConf, readResolutionOptions, type ResolutionOptionFlags } from '../resolution-options.js';
|
|
7
6
|
import type { GlobalOptions, ResolveCommandOptions } from '../types.js';
|
|
8
7
|
|
|
9
8
|
export function registerResolveCommand(
|
|
@@ -18,18 +17,8 @@ export function registerResolveCommand(
|
|
|
18
17
|
.requiredOption('-i, --identifier <identifier>', 'did:btcr2 identifier')
|
|
19
18
|
.option('-r, --resolution-options <json>', 'JSON string containing resolution options')
|
|
20
19
|
.option('-p, --resolution-options-path <path>', 'Path to a JSON file containing resolution options')
|
|
21
|
-
.option(
|
|
22
|
-
|
|
23
|
-
'Minimum block confirmations a beacon signal needs before resolution applies it '
|
|
24
|
-
+ `(positive integer, default: ${DEFAULT_MIN_CONF}). Overrides minConf inside -r/-p`,
|
|
25
|
-
parseMinConf,
|
|
26
|
-
)
|
|
27
|
-
.action(async (options: {
|
|
28
|
-
identifier: string;
|
|
29
|
-
resolutionOptions?: string;
|
|
30
|
-
resolutionOptionsPath?: string;
|
|
31
|
-
minConf?: number;
|
|
32
|
-
}) => {
|
|
20
|
+
.option('--min-conf <n>', MIN_CONF_HELP, parseMinConf)
|
|
21
|
+
.action(async (options: { identifier: string } & ResolutionOptionFlags) => {
|
|
33
22
|
const parsed = await validateResolveOptions(options);
|
|
34
23
|
const network = deriveNetwork(parsed.identifier);
|
|
35
24
|
const api = factory(network, globals());
|
|
@@ -39,56 +28,11 @@ export function registerResolveCommand(
|
|
|
39
28
|
});
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
async function validateResolveOptions(
|
|
43
|
-
identifier: string
|
|
44
|
-
|
|
45
|
-
resolutionOptionsPath?: string;
|
|
46
|
-
minConf?: number;
|
|
47
|
-
}): Promise<ResolveCommandOptions> {
|
|
31
|
+
async function validateResolveOptions(
|
|
32
|
+
options: { identifier: string } & ResolutionOptionFlags,
|
|
33
|
+
): Promise<ResolveCommandOptions> {
|
|
48
34
|
// Validate identifier format early
|
|
49
35
|
Identifier.decode(options.identifier);
|
|
50
|
-
|
|
51
|
-
let resolutionOptions = undefined;
|
|
52
|
-
if (options.resolutionOptions) {
|
|
53
|
-
try {
|
|
54
|
-
resolutionOptions = JSON.parse(options.resolutionOptions);
|
|
55
|
-
} catch {
|
|
56
|
-
throw new CLIError(
|
|
57
|
-
'Invalid resolution options. Must be a valid JSON string.',
|
|
58
|
-
'INVALID_ARGUMENT_ERROR',
|
|
59
|
-
options
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
} else if (options.resolutionOptionsPath) {
|
|
63
|
-
try {
|
|
64
|
-
const content = await readFile(options.resolutionOptionsPath, 'utf-8');
|
|
65
|
-
resolutionOptions = JSON.parse(content);
|
|
66
|
-
} catch {
|
|
67
|
-
throw new CLIError(
|
|
68
|
-
'Invalid resolution options path. Must be a valid path to a JSON file.',
|
|
69
|
-
'INVALID_ARGUMENT_ERROR',
|
|
70
|
-
options
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// The flag wins over a minConf inside the JSON options.
|
|
75
|
-
if (options.minConf !== undefined) {
|
|
76
|
-
resolutionOptions = { ...(resolutionOptions ?? {}), minConf: options.minConf };
|
|
77
|
-
}
|
|
36
|
+
const resolutionOptions = await readResolutionOptions(options);
|
|
78
37
|
return { identifier: options.identifier, options: resolutionOptions };
|
|
79
38
|
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Commander argParser for `--min-conf`. Accepts a positive integer (minimum 1),
|
|
83
|
-
* the domain the specification gives `minConf`. Errors at parse time otherwise.
|
|
84
|
-
*/
|
|
85
|
-
function parseMinConf(value: string): number {
|
|
86
|
-
if (!/^[1-9]\d*$/.test(value)) {
|
|
87
|
-
throw new CLIError(
|
|
88
|
-
'--min-conf must be a positive integer (minimum 1).',
|
|
89
|
-
'INVALID_ARGUMENT_ERROR',
|
|
90
|
-
{ value },
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
return Number(value);
|
|
94
|
-
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,156 +1,36 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { KeyManagerSigner } from '@did-btcr2/key-manager';
|
|
1
|
+
import type { PatchOperation } from '@did-btcr2/common';
|
|
3
2
|
import type { Command } from 'commander';
|
|
4
|
-
import {
|
|
5
|
-
import { CLIError } from '../error.js';
|
|
3
|
+
import type { ApiFactory } from '../config.js';
|
|
6
4
|
import { printWatchHint } from '../hints.js';
|
|
7
|
-
import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
|
|
8
5
|
import { formatResult } from '../output.js';
|
|
9
|
-
import type { GlobalOptions
|
|
6
|
+
import type { GlobalOptions } from '../types.js';
|
|
7
|
+
import { parseJsonArg, prepareWrite, registerWriteOptions, type WriteFlags } from './write.js';
|
|
10
8
|
|
|
11
9
|
export function registerUpdateCommand(
|
|
12
10
|
program : Command,
|
|
13
11
|
factory : ApiFactory,
|
|
14
12
|
globals : () => GlobalOptions,
|
|
15
13
|
): void {
|
|
16
|
-
program
|
|
14
|
+
const command = program
|
|
17
15
|
.command('update')
|
|
18
16
|
.description('Update a did:btcr2 document.')
|
|
19
|
-
.requiredOption(
|
|
20
|
-
'-s, --source-document <json>',
|
|
21
|
-
'Source DID document as JSON string',
|
|
22
|
-
parseJsonArg('--source-document'),
|
|
23
|
-
)
|
|
24
|
-
.requiredOption(
|
|
25
|
-
'--source-version-id <number>',
|
|
26
|
-
'Source version ID as a number'
|
|
27
|
-
)
|
|
17
|
+
.requiredOption('-i, --identifier <identifier>', 'did:btcr2 identifier to update')
|
|
28
18
|
.requiredOption(
|
|
29
19
|
'-p, --patches <json>',
|
|
30
20
|
'JSON Patch operations as a JSON string array',
|
|
31
21
|
parseJsonArg('--patches'),
|
|
32
|
-
)
|
|
33
|
-
.requiredOption(
|
|
34
|
-
'-m, --verification-method-id <id>',
|
|
35
|
-
'DID document verification method ID'
|
|
36
|
-
)
|
|
37
|
-
.requiredOption(
|
|
38
|
-
'-b, --beacon-id <json>',
|
|
39
|
-
'Beacon ID as a JSON string',
|
|
40
|
-
parseJsonArg('--beacon-id'),
|
|
41
|
-
)
|
|
42
|
-
.option(
|
|
43
|
-
'--publish-to-cas <mode>',
|
|
44
|
-
'Publish update artifacts to a writable CAS before broadcast: auto|always|never. '
|
|
45
|
-
+ 'CAS publication is optional; the default distributes the returned artifacts via sidecar.',
|
|
46
|
-
parsePublishToCasMode,
|
|
47
|
-
'never',
|
|
48
|
-
)
|
|
49
|
-
.option(
|
|
50
|
-
'--fee-rate <satsPerVByte>',
|
|
51
|
-
'Fee rate in sats/vByte for the beacon transaction (default: 5). '
|
|
52
|
-
+ 'Raise it under congestion so the transaction confirms.',
|
|
53
|
-
)
|
|
54
|
-
.option(
|
|
55
|
-
'--change-address <address>',
|
|
56
|
-
'Send transaction change to this address instead of the beacon address, '
|
|
57
|
-
+ 'so a DID\'s announcements are not linked on-chain (ADR 044).',
|
|
58
|
-
)
|
|
59
|
-
.action(async (options: {
|
|
60
|
-
sourceDocument : unknown;
|
|
61
|
-
sourceVersionId : string;
|
|
62
|
-
patches : unknown;
|
|
63
|
-
verificationMethodId : string;
|
|
64
|
-
beaconId : unknown;
|
|
65
|
-
publishToCas : PublishToCasMode;
|
|
66
|
-
feeRate? : string;
|
|
67
|
-
changeAddress? : string;
|
|
68
|
-
}) => {
|
|
69
|
-
if (!/^\d+$/.test(options.sourceVersionId)) {
|
|
70
|
-
throw new CLIError(
|
|
71
|
-
'--source-version-id must be a non-negative integer.',
|
|
72
|
-
'INVALID_ARGUMENT_ERROR',
|
|
73
|
-
{ value: options.sourceVersionId },
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
const parsed: UpdateCommandOptions = {
|
|
77
|
-
sourceDocument : options.sourceDocument as UpdateCommandOptions['sourceDocument'],
|
|
78
|
-
patches : options.patches as UpdateCommandOptions['patches'],
|
|
79
|
-
sourceVersionId : Number(options.sourceVersionId),
|
|
80
|
-
verificationMethodId : options.verificationMethodId,
|
|
81
|
-
beaconId : options.beaconId as UpdateCommandOptions['beaconId'],
|
|
82
|
-
};
|
|
83
|
-
const did = parsed.sourceDocument?.id;
|
|
84
|
-
if (!did) {
|
|
85
|
-
throw new CLIError(
|
|
86
|
-
'Source document must contain an "id" field.',
|
|
87
|
-
'INVALID_ARGUMENT_ERROR',
|
|
88
|
-
options
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
const network = deriveNetwork(did);
|
|
92
|
-
// Refuse to sign a mainnet update with an unencrypted dev keystore (ADR 080).
|
|
93
|
-
assertKeystoreAllowedForNetwork(network, globals());
|
|
94
|
-
const api = factory(network, globals());
|
|
95
|
-
const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
|
|
96
|
-
const signer = new KeyManagerSigner(api.kms.kms, keyId);
|
|
97
|
-
// Resolve fee-rate/change-address through the flag -> env -> profile chain
|
|
98
|
-
// into beacon broadcast options. Undefined when neither is set, so the
|
|
99
|
-
// SDK defaults (5 sat/vB, change back to the beacon address) still apply.
|
|
100
|
-
const broadcastOptions = resolveBroadcastOptions(network, globals(), {
|
|
101
|
-
feeRate : options.feeRate,
|
|
102
|
-
changeAddress : options.changeAddress,
|
|
103
|
-
});
|
|
104
|
-
// CAS publication is optional and never required: every beacon update can
|
|
105
|
-
// be completed and shared via sidecar alone. It is opt-in and defaults to
|
|
106
|
-
// 'never'; pass --publish-to-cas auto|always to publish the signed update
|
|
107
|
-
// (and, for CAS beacons, the announcement) to a writable CAS configured
|
|
108
|
-
// via --cas-rpc-url. The returned artifacts (txid, announcement, proof)
|
|
109
|
-
// are always printed for sidecar distribution regardless.
|
|
110
|
-
const data = await api.btcr2.update({
|
|
111
|
-
sourceDocument : parsed.sourceDocument,
|
|
112
|
-
patches : parsed.patches,
|
|
113
|
-
sourceVersionId : parsed.sourceVersionId,
|
|
114
|
-
verificationMethodId : parsed.verificationMethodId,
|
|
115
|
-
beaconId : parsed.beaconId,
|
|
116
|
-
signer,
|
|
117
|
-
publishToCas : options.publishToCas,
|
|
118
|
-
...(broadcastOptions ? { broadcastOptions } : {}),
|
|
119
|
-
});
|
|
120
|
-
console.log(formatResult({ action: 'update', data }, globals()));
|
|
121
|
-
printWatchHint(globals(), network, data.txid);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Commander argParser for `--publish-to-cas`. Validates the value is one of the
|
|
127
|
-
* three {@link PublishToCasMode} policies, erroring at parse time otherwise.
|
|
128
|
-
*/
|
|
129
|
-
function parsePublishToCasMode(value: string): PublishToCasMode {
|
|
130
|
-
if (value !== 'auto' && value !== 'always' && value !== 'never') {
|
|
131
|
-
throw new CLIError(
|
|
132
|
-
'--publish-to-cas must be one of "auto", "always", or "never".',
|
|
133
|
-
'INVALID_ARGUMENT_ERROR',
|
|
134
|
-
{ value },
|
|
135
22
|
);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
throw new CLIError(
|
|
150
|
-
`Invalid JSON for ${flagName}. Must be a valid JSON string.`,
|
|
151
|
-
'INVALID_ARGUMENT_ERROR',
|
|
152
|
-
{ flagName, value }
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
23
|
+
registerWriteOptions(command)
|
|
24
|
+
.action(async (options: WriteFlags & { patches: unknown }) => {
|
|
25
|
+
const g = globals();
|
|
26
|
+
const { network, api, params } = await prepareWrite(options, factory, g);
|
|
27
|
+
// The api resolves the source document if the source pair is absent,
|
|
28
|
+
// derives an omitted verification method and beacon, and publishes to
|
|
29
|
+
// the CAS only under --publish-to-cas auto|always. The returned
|
|
30
|
+
// artifacts (txid, signed update, announcement, proof) are printed for
|
|
31
|
+
// sidecar distribution in every case.
|
|
32
|
+
const data = await api.updateDid({ ...params, patches: options.patches as PatchOperation[] });
|
|
33
|
+
console.log(formatResult({ action: 'update', data }, g));
|
|
34
|
+
printWatchHint(g, network, data.txid);
|
|
35
|
+
});
|
|
156
36
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Btcr2DidDocument, DidBtcr2Api, PublishToCasMode } from '@did-btcr2/api';
|
|
2
|
+
import { KeyManagerSigner } from '@did-btcr2/key-manager';
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
import {
|
|
5
|
+
assertKeystoreAllowedForNetwork,
|
|
6
|
+
deriveNetwork,
|
|
7
|
+
resolveBroadcastOptions,
|
|
8
|
+
resolveSigningKeyRef,
|
|
9
|
+
type ApiFactory,
|
|
10
|
+
} from '../config.js';
|
|
11
|
+
import { CLIError } from '../error.js';
|
|
12
|
+
import { resolveKeyRef } from '../keystore/resolve-key-ref.js';
|
|
13
|
+
import { MIN_CONF_HELP, parseMinConf, readResolutionOptions, type ResolutionOptionFlags } from '../resolution-options.js';
|
|
14
|
+
import type { GlobalOptions, NetworkOption, UpdateCommandOptions } from '../types.js';
|
|
15
|
+
|
|
16
|
+
/** The parsed flags that `update` and `deactivate` share. */
|
|
17
|
+
export type WriteFlags = ResolutionOptionFlags & {
|
|
18
|
+
identifier : string;
|
|
19
|
+
sourceDocument? : unknown;
|
|
20
|
+
sourceVersionId? : number;
|
|
21
|
+
verificationMethodId? : string;
|
|
22
|
+
beaconId? : string;
|
|
23
|
+
publishToCas : PublishToCasMode;
|
|
24
|
+
feeRate? : string;
|
|
25
|
+
changeAddress? : string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Registers the flags that `update` and `deactivate` share. Each command
|
|
30
|
+
* registers `-i/--identifier` (and `update` its `-p/--patches`) before it
|
|
31
|
+
* calls this function, so those flags lead the help output.
|
|
32
|
+
*/
|
|
33
|
+
export function registerWriteOptions(command: Command): Command {
|
|
34
|
+
return command
|
|
35
|
+
.option(
|
|
36
|
+
'-s, --source-document <json>',
|
|
37
|
+
'Source DID document as a JSON string. Requires --source-version-id. '
|
|
38
|
+
+ 'Omit both to resolve the current document first',
|
|
39
|
+
parseJsonArg('--source-document'),
|
|
40
|
+
)
|
|
41
|
+
.option(
|
|
42
|
+
'--source-version-id <number>',
|
|
43
|
+
'Version ID of the source document, a non-negative integer. Requires --source-document',
|
|
44
|
+
parseSourceVersionId,
|
|
45
|
+
)
|
|
46
|
+
.option(
|
|
47
|
+
'-m, --verification-method-id <id>',
|
|
48
|
+
'Verification method that signs the update '
|
|
49
|
+
+ '(default: the one method of the document that publishes the signing key)',
|
|
50
|
+
)
|
|
51
|
+
.option(
|
|
52
|
+
'-b, --beacon-id <id>',
|
|
53
|
+
'Beacon service that announces the update, as a DID URL '
|
|
54
|
+
+ '(default: the only beacon of the document, else the one beacon with a spendable UTXO)',
|
|
55
|
+
)
|
|
56
|
+
.option(
|
|
57
|
+
'-r, --resolution-options <json>',
|
|
58
|
+
'Resolution options as a JSON string, for the resolution of the source document',
|
|
59
|
+
)
|
|
60
|
+
.option(
|
|
61
|
+
'--resolution-options-path <path>',
|
|
62
|
+
'Path to a JSON file with resolution options, for the resolution of the source document',
|
|
63
|
+
)
|
|
64
|
+
.option('--min-conf <n>', MIN_CONF_HELP, parseMinConf)
|
|
65
|
+
.option(
|
|
66
|
+
'--publish-to-cas <mode>',
|
|
67
|
+
'Publish update artifacts to a writable CAS before broadcast: auto|always|never. '
|
|
68
|
+
+ 'CAS publication is optional; the default distributes the returned artifacts via sidecar.',
|
|
69
|
+
parsePublishToCasMode,
|
|
70
|
+
'never',
|
|
71
|
+
)
|
|
72
|
+
.option(
|
|
73
|
+
'--fee-rate <satsPerVByte>',
|
|
74
|
+
'Fee rate in sats/vByte for the beacon transaction (default: 5). '
|
|
75
|
+
+ 'Raise it under congestion so the transaction confirms.',
|
|
76
|
+
)
|
|
77
|
+
.option(
|
|
78
|
+
'--change-address <address>',
|
|
79
|
+
'Send transaction change to this address instead of the beacon address, '
|
|
80
|
+
+ 'so a DID\'s announcements are not linked on-chain (ADR 044).',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates the shared write flags, derives the network from the identifier,
|
|
86
|
+
* applies the mainnet keystore guard, builds the signer, and returns the
|
|
87
|
+
* parameters for `updateDid` and `deactivateDid`.
|
|
88
|
+
*
|
|
89
|
+
* The checks run in this order, before any key material is read:
|
|
90
|
+
* 1. The identifier decodes and names a supported network.
|
|
91
|
+
* 2. `--source-document` and `--source-version-id` come together or not at
|
|
92
|
+
* all (ADR 101). The api refuses a half pair too; the cli names the flags.
|
|
93
|
+
* 3. The resolution flags come only without the source pair. The api ignores
|
|
94
|
+
* `resolutionOptions` when the pair is supplied (ADR 098). A silent ignore
|
|
95
|
+
* of `--min-conf` would mislead.
|
|
96
|
+
* 4. A mainnet write is refused with an unencrypted dev keystore (ADR 080).
|
|
97
|
+
*/
|
|
98
|
+
export async function prepareWrite(
|
|
99
|
+
options : WriteFlags,
|
|
100
|
+
factory : ApiFactory,
|
|
101
|
+
g : GlobalOptions,
|
|
102
|
+
): Promise<{ network: NetworkOption; api: DidBtcr2Api; params: UpdateCommandOptions }> {
|
|
103
|
+
const did = options.identifier;
|
|
104
|
+
const network = deriveNetwork(did);
|
|
105
|
+
const hasDocument = options.sourceDocument !== undefined;
|
|
106
|
+
const hasVersion = options.sourceVersionId !== undefined;
|
|
107
|
+
if (hasDocument !== hasVersion) {
|
|
108
|
+
throw new CLIError(
|
|
109
|
+
'Provide both --source-document and --source-version-id, or neither. '
|
|
110
|
+
+ 'Omit both to resolve the current document first.',
|
|
111
|
+
'INVALID_ARGUMENT_ERROR',
|
|
112
|
+
{ did },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const hasResolutionFlags = options.resolutionOptions !== undefined
|
|
116
|
+
|| options.resolutionOptionsPath !== undefined
|
|
117
|
+
|| options.minConf !== undefined;
|
|
118
|
+
if (hasDocument && hasResolutionFlags) {
|
|
119
|
+
throw new CLIError(
|
|
120
|
+
'--resolution-options, --resolution-options-path, and --min-conf apply only when '
|
|
121
|
+
+ '--source-document and --source-version-id are omitted. A supplied source pair skips resolution.',
|
|
122
|
+
'INVALID_ARGUMENT_ERROR',
|
|
123
|
+
{ did },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
assertKeystoreAllowedForNetwork(network, g);
|
|
127
|
+
const resolutionOptions = await readResolutionOptions(options);
|
|
128
|
+
const api = factory(network, g);
|
|
129
|
+
const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(g));
|
|
130
|
+
const signer = new KeyManagerSigner(api.kms.kms, keyId);
|
|
131
|
+
// Resolve fee-rate/change-address through the flag, env, and profile layers
|
|
132
|
+
// into beacon broadcast options. Undefined when no layer sets one, so the
|
|
133
|
+
// SDK defaults (5 sat/vB, change back to the beacon address) still apply.
|
|
134
|
+
const broadcastOptions = resolveBroadcastOptions(network, g, {
|
|
135
|
+
feeRate : options.feeRate,
|
|
136
|
+
changeAddress : options.changeAddress,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
network,
|
|
140
|
+
api,
|
|
141
|
+
params : {
|
|
142
|
+
did,
|
|
143
|
+
signer,
|
|
144
|
+
sourceDocument : options.sourceDocument as Btcr2DidDocument | undefined,
|
|
145
|
+
sourceVersionId : options.sourceVersionId,
|
|
146
|
+
verificationMethodId : options.verificationMethodId,
|
|
147
|
+
beaconId : options.beaconId,
|
|
148
|
+
resolutionOptions,
|
|
149
|
+
publishToCas : options.publishToCas,
|
|
150
|
+
broadcastOptions,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Commander argParser for `--source-version-id`: digits only, a non-negative integer. */
|
|
156
|
+
function parseSourceVersionId(value: string): number {
|
|
157
|
+
if (!/^\d+$/.test(value)) {
|
|
158
|
+
throw new CLIError(
|
|
159
|
+
'--source-version-id must be a non-negative integer.',
|
|
160
|
+
'INVALID_ARGUMENT_ERROR',
|
|
161
|
+
{ value },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return Number(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Commander argParser for `--publish-to-cas`. Validates the value is one of the
|
|
169
|
+
* three {@link PublishToCasMode} policies, erroring at parse time otherwise.
|
|
170
|
+
*/
|
|
171
|
+
function parsePublishToCasMode(value: string): PublishToCasMode {
|
|
172
|
+
if (value !== 'auto' && value !== 'always' && value !== 'never') {
|
|
173
|
+
throw new CLIError(
|
|
174
|
+
'--publish-to-cas must be one of "auto", "always", or "never".',
|
|
175
|
+
'INVALID_ARGUMENT_ERROR',
|
|
176
|
+
{ value },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns a commander argParser that validates JSON.
|
|
184
|
+
* Errors at parse time with a clear flag reference.
|
|
185
|
+
*/
|
|
186
|
+
export function parseJsonArg(flagName: string): (value: string) => unknown {
|
|
187
|
+
return (value: string): unknown => {
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(value);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new CLIError(
|
|
192
|
+
`Invalid JSON for ${flagName}. Must be a valid JSON string.`,
|
|
193
|
+
'INVALID_ARGUMENT_ERROR',
|
|
194
|
+
{ flagName, value }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|