import type { PublishToCasMode } from '@did-btcr2/api'; import { KeyManagerSigner } from '@did-btcr2/key-manager'; import type { Command } from 'commander'; import { assertKeystoreAllowedForNetwork, deriveNetwork, resolveBroadcastOptions, resolveSigningKeyRef, type ApiFactory } from '../config.js'; import { CLIError } from '../error.js'; import { printWatchHint } from '../hints.js'; import { resolveKeyRef } from '../keystore/resolve-key-ref.js'; import { formatResult } from '../output.js'; import type { GlobalOptions, UpdateCommandOptions } from '../types.js'; export function registerUpdateCommand( program : Command, factory : ApiFactory, globals : () => GlobalOptions, ): void { program .command('update') .description('Update a did:btcr2 document.') .requiredOption( '-s, --source-document ', 'Source DID document as JSON string', parseJsonArg('--source-document'), ) .requiredOption( '--source-version-id ', 'Source version ID as a number' ) .requiredOption( '-p, --patches ', 'JSON Patch operations as a JSON string array', parseJsonArg('--patches'), ) .requiredOption( '-m, --verification-method-id ', 'DID document verification method ID' ) .requiredOption( '-b, --beacon-id ', 'Beacon ID as a JSON string', parseJsonArg('--beacon-id'), ) .option( '--publish-to-cas ', 'Publish update artifacts to a writable CAS before broadcast: auto|always|never. ' + 'CAS publication is optional; the default distributes the returned artifacts via sidecar.', parsePublishToCasMode, 'never', ) .option( '--fee-rate ', 'Fee rate in sats/vByte for the beacon transaction (default: 5). ' + 'Raise it under congestion so the transaction confirms.', ) .option( '--change-address
', 'Send transaction change to this address instead of the beacon address, ' + 'so a DID\'s announcements are not linked on-chain (ADR 044).', ) .action(async (options: { sourceDocument : unknown; sourceVersionId : string; patches : unknown; verificationMethodId : string; beaconId : unknown; publishToCas : PublishToCasMode; feeRate? : string; changeAddress? : string; }) => { if (!/^\d+$/.test(options.sourceVersionId)) { throw new CLIError( '--source-version-id must be a non-negative integer.', 'INVALID_ARGUMENT_ERROR', { value: options.sourceVersionId }, ); } const parsed: UpdateCommandOptions = { sourceDocument : options.sourceDocument as UpdateCommandOptions['sourceDocument'], patches : options.patches as UpdateCommandOptions['patches'], sourceVersionId : Number(options.sourceVersionId), verificationMethodId : options.verificationMethodId, beaconId : options.beaconId as UpdateCommandOptions['beaconId'], }; const did = parsed.sourceDocument?.id; if (!did) { throw new CLIError( 'Source document must contain an "id" field.', 'INVALID_ARGUMENT_ERROR', options ); } const network = deriveNetwork(did); // Refuse to sign a mainnet update with an unencrypted dev keystore (ADR 080). assertKeystoreAllowedForNetwork(network, globals()); const api = factory(network, globals()); const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals())); const signer = new KeyManagerSigner(api.kms.kms, keyId); // Resolve fee-rate/change-address through the flag -> env -> profile chain // into beacon broadcast options. Undefined when neither is set, so the // SDK defaults (5 sat/vB, change back to the beacon address) still apply. const broadcastOptions = resolveBroadcastOptions(network, globals(), { feeRate : options.feeRate, changeAddress : options.changeAddress, }); // CAS publication is optional and never required: every beacon update can // be completed and shared via sidecar alone. It is opt-in and defaults to // 'never'; pass --publish-to-cas auto|always to publish the signed update // (and, for CAS beacons, the announcement) to a writable CAS configured // via --cas-rpc-url. The returned artifacts (txid, announcement, proof) // are always printed for sidecar distribution regardless. const data = await api.btcr2.update({ sourceDocument : parsed.sourceDocument, patches : parsed.patches, sourceVersionId : parsed.sourceVersionId, verificationMethodId : parsed.verificationMethodId, beaconId : parsed.beaconId, signer, publishToCas : options.publishToCas, ...(broadcastOptions ? { broadcastOptions } : {}), }); console.log(formatResult({ action: 'update', data }, globals())); printWatchHint(globals(), network, data.txid); }); } /** * Commander argParser for `--publish-to-cas`. Validates the value is one of the * three {@link PublishToCasMode} policies, erroring at parse time otherwise. */ function parsePublishToCasMode(value: string): PublishToCasMode { if (value !== 'auto' && value !== 'always' && value !== 'never') { throw new CLIError( '--publish-to-cas must be one of "auto", "always", or "never".', 'INVALID_ARGUMENT_ERROR', { value }, ); } return value; } /** * Returns a commander argParser that validates JSON. * Errors at parse time with a clear flag reference. */ function parseJsonArg(flagName: string): (value: string) => unknown { return (value: string): unknown => { try { return JSON.parse(value); } catch { throw new CLIError( `Invalid JSON for ${flagName}. Must be a valid JSON string.`, 'INVALID_ARGUMENT_ERROR', { flagName, value } ); } }; }