@@ -4,6 +4,7 @@ import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
44import { join } from 'node:path'
55import { tmpdir } from 'node:os'
66import { resolveDIDFromLog } from '@interop/did-method-webvh'
7+ import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
78import { makeDidCommand , parseDidLog } from './did.js'
89import { makeKeyCommand } from './key.js'
910
@@ -984,6 +985,183 @@ describe('di did', () => {
984985 }
985986 } )
986987
988+ it ( '--update-key authorizes every supplied key' , async ( ) => {
989+ const { didsDir, did, updateKeysPath } = await createSavedWebvh ( [
990+ '--no-prerotation'
991+ ] )
992+ try {
993+ const before = await readJson < {
994+ active : { publicKeyMultibase : string }
995+ } > ( updateKeysPath )
996+ const activePub = before . active . publicKeyMultibase
997+ const external = await Ed25519VerificationKey . generate ( )
998+ const { publicKeyMultibase : externalPub } = ( await external . export ( {
999+ publicKey : true
1000+ } ) ) as { publicKeyMultibase : string }
1001+
1002+ await makeDidCommand ( ) . parseAsync (
1003+ [
1004+ 'webvh' ,
1005+ 'rotate-keys' ,
1006+ did ,
1007+ '--update-key' ,
1008+ activePub ,
1009+ externalPub ,
1010+ '--keep-old-key' ,
1011+ '--yes'
1012+ ] ,
1013+ { from : 'user' }
1014+ )
1015+
1016+ const meta = await resolveStoredWebvhMeta ( didsDir , did )
1017+ // Both supplied keys are authorized -- no silent drop of the extras.
1018+ assert . deepEqual ( meta . updateKeys , [ activePub , externalPub ] )
1019+ } finally {
1020+ await rm ( didsDir , { recursive : true } )
1021+ }
1022+ } )
1023+
1024+ it ( 'stage-only --enable-prerotation leaves a multi-key authorized set unchanged' , async ( ) => {
1025+ const { didsDir, did, updateKeysPath } = await createSavedWebvh ( [
1026+ '--no-prerotation'
1027+ ] )
1028+ try {
1029+ const before = await readJson < {
1030+ active : { publicKeyMultibase : string }
1031+ } > ( updateKeysPath )
1032+ const activePub = before . active . publicKeyMultibase
1033+ const external = await Ed25519VerificationKey . generate ( )
1034+ const { publicKeyMultibase : externalPub } = ( await external . export ( {
1035+ publicKey : true
1036+ } ) ) as { publicKeyMultibase : string }
1037+ // Authorize a second (externally-held) key alongside the local one,
1038+ // keeping the local secret in `retired` so it can still sign.
1039+ await makeDidCommand ( ) . parseAsync (
1040+ [
1041+ 'webvh' ,
1042+ 'rotate-keys' ,
1043+ did ,
1044+ '--update-key' ,
1045+ activePub ,
1046+ externalPub ,
1047+ '--keep-old-key' ,
1048+ '--yes'
1049+ ] ,
1050+ { from : 'user' }
1051+ )
1052+ logs . length = 0
1053+
1054+ await makeDidCommand ( ) . parseAsync (
1055+ [ 'webvh' , 'rotate-keys' , did , '--enable-prerotation' , '--yes' ] ,
1056+ { from : 'user' }
1057+ )
1058+
1059+ const meta = await resolveStoredWebvhMeta ( didsDir , did )
1060+ // Stage only: pre-rotation is armed without revoking the co-authorized
1061+ // key.
1062+ assert . equal ( meta . prerotation , true )
1063+ assert . deepEqual ( meta . updateKeys , [ activePub , externalPub ] )
1064+ } finally {
1065+ await rm ( didsDir , { recursive : true } )
1066+ }
1067+ } )
1068+
1069+ it ( 'refuses without --yes when stdin is not interactive' , async ( ) => {
1070+ const { didsDir, did } = await createSavedWebvh ( )
1071+ try {
1072+ await makeDidCommand ( ) . parseAsync ( [ 'webvh' , 'rotate-keys' , did ] , {
1073+ from : 'user'
1074+ } )
1075+ assert . equal ( exitCode , 1 )
1076+ assert . ok ( errors . some ( line => line . includes ( 'pass --yes' ) ) )
1077+ // Nothing was appended.
1078+ const meta = await resolveStoredWebvhMeta ( didsDir , did )
1079+ assert . equal ( meta . versionId . split ( '-' ) [ 0 ] , '1' )
1080+ } finally {
1081+ await rm ( didsDir , { recursive : true } )
1082+ }
1083+ } )
1084+
1085+ it ( 'reports a corrupt update-keys sidecar instead of crashing' , async ( ) => {
1086+ const { didsDir, did, updateKeysPath } = await createSavedWebvh ( )
1087+ try {
1088+ // Simulate a truncated (interrupted) sidecar write.
1089+ await writeFile ( updateKeysPath , '{ "active": { "publicKeyM' , 'utf8' )
1090+ await makeDidCommand ( ) . parseAsync (
1091+ [ 'webvh' , 'rotate-keys' , did , '--yes' ] ,
1092+ { from : 'user' }
1093+ )
1094+ assert . equal ( exitCode , 1 )
1095+ assert . ok (
1096+ errors . some ( line =>
1097+ line . includes ( 'Could not parse the update-keys sidecar' )
1098+ )
1099+ )
1100+ } finally {
1101+ await rm ( didsDir , { recursive : true } )
1102+ }
1103+ } )
1104+
1105+ it ( 'recovers when an interrupted rotation left the sidecar ahead of the log' , async ( ) => {
1106+ const { didsDir, did } = await createSavedWebvh ( )
1107+ try {
1108+ const logPath = join ( didsDir , 'webvh' , `${ did } .jsonl` )
1109+ const beforeLog = await readFile ( logPath , 'utf8' )
1110+ await makeDidCommand ( ) . parseAsync (
1111+ [ 'webvh' , 'rotate-keys' , did , '--yes' ] ,
1112+ { from : 'user' }
1113+ )
1114+ // Simulate the interruption: the sidecar advanced, the log write was
1115+ // lost.
1116+ await writeFile ( logPath , beforeLog , 'utf8' )
1117+ logs . length = 0
1118+
1119+ await makeDidCommand ( ) . parseAsync (
1120+ [ 'webvh' , 'rotate-keys' , did , '--yes' ] ,
1121+ { from : 'user' }
1122+ )
1123+
1124+ assert . equal ( exitCode , undefined )
1125+ const meta = await resolveStoredWebvhMeta ( didsDir , did )
1126+ assert . equal ( meta . versionId . split ( '-' ) [ 0 ] , '2' )
1127+ assert . equal ( meta . prerotation , true )
1128+ } finally {
1129+ await rm ( didsDir , { recursive : true } )
1130+ }
1131+ } )
1132+
1133+ it ( '--with-seed treats an empty SECRET_KEY_SEED as unset' , async ( ) => {
1134+ const { didsDir, did } = await createSavedWebvh ( )
1135+ try {
1136+ process . env . SECRET_KEY_SEED = ''
1137+ await makeDidCommand ( ) . parseAsync (
1138+ [ 'webvh' , 'rotate-keys' , did , '--with-seed' , '--yes' ] ,
1139+ { from : 'user' }
1140+ )
1141+ assert . equal ( exitCode , undefined )
1142+ const parsed = JSON . parse ( logs . join ( '\n' ) )
1143+ // A seed was generated rather than the empty env value being used.
1144+ assert . match ( parsed . secretKeySeed , / ^ z / )
1145+ } finally {
1146+ await rm ( didsDir , { recursive : true } )
1147+ }
1148+ } )
1149+
1150+ it ( 'reports an invalid SECRET_KEY_SEED instead of crashing' , async ( ) => {
1151+ const { didsDir, did } = await createSavedWebvh ( )
1152+ try {
1153+ process . env . SECRET_KEY_SEED = 'not-a-multibase-seed'
1154+ await makeDidCommand ( ) . parseAsync (
1155+ [ 'webvh' , 'rotate-keys' , did , '--with-seed' , '--yes' ] ,
1156+ { from : 'user' }
1157+ )
1158+ assert . equal ( exitCode , 1 )
1159+ assert . ok ( errors . some ( line => line . includes ( 'Invalid SECRET_KEY_SEED' ) ) )
1160+ } finally {
1161+ await rm ( didsDir , { recursive : true } )
1162+ }
1163+ } )
1164+
9871165 it ( '--with-seed emits the staged next key seed' , async ( ) => {
9881166 const { didsDir, did } = await createSavedWebvh ( )
9891167 try {
0 commit comments