@interop/wallet-core 0.62.0 → 0.65.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 (312) hide show
  1. package/README.md +41 -16
  2. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts +46 -26
  3. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts.map +1 -1
  4. package/dist/clientAnnex/credentialAnchoredGenesis.js +91 -39
  5. package/dist/clientAnnex/credentialAnchoredGenesis.js.map +1 -1
  6. package/dist/clientAnnex/establish.d.ts +55 -34
  7. package/dist/clientAnnex/establish.d.ts.map +1 -1
  8. package/dist/clientAnnex/establish.js +102 -56
  9. package/dist/clientAnnex/establish.js.map +1 -1
  10. package/dist/clientAnnex/forget.d.ts +16 -2
  11. package/dist/clientAnnex/forget.d.ts.map +1 -1
  12. package/dist/clientAnnex/forget.js +11 -9
  13. package/dist/clientAnnex/forget.js.map +1 -1
  14. package/dist/clientAnnex/forgetLast.d.ts +62 -18
  15. package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
  16. package/dist/clientAnnex/forgetLast.js +92 -26
  17. package/dist/clientAnnex/forgetLast.js.map +1 -1
  18. package/dist/clientAnnex/gc.d.ts +3 -2
  19. package/dist/clientAnnex/gc.d.ts.map +1 -1
  20. package/dist/clientAnnex/gc.js +7 -7
  21. package/dist/clientAnnex/gc.js.map +1 -1
  22. package/dist/clientAnnex/heal.d.ts +38 -5
  23. package/dist/clientAnnex/heal.d.ts.map +1 -1
  24. package/dist/clientAnnex/heal.js +346 -236
  25. package/dist/clientAnnex/heal.js.map +1 -1
  26. package/dist/clientAnnex/index.d.ts +21 -5
  27. package/dist/clientAnnex/index.d.ts.map +1 -1
  28. package/dist/clientAnnex/index.js +24 -5
  29. package/dist/clientAnnex/index.js.map +1 -1
  30. package/dist/clientAnnex/ladder.d.ts +58 -0
  31. package/dist/clientAnnex/ladder.d.ts.map +1 -1
  32. package/dist/clientAnnex/ladder.js +222 -33
  33. package/dist/clientAnnex/ladder.js.map +1 -1
  34. package/dist/clientAnnex/ladderAnchored.d.ts +148 -22
  35. package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
  36. package/dist/clientAnnex/ladderAnchored.js +391 -346
  37. package/dist/clientAnnex/ladderAnchored.js.map +1 -1
  38. package/dist/clientAnnex/log.d.ts +162 -43
  39. package/dist/clientAnnex/log.d.ts.map +1 -1
  40. package/dist/clientAnnex/log.js +423 -192
  41. package/dist/clientAnnex/log.js.map +1 -1
  42. package/dist/clientAnnex/mend.d.ts +12 -6
  43. package/dist/clientAnnex/mend.d.ts.map +1 -1
  44. package/dist/clientAnnex/mend.js +26 -12
  45. package/dist/clientAnnex/mend.js.map +1 -1
  46. package/dist/clientAnnex/recoveryLadderAnchored.d.ts +6 -4
  47. package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
  48. package/dist/clientAnnex/recoveryLadderAnchored.js +56 -35
  49. package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -1
  50. package/dist/clientAnnex/spaceCapability.d.ts +123 -0
  51. package/dist/clientAnnex/spaceCapability.d.ts.map +1 -0
  52. package/dist/clientAnnex/spaceCapability.js +152 -0
  53. package/dist/clientAnnex/spaceCapability.js.map +1 -0
  54. package/dist/clientAnnex/stages.d.ts +63 -0
  55. package/dist/clientAnnex/stages.d.ts.map +1 -0
  56. package/dist/clientAnnex/stages.js +64 -0
  57. package/dist/clientAnnex/stages.js.map +1 -0
  58. package/dist/clientAnnex/zcap.d.ts +1 -1
  59. package/dist/clientAnnex/zcap.d.ts.map +1 -1
  60. package/dist/clientAnnex/zcap.js +42 -35
  61. package/dist/clientAnnex/zcap.js.map +1 -1
  62. package/dist/clients/policy.d.ts +15 -1
  63. package/dist/clients/policy.d.ts.map +1 -1
  64. package/dist/clients/policy.js +12 -6
  65. package/dist/clients/policy.js.map +1 -1
  66. package/dist/clients/revocation.d.ts +18 -10
  67. package/dist/clients/revocation.d.ts.map +1 -1
  68. package/dist/clients/revocation.js +13 -5
  69. package/dist/clients/revocation.js.map +1 -1
  70. package/dist/clients/rosterPolicy.d.ts +10 -2
  71. package/dist/clients/rosterPolicy.d.ts.map +1 -1
  72. package/dist/clients/rosterPolicy.js +41 -24
  73. package/dist/clients/rosterPolicy.js.map +1 -1
  74. package/dist/descriptors/acquire.d.ts.map +1 -1
  75. package/dist/descriptors/acquire.js +6 -23
  76. package/dist/descriptors/acquire.js.map +1 -1
  77. package/dist/descriptors/cipher.d.ts.map +1 -1
  78. package/dist/descriptors/cipher.js +5 -0
  79. package/dist/descriptors/cipher.js.map +1 -1
  80. package/dist/descriptors/errors.d.ts +43 -0
  81. package/dist/descriptors/errors.d.ts.map +1 -0
  82. package/dist/descriptors/errors.js +45 -0
  83. package/dist/descriptors/errors.js.map +1 -0
  84. package/dist/descriptors/index.d.ts +5 -0
  85. package/dist/descriptors/index.d.ts.map +1 -1
  86. package/dist/descriptors/index.js +5 -0
  87. package/dist/descriptors/index.js.map +1 -1
  88. package/dist/enrollment/enrollment.d.ts +37 -10
  89. package/dist/enrollment/enrollment.d.ts.map +1 -1
  90. package/dist/enrollment/enrollment.js +52 -15
  91. package/dist/enrollment/enrollment.js.map +1 -1
  92. package/dist/genesis/accountGenesis.d.ts +42 -11
  93. package/dist/genesis/accountGenesis.d.ts.map +1 -1
  94. package/dist/genesis/accountGenesis.js +80 -22
  95. package/dist/genesis/accountGenesis.js.map +1 -1
  96. package/dist/genesis/index.d.ts +3 -1
  97. package/dist/genesis/index.d.ts.map +1 -1
  98. package/dist/genesis/index.js +3 -1
  99. package/dist/genesis/index.js.map +1 -1
  100. package/dist/identity/agents.d.ts +17 -1
  101. package/dist/identity/agents.d.ts.map +1 -1
  102. package/dist/identity/agents.js +23 -7
  103. package/dist/identity/agents.js.map +1 -1
  104. package/dist/identity/index.d.ts +3 -1
  105. package/dist/identity/index.d.ts.map +1 -1
  106. package/dist/identity/index.js +3 -1
  107. package/dist/identity/index.js.map +1 -1
  108. package/dist/index.d.ts +6 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/keyring/index.d.ts +2 -1
  113. package/dist/keyring/index.d.ts.map +1 -1
  114. package/dist/keyring/index.js +2 -1
  115. package/dist/keyring/index.js.map +1 -1
  116. package/dist/keyring/record.d.ts.map +1 -1
  117. package/dist/keyring/record.js +3 -6
  118. package/dist/keyring/record.js.map +1 -1
  119. package/dist/keyring/unlockSpace.d.ts +14 -5
  120. package/dist/keyring/unlockSpace.d.ts.map +1 -1
  121. package/dist/keyring/unlockSpace.js +31 -36
  122. package/dist/keyring/unlockSpace.js.map +1 -1
  123. package/dist/keys/index.d.ts +10 -6
  124. package/dist/keys/index.d.ts.map +1 -1
  125. package/dist/keys/index.js +9 -5
  126. package/dist/keys/index.js.map +1 -1
  127. package/dist/keys/rosterLogStore.d.ts +12 -11
  128. package/dist/keys/rosterLogStore.d.ts.map +1 -1
  129. package/dist/keys/rosterLogStore.js +15 -13
  130. package/dist/keys/rosterLogStore.js.map +1 -1
  131. package/dist/keys/rosterStore.d.ts +3 -6
  132. package/dist/keys/rosterStore.d.ts.map +1 -1
  133. package/dist/keys/rosterStore.js +10 -9
  134. package/dist/keys/rosterStore.js.map +1 -1
  135. package/dist/keys/spaceEpochs.d.ts.map +1 -1
  136. package/dist/keys/spaceEpochs.js +2 -3
  137. package/dist/keys/spaceEpochs.js.map +1 -1
  138. package/dist/keys/userKeyRoster.d.ts +57 -7
  139. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  140. package/dist/keys/userKeyRoster.js +222 -17
  141. package/dist/keys/userKeyRoster.js.map +1 -1
  142. package/dist/keys/userKeyRosterCascade.d.ts +7 -6
  143. package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
  144. package/dist/keys/userKeyRosterCascade.js +7 -6
  145. package/dist/keys/userKeyRosterCascade.js.map +1 -1
  146. package/dist/keys/wasLabelsStore.d.ts +9 -2
  147. package/dist/keys/wasLabelsStore.d.ts.map +1 -1
  148. package/dist/keys/wasLabelsStore.js +14 -6
  149. package/dist/keys/wasLabelsStore.js.map +1 -1
  150. package/dist/log.d.ts +7 -2
  151. package/dist/log.d.ts.map +1 -1
  152. package/dist/log.js +6 -1
  153. package/dist/log.js.map +1 -1
  154. package/dist/recovery/index.d.ts +12 -8
  155. package/dist/recovery/index.d.ts.map +1 -1
  156. package/dist/recovery/index.js +11 -7
  157. package/dist/recovery/index.js.map +1 -1
  158. package/dist/recovery/recoveryCode.d.ts +27 -7
  159. package/dist/recovery/recoveryCode.d.ts.map +1 -1
  160. package/dist/recovery/recoveryCode.js +18 -6
  161. package/dist/recovery/recoveryCode.js.map +1 -1
  162. package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
  163. package/dist/recovery/recoveryDelegation.js +21 -34
  164. package/dist/recovery/recoveryDelegation.js.map +1 -1
  165. package/dist/recovery/recoveryWebvh.d.ts +75 -63
  166. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  167. package/dist/recovery/recoveryWebvh.js +127 -116
  168. package/dist/recovery/recoveryWebvh.js.map +1 -1
  169. package/dist/request/classify.d.ts +32 -8
  170. package/dist/request/classify.d.ts.map +1 -1
  171. package/dist/request/classify.js +39 -14
  172. package/dist/request/classify.js.map +1 -1
  173. package/dist/request/ephemeralExchange.d.ts +1 -6
  174. package/dist/request/ephemeralExchange.d.ts.map +1 -1
  175. package/dist/request/ephemeralExchange.js.map +1 -1
  176. package/dist/request/onboarding.d.ts.map +1 -1
  177. package/dist/request/onboarding.js +2 -2
  178. package/dist/request/onboarding.js.map +1 -1
  179. package/dist/request/parse.d.ts.map +1 -1
  180. package/dist/request/parse.js +2 -3
  181. package/dist/request/parse.js.map +1 -1
  182. package/dist/resourceLog/controller.d.ts +34 -12
  183. package/dist/resourceLog/controller.d.ts.map +1 -1
  184. package/dist/resourceLog/controller.js +79 -86
  185. package/dist/resourceLog/controller.js.map +1 -1
  186. package/dist/resourceLog/document.d.ts +182 -0
  187. package/dist/resourceLog/document.d.ts.map +1 -0
  188. package/dist/resourceLog/document.js +159 -0
  189. package/dist/resourceLog/document.js.map +1 -0
  190. package/dist/resourceLog/errors.d.ts +47 -8
  191. package/dist/resourceLog/errors.d.ts.map +1 -1
  192. package/dist/resourceLog/errors.js +54 -8
  193. package/dist/resourceLog/errors.js.map +1 -1
  194. package/dist/resourceLog/index.d.ts +7 -3
  195. package/dist/resourceLog/index.d.ts.map +1 -1
  196. package/dist/resourceLog/index.js +7 -3
  197. package/dist/resourceLog/index.js.map +1 -1
  198. package/dist/resourceLog/ladderRungs.d.ts +35 -0
  199. package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
  200. package/dist/resourceLog/ladderRungs.js +352 -0
  201. package/dist/resourceLog/ladderRungs.js.map +1 -0
  202. package/dist/resourceLog/license.d.ts +42 -17
  203. package/dist/resourceLog/license.d.ts.map +1 -1
  204. package/dist/resourceLog/license.js +38 -24
  205. package/dist/resourceLog/license.js.map +1 -1
  206. package/dist/space/activity.d.ts +15 -15
  207. package/dist/space/activity.d.ts.map +1 -1
  208. package/dist/space/activity.js +15 -15
  209. package/dist/space/activity.js.map +1 -1
  210. package/dist/space/collections.d.ts +11 -0
  211. package/dist/space/collections.d.ts.map +1 -1
  212. package/dist/space/collections.js +13 -0
  213. package/dist/space/collections.js.map +1 -1
  214. package/dist/space/deleteSpace.d.ts +28 -0
  215. package/dist/space/deleteSpace.d.ts.map +1 -0
  216. package/dist/space/deleteSpace.js +44 -0
  217. package/dist/space/deleteSpace.js.map +1 -0
  218. package/dist/space/errors.d.ts.map +1 -1
  219. package/dist/space/errors.js +0 -1
  220. package/dist/space/errors.js.map +1 -1
  221. package/dist/space/index.d.ts +6 -0
  222. package/dist/space/index.d.ts.map +1 -1
  223. package/dist/space/index.js +6 -0
  224. package/dist/space/index.js.map +1 -1
  225. package/dist/space/plaintextCollection.d.ts +43 -0
  226. package/dist/space/plaintextCollection.d.ts.map +1 -0
  227. package/dist/space/plaintextCollection.js +17 -0
  228. package/dist/space/plaintextCollection.js.map +1 -0
  229. package/dist/stages.d.ts +23 -0
  230. package/dist/stages.d.ts.map +1 -0
  231. package/dist/stages.js +23 -0
  232. package/dist/stages.js.map +1 -0
  233. package/dist/sync/index.d.ts +7 -0
  234. package/dist/sync/index.d.ts.map +1 -1
  235. package/dist/sync/index.js +7 -0
  236. package/dist/sync/index.js.map +1 -1
  237. package/dist/sync/push.js +4 -4
  238. package/dist/sync/push.js.map +1 -1
  239. package/dist/sync/remint.js +2 -2
  240. package/dist/sync/remint.js.map +1 -1
  241. package/dist/sync/types.d.ts +41 -1
  242. package/dist/sync/types.d.ts.map +1 -1
  243. package/dist/sync/types.js +47 -1
  244. package/dist/sync/types.js.map +1 -1
  245. package/dist/unlock/index.d.ts +5 -1
  246. package/dist/unlock/index.d.ts.map +1 -1
  247. package/dist/unlock/index.js +5 -1
  248. package/dist/unlock/index.js.map +1 -1
  249. package/dist/unlock/retire.d.ts +35 -7
  250. package/dist/unlock/retire.d.ts.map +1 -1
  251. package/dist/unlock/retire.js +52 -14
  252. package/dist/unlock/retire.js.map +1 -1
  253. package/dist/unlock/standingClient.d.ts.map +1 -1
  254. package/dist/unlock/standingClient.js +5 -1
  255. package/dist/unlock/standingClient.js.map +1 -1
  256. package/dist/unlock/standingWebvh.d.ts +217 -35
  257. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  258. package/dist/unlock/standingWebvh.js +469 -210
  259. package/dist/unlock/standingWebvh.js.map +1 -1
  260. package/dist/webvh/accountEntry.d.ts +146 -0
  261. package/dist/webvh/accountEntry.d.ts.map +1 -0
  262. package/dist/webvh/accountEntry.js +239 -0
  263. package/dist/webvh/accountEntry.js.map +1 -0
  264. package/dist/webvh/didWeb.d.ts +12 -7
  265. package/dist/webvh/didWeb.d.ts.map +1 -1
  266. package/dist/webvh/didWeb.js +2 -2
  267. package/dist/webvh/didWeb.js.map +1 -1
  268. package/dist/webvh/didWebProjection.d.ts +164 -0
  269. package/dist/webvh/didWebProjection.d.ts.map +1 -0
  270. package/dist/webvh/didWebProjection.js +230 -0
  271. package/dist/webvh/didWebProjection.js.map +1 -0
  272. package/dist/webvh/didWebvh.d.ts +275 -136
  273. package/dist/webvh/didWebvh.d.ts.map +1 -1
  274. package/dist/webvh/didWebvh.js +307 -340
  275. package/dist/webvh/didWebvh.js.map +1 -1
  276. package/dist/webvh/enrollClient.d.ts +65 -0
  277. package/dist/webvh/enrollClient.d.ts.map +1 -0
  278. package/dist/webvh/enrollClient.js +172 -0
  279. package/dist/webvh/enrollClient.js.map +1 -0
  280. package/dist/webvh/index.d.ts +37 -12
  281. package/dist/webvh/index.d.ts.map +1 -1
  282. package/dist/webvh/index.js +33 -10
  283. package/dist/webvh/index.js.map +1 -1
  284. package/dist/webvh/listClients.d.ts +1 -35
  285. package/dist/webvh/listClients.d.ts.map +1 -1
  286. package/dist/webvh/listClients.js +2 -30
  287. package/dist/webvh/listClients.js.map +1 -1
  288. package/dist/webvh/revokeClient.d.ts +41 -11
  289. package/dist/webvh/revokeClient.d.ts.map +1 -1
  290. package/dist/webvh/revokeClient.js +99 -48
  291. package/dist/webvh/revokeClient.js.map +1 -1
  292. package/dist/webvh/standingZcap.d.ts +61 -14
  293. package/dist/webvh/standingZcap.d.ts.map +1 -1
  294. package/dist/webvh/standingZcap.js +91 -0
  295. package/dist/webvh/standingZcap.js.map +1 -1
  296. package/dist/webvh/verifyLog.d.ts +59 -5
  297. package/dist/webvh/verifyLog.d.ts.map +1 -1
  298. package/dist/webvh/verifyLog.js +76 -11
  299. package/dist/webvh/verifyLog.js.map +1 -1
  300. package/dist/webvh/wasIdStore.d.ts +8 -8
  301. package/dist/webvh/wasIdStore.d.ts.map +1 -1
  302. package/dist/webvh/wasIdStore.js +39 -14
  303. package/dist/webvh/wasIdStore.js.map +1 -1
  304. package/dist/webvh/zcap.d.ts +14 -1
  305. package/dist/webvh/zcap.d.ts.map +1 -1
  306. package/dist/webvh/zcap.js +3 -27
  307. package/dist/webvh/zcap.js.map +1 -1
  308. package/package.json +2 -2
  309. package/dist/webvh/keyAgreement.d.ts +0 -97
  310. package/dist/webvh/keyAgreement.d.ts.map +0 -1
  311. package/dist/webvh/keyAgreement.js +0 -71
  312. package/dist/webvh/keyAgreement.js.map +0 -1
@@ -38,7 +38,7 @@
38
38
  * whole document inventory out; the last enrolled client refuses
39
39
  * ({@link LastEnrolledClientForgetError}).
40
40
  * - {@link strikeLadderVmWebvh} / {@link installLadderVmWebvh} /
41
- * {@link forgetLastWebvhClient} -- the entries of the LAST enrolled
41
+ * `forgetLastWebvhClient` -- the entries of the LAST enrolled
42
42
  * client's forget (decision 0004's 2026-08-19 amendment): the credential's
43
43
  * own ladder VM struck and then reinstalled while the client stands (the
44
44
  * pair supplying the transition's inventory-changing document version),
@@ -53,12 +53,14 @@
53
53
  * from durable state alone, on the recovery continuation's pattern.
54
54
  */
55
55
  import { deriveNextKeyHash, updateDID } from '@interop/did-method-webvh';
56
- import { assertCanonicalClientKeys, assertCarryOverCommitments, concludeWithPublishedLog, createLadderAnchoredWebvhLog, didWebvhControllerTemplate, genesisNextKeyHashes, ladderVerificationMethod, markedVerificationMethodPair, pinOfLog, publishWebvhLog, putLogResource, readPublishedLog, relationIds, servedHead, updateKeySigner, withLogConflictRetry, writeKeysJson } from '../webvh/didWebvh.js';
56
+ import { assertCanonicalClientKeys, backfillKeyMapWebvhBlock, concludeWithPublishedLog, createLadderAnchoredWebvhLog, didWebvhControllerTemplate, genesisNextKeyHashes, ladderVerificationMethod, markedVerificationMethodPair, pinOfLog, publishEntryPinned, publishWebvhLog, readPublishedLog, readPublishedLogOrThrow, servedHead, updateKeySigner, withLogConflictRetry, writeKeysJson } from '../webvh/didWebvh.js';
57
+ import { putDidWebProjection } from '../webvh/didWebProjection.js';
57
58
  import { accountLogPinId } from '../webvh/verifyLog.js';
58
- import { ladderVmIds } from '../webvh/listClients.js';
59
+ import { ladderVmIds, relationIds } from '../resourceLog/document.js';
59
60
  import { clientRemovalFields, clientRemovalTarget } from '../webvh/revokeClient.js';
60
- import { readLogOrThrow, unlockKeyVerificationMethod } from '../unlock/standingWebvh.js';
61
+ import { unlockKeyVerificationMethod } from '../unlock/standingWebvh.js';
61
62
  import { attributeLadderRung, ladderRung, ladderVmKeyMultibase } from './ladder.js';
63
+ import { signAccountEntry } from '../webvh/accountEntry.js';
62
64
  /**
63
65
  * LADDER-ANCHORED GENESIS: assembles the one-entry did:webvh log of an account
64
66
  * with zero enrolled clients, anchored on the minting credential's
@@ -94,7 +96,7 @@ import { attributeLadderRung, ladderRung, ladderVmKeyMultibase } from './ladder.
94
96
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
95
97
  * @param options.keyAgreement {UnlockKeyAgreementPublication} the
96
98
  * credential's key-agreement publication (commitment or verbatim)
97
- * @returns {Promise<{ log: DIDLog, webDoc: object, did: string }>}
99
+ * @returns {Promise<CreatedWebvhLog>}
98
100
  */
99
101
  export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, didWebKeys, ladderSeed, keyAgreement }) {
100
102
  const rung0 = await ladderRung({ ladderSeed, index: 0 });
@@ -145,6 +147,9 @@ export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, di
145
147
  * the wallet keeps a KMS; folded into the CREATE path only (see the
146
148
  * adoption note in the body), which also records the minted DID into
147
149
  * keys.json's webvh block as the enrolled-client ensure does
150
+ * @param [options.keysJsonEtag] {string} the ETag the KMS-authentication
151
+ * stage's own `keys.json` write returned, carried as the `ifMatch` of that
152
+ * rewrite
148
153
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
149
154
  * @param options.keyAgreement {UnlockKeyAgreementPublication} the
150
155
  * credential's key-agreement publication (commitment or verbatim)
@@ -154,7 +159,18 @@ export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, di
154
159
  * @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
155
160
  * pins; the account log's slot is keyed by `accountLogPinId` over the
156
161
  * `spaceId` above
157
- * @returns {Promise<{ did: string }>}
162
+ * @returns {Promise<{ did: string, published: PublishedWebvhLog,
163
+ * logMinted: boolean }>} `published` is the head this stage stands on --
164
+ * the served one on the adopt branch, the minted one paired with its
165
+ * create PUT's ETag on the create branch -- so the stage after it can build
166
+ * on this head instead of re-reading the log this one just read or wrote.
167
+ * Its `etag` is absent against a backend that serves none, which is one
168
+ * case a later stage must still read for itself. `logMinted` says WHICH
169
+ * branch produced it, which a later stage needs before reusing it: a
170
+ * minted head is one no other writer could have held a moment ago, while
171
+ * an adopted head is a snapshot of an account other clients are free to
172
+ * write to, and the parts of it no ETag protects (the document's
173
+ * completion tests) can be stale by the time a later stage reads them
158
174
  */
159
175
  export async function ensureLadderAnchoredDidWebvh(options) {
160
176
  return withLogConflictRetry(() => ensureLadderAnchoredDidWebvhOnce(options));
@@ -164,9 +180,10 @@ export async function ensureLadderAnchoredDidWebvh(options) {
164
180
  * conflict retry.
165
181
  *
166
182
  * @param options {object} see {@link ensureLadderAnchoredDidWebvh}
167
- * @returns {Promise<{ did: string }>}
183
+ * @returns {Promise<{ did: string, published: PublishedWebvhLog,
184
+ * logMinted: boolean }>}
168
185
  */
169
- async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, ladderSeed, keyAgreement, expectedDid, pinStore }) {
186
+ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, keysJsonEtag, ladderSeed, keyAgreement, expectedDid, pinStore }) {
170
187
  const logId = accountLogPinId({ spaceId });
171
188
  const published = await readPublishedLog({
172
189
  idStore,
@@ -184,9 +201,21 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
184
201
  // edits it, and a log published without the KMS convenience key is
185
202
  // healed by a later login, not here.
186
203
  await attributeLadderRung({ ladderSeed, published });
187
- // Heals a did.json left lagging by a torn earlier publish.
204
+ // Heals a keys.json left without its `webvh` block by a run torn between
205
+ // the genesis entry and the rewrite: the served map's own binding gains
206
+ // the DID this log resolves to. Gated on this run keeping a KMS, so a
207
+ // KMS-less wallet spends no read on a resource it never writes.
208
+ if (didWebKeys) {
209
+ await backfillKeyMapWebvhBlock({ idStore, did: published.did });
210
+ }
211
+ // Heals a did.json left lagging by a torn earlier publish of this
212
+ // controller-invoking genesis; a lag left by a later ladder-signed entry
213
+ // is `ensureDidWebProjection`'s to mend.
188
214
  const { did } = await concludeWithPublishedLog({ idStore, published });
189
- return { did };
215
+ // The served head verbatim: the projection PUT above touches no log, so
216
+ // the read's own ETag is still the log's validator. `logMinted: false`
217
+ // marks it as a snapshot of an account other clients may be writing to.
218
+ return { did, published, logMinted: false };
190
219
  }
191
220
  const created = await createLadderAnchoredAccountLog({
192
221
  wasServerUrl,
@@ -195,7 +224,7 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
195
224
  ladderSeed,
196
225
  keyAgreement
197
226
  });
198
- await publishWebvhLog({
227
+ const written = await publishWebvhLog({
199
228
  idStore,
200
229
  log: created.log,
201
230
  webDoc: created.webDoc,
@@ -216,26 +245,86 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
216
245
  await writeKeysJson({
217
246
  idStore,
218
247
  didWebKeys,
219
- webvh: { did: created.did }
248
+ webvh: { did: created.did },
249
+ ...(keysJsonEtag !== undefined && { ifMatch: keysJsonEtag })
220
250
  });
221
251
  }
222
- return { did: created.did };
252
+ // The head this run just wrote, assembled from what `createDID` already
253
+ // resolved plus the create PUT's own validator -- no second read, and no
254
+ // second resolve.
255
+ return {
256
+ did: created.did,
257
+ logMinted: true,
258
+ published: {
259
+ log: created.log,
260
+ did: created.did,
261
+ doc: created.doc,
262
+ updateKeys: created.updateKeys,
263
+ nextKeyHashes: created.nextKeyHashes,
264
+ ...(written.etag !== undefined ? { etag: written.etag } : {})
265
+ }
266
+ };
223
267
  }
224
268
  /**
225
- * Publishes `did.jsonl` through the narrow seam -- the log only, never
226
- * `did.json` (the bridge delegation covers nothing else; the enrolled session
227
- * republishes the projection once it is the controller). Conditional on the
228
- * read the entry was built on; a lost race surfaces as a
229
- * `WebvhLogConflictError` (the mapping lives in `putLogResource`).
269
+ * ONE LADDER-SIGNED ACCOUNT-LOG ENTRY: the account-entry seam
270
+ * ({@link signAccountEntry}) on its ladder arm, narrowed to the outcome the
271
+ * annex's ceremonies read. The seam owns the nine steps -- the pinned read,
272
+ * the rung attribution, the rung's carry-over hash, the carry-over
273
+ * precondition, the update-key signer, the self-reveal union into
274
+ * `updateKeys`, the carry-over union into `nextKeyHashes`, the conditional
275
+ * publish (`did.jsonl` alone, the bridge's whole reach), and the pin advance.
276
+ *
277
+ * No conflict retry of its own: a lost compare-and-swap surfaces as a
278
+ * `WebvhLogConflictError` for the caller's {@link withLogConflictRetry} to
279
+ * re-run, which is what re-attributes the rung and climbs to the winner's
280
+ * committed one (the retry-up-the-ladder resolution).
230
281
  *
231
282
  * @param options {object}
232
- * @param options.store {UnlockLogStore}
233
- * @param options.log {DIDLog}
234
- * @param [options.ifMatch] {string} publish only if `did.jsonl` is unchanged
235
- * @returns {Promise<void>}
283
+ * @param options.store {UnlockLogStore} public log read + delegated PUT
284
+ * @param options.ladderSeed {Uint8Array} the credential's ladder seed
285
+ * @param [options.expectedDid] {string} the account DID the log must resolve
286
+ * to, from the caller's stored account pointer
287
+ * @param [options.pinStore] {ResourceLogPinStore} the caller's chain-head
288
+ * pins: the read is checked against the pinned head, and the pin advances
289
+ * to the head this entry publishes
290
+ * @param [options.logId] {string} the account log's pin slot
291
+ * (`accountLogPinId({ spaceId })`); required whenever a `pinStore` is
292
+ * supplied
293
+ * @param [options.skip] {function} `(published) => boolean` -- run on the
294
+ * read, before any attribution; `true` returns `skipped` with nothing
295
+ * published
296
+ * @param options.build {function}
297
+ * `({ published, rung, state }) => LadderSignedEntry | undefined` -- the
298
+ * entry's own members, or `undefined` to decline
299
+ * @param [options.beforePublish] {function} `({ updated }) => Promise<void>`
300
+ * -- the pre-publish seam, for the ceremonies that PUT their post-entry
301
+ * `did:web` projection while the authority they are about to end can still
302
+ * write it. See {@link signAccountEntry}
303
+ * @returns {Promise<LadderSignedEntryOutcome>}
236
304
  */
237
- async function publishLogOnly({ store, log, ifMatch }) {
238
- await putLogResource({ store, log, ifMatch });
305
+ export async function ladderSignedAccountEntry({ store, ladderSeed, expectedDid, pinStore, logId, skip, build, beforePublish }) {
306
+ const outcome = await signAccountEntry({
307
+ idStore: store,
308
+ signer: { kind: 'ladder', ladderSeed },
309
+ build: ({ published, rung, state }) => build({ published, rung: rung, state: state }),
310
+ ...(skip ? { skip } : {}),
311
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
312
+ ...(pinStore ? { pinStore } : {}),
313
+ ...(logId !== undefined ? { logId } : {}),
314
+ missingMessage: 'did:webvh: did.jsonl is missing; nothing to enroll into.',
315
+ ...(beforePublish ? { beforePublish } : {})
316
+ });
317
+ if (outcome.skipped) {
318
+ return { skipped: true, published: outcome.published };
319
+ }
320
+ return {
321
+ skipped: false,
322
+ published: outcome.published,
323
+ rung: outcome.rung,
324
+ rungHash: outcome.rungHash,
325
+ state: outcome.state,
326
+ ...(outcome.updated ? { updated: outcome.updated } : {})
327
+ };
239
328
  }
240
329
  /**
241
330
  * SELF-ENROLLMENT (run by the credential-derived client through the delegated
@@ -406,83 +495,63 @@ export class BuiltOnHeadNotReachedError extends Error {
406
495
  * @returns {Promise<{ did: string, webDoc?: object, committed: boolean }>}
407
496
  */
408
497
  async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, newClientUpdateSeeds, onCommitted, builtOnHead, expectedDid, pinStore, logId }) {
409
- // Each attempt's own read is what the CAS publish is built on, so the
410
- // continuity check runs here -- and again on the retry-up-the-ladder
411
- // re-run -- not only on the verify that follows both entries.
412
- const pinned = {
413
- ...(pinStore ? { pinStore } : {}),
414
- ...(logId !== undefined ? { logId } : {})
415
- };
416
- let published = await readLogOrThrow({
498
+ // The reveal-and-commit entry, through the shared preamble and postamble.
499
+ // It is skipped when a torn earlier run already published it (the rung
500
+ // revealed AND every needed hash committed).
501
+ const reveal = await ladderSignedAccountEntry({
417
502
  store,
418
- ...(expectedDid !== undefined ? { expectedDid } : {}),
419
- ...pinned
420
- });
421
- // The resume marker, checked before anything else -- the completion check
422
- // included, so a truncated served log is refused rather than read as "not
423
- // complete yet" and rebuilt over.
424
- if (builtOnHead) {
425
- const genesisScid = published.log[0]?.parameters.scid ?? '';
426
- const reached = published.log.some(entry => entry.versionId === builtOnHead.versionId);
427
- if (genesisScid !== builtOnHead.scid || !reached) {
428
- throw new BuiltOnHeadNotReachedError({ builtOnHead });
429
- }
430
- }
431
- // Already complete (a torn earlier run finished the add entry): the new
432
- // client's update key is authorized, which only the add entry writes. The
433
- // seam is deliberately NOT entered here -- nothing is about to be
434
- // published, so there is no pivot to persist ahead of.
435
- if (published.updateKeys.includes(newClientKeys.updateKeyMultibase)) {
436
- return { did: published.did, committed: false };
437
- }
438
- // Which rung is current, recovered from the log itself. Fails closed with
439
- // `LadderAttributionError` for a revoked (or never-bound) credential and
440
- // for any ambiguous history.
441
- const { rung, state } = await attributeLadderRung({ ladderSeed, published });
442
- const nextRung = await ladderRung({ ladderSeed, index: rung.index + 1 });
443
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
444
- const newUpdateHash = await deriveNextKeyHash(newClientKeys.updateKeyMultibase);
445
- const newStagedHash = await deriveNextKeyHash(newClientKeys.stagedUpdateKeyMultibase);
446
- const nextRungHash = await deriveNextKeyHash(nextRung.keyMultibase);
447
- // The reveal-and-commit entry, skipped when a torn earlier run already
448
- // published it (the rung revealed AND every needed hash committed).
449
- const revealed = state === 'revealed';
450
- const committed = [newUpdateHash, newStagedHash, nextRungHash].every(hash => published.nextKeyHashes.includes(hash));
451
- if (!revealed || !committed) {
452
- await assertCarryOverCommitments({ published });
453
- const signer = await updateKeySigner({ seed: rung.seed });
454
- const updated = await updateDID({
455
- log: published.log,
456
- signer,
457
- alsoKnownAsWeb: true,
458
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
459
- // The spent rung's own hash is kept through this entry (so a resumed
460
- // commit can re-state the revealed key); the add entry drops it, while
461
- // the next rung's hash stays as the credential's standing commitment.
462
- nextKeyHashes: [
463
- ...new Set([
464
- ...published.nextKeyHashes,
465
- rungHash,
466
- newUpdateHash,
467
- newStagedHash,
468
- nextRungHash
469
- ])
470
- ]
471
- });
472
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
473
- // Advance the pin to what the reveal entry just published, so the re-read
474
- // below (and any read after a tear here) refuses a host that rolls the
475
- // log back behind it.
476
- if (pinStore && logId !== undefined) {
477
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
503
+ ladderSeed,
504
+ expectedDid,
505
+ pinStore,
506
+ logId,
507
+ skip: read => {
508
+ // The resume marker, checked before anything else -- the completion
509
+ // check included, so a truncated served log is refused rather than read
510
+ // as "not complete yet" and rebuilt over.
511
+ if (builtOnHead) {
512
+ const genesisScid = read.log[0]?.parameters.scid ?? '';
513
+ const reached = read.log.some(entry => entry.versionId === builtOnHead.versionId);
514
+ if (genesisScid !== builtOnHead.scid || !reached) {
515
+ throw new BuiltOnHeadNotReachedError({ builtOnHead });
516
+ }
517
+ }
518
+ // Already complete (a torn earlier run finished the add entry): the new
519
+ // client's update key is authorized, which only the add entry writes.
520
+ // The seam is deliberately NOT entered on this path -- nothing is about
521
+ // to be published, so there is no pivot to persist ahead of.
522
+ return read.updateKeys.includes(newClientKeys.updateKeyMultibase);
523
+ },
524
+ build: async ({ published: read, rung, state }) => {
525
+ const nextRung = await ladderRung({ ladderSeed, index: rung.index + 1 });
526
+ const newUpdateHash = await deriveNextKeyHash(newClientKeys.updateKeyMultibase);
527
+ const newStagedHash = await deriveNextKeyHash(newClientKeys.stagedUpdateKeyMultibase);
528
+ const nextRungHash = await deriveNextKeyHash(nextRung.keyMultibase);
529
+ const committed = [newUpdateHash, newStagedHash, nextRungHash].every(hash => read.nextKeyHashes.includes(hash));
530
+ if (state === 'revealed' && committed) {
531
+ return undefined;
532
+ }
533
+ // The spent rung's own hash is kept through this entry by the shared
534
+ // carry-over union (so a resumed commit can re-state the revealed key);
535
+ // the add entry drops it, while the next rung's hash stays as the
536
+ // credential's standing commitment. The three land after it, in the
537
+ // append order `decisions/0007` ratifies.
538
+ return { commitHashes: [newUpdateHash, newStagedHash, nextRungHash] };
478
539
  }
479
- // The same account the reveal entry just extended, under the same pin.
480
- published = await readLogOrThrow({
481
- store,
482
- expectedDid: published.did,
483
- ...pinned
484
- });
540
+ });
541
+ if (reveal.skipped) {
542
+ return { did: reveal.published.did, committed: false };
485
543
  }
544
+ const { rung, rungHash } = reveal;
545
+ // The same account the reveal entry just extended, under the same pin.
546
+ const published = reveal.updated
547
+ ? await readPublishedLogOrThrow({
548
+ idStore: store,
549
+ expectedDid: reveal.published.did,
550
+ pinStore,
551
+ logId,
552
+ missingMessage: 'did:webvh: did.jsonl is missing; nothing to enroll into.'
553
+ })
554
+ : reveal.published;
486
555
  // The persist-before-publish seam: the pending client-key record is
487
556
  // persisted client-local HERE, on the head the add entry is about to be
488
557
  // built on, before that entry -- the ceremony's pivot -- publishes a client
@@ -536,12 +605,13 @@ async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, new
536
605
  });
537
606
  // Conditional on the read this entry was built on: the re-read above when
538
607
  // the commit entry ran here, the first read when it was skipped.
539
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
540
- // Advance the pin to what this entry just published, so a host rolling the
541
- // log back straight afterwards is refused on the next read.
542
- if (pinStore && logId !== undefined) {
543
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
544
- }
608
+ await publishEntryPinned({
609
+ store,
610
+ log: updated.log,
611
+ ifMatch: published.etag,
612
+ pinStore,
613
+ logId
614
+ });
545
615
  return { did: updated.did, webDoc: updated.webDoc, committed: true };
546
616
  }
547
617
  /**
@@ -605,6 +675,13 @@ export class LastEnrolledClientForgetError extends Error {
605
675
  * @param options {object}
606
676
  * @param options.store {UnlockLogStore} the credential's delegated
607
677
  * `did.jsonl` bridge store
678
+ * @param [options.projectionStore] {object} an `id`-collection store the
679
+ * FORGETTING client can still write through (its own root-invoking store):
680
+ * the post-removal `did:web` projection is PUT through it immediately
681
+ * before the removal entry publishes, and only when this run publishes that
682
+ * entry. See {@link clientForgetEntryOnce} for the ordering rationale.
683
+ * Omitted, `did.json` keeps naming the forgotten client until some later
684
+ * writer runs `ensureDidWebProjection`
608
685
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
609
686
  * @param options.forgottenClient {RevokedClientKeys} this client's public
610
687
  * halves; an `updateKeyMultibase` the log does not authorize (stale, or the
@@ -626,120 +703,130 @@ export class LastEnrolledClientForgetError extends Error {
626
703
  * on the idempotent no-op path)
627
704
  */
628
705
  export async function forgetWebvhClient(options) {
629
- return withLogConflictRetry(() => clientForgetEntryOnce({ ...options, transition: false }));
706
+ return withLogConflictRetry(() => clientForgetEntryOnce({ ...options, assertRemovable: assertNotLastClient }));
630
707
  }
631
708
  /**
632
- * THE LAST-CLIENT REMOVAL ENTRY (the two-entry transition ceremony's second
633
- * entry): {@link forgetWebvhClient}'s removal shape with the last-client
634
- * refusal inverted -- the forgotten client IS the last enrolled client, and
635
- * the account stays invocable because the ladder VM the install entry
636
- * published ({@link installLadderVmWebvh}) remains in the document. A
637
- * document NOT carrying this credential's ladder VM refuses: publishing the
638
- * entry would strand the account with neither an enrolled client nor the
639
- * ladder anchor. Run only from the composed ceremony
640
- * (`forgetLastEnrolledClient`), which sequences the install entry and the
641
- * delegation revocations before it.
709
+ * The plain forget's removability invariant: `capabilityInvocation` lists
710
+ * exactly the enrolled clients' signing keys (a recovery code's key is
711
+ * `keyAgreement`-only and the KMS convenience key `authentication`-only), so
712
+ * the forgotten client standing alone there means removing it strands the
713
+ * account. The transition ceremony supplies its own invariant instead
714
+ * (`forgetLast.ts`), which is why this one is injected rather than selected
715
+ * by a flag inside the shared entry builder.
642
716
  *
643
- * @param options {object} see {@link forgetWebvhClient}
644
- * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
717
+ * @param options {object}
718
+ * @param options.published {PublishedWebvhLog}
719
+ * @param options.target {ClientRemovalTarget}
720
+ * @returns {void}
645
721
  */
646
- export async function forgetLastWebvhClient(options) {
647
- return withLogConflictRetry(() => clientForgetEntryOnce({ ...options, transition: true }));
722
+ function assertNotLastClient({ published, target }) {
723
+ const invocationIds = relationIds(published.doc.capabilityInvocation);
724
+ if (invocationIds.includes(target.signingVmId) &&
725
+ invocationIds.every(id => id === target.signingVmId)) {
726
+ throw new LastEnrolledClientForgetError();
727
+ }
648
728
  }
649
729
  /**
650
- * One attempt of {@link forgetWebvhClient} or {@link forgetLastWebvhClient},
651
- * re-invoked by the conflict retry. The two share everything but the guard:
652
- * the plain forget refuses the last enrolled client, the transition removal
653
- * requires the ladder VM already installed instead.
730
+ * One attempt of {@link forgetWebvhClient} or `forgetLastWebvhClient`
731
+ * (`forgetLast.ts`), re-invoked by their conflict retries. The two share the
732
+ * whole removal entry and differ only in what makes the removal admissible,
733
+ * which each supplies as `assertRemovable` -- the plain forget refuses the
734
+ * last enrolled client, the transition removal requires the ladder VM already
735
+ * installed. The invariant travels with the ceremony that owns it rather than
736
+ * living here as a flag.
737
+ *
738
+ * THE PROJECTION IS PUBLISHED BEFORE THE ENTRY, and the order is forced: the
739
+ * removal entry is ladder-signed and publishes `did.jsonl` alone (the bridge
740
+ * delegation covers nothing else), while the forgotten client's authority
741
+ * dies at that entry under the current-key-set rule. So the post-removal
742
+ * `did:web` projection has to be written while that client can still write
743
+ * it. A run torn between the projection PUT and the entry leaves `did.json`
744
+ * omitting a client the log still lists, which is fail-closed for a `did:web`
745
+ * verifier and is re-PUT by the re-run; the reverse order would leave the
746
+ * revoked client standing in `did.json` with nothing left able to remove it.
747
+ * The idempotent already-forgotten path writes no projection at all: the
748
+ * removal entry landed on an earlier run, so this client's authority is
749
+ * already gone and its store can only be refused. A projection that path
750
+ * leaves stale is mended by the next transient visit's
751
+ * `ensureDidWebProjection`, which invokes under its generation delegation.
654
752
  *
655
753
  * @param options {object} see {@link forgetWebvhClient}, plus:
656
- * @param options.transition {boolean} `true` for the last-client removal
657
- * entry (require the installed ladder VM), `false` for the plain forget
658
- * (refuse the last client)
754
+ * @param options.assertRemovable {function}
755
+ * `({ published, target }) => void` -- run on the read, after the
756
+ * idempotent already-forgotten check and before anything is attributed or
757
+ * built; it throws to refuse the removal
659
758
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
660
759
  */
661
- async function clientForgetEntryOnce({ store, ladderSeed, forgottenClient, knownLatentHashes = [], expectedDid, pinStore, logId, transition }) {
662
- // Each attempt's own read is what the CAS publish is built on, so the
663
- // continuity check runs here, not only on the orchestrator's pre-read.
664
- const published = await readLogOrThrow({
760
+ export async function clientForgetEntryOnce({ store, projectionStore, ladderSeed, forgottenClient, knownLatentHashes = [], expectedDid, pinStore, logId, assertRemovable }) {
761
+ // Resolved by the skip hook on the read the entry is built on, and used by
762
+ // the build below -- the same snapshot, never a second read.
763
+ let target;
764
+ const entry = await ladderSignedAccountEntry({
665
765
  store,
666
- ...(expectedDid !== undefined ? { expectedDid } : {}),
667
- ...(pinStore ? { pinStore } : {}),
668
- ...(logId !== undefined ? { logId } : {})
669
- });
670
- const { did, doc } = published;
671
- const target = await clientRemovalTarget({
672
- published,
673
- client: forgottenClient
674
- });
675
- if (!target.present) {
676
- // Already forgotten (a torn earlier run finished the entry). No did.json
677
- // heal here: the bridge covers did.jsonl only.
678
- return { did, doc, log: published.log };
679
- }
680
- if (transition) {
681
- // The no-neither invariant, checked rather than assumed: the removal may
682
- // only publish while the ladder VM stands in the document (the install
683
- // entry ran), or the account would land with nothing that can anchor it.
684
- const ladderVmId = `${did}#${await ladderVmKeyMultibase({ ladderSeed })}`;
685
- if (!ladderVmIds({ doc }).includes(ladderVmId)) {
686
- throw new Error('did:webvh: the ladder VM is not installed in the document; the ' +
687
- 'last-client removal entry would strand the account (the install ' +
688
- 'entry runs first).');
689
- }
690
- }
691
- else {
692
- // The last-client refusal: capabilityInvocation lists exactly the
693
- // enrolled clients' signing keys (a recovery code's key is
694
- // keyAgreement-only and the KMS convenience authentication-only), so the
695
- // forgotten client standing alone there means removing it strands the
696
- // account.
697
- const invocationIds = relationIds(doc.capabilityInvocation);
698
- if (invocationIds.includes(target.signingVmId) &&
699
- invocationIds.every(id => id === target.signingVmId)) {
700
- throw new LastEnrolledClientForgetError();
701
- }
702
- }
703
- // Which rung is current, recovered from the log itself. Fails closed with
704
- // `LadderAttributionError` for a revoked (or never-bound) credential and
705
- // for any ambiguous history.
706
- const { rung } = await attributeLadderRung({ ladderSeed, published });
707
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
708
- await assertCarryOverCommitments({ published });
709
- // The ladder vouches for its own commitments: a self-enrolled client's
710
- // staged hash was committed in the same reveal entry as the next rung's
711
- // hash, so without these the staged-hash attribution cannot tell the two
712
- // apart. Every hash a reveal entry can have committed is for a rung at or
713
- // one past the current index.
714
- const ladderHashes = [];
715
- for (let index = 0; index <= rung.index + 1; index++) {
716
- const laddered = await ladderRung({ ladderSeed, index });
717
- ladderHashes.push(await deriveNextKeyHash(laddered.keyMultibase));
718
- }
719
- const fields = await clientRemovalFields({
720
- published,
721
- target,
722
- knownLatentHashes: [...knownLatentHashes, ...ladderHashes]
723
- });
724
- const signer = await updateKeySigner({ seed: rung.seed });
725
- const updated = await updateDID({
726
- log: published.log,
727
- signer,
728
- alsoKnownAsWeb: true,
729
- ...fields,
730
- // The acting rung reveals itself in the entry it signs (its hash stands
731
- // committed, or the rung is already revealed), and its own hash is kept
732
- // committed so the carry-over convention holds for the next entry.
733
- updateKeys: [...new Set([...fields.updateKeys, rung.keyMultibase])],
734
- nextKeyHashes: [...new Set([...fields.nextKeyHashes, rungHash])]
766
+ ladderSeed,
767
+ expectedDid,
768
+ pinStore,
769
+ logId,
770
+ skip: async (published) => {
771
+ target = await clientRemovalTarget({
772
+ published,
773
+ client: forgottenClient
774
+ });
775
+ if (!target.present) {
776
+ // Already forgotten (a torn earlier run finished the entry). No
777
+ // projection is written on this path: this client's verification
778
+ // methods left the document with that entry, so its store is
779
+ // authorized for nothing. The next transient visit's
780
+ // `ensureDidWebProjection` is the mender.
781
+ return true;
782
+ }
783
+ await assertRemovable({ published, target });
784
+ return false;
785
+ },
786
+ build: async ({ published, rung }) => {
787
+ // The ladder vouches for its own commitments: a self-enrolled client's
788
+ // staged hash was committed in the same reveal entry as the next rung's
789
+ // hash, so without these the staged-hash attribution cannot tell the
790
+ // two apart. Every hash a reveal entry can have committed is for a rung
791
+ // at or one past the current index.
792
+ const ladderHashes = [];
793
+ for (let index = 0; index <= rung.index + 1; index++) {
794
+ const laddered = await ladderRung({ ladderSeed, index });
795
+ ladderHashes.push(await deriveNextKeyHash(laddered.keyMultibase));
796
+ }
797
+ // The removal's own filtered sets; the acting rung's key and hash are
798
+ // unioned back in by the shared carry-over conventions.
799
+ return clientRemovalFields({
800
+ published,
801
+ target: target,
802
+ knownLatentHashes: [...knownLatentHashes, ...ladderHashes]
803
+ });
804
+ },
805
+ // The post-removal projection, published while the client being removed
806
+ // can still write it (see the header). `webDoc` is the `alsoKnownAsWeb`
807
+ // projection `ladderSignedAccountEntry` always asks `updateDID` for.
808
+ ...(projectionStore
809
+ ? {
810
+ beforePublish: async ({ updated }) => {
811
+ if (!updated.webDoc) {
812
+ // `publishUpdatedLog` states the same invariant: every entry is
813
+ // built with `alsoKnownAsWeb`, so a missing projection is a
814
+ // defect. Refusing here is what keeps the removal entry from
815
+ // publishing with `did.json` left naming the removed client,
816
+ // which on a client-less account nothing could mend.
817
+ throw new Error('did:webvh: updateDID returned no webDoc despite the ' +
818
+ 'did:web alsoKnownAs.');
819
+ }
820
+ await putDidWebProjection({
821
+ store: projectionStore,
822
+ webDoc: updated.webDoc
823
+ });
824
+ }
825
+ }
826
+ : {})
735
827
  });
736
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
737
- // Advance the pin to what this entry just published, so a host rolling the
738
- // log back straight afterwards is refused on the next read.
739
- if (pinStore && logId !== undefined) {
740
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
741
- }
742
- return { did: updated.did, doc: updated.doc, log: updated.log };
828
+ const settled = entry.updated ?? entry.published;
829
+ return { did: settled.did, doc: settled.doc, log: settled.log };
743
830
  }
744
831
  /**
745
832
  * THE LADDER-VM INSTALL ENTRY (the two-entry transition ceremony's first
@@ -794,72 +881,83 @@ export async function installLadderVmWebvh(options) {
794
881
  * @param options {object} see {@link installLadderVmWebvh}
795
882
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, installed: boolean }>}
796
883
  */
797
- async function installLadderVmWebvhOnce({ store, ladderSeed, expectedDid, pinStore, logId }) {
798
- // Each attempt's own read is what the CAS publish is built on, so the
799
- // continuity check runs here, not only on the orchestrator's pre-read.
800
- const published = await readLogOrThrow({
801
- store,
802
- ...(expectedDid !== undefined ? { expectedDid } : {}),
803
- ...(pinStore ? { pinStore } : {}),
804
- ...(logId !== undefined ? { logId } : {})
884
+ async function installLadderVmWebvhOnce(options) {
885
+ const { changed, ...settled } = await setLadderVmPresenceOnce({
886
+ ...options,
887
+ present: true
805
888
  });
806
- const { did, doc } = published;
889
+ return { ...settled, installed: changed };
890
+ }
891
+ /**
892
+ * ONE LADDER-VM PRESENCE ENTRY, in either direction: `present: true`
893
+ * publishes this credential's ladder VM, `present: false` strikes it, and the
894
+ * entries are otherwise the same edit read backwards -- the VM in or out of
895
+ * `verificationMethod`, `assertionMethod`, and `capabilityDelegation`, with
896
+ * `authentication`, `keyAgreement`, and `capabilityInvocation` re-stated
897
+ * untouched. That relation asymmetry (`assertionMethod` and
898
+ * `capabilityDelegation` only, never `authentication` or
899
+ * `capabilityInvocation`) is what the recognition reads and what keeps a
900
+ * ladder VM out of every client listing, so the two directions must agree on
901
+ * it exactly; stating it once is the point of the merge.
902
+ *
903
+ * The id derives from the ladder seed, so the entry reaches ONE credential's
904
+ * VM: another standing credential's ladder VM, and every enrolled client's
905
+ * inventory, stand untouched. Idempotent in both directions -- a document
906
+ * already in the asked-for state is a no-op returning `changed: false`,
907
+ * detected BEFORE the rung attribution so a re-run over a retired credential
908
+ * returns unchanged rather than failing closed.
909
+ *
910
+ * @param options {object} see {@link installLadderVmWebvh}, plus:
911
+ * @param options.present {boolean} the state the entry leaves the ladder VM
912
+ * in
913
+ * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, changed: boolean }>}
914
+ */
915
+ async function setLadderVmPresenceOnce({ store, ladderSeed, expectedDid, pinStore, logId, present }) {
807
916
  const ladderVmKey = await ladderVmKeyMultibase({ ladderSeed });
808
- const ladderVmId = `${did}#${ladderVmKey}`;
809
- if (ladderVmIds({ doc }).includes(ladderVmId)) {
810
- // Already installed (a torn earlier run published the entry, or the
811
- // account is mid-transition).
812
- return { did, doc, log: published.log, installed: false };
813
- }
814
- // Which rung is current, recovered from the log itself. Fails closed with
815
- // `LadderAttributionError` for a revoked (or never-bound) credential and
816
- // for any ambiguous history.
817
- const { rung } = await attributeLadderRung({ ladderSeed, published });
818
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
819
- await assertCarryOverCommitments({ published });
820
- const existingMethods = (doc.verificationMethod ?? []);
821
- const verificationMethods = [
822
- ...existingMethods.filter(method => method.id !== ladderVmId),
823
- ladderVerificationMethod({
824
- controller: did,
825
- publicKeyMultibase: ladderVmKey
826
- })
827
- ];
828
- // The ladder VM's relation asymmetry: `assertionMethod` and
829
- // `capabilityDelegation` only -- no `authentication`, no
830
- // `capabilityInvocation` -- which is also what keeps it out of every client
831
- // listing.
832
- const withVm = (relation) => [
833
- ...new Set([...relationIds(relation), ladderVmId])
834
- ];
835
- const signer = await updateKeySigner({ seed: rung.seed });
836
- const updated = await updateDID({
837
- log: published.log,
838
- signer,
839
- alsoKnownAsWeb: true,
840
- // The acting rung reveals itself in the entry it signs (its hash stands
841
- // committed, or the rung is already revealed), and its own hash is kept
842
- // committed so the carry-over convention holds for the next entry.
843
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
844
- nextKeyHashes: [...new Set([...published.nextKeyHashes, rungHash])],
845
- verificationMethods,
846
- authentication: relationIds(doc.authentication),
847
- assertionMethod: withVm(doc.assertionMethod),
848
- keyAgreement: relationIds(doc.keyAgreement),
849
- capabilityInvocation: relationIds(doc.capabilityInvocation),
850
- capabilityDelegation: withVm(doc.capabilityDelegation)
917
+ const entry = await ladderSignedAccountEntry({
918
+ store,
919
+ ladderSeed,
920
+ expectedDid,
921
+ pinStore,
922
+ logId,
923
+ // Already in the asked-for state: a torn earlier run published the entry,
924
+ // the account is mid-transition, or the credential never bound a VM here.
925
+ skip: published => ladderVmIds({ doc: published.doc }).includes(`${published.did}#${ladderVmKey}`) === present,
926
+ build: ({ published }) => {
927
+ const { did, doc } = published;
928
+ const ladderVmId = `${did}#${ladderVmKey}`;
929
+ const withoutVm = (relation) => relationIds(relation).filter(id => id !== ladderVmId);
930
+ const withVm = (relation) => [...new Set([...relationIds(relation), ladderVmId])];
931
+ const inRelation = present ? withVm : withoutVm;
932
+ const otherMethods = (doc.verificationMethod ?? []).filter(method => method.id !== ladderVmId);
933
+ return {
934
+ verificationMethods: present
935
+ ? [
936
+ ...otherMethods,
937
+ ladderVerificationMethod({
938
+ controller: did,
939
+ publicKeyMultibase: ladderVmKey
940
+ })
941
+ ]
942
+ : otherMethods,
943
+ // The ladder VM's relation asymmetry: `assertionMethod` and
944
+ // `capabilityDelegation` only -- no `authentication`, no
945
+ // `capabilityInvocation` -- which is also what keeps it out of every
946
+ // client listing.
947
+ authentication: relationIds(doc.authentication),
948
+ assertionMethod: inRelation(doc.assertionMethod),
949
+ keyAgreement: relationIds(doc.keyAgreement),
950
+ capabilityInvocation: relationIds(doc.capabilityInvocation),
951
+ capabilityDelegation: inRelation(doc.capabilityDelegation)
952
+ };
953
+ }
851
954
  });
852
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
853
- // Advance the pin to what this entry just published, so a host rolling the
854
- // log back straight afterwards is refused on the next read.
855
- if (pinStore && logId !== undefined) {
856
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
857
- }
955
+ const settled = entry.updated ?? entry.published;
858
956
  return {
859
- did: updated.did,
860
- doc: updated.doc,
861
- log: updated.log,
862
- installed: true
957
+ did: settled.did,
958
+ doc: settled.doc,
959
+ log: settled.log,
960
+ changed: entry.updated !== undefined
863
961
  };
864
962
  }
865
963
  /**
@@ -916,54 +1014,12 @@ export async function strikeLadderVmWebvh(options) {
916
1014
  * @param options {object} see {@link strikeLadderVmWebvh}
917
1015
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, struck: boolean }>}
918
1016
  */
919
- async function strikeLadderVmWebvhOnce({ store, ladderSeed, expectedDid, pinStore, logId }) {
920
- // Each attempt's own read is what the CAS publish is built on, so the
921
- // continuity check runs here, not only on the orchestrator's pre-read.
922
- const published = await readLogOrThrow({
923
- store,
924
- ...(expectedDid !== undefined ? { expectedDid } : {}),
925
- ...(pinStore ? { pinStore } : {}),
926
- ...(logId !== undefined ? { logId } : {})
927
- });
928
- const { did, doc } = published;
929
- const ladderVmId = `${did}#${await ladderVmKeyMultibase({ ladderSeed })}`;
930
- if (!ladderVmIds({ doc }).includes(ladderVmId)) {
931
- // Nothing of this credential's to strike (a torn earlier run published
932
- // the entry, or the credential never bound a VM here).
933
- return { did, doc, log: published.log, struck: false };
934
- }
935
- // Which rung is current, recovered from the log itself. Fails closed with
936
- // `LadderAttributionError` for a revoked (or never-bound) credential and
937
- // for any ambiguous history.
938
- const { rung } = await attributeLadderRung({ ladderSeed, published });
939
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
940
- await assertCarryOverCommitments({ published });
941
- const existingMethods = (doc.verificationMethod ?? []);
942
- const withoutVm = (relation) => relationIds(relation).filter(id => id !== ladderVmId);
943
- const signer = await updateKeySigner({ seed: rung.seed });
944
- const updated = await updateDID({
945
- log: published.log,
946
- signer,
947
- alsoKnownAsWeb: true,
948
- // The acting rung reveals itself in the entry it signs (its hash stands
949
- // committed, or the rung is already revealed), and its own hash is kept
950
- // committed so the carry-over convention holds for the next entry.
951
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
952
- nextKeyHashes: [...new Set([...published.nextKeyHashes, rungHash])],
953
- verificationMethods: existingMethods.filter(method => method.id !== ladderVmId),
954
- authentication: relationIds(doc.authentication),
955
- assertionMethod: withoutVm(doc.assertionMethod),
956
- keyAgreement: relationIds(doc.keyAgreement),
957
- capabilityInvocation: relationIds(doc.capabilityInvocation),
958
- capabilityDelegation: withoutVm(doc.capabilityDelegation)
1017
+ async function strikeLadderVmWebvhOnce(options) {
1018
+ const { changed, ...settled } = await setLadderVmPresenceOnce({
1019
+ ...options,
1020
+ present: false
959
1021
  });
960
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
961
- // Advance the pin to what this entry just published, so a host rolling the
962
- // log back straight afterwards is refused on the next read.
963
- if (pinStore && logId !== undefined) {
964
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
965
- }
966
- return { did: updated.did, doc: updated.doc, log: updated.log, struck: true };
1022
+ return { ...settled, struck: changed };
967
1023
  }
968
1024
  /**
969
1025
  * THE STANDALONE REVEAL-AND-COMMIT ENTRY: reveals this credential's currently
@@ -1005,38 +1061,27 @@ async function strikeLadderVmWebvhOnce({ store, ladderSeed, expectedDid, pinStor
1005
1061
  * entry
1006
1062
  */
1007
1063
  export async function revealLadderRungWebvh({ store, ladderSeed, expectedDid, pinStore, logId }) {
1008
- const published = await readLogOrThrow({
1064
+ const entry = await ladderSignedAccountEntry({
1009
1065
  store,
1010
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1011
- ...(pinStore ? { pinStore } : {}),
1012
- ...(logId !== undefined ? { logId } : {})
1013
- });
1014
- const { rung, state } = await attributeLadderRung({ ladderSeed, published });
1015
- if (state === 'revealed') {
1016
- return { revealed: false };
1017
- }
1018
- await assertCarryOverCommitments({ published });
1019
- const nextRung = await ladderRung({ ladderSeed, index: rung.index + 1 });
1020
- const signer = await updateKeySigner({ seed: rung.seed });
1021
- const updated = await updateDID({
1022
- log: published.log,
1023
- signer,
1024
- alsoKnownAsWeb: true,
1025
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
1026
- nextKeyHashes: [
1027
- ...new Set([
1028
- ...published.nextKeyHashes,
1029
- await deriveNextKeyHash(rung.keyMultibase),
1030
- await deriveNextKeyHash(nextRung.keyMultibase)
1031
- ])
1032
- ]
1066
+ ladderSeed,
1067
+ expectedDid,
1068
+ pinStore,
1069
+ logId,
1070
+ build: async ({ rung, state }) => {
1071
+ // A rung already revealed (a torn earlier run, or a racing ceremony
1072
+ // that got there first) leaves nothing to publish. The decline sits
1073
+ // after the attribution because the attribution is what answers it.
1074
+ if (state === 'revealed') {
1075
+ return undefined;
1076
+ }
1077
+ // The entry commits the next rung only; the acting rung's own key and
1078
+ // carry-over hash come from the shared reveal and carry-over unions.
1079
+ const nextRung = await ladderRung({ ladderSeed, index: rung.index + 1 });
1080
+ return {
1081
+ commitHashes: [await deriveNextKeyHash(nextRung.keyMultibase)]
1082
+ };
1083
+ }
1033
1084
  });
1034
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
1035
- // Advance the pin to what this entry just published, so a host rolling the
1036
- // log back straight afterwards is refused on the next read.
1037
- if (pinStore && logId !== undefined) {
1038
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1039
- }
1040
- return { revealed: true };
1085
+ return { revealed: entry.updated !== undefined };
1041
1086
  }
1042
1087
  //# sourceMappingURL=ladderAnchored.js.map