@interop/wallet-core 0.61.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 (313) 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 +126 -35
  15. package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
  16. package/dist/clientAnnex/forgetLast.js +208 -60
  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 +101 -33
  23. package/dist/clientAnnex/heal.d.ts.map +1 -1
  24. package/dist/clientAnnex/heal.js +559 -212
  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 +304 -18
  31. package/dist/clientAnnex/ladder.d.ts.map +1 -1
  32. package/dist/clientAnnex/ladder.js +942 -64
  33. package/dist/clientAnnex/ladder.js.map +1 -1
  34. package/dist/clientAnnex/ladderAnchored.d.ts +261 -28
  35. package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
  36. package/dist/clientAnnex/ladderAnchored.js +521 -295
  37. package/dist/clientAnnex/ladderAnchored.js.map +1 -1
  38. package/dist/clientAnnex/log.d.ts +193 -38
  39. package/dist/clientAnnex/log.d.ts.map +1 -1
  40. package/dist/clientAnnex/log.js +445 -190
  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 +38 -11
  47. package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
  48. package/dist/clientAnnex/recoveryLadderAnchored.js +177 -55
  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 +24 -8
  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 +89 -9
  139. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  140. package/dist/keys/userKeyRoster.js +262 -19
  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 +4 -1
  163. package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
  164. package/dist/recovery/recoveryDelegation.js +25 -36
  165. package/dist/recovery/recoveryDelegation.js.map +1 -1
  166. package/dist/recovery/recoveryWebvh.d.ts +117 -65
  167. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  168. package/dist/recovery/recoveryWebvh.js +279 -126
  169. package/dist/recovery/recoveryWebvh.js.map +1 -1
  170. package/dist/request/classify.d.ts +32 -8
  171. package/dist/request/classify.d.ts.map +1 -1
  172. package/dist/request/classify.js +39 -14
  173. package/dist/request/classify.js.map +1 -1
  174. package/dist/request/ephemeralExchange.d.ts +1 -6
  175. package/dist/request/ephemeralExchange.d.ts.map +1 -1
  176. package/dist/request/ephemeralExchange.js.map +1 -1
  177. package/dist/request/onboarding.d.ts.map +1 -1
  178. package/dist/request/onboarding.js +2 -2
  179. package/dist/request/onboarding.js.map +1 -1
  180. package/dist/request/parse.d.ts.map +1 -1
  181. package/dist/request/parse.js +2 -3
  182. package/dist/request/parse.js.map +1 -1
  183. package/dist/resourceLog/controller.d.ts +34 -12
  184. package/dist/resourceLog/controller.d.ts.map +1 -1
  185. package/dist/resourceLog/controller.js +79 -86
  186. package/dist/resourceLog/controller.js.map +1 -1
  187. package/dist/resourceLog/document.d.ts +182 -0
  188. package/dist/resourceLog/document.d.ts.map +1 -0
  189. package/dist/resourceLog/document.js +159 -0
  190. package/dist/resourceLog/document.js.map +1 -0
  191. package/dist/resourceLog/errors.d.ts +47 -8
  192. package/dist/resourceLog/errors.d.ts.map +1 -1
  193. package/dist/resourceLog/errors.js +54 -8
  194. package/dist/resourceLog/errors.js.map +1 -1
  195. package/dist/resourceLog/index.d.ts +7 -3
  196. package/dist/resourceLog/index.d.ts.map +1 -1
  197. package/dist/resourceLog/index.js +7 -3
  198. package/dist/resourceLog/index.js.map +1 -1
  199. package/dist/resourceLog/ladderRungs.d.ts +35 -0
  200. package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
  201. package/dist/resourceLog/ladderRungs.js +352 -0
  202. package/dist/resourceLog/ladderRungs.js.map +1 -0
  203. package/dist/resourceLog/license.d.ts +42 -17
  204. package/dist/resourceLog/license.d.ts.map +1 -1
  205. package/dist/resourceLog/license.js +38 -24
  206. package/dist/resourceLog/license.js.map +1 -1
  207. package/dist/space/activity.d.ts +15 -15
  208. package/dist/space/activity.d.ts.map +1 -1
  209. package/dist/space/activity.js +15 -15
  210. package/dist/space/activity.js.map +1 -1
  211. package/dist/space/collections.d.ts +11 -0
  212. package/dist/space/collections.d.ts.map +1 -1
  213. package/dist/space/collections.js +13 -0
  214. package/dist/space/collections.js.map +1 -1
  215. package/dist/space/deleteSpace.d.ts +28 -0
  216. package/dist/space/deleteSpace.d.ts.map +1 -0
  217. package/dist/space/deleteSpace.js +44 -0
  218. package/dist/space/deleteSpace.js.map +1 -0
  219. package/dist/space/errors.d.ts.map +1 -1
  220. package/dist/space/errors.js +0 -1
  221. package/dist/space/errors.js.map +1 -1
  222. package/dist/space/index.d.ts +6 -0
  223. package/dist/space/index.d.ts.map +1 -1
  224. package/dist/space/index.js +6 -0
  225. package/dist/space/index.js.map +1 -1
  226. package/dist/space/plaintextCollection.d.ts +43 -0
  227. package/dist/space/plaintextCollection.d.ts.map +1 -0
  228. package/dist/space/plaintextCollection.js +17 -0
  229. package/dist/space/plaintextCollection.js.map +1 -0
  230. package/dist/stages.d.ts +23 -0
  231. package/dist/stages.d.ts.map +1 -0
  232. package/dist/stages.js +23 -0
  233. package/dist/stages.js.map +1 -0
  234. package/dist/sync/index.d.ts +7 -0
  235. package/dist/sync/index.d.ts.map +1 -1
  236. package/dist/sync/index.js +7 -0
  237. package/dist/sync/index.js.map +1 -1
  238. package/dist/sync/push.js +4 -4
  239. package/dist/sync/push.js.map +1 -1
  240. package/dist/sync/remint.js +2 -2
  241. package/dist/sync/remint.js.map +1 -1
  242. package/dist/sync/types.d.ts +41 -1
  243. package/dist/sync/types.d.ts.map +1 -1
  244. package/dist/sync/types.js +47 -1
  245. package/dist/sync/types.js.map +1 -1
  246. package/dist/unlock/index.d.ts +6 -2
  247. package/dist/unlock/index.d.ts.map +1 -1
  248. package/dist/unlock/index.js +5 -1
  249. package/dist/unlock/index.js.map +1 -1
  250. package/dist/unlock/retire.d.ts +95 -14
  251. package/dist/unlock/retire.d.ts.map +1 -1
  252. package/dist/unlock/retire.js +102 -10
  253. package/dist/unlock/retire.js.map +1 -1
  254. package/dist/unlock/standingClient.d.ts.map +1 -1
  255. package/dist/unlock/standingClient.js +5 -1
  256. package/dist/unlock/standingClient.js.map +1 -1
  257. package/dist/unlock/standingWebvh.d.ts +341 -49
  258. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  259. package/dist/unlock/standingWebvh.js +608 -164
  260. package/dist/unlock/standingWebvh.js.map +1 -1
  261. package/dist/webvh/accountEntry.d.ts +146 -0
  262. package/dist/webvh/accountEntry.d.ts.map +1 -0
  263. package/dist/webvh/accountEntry.js +239 -0
  264. package/dist/webvh/accountEntry.js.map +1 -0
  265. package/dist/webvh/didWeb.d.ts +12 -7
  266. package/dist/webvh/didWeb.d.ts.map +1 -1
  267. package/dist/webvh/didWeb.js +2 -2
  268. package/dist/webvh/didWeb.js.map +1 -1
  269. package/dist/webvh/didWebProjection.d.ts +164 -0
  270. package/dist/webvh/didWebProjection.d.ts.map +1 -0
  271. package/dist/webvh/didWebProjection.js +230 -0
  272. package/dist/webvh/didWebProjection.js.map +1 -0
  273. package/dist/webvh/didWebvh.d.ts +278 -138
  274. package/dist/webvh/didWebvh.d.ts.map +1 -1
  275. package/dist/webvh/didWebvh.js +314 -345
  276. package/dist/webvh/didWebvh.js.map +1 -1
  277. package/dist/webvh/enrollClient.d.ts +65 -0
  278. package/dist/webvh/enrollClient.d.ts.map +1 -0
  279. package/dist/webvh/enrollClient.js +172 -0
  280. package/dist/webvh/enrollClient.js.map +1 -0
  281. package/dist/webvh/index.d.ts +37 -12
  282. package/dist/webvh/index.d.ts.map +1 -1
  283. package/dist/webvh/index.js +33 -10
  284. package/dist/webvh/index.js.map +1 -1
  285. package/dist/webvh/listClients.d.ts +1 -33
  286. package/dist/webvh/listClients.d.ts.map +1 -1
  287. package/dist/webvh/listClients.js +2 -28
  288. package/dist/webvh/listClients.js.map +1 -1
  289. package/dist/webvh/revokeClient.d.ts +89 -11
  290. package/dist/webvh/revokeClient.d.ts.map +1 -1
  291. package/dist/webvh/revokeClient.js +182 -47
  292. package/dist/webvh/revokeClient.js.map +1 -1
  293. package/dist/webvh/standingZcap.d.ts +61 -14
  294. package/dist/webvh/standingZcap.d.ts.map +1 -1
  295. package/dist/webvh/standingZcap.js +91 -0
  296. package/dist/webvh/standingZcap.js.map +1 -1
  297. package/dist/webvh/verifyLog.d.ts +59 -5
  298. package/dist/webvh/verifyLog.d.ts.map +1 -1
  299. package/dist/webvh/verifyLog.js +76 -11
  300. package/dist/webvh/verifyLog.js.map +1 -1
  301. package/dist/webvh/wasIdStore.d.ts +8 -8
  302. package/dist/webvh/wasIdStore.d.ts.map +1 -1
  303. package/dist/webvh/wasIdStore.js +39 -14
  304. package/dist/webvh/wasIdStore.js.map +1 -1
  305. package/dist/webvh/zcap.d.ts +14 -1
  306. package/dist/webvh/zcap.d.ts.map +1 -1
  307. package/dist/webvh/zcap.js +3 -27
  308. package/dist/webvh/zcap.js.map +1 -1
  309. package/package.json +5 -5
  310. package/dist/webvh/keyAgreement.d.ts +0 -72
  311. package/dist/webvh/keyAgreement.d.ts.map +0 -1
  312. package/dist/webvh/keyAgreement.js +0 -47
  313. package/dist/webvh/keyAgreement.js.map +0 -1
@@ -25,22 +25,26 @@
25
25
  * hash. The credential's own inventory -- its `keyAgreement` entry and the
26
26
  * freshly committed `hash(rung i + 1)` -- stands untouched, ready for
27
27
  * the next self-enrollment. Nothing is spent, and no replacement exists.
28
- * When the account was LADDER-ANCHORED, the same atomic entry removes
29
- * the ladder VM from the document and its relations -- the transitional
30
- * key exists only while no enrolled client does, and folding the removal
31
- * in leaves no window with neither.
28
+ * Every standing credential's ladder VM is left alone: a VM's life is
29
+ * keyed to its credential rather than to the account's client census, so
30
+ * it stands alongside the client this entry publishes.
31
+ *
32
+ * - {@link revealLadderRungWebvh} -- the reveal-and-commit entry on its own,
33
+ * for a credential-only visit that needs to sign one account-log entry
34
+ * after a self-enrollment spent the previously revealed rung.
32
35
  *
33
36
  * - {@link forgetWebvhClient} -- self-enrollment in reverse: one atomic
34
37
  * ladder-signed removal entry through the bridge takes an enrolled client's
35
38
  * whole document inventory out; the last enrolled client refuses
36
39
  * ({@link LastEnrolledClientForgetError}).
37
- * - {@link installLadderVmWebvh} / {@link forgetLastWebvhClient} -- the two
38
- * entries of the LAST enrolled client's forget (decision 0004's 2026-08-19
39
- * amendment): an install entry publishing the ladder VM while the client
40
- * stands (the both-present transitional state), then -- after the
41
- * revocations the composed ceremony runs in between -- a removal entry
42
- * that takes the client out while the installed VM keeps the account
43
- * ladder-anchored. The composed ceremony is `forgetLast.ts`.
40
+ * - {@link strikeLadderVmWebvh} / {@link installLadderVmWebvh} /
41
+ * `forgetLastWebvhClient` -- the entries of the LAST enrolled
42
+ * client's forget (decision 0004's 2026-08-19 amendment): the credential's
43
+ * own ladder VM struck and then reinstalled while the client stands (the
44
+ * pair supplying the transition's inventory-changing document version),
45
+ * then -- after the revocations the composed ceremony runs in between -- a
46
+ * removal entry that takes the client out while the reinstalled VM keeps
47
+ * the account ladder-anchored. The composed ceremony is `forgetLast.ts`.
44
48
  *
45
49
  * Which rung is current is recovered from the log itself
46
50
  * (`attributeLadderRung`, fail-closed); a lost compare-and-swap race re-runs,
@@ -49,12 +53,14 @@
49
53
  * from durable state alone, on the recovery continuation's pattern.
50
54
  */
51
55
  import { deriveNextKeyHash, updateDID } from '@interop/did-method-webvh';
52
- 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';
53
58
  import { accountLogPinId } from '../webvh/verifyLog.js';
54
- import { ladderVmIds } from '../webvh/listClients.js';
59
+ import { ladderVmIds, relationIds } from '../resourceLog/document.js';
55
60
  import { clientRemovalFields, clientRemovalTarget } from '../webvh/revokeClient.js';
56
- import { readLogOrThrow, unlockKeyVerificationMethod } from '../unlock/standingWebvh.js';
61
+ import { unlockKeyVerificationMethod } from '../unlock/standingWebvh.js';
57
62
  import { attributeLadderRung, ladderRung, ladderVmKeyMultibase } from './ladder.js';
63
+ import { signAccountEntry } from '../webvh/accountEntry.js';
58
64
  /**
59
65
  * LADDER-ANCHORED GENESIS: assembles the one-entry did:webvh log of an account
60
66
  * with zero enrolled clients, anchored on the minting credential's
@@ -69,9 +75,10 @@ import { attributeLadderRung, ladderRung, ladderVmKeyMultibase } from './ladder.
69
75
  * separate bind entry ({@link publishUnlockKey}) -- so the genesis
70
76
  * `keyAgreement` array holds only the credential's entry.
71
77
  *
72
- * The ladder-anchored window this opens is closed by the credential's first
73
- * self-enrollment ({@link selfEnrollWebvhClient}), whose add entry
74
- * atomically publishes the client, retires rung 0, and removes the ladder VM.
78
+ * The account stays ladder-anchored until a self-enrollment
79
+ * ({@link selfEnrollWebvhClient}) publishes a client and retires rung 0. That
80
+ * entry leaves the ladder VM standing: it is struck when the credential
81
+ * retires, not when a client arrives.
75
82
  *
76
83
  * When the wallet keeps a KMS, `didWebKeys` folds the KMS-held
77
84
  * authentication key into the entry under `authentication` only -- the
@@ -89,7 +96,7 @@ import { attributeLadderRung, ladderRung, ladderVmKeyMultibase } from './ladder.
89
96
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
90
97
  * @param options.keyAgreement {UnlockKeyAgreementPublication} the
91
98
  * credential's key-agreement publication (commitment or verbatim)
92
- * @returns {Promise<{ log: DIDLog, webDoc: object, did: string }>}
99
+ * @returns {Promise<CreatedWebvhLog>}
93
100
  */
94
101
  export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, didWebKeys, ladderSeed, keyAgreement }) {
95
102
  const rung0 = await ladderRung({ ladderSeed, index: 0 });
@@ -140,6 +147,9 @@ export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, di
140
147
  * the wallet keeps a KMS; folded into the CREATE path only (see the
141
148
  * adoption note in the body), which also records the minted DID into
142
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
143
153
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
144
154
  * @param options.keyAgreement {UnlockKeyAgreementPublication} the
145
155
  * credential's key-agreement publication (commitment or verbatim)
@@ -149,7 +159,18 @@ export async function createLadderAnchoredAccountLog({ wasServerUrl, spaceId, di
149
159
  * @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
150
160
  * pins; the account log's slot is keyed by `accountLogPinId` over the
151
161
  * `spaceId` above
152
- * @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
153
174
  */
154
175
  export async function ensureLadderAnchoredDidWebvh(options) {
155
176
  return withLogConflictRetry(() => ensureLadderAnchoredDidWebvhOnce(options));
@@ -159,9 +180,10 @@ export async function ensureLadderAnchoredDidWebvh(options) {
159
180
  * conflict retry.
160
181
  *
161
182
  * @param options {object} see {@link ensureLadderAnchoredDidWebvh}
162
- * @returns {Promise<{ did: string }>}
183
+ * @returns {Promise<{ did: string, published: PublishedWebvhLog,
184
+ * logMinted: boolean }>}
163
185
  */
164
- async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, ladderSeed, keyAgreement, expectedDid, pinStore }) {
186
+ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, keysJsonEtag, ladderSeed, keyAgreement, expectedDid, pinStore }) {
165
187
  const logId = accountLogPinId({ spaceId });
166
188
  const published = await readPublishedLog({
167
189
  idStore,
@@ -179,9 +201,21 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
179
201
  // edits it, and a log published without the KMS convenience key is
180
202
  // healed by a later login, not here.
181
203
  await attributeLadderRung({ ladderSeed, published });
182
- // 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.
183
214
  const { did } = await concludeWithPublishedLog({ idStore, published });
184
- 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 };
185
219
  }
186
220
  const created = await createLadderAnchoredAccountLog({
187
221
  wasServerUrl,
@@ -190,7 +224,7 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
190
224
  ladderSeed,
191
225
  keyAgreement
192
226
  });
193
- await publishWebvhLog({
227
+ const written = await publishWebvhLog({
194
228
  idStore,
195
229
  log: created.log,
196
230
  webDoc: created.webDoc,
@@ -211,26 +245,86 @@ async function ensureLadderAnchoredDidWebvhOnce({ idStore, wasServerUrl, spaceId
211
245
  await writeKeysJson({
212
246
  idStore,
213
247
  didWebKeys,
214
- webvh: { did: created.did }
248
+ webvh: { did: created.did },
249
+ ...(keysJsonEtag !== undefined && { ifMatch: keysJsonEtag })
215
250
  });
216
251
  }
217
- 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
+ };
218
267
  }
219
268
  /**
220
- * Publishes `did.jsonl` through the narrow seam -- the log only, never
221
- * `did.json` (the bridge delegation covers nothing else; the enrolled session
222
- * republishes the projection once it is the controller). Conditional on the
223
- * read the entry was built on; a lost race surfaces as a
224
- * `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).
225
281
  *
226
282
  * @param options {object}
227
- * @param options.store {UnlockLogStore}
228
- * @param options.log {DIDLog}
229
- * @param [options.ifMatch] {string} publish only if `did.jsonl` is unchanged
230
- * @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>}
231
304
  */
232
- async function publishLogOnly({ store, log, ifMatch }) {
233
- 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
+ };
234
328
  }
235
329
  /**
236
330
  * SELF-ENROLLMENT (run by the credential-derived client through the delegated
@@ -401,83 +495,63 @@ export class BuiltOnHeadNotReachedError extends Error {
401
495
  * @returns {Promise<{ did: string, webDoc?: object, committed: boolean }>}
402
496
  */
403
497
  async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, newClientUpdateSeeds, onCommitted, builtOnHead, expectedDid, pinStore, logId }) {
404
- // Each attempt's own read is what the CAS publish is built on, so the
405
- // continuity check runs here -- and again on the retry-up-the-ladder
406
- // re-run -- not only on the verify that follows both entries.
407
- const pinned = {
408
- ...(pinStore ? { pinStore } : {}),
409
- ...(logId !== undefined ? { logId } : {})
410
- };
411
- 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({
412
502
  store,
413
- ...(expectedDid !== undefined ? { expectedDid } : {}),
414
- ...pinned
415
- });
416
- // The resume marker, checked before anything else -- the completion check
417
- // included, so a truncated served log is refused rather than read as "not
418
- // complete yet" and rebuilt over.
419
- if (builtOnHead) {
420
- const genesisScid = published.log[0]?.parameters.scid ?? '';
421
- const reached = published.log.some(entry => entry.versionId === builtOnHead.versionId);
422
- if (genesisScid !== builtOnHead.scid || !reached) {
423
- throw new BuiltOnHeadNotReachedError({ builtOnHead });
424
- }
425
- }
426
- // Already complete (a torn earlier run finished the add entry): the new
427
- // client's update key is authorized, which only the add entry writes. The
428
- // seam is deliberately NOT entered here -- nothing is about to be
429
- // published, so there is no pivot to persist ahead of.
430
- if (published.updateKeys.includes(newClientKeys.updateKeyMultibase)) {
431
- return { did: published.did, committed: false };
432
- }
433
- // Which rung is current, recovered from the log itself. Fails closed with
434
- // `LadderAttributionError` for a revoked (or never-bound) credential and
435
- // for any ambiguous history.
436
- const { rung, state } = await attributeLadderRung({ ladderSeed, published });
437
- const nextRung = await ladderRung({ ladderSeed, index: rung.index + 1 });
438
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
439
- const newUpdateHash = await deriveNextKeyHash(newClientKeys.updateKeyMultibase);
440
- const newStagedHash = await deriveNextKeyHash(newClientKeys.stagedUpdateKeyMultibase);
441
- const nextRungHash = await deriveNextKeyHash(nextRung.keyMultibase);
442
- // The reveal-and-commit entry, skipped when a torn earlier run already
443
- // published it (the rung revealed AND every needed hash committed).
444
- const revealed = state === 'revealed';
445
- const committed = [newUpdateHash, newStagedHash, nextRungHash].every(hash => published.nextKeyHashes.includes(hash));
446
- if (!revealed || !committed) {
447
- await assertCarryOverCommitments({ published });
448
- const signer = await updateKeySigner({ seed: rung.seed });
449
- const updated = await updateDID({
450
- log: published.log,
451
- signer,
452
- alsoKnownAsWeb: true,
453
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
454
- // The spent rung's own hash is kept through this entry (so a resumed
455
- // commit can re-state the revealed key); the add entry drops it, while
456
- // the next rung's hash stays as the credential's standing commitment.
457
- nextKeyHashes: [
458
- ...new Set([
459
- ...published.nextKeyHashes,
460
- rungHash,
461
- newUpdateHash,
462
- newStagedHash,
463
- nextRungHash
464
- ])
465
- ]
466
- });
467
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
468
- // Advance the pin to what the reveal entry just published, so the re-read
469
- // below (and any read after a tear here) refuses a host that rolls the
470
- // log back behind it.
471
- if (pinStore && logId !== undefined) {
472
- 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] };
473
539
  }
474
- // The same account the reveal entry just extended, under the same pin.
475
- published = await readLogOrThrow({
476
- store,
477
- expectedDid: published.did,
478
- ...pinned
479
- });
540
+ });
541
+ if (reveal.skipped) {
542
+ return { did: reveal.published.did, committed: false };
480
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;
481
555
  // The persist-before-publish seam: the pending client-key record is
482
556
  // persisted client-local HERE, on the head the add entry is about to be
483
557
  // built on, before that entry -- the ceremony's pivot -- publishes a client
@@ -491,13 +565,11 @@ async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, new
491
565
  // client's update key, whose hash the commit entry just committed.
492
566
  const { did, doc } = published;
493
567
  const vmId = (publicKeyMultibase) => `${did}#${publicKeyMultibase}`;
494
- // When this is the FIRST self-enrollment of a ladder-anchored account, the
495
- // same atomic entry ends the ladder-anchored window: every ladder VM (the
496
- // relation-asymmetry recognition) leaves the document and its relations
497
- // here, so no window exists where the account has neither an enrolled client
498
- // nor the ladder VM. On an account with enrolled clients the recognition
499
- // finds none and the filters are no-ops.
500
- const ladderVms = ladderVmIds({ doc });
568
+ // Enrollment does not touch any ladder VM. A ladder VM's life is keyed to
569
+ // its credential: the standing establishment installs it, the credential's
570
+ // retirement strikes it, and every standing credential's VM stays in the
571
+ // document alongside the enrolled clients this entry publishes. The only
572
+ // ladder state this entry changes is the spent rung's, below.
501
573
  const addedMethods = markedVerificationMethodPair({
502
574
  controller: did,
503
575
  signingKeyMultibase: newClientKeys.signingKeyMultibase,
@@ -505,11 +577,10 @@ async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, new
505
577
  });
506
578
  const existingMethods = (doc.verificationMethod ?? []);
507
579
  const verificationMethods = [
508
- ...existingMethods.filter(method => !addedMethods.some(added => added.id === method.id) &&
509
- (method.id === undefined || !ladderVms.includes(method.id))),
580
+ ...existingMethods.filter(method => !addedMethods.some(added => added.id === method.id)),
510
581
  ...addedMethods
511
582
  ];
512
- const withReference = (relation, id) => [...new Set([...relationIds(relation), id])].filter(referencedId => !ladderVms.includes(referencedId));
583
+ const withReference = (relation, id) => [...new Set([...relationIds(relation), id])];
513
584
  const signingVmId = vmId(newClientKeys.signingKeyMultibase);
514
585
  const signer = await updateKeySigner({
515
586
  seed: newClientUpdateSeeds.updateSeed
@@ -534,12 +605,13 @@ async function selfEnrollWebvhClientOnce({ store, ladderSeed, newClientKeys, new
534
605
  });
535
606
  // Conditional on the read this entry was built on: the re-read above when
536
607
  // the commit entry ran here, the first read when it was skipped.
537
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
538
- // Advance the pin to what this entry just published, so a host rolling the
539
- // log back straight afterwards is refused on the next read.
540
- if (pinStore && logId !== undefined) {
541
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
542
- }
608
+ await publishEntryPinned({
609
+ store,
610
+ log: updated.log,
611
+ ifMatch: published.etag,
612
+ pinStore,
613
+ logId
614
+ });
543
615
  return { did: updated.did, webDoc: updated.webDoc, committed: true };
544
616
  }
545
617
  /**
@@ -603,6 +675,13 @@ export class LastEnrolledClientForgetError extends Error {
603
675
  * @param options {object}
604
676
  * @param options.store {UnlockLogStore} the credential's delegated
605
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`
606
685
  * @param options.ladderSeed {Uint8Array} the credential's ladder seed
607
686
  * @param options.forgottenClient {RevokedClientKeys} this client's public
608
687
  * halves; an `updateKeyMultibase` the log does not authorize (stale, or the
@@ -624,127 +703,139 @@ export class LastEnrolledClientForgetError extends Error {
624
703
  * on the idempotent no-op path)
625
704
  */
626
705
  export async function forgetWebvhClient(options) {
627
- return withLogConflictRetry(() => clientForgetEntryOnce({ ...options, transition: false }));
706
+ return withLogConflictRetry(() => clientForgetEntryOnce({ ...options, assertRemovable: assertNotLastClient }));
628
707
  }
629
708
  /**
630
- * THE LAST-CLIENT REMOVAL ENTRY (the two-entry transition ceremony's second
631
- * entry): {@link forgetWebvhClient}'s removal shape with the last-client
632
- * refusal inverted -- the forgotten client IS the last enrolled client, and
633
- * the account stays invocable because the ladder VM the install entry
634
- * published ({@link installLadderVmWebvh}) remains in the document. A
635
- * document NOT carrying this credential's ladder VM refuses: publishing the
636
- * entry would strand the account with neither an enrolled client nor the
637
- * ladder anchor. Run only from the composed ceremony
638
- * (`forgetLastEnrolledClient`), which sequences the install entry and the
639
- * delegation revocations before it.
640
- *
641
- * @param options {object} see {@link forgetWebvhClient}
642
- * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
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.
716
+ *
717
+ * @param options {object}
718
+ * @param options.published {PublishedWebvhLog}
719
+ * @param options.target {ClientRemovalTarget}
720
+ * @returns {void}
643
721
  */
644
- export async function forgetLastWebvhClient(options) {
645
- 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
+ }
646
728
  }
647
729
  /**
648
- * One attempt of {@link forgetWebvhClient} or {@link forgetLastWebvhClient},
649
- * re-invoked by the conflict retry. The two share everything but the guard:
650
- * the plain forget refuses the last enrolled client, the transition removal
651
- * 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.
652
752
  *
653
753
  * @param options {object} see {@link forgetWebvhClient}, plus:
654
- * @param options.transition {boolean} `true` for the last-client removal
655
- * entry (require the installed ladder VM), `false` for the plain forget
656
- * (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
657
758
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
658
759
  */
659
- async function clientForgetEntryOnce({ store, ladderSeed, forgottenClient, knownLatentHashes = [], expectedDid, pinStore, logId, transition }) {
660
- // Each attempt's own read is what the CAS publish is built on, so the
661
- // continuity check runs here, not only on the orchestrator's pre-read.
662
- 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({
663
765
  store,
664
- ...(expectedDid !== undefined ? { expectedDid } : {}),
665
- ...(pinStore ? { pinStore } : {}),
666
- ...(logId !== undefined ? { logId } : {})
667
- });
668
- const { did, doc } = published;
669
- const target = await clientRemovalTarget({
670
- published,
671
- client: forgottenClient
672
- });
673
- if (!target.present) {
674
- // Already forgotten (a torn earlier run finished the entry). No did.json
675
- // heal here: the bridge covers did.jsonl only.
676
- return { did, doc, log: published.log };
677
- }
678
- if (transition) {
679
- // The no-neither invariant, checked rather than assumed: the removal may
680
- // only publish while the ladder VM stands in the document (the install
681
- // entry ran), or the account would land with nothing that can anchor it.
682
- const ladderVmId = `${did}#${await ladderVmKeyMultibase({ ladderSeed })}`;
683
- if (!ladderVmIds({ doc }).includes(ladderVmId)) {
684
- throw new Error('did:webvh: the ladder VM is not installed in the document; the ' +
685
- 'last-client removal entry would strand the account (the install ' +
686
- 'entry runs first).');
687
- }
688
- }
689
- else {
690
- // The last-client refusal: capabilityInvocation lists exactly the
691
- // enrolled clients' signing keys (a recovery code's key is
692
- // keyAgreement-only and the KMS convenience authentication-only), so the
693
- // forgotten client standing alone there means removing it strands the
694
- // account.
695
- const invocationIds = relationIds(doc.capabilityInvocation);
696
- if (invocationIds.includes(target.signingVmId) &&
697
- invocationIds.every(id => id === target.signingVmId)) {
698
- throw new LastEnrolledClientForgetError();
699
- }
700
- }
701
- // Which rung is current, recovered from the log itself. Fails closed with
702
- // `LadderAttributionError` for a revoked (or never-bound) credential and
703
- // for any ambiguous history.
704
- const { rung } = await attributeLadderRung({ ladderSeed, published });
705
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
706
- await assertCarryOverCommitments({ published });
707
- // The ladder vouches for its own commitments: a self-enrolled client's
708
- // staged hash was committed in the same reveal entry as the next rung's
709
- // hash, so without these the staged-hash attribution cannot tell the two
710
- // apart. Every hash a reveal entry can have committed is for a rung at or
711
- // one past the current index.
712
- const ladderHashes = [];
713
- for (let index = 0; index <= rung.index + 1; index++) {
714
- const laddered = await ladderRung({ ladderSeed, index });
715
- ladderHashes.push(await deriveNextKeyHash(laddered.keyMultibase));
716
- }
717
- const fields = await clientRemovalFields({
718
- published,
719
- target,
720
- knownLatentHashes: [...knownLatentHashes, ...ladderHashes]
721
- });
722
- const signer = await updateKeySigner({ seed: rung.seed });
723
- const updated = await updateDID({
724
- log: published.log,
725
- signer,
726
- alsoKnownAsWeb: true,
727
- ...fields,
728
- // The acting rung reveals itself in the entry it signs (its hash stands
729
- // committed, or the rung is already revealed), and its own hash is kept
730
- // committed so the carry-over convention holds for the next entry.
731
- updateKeys: [...new Set([...fields.updateKeys, rung.keyMultibase])],
732
- 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
+ : {})
733
827
  });
734
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
735
- // Advance the pin to what this entry just published, so a host rolling the
736
- // log back straight afterwards is refused on the next read.
737
- if (pinStore && logId !== undefined) {
738
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
739
- }
740
- 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 };
741
830
  }
742
831
  /**
743
832
  * THE LADDER-VM INSTALL ENTRY (the two-entry transition ceremony's first
744
833
  * entry): publishes the credential's ladder VM -- the stable sibling, under
745
- * `assertionMethod` and `capabilityDelegation` only -- while the last
746
- * enrolled client's whole inventory stays untouched: the both-present
747
- * transitional state the no-neither invariant permits. The entry is
834
+ * `assertionMethod` and `capabilityDelegation` only -- leaving every enrolled
835
+ * client's inventory untouched. A ladder VM and enrolled clients are
836
+ * co-resident by design: the VM's life is keyed to its credential, so the
837
+ * document carries one per standing credential for as long as that credential
838
+ * stands. The entry is
748
839
  * ladder-signed by the attributed rung, which reveals itself into
749
840
  * `updateKeys` with its own hash kept committed (the carry-over convention),
750
841
  * exactly the removal entry's rung math -- so a torn ceremony's re-run
@@ -790,72 +881,207 @@ export async function installLadderVmWebvh(options) {
790
881
  * @param options {object} see {@link installLadderVmWebvh}
791
882
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, installed: boolean }>}
792
883
  */
793
- async function installLadderVmWebvhOnce({ store, ladderSeed, expectedDid, pinStore, logId }) {
794
- // Each attempt's own read is what the CAS publish is built on, so the
795
- // continuity check runs here, not only on the orchestrator's pre-read.
796
- const published = await readLogOrThrow({
797
- store,
798
- ...(expectedDid !== undefined ? { expectedDid } : {}),
799
- ...(pinStore ? { pinStore } : {}),
800
- ...(logId !== undefined ? { logId } : {})
884
+ async function installLadderVmWebvhOnce(options) {
885
+ const { changed, ...settled } = await setLadderVmPresenceOnce({
886
+ ...options,
887
+ present: true
801
888
  });
802
- 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 }) {
803
916
  const ladderVmKey = await ladderVmKeyMultibase({ ladderSeed });
804
- const ladderVmId = `${did}#${ladderVmKey}`;
805
- if (ladderVmIds({ doc }).includes(ladderVmId)) {
806
- // Already installed (a torn earlier run published the entry, or the
807
- // account is mid-transition).
808
- return { did, doc, log: published.log, installed: false };
809
- }
810
- // Which rung is current, recovered from the log itself. Fails closed with
811
- // `LadderAttributionError` for a revoked (or never-bound) credential and
812
- // for any ambiguous history.
813
- const { rung } = await attributeLadderRung({ ladderSeed, published });
814
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
815
- await assertCarryOverCommitments({ published });
816
- const existingMethods = (doc.verificationMethod ?? []);
817
- const verificationMethods = [
818
- ...existingMethods.filter(method => method.id !== ladderVmId),
819
- ladderVerificationMethod({
820
- controller: did,
821
- publicKeyMultibase: ladderVmKey
822
- })
823
- ];
824
- // The ladder VM's relation asymmetry: `assertionMethod` and
825
- // `capabilityDelegation` only -- no `authentication`, no
826
- // `capabilityInvocation` -- which is also what keeps it out of every client
827
- // listing.
828
- const withVm = (relation) => [
829
- ...new Set([...relationIds(relation), ladderVmId])
830
- ];
831
- const signer = await updateKeySigner({ seed: rung.seed });
832
- const updated = await updateDID({
833
- log: published.log,
834
- signer,
835
- alsoKnownAsWeb: true,
836
- // The acting rung reveals itself in the entry it signs (its hash stands
837
- // committed, or the rung is already revealed), and its own hash is kept
838
- // committed so the carry-over convention holds for the next entry.
839
- updateKeys: [...new Set([...published.updateKeys, rung.keyMultibase])],
840
- nextKeyHashes: [...new Set([...published.nextKeyHashes, rungHash])],
841
- verificationMethods,
842
- authentication: relationIds(doc.authentication),
843
- assertionMethod: withVm(doc.assertionMethod),
844
- keyAgreement: relationIds(doc.keyAgreement),
845
- capabilityInvocation: relationIds(doc.capabilityInvocation),
846
- 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
+ }
847
954
  });
848
- await publishLogOnly({ store, log: updated.log, ifMatch: published.etag });
849
- // Advance the pin to what this entry just published, so a host rolling the
850
- // log back straight afterwards is refused on the next read.
851
- if (pinStore && logId !== undefined) {
852
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
853
- }
955
+ const settled = entry.updated ?? entry.published;
854
956
  return {
855
- did: updated.did,
856
- doc: updated.doc,
857
- log: updated.log,
858
- installed: true
957
+ did: settled.did,
958
+ doc: settled.doc,
959
+ log: settled.log,
960
+ changed: entry.updated !== undefined
859
961
  };
860
962
  }
963
+ /**
964
+ * THE LADDER-VM STRIKE ENTRY: takes THIS credential's ladder VM out of the
965
+ * document -- from `verificationMethod`, `assertionMethod`, and
966
+ * `capabilityDelegation` -- and touches nothing else. The id comes from the
967
+ * ladder seed, so the strike reaches one credential's VM: another standing
968
+ * credential's ladder VM, and every enrolled client's inventory, stand
969
+ * untouched. The account-wide filter the transient recovery's add-and-retire
970
+ * entry runs is a different rule and stays there.
971
+ *
972
+ * Paired with {@link installLadderVmWebvh} it is the last-client
973
+ * transition's opening move: the strike, then the reinstall of the identical
974
+ * VM. That pair is what supplies the transition with an
975
+ * inventory-changing document version for its one ladder-signed roster
976
+ * append, on an account whose VM already stands (the credential-keyed
977
+ * lifecycle installs it at bind time). The republished node is the same
978
+ * `<accountDid>#<multibase>`, and a zcap delegation proof carries no version
979
+ * anchor, so any unexpired ladder-signed delegation resumes verifying.
980
+ *
981
+ * Ladder-signed by the attributed rung, which reveals itself into
982
+ * `updateKeys` with its own hash kept committed (the carry-over convention)
983
+ * -- the install and removal entries' rung math exactly.
984
+ *
985
+ * Idempotent: a document carrying no ladder VM of this credential's returns
986
+ * unchanged with `struck: false`. The entry publishes conditionally on the
987
+ * log this call read; a lost race re-runs, re-attributes, and rebases on the
988
+ * winner's head.
989
+ *
990
+ * @param options {object}
991
+ * @param options.store {UnlockLogStore} the credential's delegated
992
+ * `did.jsonl` bridge store
993
+ * @param options.ladderSeed {Uint8Array} the credential's ladder seed
994
+ * @param [options.expectedDid] {string} the account DID the log must resolve
995
+ * to, from the caller's stored account pointer
996
+ * @param [options.pinStore] {ResourceLogPinStore} the caller's chain-head
997
+ * pins: the read inside each attempt is checked against the pinned head,
998
+ * and the pin advances to the head this entry publishes
999
+ * @param [options.logId] {string} the account log's pin slot
1000
+ * (`accountLogPinId({ spaceId })`); required whenever a `pinStore` is
1001
+ * supplied
1002
+ * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, struck: boolean }>}
1003
+ * the account DID and the document and log as the strike entry leaves them
1004
+ * (unchanged on the idempotent no-op path); `struck` says whether the entry
1005
+ * ran on this call
1006
+ */
1007
+ export async function strikeLadderVmWebvh(options) {
1008
+ return withLogConflictRetry(() => strikeLadderVmWebvhOnce(options));
1009
+ }
1010
+ /**
1011
+ * One attempt of {@link strikeLadderVmWebvh}, re-invoked by the conflict
1012
+ * retry.
1013
+ *
1014
+ * @param options {object} see {@link strikeLadderVmWebvh}
1015
+ * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog, struck: boolean }>}
1016
+ */
1017
+ async function strikeLadderVmWebvhOnce(options) {
1018
+ const { changed, ...settled } = await setLadderVmPresenceOnce({
1019
+ ...options,
1020
+ present: false
1021
+ });
1022
+ return { ...settled, struck: changed };
1023
+ }
1024
+ /**
1025
+ * THE STANDALONE REVEAL-AND-COMMIT ENTRY: reveals this credential's currently
1026
+ * committed ladder rung into `updateKeys`, keeping its own hash committed and
1027
+ * committing the next rung's (the carry-over convention). It is the same
1028
+ * entry {@link selfEnrollWebvhClient} writes first, minus the enrolling
1029
+ * client's hashes, and it exists so a credential-only visit can sign an
1030
+ * account-log entry of its own -- the `#DelegatedClients` pointer move -- on
1031
+ * an account whose self-enrollment already spent the previously revealed
1032
+ * rung.
1033
+ *
1034
+ * A rung already revealed (a torn earlier run, or a racing ceremony that got
1035
+ * there first) is a no-op: nothing is published and `revealed: false` comes
1036
+ * back. A ladder the log commits no rung of at all, and an ambiguous
1037
+ * attribution, fail closed with `LadderAttributionError`.
1038
+ *
1039
+ * This entry retires nothing. The revealed rung stands in `updateKeys` until
1040
+ * a later self-enrollment's add entry spends it or the credential retires;
1041
+ * the caller's comment states the consequence at its own site.
1042
+ *
1043
+ * No conflict retry of its own: a caller pairing this entry with a second one
1044
+ * must run both inside ONE {@link withLogConflictRetry}, so a race lost
1045
+ * between them re-runs the attribution rather than signing with a rung the
1046
+ * winner consumed.
1047
+ *
1048
+ * @param options {object}
1049
+ * @param options.store {UnlockLogStore} the credential's delegated
1050
+ * `did.jsonl` bridge store
1051
+ * @param options.ladderSeed {Uint8Array} the credential's ladder seed
1052
+ * @param [options.expectedDid] {string} the account DID the log must
1053
+ * resolve to, from the caller's stored account pointer
1054
+ * @param [options.pinStore] {ResourceLogPinStore} the caller's chain-head
1055
+ * pins: the read is checked against the pinned head, and the pin advances
1056
+ * to the head this entry publishes
1057
+ * @param [options.logId] {string} the account log's pin slot
1058
+ * (`accountLogPinId({ spaceId })`); required whenever a `pinStore` is
1059
+ * supplied
1060
+ * @returns {Promise<{ revealed: boolean }>} whether this call published the
1061
+ * entry
1062
+ */
1063
+ export async function revealLadderRungWebvh({ store, ladderSeed, expectedDid, pinStore, logId }) {
1064
+ const entry = await ladderSignedAccountEntry({
1065
+ store,
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
+ }
1084
+ });
1085
+ return { revealed: entry.updated !== undefined };
1086
+ }
861
1087
  //# sourceMappingURL=ladderAnchored.js.map