@interop/wallet-core 0.62.0 → 0.66.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 (321) 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 +119 -35
  7. package/dist/clientAnnex/establish.d.ts.map +1 -1
  8. package/dist/clientAnnex/establish.js +227 -160
  9. package/dist/clientAnnex/establish.js.map +1 -1
  10. package/dist/clientAnnex/forget.d.ts +18 -8
  11. package/dist/clientAnnex/forget.d.ts.map +1 -1
  12. package/dist/clientAnnex/forget.js +37 -64
  13. package/dist/clientAnnex/forget.js.map +1 -1
  14. package/dist/clientAnnex/forgetLast.d.ts +85 -38
  15. package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
  16. package/dist/clientAnnex/forgetLast.js +155 -98
  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 +16 -7
  21. package/dist/clientAnnex/gc.js.map +1 -1
  22. package/dist/clientAnnex/heal.d.ts +65 -14
  23. package/dist/clientAnnex/heal.d.ts.map +1 -1
  24. package/dist/clientAnnex/heal.js +397 -298
  25. package/dist/clientAnnex/heal.js.map +1 -1
  26. package/dist/clientAnnex/index.d.ts +23 -7
  27. package/dist/clientAnnex/index.d.ts.map +1 -1
  28. package/dist/clientAnnex/index.js +26 -7
  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 +249 -41
  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 +237 -53
  39. package/dist/clientAnnex/log.d.ts.map +1 -1
  40. package/dist/clientAnnex/log.js +533 -198
  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 +44 -72
  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 +22 -8
  124. package/dist/keys/index.d.ts.map +1 -1
  125. package/dist/keys/index.js +20 -6
  126. package/dist/keys/index.js.map +1 -1
  127. package/dist/keys/rosterLogStore.d.ts +15 -12
  128. package/dist/keys/rosterLogStore.d.ts.map +1 -1
  129. package/dist/keys/rosterLogStore.js +21 -16
  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/userKeyCascade.d.ts +8 -5
  139. package/dist/keys/userKeyCascade.d.ts.map +1 -1
  140. package/dist/keys/userKeyCascade.js +7 -6
  141. package/dist/keys/userKeyCascade.js.map +1 -1
  142. package/dist/keys/userKeyRoster.d.ts +95 -8
  143. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  144. package/dist/keys/userKeyRoster.js +278 -35
  145. package/dist/keys/userKeyRoster.js.map +1 -1
  146. package/dist/keys/userKeyRosterCascade.d.ts +117 -16
  147. package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
  148. package/dist/keys/userKeyRosterCascade.js +184 -43
  149. package/dist/keys/userKeyRosterCascade.js.map +1 -1
  150. package/dist/keys/wasLabelsStore.d.ts +9 -2
  151. package/dist/keys/wasLabelsStore.d.ts.map +1 -1
  152. package/dist/keys/wasLabelsStore.js +14 -6
  153. package/dist/keys/wasLabelsStore.js.map +1 -1
  154. package/dist/log.d.ts +7 -2
  155. package/dist/log.d.ts.map +1 -1
  156. package/dist/log.js +6 -1
  157. package/dist/log.js.map +1 -1
  158. package/dist/recovery/index.d.ts +13 -9
  159. package/dist/recovery/index.d.ts.map +1 -1
  160. package/dist/recovery/index.js +12 -8
  161. package/dist/recovery/index.js.map +1 -1
  162. package/dist/recovery/recoveryCode.d.ts +27 -7
  163. package/dist/recovery/recoveryCode.d.ts.map +1 -1
  164. package/dist/recovery/recoveryCode.js +18 -6
  165. package/dist/recovery/recoveryCode.js.map +1 -1
  166. package/dist/recovery/recoveryDelegation.d.ts +23 -0
  167. package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
  168. package/dist/recovery/recoveryDelegation.js +66 -46
  169. package/dist/recovery/recoveryDelegation.js.map +1 -1
  170. package/dist/recovery/recoveryWebvh.d.ts +75 -63
  171. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  172. package/dist/recovery/recoveryWebvh.js +127 -116
  173. package/dist/recovery/recoveryWebvh.js.map +1 -1
  174. package/dist/request/appKey.d.ts +35 -1
  175. package/dist/request/appKey.d.ts.map +1 -1
  176. package/dist/request/appKey.js +29 -10
  177. package/dist/request/appKey.js.map +1 -1
  178. package/dist/request/classify.d.ts +32 -8
  179. package/dist/request/classify.d.ts.map +1 -1
  180. package/dist/request/classify.js +39 -14
  181. package/dist/request/classify.js.map +1 -1
  182. package/dist/request/ephemeralExchange.d.ts +1 -6
  183. package/dist/request/ephemeralExchange.d.ts.map +1 -1
  184. package/dist/request/ephemeralExchange.js.map +1 -1
  185. package/dist/request/onboarding.d.ts.map +1 -1
  186. package/dist/request/onboarding.js +2 -2
  187. package/dist/request/onboarding.js.map +1 -1
  188. package/dist/request/parse.d.ts.map +1 -1
  189. package/dist/request/parse.js +2 -3
  190. package/dist/request/parse.js.map +1 -1
  191. package/dist/resourceLog/controller.d.ts +34 -12
  192. package/dist/resourceLog/controller.d.ts.map +1 -1
  193. package/dist/resourceLog/controller.js +79 -86
  194. package/dist/resourceLog/controller.js.map +1 -1
  195. package/dist/resourceLog/document.d.ts +182 -0
  196. package/dist/resourceLog/document.d.ts.map +1 -0
  197. package/dist/resourceLog/document.js +159 -0
  198. package/dist/resourceLog/document.js.map +1 -0
  199. package/dist/resourceLog/errors.d.ts +47 -8
  200. package/dist/resourceLog/errors.d.ts.map +1 -1
  201. package/dist/resourceLog/errors.js +54 -8
  202. package/dist/resourceLog/errors.js.map +1 -1
  203. package/dist/resourceLog/index.d.ts +7 -3
  204. package/dist/resourceLog/index.d.ts.map +1 -1
  205. package/dist/resourceLog/index.js +7 -3
  206. package/dist/resourceLog/index.js.map +1 -1
  207. package/dist/resourceLog/ladderRungs.d.ts +35 -0
  208. package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
  209. package/dist/resourceLog/ladderRungs.js +352 -0
  210. package/dist/resourceLog/ladderRungs.js.map +1 -0
  211. package/dist/resourceLog/license.d.ts +42 -17
  212. package/dist/resourceLog/license.d.ts.map +1 -1
  213. package/dist/resourceLog/license.js +38 -24
  214. package/dist/resourceLog/license.js.map +1 -1
  215. package/dist/space/activity.d.ts +15 -15
  216. package/dist/space/activity.d.ts.map +1 -1
  217. package/dist/space/activity.js +15 -15
  218. package/dist/space/activity.js.map +1 -1
  219. package/dist/space/collections.d.ts +11 -0
  220. package/dist/space/collections.d.ts.map +1 -1
  221. package/dist/space/collections.js +13 -0
  222. package/dist/space/collections.js.map +1 -1
  223. package/dist/space/deleteSpace.d.ts +28 -0
  224. package/dist/space/deleteSpace.d.ts.map +1 -0
  225. package/dist/space/deleteSpace.js +44 -0
  226. package/dist/space/deleteSpace.js.map +1 -0
  227. package/dist/space/errors.d.ts.map +1 -1
  228. package/dist/space/errors.js +0 -1
  229. package/dist/space/errors.js.map +1 -1
  230. package/dist/space/index.d.ts +6 -0
  231. package/dist/space/index.d.ts.map +1 -1
  232. package/dist/space/index.js +6 -0
  233. package/dist/space/index.js.map +1 -1
  234. package/dist/space/plaintextCollection.d.ts +43 -0
  235. package/dist/space/plaintextCollection.d.ts.map +1 -0
  236. package/dist/space/plaintextCollection.js +17 -0
  237. package/dist/space/plaintextCollection.js.map +1 -0
  238. package/dist/stages.d.ts +23 -0
  239. package/dist/stages.d.ts.map +1 -0
  240. package/dist/stages.js +23 -0
  241. package/dist/stages.js.map +1 -0
  242. package/dist/sync/index.d.ts +7 -0
  243. package/dist/sync/index.d.ts.map +1 -1
  244. package/dist/sync/index.js +7 -0
  245. package/dist/sync/index.js.map +1 -1
  246. package/dist/sync/push.js +4 -4
  247. package/dist/sync/push.js.map +1 -1
  248. package/dist/sync/remint.js +2 -2
  249. package/dist/sync/remint.js.map +1 -1
  250. package/dist/sync/types.d.ts +41 -1
  251. package/dist/sync/types.d.ts.map +1 -1
  252. package/dist/sync/types.js +47 -1
  253. package/dist/sync/types.js.map +1 -1
  254. package/dist/unlock/index.d.ts +5 -1
  255. package/dist/unlock/index.d.ts.map +1 -1
  256. package/dist/unlock/index.js +5 -1
  257. package/dist/unlock/index.js.map +1 -1
  258. package/dist/unlock/retire.d.ts +35 -7
  259. package/dist/unlock/retire.d.ts.map +1 -1
  260. package/dist/unlock/retire.js +52 -14
  261. package/dist/unlock/retire.js.map +1 -1
  262. package/dist/unlock/standingClient.d.ts.map +1 -1
  263. package/dist/unlock/standingClient.js +5 -1
  264. package/dist/unlock/standingClient.js.map +1 -1
  265. package/dist/unlock/standingWebvh.d.ts +217 -35
  266. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  267. package/dist/unlock/standingWebvh.js +469 -210
  268. package/dist/unlock/standingWebvh.js.map +1 -1
  269. package/dist/webvh/accountEntry.d.ts +146 -0
  270. package/dist/webvh/accountEntry.d.ts.map +1 -0
  271. package/dist/webvh/accountEntry.js +239 -0
  272. package/dist/webvh/accountEntry.js.map +1 -0
  273. package/dist/webvh/didWeb.d.ts +12 -7
  274. package/dist/webvh/didWeb.d.ts.map +1 -1
  275. package/dist/webvh/didWeb.js +2 -2
  276. package/dist/webvh/didWeb.js.map +1 -1
  277. package/dist/webvh/didWebProjection.d.ts +164 -0
  278. package/dist/webvh/didWebProjection.d.ts.map +1 -0
  279. package/dist/webvh/didWebProjection.js +230 -0
  280. package/dist/webvh/didWebProjection.js.map +1 -0
  281. package/dist/webvh/didWebvh.d.ts +275 -136
  282. package/dist/webvh/didWebvh.d.ts.map +1 -1
  283. package/dist/webvh/didWebvh.js +307 -340
  284. package/dist/webvh/didWebvh.js.map +1 -1
  285. package/dist/webvh/enrollClient.d.ts +65 -0
  286. package/dist/webvh/enrollClient.d.ts.map +1 -0
  287. package/dist/webvh/enrollClient.js +172 -0
  288. package/dist/webvh/enrollClient.js.map +1 -0
  289. package/dist/webvh/index.d.ts +37 -12
  290. package/dist/webvh/index.d.ts.map +1 -1
  291. package/dist/webvh/index.js +33 -10
  292. package/dist/webvh/index.js.map +1 -1
  293. package/dist/webvh/listClients.d.ts +1 -35
  294. package/dist/webvh/listClients.d.ts.map +1 -1
  295. package/dist/webvh/listClients.js +2 -30
  296. package/dist/webvh/listClients.js.map +1 -1
  297. package/dist/webvh/revokeClient.d.ts +41 -11
  298. package/dist/webvh/revokeClient.d.ts.map +1 -1
  299. package/dist/webvh/revokeClient.js +99 -48
  300. package/dist/webvh/revokeClient.js.map +1 -1
  301. package/dist/webvh/standingZcap.d.ts +61 -14
  302. package/dist/webvh/standingZcap.d.ts.map +1 -1
  303. package/dist/webvh/standingZcap.js +91 -0
  304. package/dist/webvh/standingZcap.js.map +1 -1
  305. package/dist/webvh/verifyLog.d.ts +59 -5
  306. package/dist/webvh/verifyLog.d.ts.map +1 -1
  307. package/dist/webvh/verifyLog.js +76 -11
  308. package/dist/webvh/verifyLog.js.map +1 -1
  309. package/dist/webvh/wasIdStore.d.ts +8 -8
  310. package/dist/webvh/wasIdStore.d.ts.map +1 -1
  311. package/dist/webvh/wasIdStore.js +39 -14
  312. package/dist/webvh/wasIdStore.js.map +1 -1
  313. package/dist/webvh/zcap.d.ts +14 -1
  314. package/dist/webvh/zcap.d.ts.map +1 -1
  315. package/dist/webvh/zcap.js +3 -27
  316. package/dist/webvh/zcap.js.map +1 -1
  317. package/package.json +2 -2
  318. package/dist/webvh/keyAgreement.d.ts +0 -97
  319. package/dist/webvh/keyAgreement.d.ts.map +0 -1
  320. package/dist/webvh/keyAgreement.js +0 -71
  321. package/dist/webvh/keyAgreement.js.map +0 -1
@@ -52,11 +52,13 @@ import { createDID, deriveNextKeyHash, updateDID } from '@interop/did-method-web
52
52
  import { rootCapabilityId, spaceItems, spacePath, toUrl } from '@interop/was-client/paths';
53
53
  import { base64urlnopad } from '@scure/base';
54
54
  import { DID_LOG_RESOURCE } from '../space/collections.js';
55
+ import { plaintextCollection } from '../space/plaintextCollection.js';
55
56
  import { resourceLogPinId } from '@interop/vh-resource-log';
56
57
  import { clientAnnexRung } from './ladder.js';
57
- import { assertCarryOverCommitments, concludeWithPublishedLog, didWebvhControllerTemplate, MULTIKEY_VM_TYPE, pinOfLog, publishUpdatedLog, putLogResource, readPublishedLog, relationIds, updateKeyMultibase, updateKeySigner, withLogConflictRetry } from '../webvh/didWebvh.js';
58
- import { delegationKeyInDocument } from '../webvh/listClients.js';
59
- import { delegationProofKeyId, STANDING_ZCAP_TTL_MS, zcapExpiring } from '../webvh/standingZcap.js';
58
+ import { assertCarryOverCommitments, assertPublishedLogDid, concludeWithPublishedLog, currentLogParameters, didWebvhControllerTemplate, MULTIKEY_VM_TYPE, pinOfLog, putLogResource, readPublishedLogOrThrow, updateKeySigner, WebvhLogConflictError, withLogConflictRetry } from '../webvh/didWebvh.js';
59
+ import { signAccountEntry } from '../webvh/accountEntry.js';
60
+ import { relationIds } from '../resourceLog/document.js';
61
+ import { STANDING_ZCAP_TTL_MS, standingZcapStale } from '../webvh/standingZcap.js';
60
62
  import { wasWebvhLogStore } from '../webvh/wasIdStore.js';
61
63
  /**
62
64
  * The Space Description `type` array of the auxiliary annex Space, set at
@@ -256,24 +258,30 @@ export async function createClientAnnexLog({ wasServerUrl, spaceId, generationId
256
258
  * @param options.controller {string} the Space controller (the account
257
259
  * did:webvh where it exists; a bootstrap did:key on a ladder-anchored signup,
258
260
  * promoted the same way the account Space's controller is)
259
- * @returns {Promise<void>}
261
+ * @returns {Promise<SpaceDescription>} the Space Description this call read
262
+ * (the Space already existed) or wrote (it did not), so a caller flipping
263
+ * the controller straight afterwards can hand it back as `current` instead
264
+ * of paying for a second read.
260
265
  */
261
266
  export async function ensureClientAnnexSpace({ was, spaceId, controller }) {
262
267
  const space = was.space(spaceId);
263
268
  const current = await space.describe();
264
269
  if (current === null) {
265
- await space.configure({
270
+ // `current: null` is the answer this read just produced, so was-client
271
+ // skips its own pre-merge describe rather than repeating it.
272
+ return space.configure({
273
+ current: null,
266
274
  controller,
267
275
  type: CLIENT_ANNEX_SPACE_TYPE,
268
276
  force: true
269
277
  });
270
- return;
271
278
  }
272
279
  if (!current.type?.includes(DELEGATED_CLIENTS_SPACE_TYPE)) {
273
280
  throw new Error(`The Space "${spaceId}" exists but is not typed as the ` +
274
281
  'delegated-clients auxiliary Space; its type is immutable, so it ' +
275
282
  'cannot hold client-annex generations.');
276
283
  }
284
+ return current;
277
285
  }
278
286
  /**
279
287
  * Mints a fresh annex generation with controller-tier signing: ensures
@@ -307,13 +315,23 @@ export async function ensureClientAnnexSpace({ was, spaceId, controller }) {
307
315
  * @param options.nextKeyHashes {string[]} every standing credential's
308
316
  * rung-0 hash, the minting credential's included
309
317
  * @param options.signer {Signer} the minting credential's rung-0 signer
310
- * @returns {Promise<{ did: string; generationId: string; log: DIDLog;
311
- * doc: DIDDoc }>}
318
+ * @returns {Promise<PublishedWebvhLog & { generationId: string;
319
+ * spaceDescription?: SpaceDescription }>} the published head of the
320
+ * genesis log, which a stage building the generation's next entry can
321
+ * stand on instead of re-reading it, carrying the PUT's own ETag when the
322
+ * store handed one back. `spaceDescription` is the auxiliary Space's
323
+ * Description as the ensure read or wrote it, so a caller flipping the
324
+ * controller afterwards need not re-read it. Always present here, since
325
+ * this minter always runs the ensure.
312
326
  */
313
327
  export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
314
- await ensureClientAnnexSpace({ was, spaceId, controller });
328
+ const spaceDescription = await ensureClientAnnexSpace({
329
+ was,
330
+ spaceId,
331
+ controller
332
+ });
315
333
  const generationId = mintGenerationId();
316
- return publishClientAnnexGenesis({
334
+ const published = await publishClientAnnexGenesis({
317
335
  was,
318
336
  wasServerUrl,
319
337
  spaceId,
@@ -322,6 +340,7 @@ export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, co
322
340
  nextKeyHashes,
323
341
  signer
324
342
  });
343
+ return { ...published, spaceDescription };
325
344
  }
326
345
  /**
327
346
  * The shared genesis-publish tail of both generation minters: creates the
@@ -338,19 +357,29 @@ export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, co
338
357
  * @param options.signer {Signer}
339
358
  * @param [options.capability] {IZcap} an invocation capability the
340
359
  * collection create and the genesis publish ride (a delegated minter)
341
- * @returns {Promise<{ did: string; generationId: string; log: DIDLog;
342
- * doc: DIDDoc }>}
360
+ * @returns {Promise<PublishedWebvhLog & { generationId: string }>} the
361
+ * published head of the log this call just wrote -- the genesis log, its
362
+ * DID and document, the effective update-key parameters, and the PUT's own
363
+ * ETag when the store handed one back. It is what a stage building the
364
+ * generation's next entry can stand on instead of re-reading the log
365
+ * (see {@link ensureGenerationDelegationCurrent}'s `published`); the ETag
366
+ * is absent against a backend that serves none, where a caller must read
367
+ * for itself rather than degrade its compare-and-swap to an unconditional
368
+ * write.
343
369
  */
344
370
  async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer, capability }) {
345
371
  // The generation collection must exist before its first resource PUT; a
346
- // fresh random generation id means this is always a create. Plaintext on
347
- // purpose:
348
- // the server resolves the annex DID out of its own storage, and the
349
- // collection is capability-gated rather than encrypted.
350
- await was
351
- .space(spaceId, capability !== undefined ? { capability } : {})
352
- .collection(generationId, { encryption: 'plaintext' })
353
- .configure({ name: generationId, force: true });
372
+ // fresh random generation id means this is always a create, so the truthful
373
+ // `current: null` skips was-client's pre-merge describe of a Collection
374
+ // that cannot exist. Plaintext on purpose: the server resolves the annex
375
+ // DID out of its own storage, and the collection is capability-gated rather
376
+ // than encrypted.
377
+ await plaintextCollection({
378
+ was,
379
+ spaceId,
380
+ collectionId: generationId,
381
+ capability
382
+ }).configure({ current: null, name: generationId, force: true });
354
383
  const created = await createClientAnnexLog({
355
384
  wasServerUrl,
356
385
  spaceId,
@@ -359,7 +388,7 @@ async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generatio
359
388
  nextKeyHashes,
360
389
  signer
361
390
  });
362
- await putLogResource({
391
+ const written = await putLogResource({
363
392
  store: clientAnnexLogStore({
364
393
  was,
365
394
  spaceId,
@@ -369,7 +398,12 @@ async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generatio
369
398
  log: created.log,
370
399
  ifNoneMatch: true
371
400
  });
372
- return { ...created, generationId };
401
+ return {
402
+ ...created,
403
+ generationId,
404
+ ...currentLogParameters(created),
405
+ ...(written.etag !== undefined ? { etag: written.etag } : {})
406
+ };
373
407
  }
374
408
  /**
375
409
  * Mints a fresh annex generation signed by a standing CREDENTIAL's
@@ -403,16 +437,22 @@ async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generatio
403
437
  * subtree). The typed-Space ensure is then skipped: the delegation's target
404
438
  * covers the collections beneath the Space, never the Space Description,
405
439
  * and a standing sibling delegation presupposes the auxiliary Space
406
- * @returns {Promise<{ did: string; generationId: string; log: DIDLog;
407
- * doc: DIDDoc }>}
440
+ * @returns {Promise<PublishedWebvhLog & { generationId: string;
441
+ * spaceDescription?: SpaceDescription }>} the published head of the
442
+ * genesis log, which a stage building the generation's next entry can
443
+ * stand on instead of re-reading it, carrying the PUT's own ETag when the
444
+ * store handed one back. `spaceDescription` is the auxiliary Space's
445
+ * Description as the ensure read or wrote it, so a caller flipping the
446
+ * controller afterwards need not re-read it. Present exactly when the
447
+ * ensure ran, so absent under a supplied `capability`.
408
448
  */
409
449
  export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, ladderSeed, extraNextKeyHashes = [], capability }) {
410
- if (capability === undefined) {
411
- await ensureClientAnnexSpace({ was, spaceId, controller });
412
- }
450
+ const spaceDescription = capability === undefined
451
+ ? await ensureClientAnnexSpace({ was, spaceId, controller })
452
+ : undefined;
413
453
  const generationId = mintGenerationId();
414
454
  const rung = await clientAnnexRung({ ladderSeed, generationId });
415
- return publishClientAnnexGenesis({
455
+ const published = await publishClientAnnexGenesis({
416
456
  was,
417
457
  wasServerUrl,
418
458
  spaceId,
@@ -425,6 +465,10 @@ export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, s
425
465
  signer: await updateKeySigner({ seed: rung.seed }),
426
466
  ...(capability !== undefined ? { capability } : {})
427
467
  });
468
+ return {
469
+ ...published,
470
+ ...(spaceDescription !== undefined ? { spaceDescription } : {})
471
+ };
428
472
  }
429
473
  /**
430
474
  * The type IRI of the account document's delegated-clients service entry --
@@ -481,6 +525,61 @@ export function delegatedClientsPointer({ doc }) {
481
525
  }
482
526
  return undefined;
483
527
  }
528
+ /**
529
+ * Every auxiliary annex Space the account log's `#DelegatedClients` pointer
530
+ * has ever named, oldest first and one entry per Space. A pointer entry is
531
+ * append-only: a superseded value stops being current but its Space survives
532
+ * the move, so an enumeration that reads only the resolved document (the
533
+ * {@link delegatedClientsPointer} case) misses every Space a generation swap
534
+ * has left behind. Several generations of one Space collapse to the entry
535
+ * that named it first.
536
+ *
537
+ * The account deletion ceremony is the reader: it must name each auxiliary
538
+ * Space it is about to destroy, and the log is the only durable record of
539
+ * the superseded ones. Each entry carries the annex DID and its host beside
540
+ * the Space id, because an account that has migrated hosts leaves entries
541
+ * this deployment cannot address: deleting `spaceId` on the CURRENT host
542
+ * would address a Space that is not the one the entry names, and a 404 there
543
+ * would otherwise read as a clean deletion. Such an entry is the caller's to
544
+ * report as a residue.
545
+ *
546
+ * The acting unlock record's own `delegatedClients` sibling target is NOT
547
+ * included here; a caller that wants it unions it in itself, since a torn
548
+ * establishment can converge on a Space no pointer entry ever named.
549
+ *
550
+ * An entry carrying no document state, and an endpoint that does not parse
551
+ * as a client-annex DID, are skipped rather than refused: the walk is an
552
+ * enumeration aid, and a caller cannot act on an id it could not read.
553
+ *
554
+ * @param options {object}
555
+ * @param options.log {DIDLog} the VERIFIED account log
556
+ * @returns {Array<{ did: string, host: string, spaceId: string }>} the
557
+ * Spaces, in log order
558
+ */
559
+ export function delegatedClientsSpaceHistory({ log }) {
560
+ const spaces = [];
561
+ for (const entry of log) {
562
+ if (entry?.state === undefined || entry.state === null) {
563
+ continue;
564
+ }
565
+ const pointed = delegatedClientsPointer({ doc: entry.state });
566
+ if (pointed === undefined) {
567
+ continue;
568
+ }
569
+ let parts;
570
+ try {
571
+ parts = clientAnnexDidParts({ did: pointed });
572
+ }
573
+ catch {
574
+ continue;
575
+ }
576
+ if (spaces.some(space => space.spaceId === parts.spaceId)) {
577
+ continue;
578
+ }
579
+ spaces.push({ did: pointed, host: parts.host, spaceId: parts.spaceId });
580
+ }
581
+ return spaces;
582
+ }
484
583
  /**
485
584
  * The account document's `service` array with the delegated-clients pointer
486
585
  * set to `clientAnnexDid`. An existing pointer entry is re-pointed in place,
@@ -901,33 +1000,42 @@ function withGenerationDelegationEntry({ doc, clientAnnexDid, delegation }) {
901
1000
  ];
902
1001
  }
903
1002
  /**
904
- * Parses the auxiliary Space id and generation id out of an annex DID
905
- * string. Both are permanent substrings of every annex DID by
1003
+ * Parses the host, the auxiliary Space id and the generation id out of an
1004
+ * annex DID string. All three are permanent substrings of every annex DID by
906
1005
  * construction: the generation id is the final path segment of the annex
907
1006
  * DID (`did:webvh:<scid>:<host>:...:space:<spaceId>:<generationId>`), and it
908
1007
  * is the generation-identifying half of the annex rung HKDF
909
1008
  * labels, so this parse is what lets an enrollee derive its writing key from
910
1009
  * the pointer alone -- no log read, no registry.
911
1010
  *
1011
+ * The host is the method-specific id's second segment, percent-decoded (a
1012
+ * port rides as `%3A` inside the one segment). A caller enumerating Spaces
1013
+ * out of a log compares it against the deployment it is talking to: an
1014
+ * account that has migrated hosts carries entries naming the old one, which
1015
+ * this deployment cannot address.
1016
+ *
912
1017
  * @param options {object}
913
1018
  * @param options.did {string} an annex did:webvh string
914
- * @returns {{ spaceId: string, generationId: string }}
1019
+ * @returns {{ host: string, spaceId: string, generationId: string }}
915
1020
  */
916
1021
  export function clientAnnexDidParts({ did }) {
917
1022
  const parts = did.split(':');
918
1023
  const generationId = parts[parts.length - 1];
919
1024
  const spaceId = parts[parts.length - 2];
1025
+ const host = parts[3];
920
1026
  if (parts.length < 7 ||
921
1027
  parts[0] !== 'did' ||
922
1028
  parts[1] !== 'webvh' ||
923
1029
  parts[parts.length - 3] !== 'space' ||
924
1030
  generationId === undefined ||
925
1031
  spaceId === undefined ||
926
- spaceId.length === 0) {
1032
+ spaceId.length === 0 ||
1033
+ host === undefined ||
1034
+ host.length === 0) {
927
1035
  throw new Error(`Not a client annex did:webvh: "${did}".`);
928
1036
  }
929
1037
  assertGenerationId(generationId);
930
- return { spaceId, generationId };
1038
+ return { host: decodeURIComponent(host), spaceId, generationId };
931
1039
  }
932
1040
  /**
933
1041
  * Thrown when the published annex log commits neither the writing
@@ -944,6 +1052,37 @@ export class ClientAnnexRungUncommittedError extends Error {
944
1052
  this.name = 'ClientAnnexRungUncommittedError';
945
1053
  }
946
1054
  }
1055
+ /**
1056
+ * Derives a generation's rung-0 key for a ladder and admits it as the writer
1057
+ * of the entry about to be built: it must stand revealed in the log's
1058
+ * `updateKeys`, or its hash must stand committed in `nextKeyHashes`.
1059
+ *
1060
+ * The one implementation of the annex's writer-admission rule, which every
1061
+ * entry builder in this module applies before it mints anything. Annex
1062
+ * entries verify against the log's own hash-commitment chain, so a key that
1063
+ * is neither revealed nor committed can never be made to verify mid-log --
1064
+ * the caller supplies only the refusal message, since what a locked-out
1065
+ * credential should do next differs per ceremony.
1066
+ *
1067
+ * @param options {object}
1068
+ * @param options.ladderSeed {Uint8Array} the acting credential's ladder seed
1069
+ * @param options.generationId {string}
1070
+ * @param options.updateKeys {string[]} the keys the entry will be verified
1071
+ * against (the retired members already excluded, where a ceremony excludes
1072
+ * them)
1073
+ * @param options.nextKeyHashes {string[]} likewise
1074
+ * @param options.message {string} the refusal's message
1075
+ * @returns {Promise<{ seed: Uint8Array; keyMultibase: string }>}
1076
+ */
1077
+ async function admitActingRung({ ladderSeed, generationId, updateKeys, nextKeyHashes, message }) {
1078
+ const rung = await clientAnnexRung({ ladderSeed, generationId });
1079
+ const rungHash = await deriveNextKeyHash(rung.keyMultibase);
1080
+ if (!updateKeys.includes(rung.keyMultibase) &&
1081
+ !nextKeyHashes.includes(rungHash)) {
1082
+ throw new ClientAnnexRungUncommittedError(message);
1083
+ }
1084
+ return rung;
1085
+ }
947
1086
  /**
948
1087
  * Reads and resolves the published annex log through the narrow seam, or
949
1088
  * throws when the generation's `did.jsonl` is missing (an unpointed or
@@ -957,18 +1096,14 @@ export class ClientAnnexRungUncommittedError extends Error {
957
1096
  * @returns {Promise<PublishedWebvhLog>}
958
1097
  */
959
1098
  async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }) {
960
- // readPublishedLog only calls getIdResourceRaw, so the narrow seam is safe.
961
- const published = await readPublishedLog({
1099
+ return readPublishedLogOrThrow({
962
1100
  idStore: store,
963
1101
  ...(expectedDid !== undefined ? { expectedDid } : {}),
964
1102
  ...(pinStore !== undefined ? { pinStore } : {}),
965
- ...(logId !== undefined ? { logId } : {})
1103
+ ...(logId !== undefined ? { logId } : {}),
1104
+ missingMessage: 'client annex: did.jsonl is missing; the generation was never minted ' +
1105
+ 'or has been collected.'
966
1106
  });
967
- if (!published) {
968
- throw new Error('client annex: did.jsonl is missing; the generation was never minted or ' +
969
- 'has been collected.');
970
- }
971
- return published;
972
1107
  }
973
1108
  /**
974
1109
  * TRANSIENT ENROLLMENT: publishes one per-visit verification method into a
@@ -1032,27 +1167,56 @@ async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }
1032
1167
  * transient session passes an in-memory store)
1033
1168
  * @param [options.logId] {string} the generation's pin-slot key, from
1034
1169
  * {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
1170
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1171
+ * read and verified under this same pin slot, so the enrollment builds its
1172
+ * entry on it instead of spending a second round trip on the same log (the
1173
+ * transient visit's one-read composition). The FIRST attempt alone rides it:
1174
+ * a lost compare-and-swap means the head is stale by definition, so the
1175
+ * conflict retry re-reads under the pin. That threaded attempt is EXTRA
1176
+ * rather than one of the retry's three: a caller who saved a read is left
1177
+ * with the same conflict budget as one who did not
1035
1178
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
1036
1179
  */
1037
- export async function enrollClientAnnexTransientClient(options) {
1038
- return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(options));
1180
+ export async function enrollClientAnnexTransientClient({ published: threadedHead, ...rest }) {
1181
+ if (threadedHead !== undefined) {
1182
+ try {
1183
+ return await enrollClientAnnexTransientClientOnce({
1184
+ ...rest,
1185
+ published: threadedHead
1186
+ });
1187
+ }
1188
+ catch (err) {
1189
+ // A lost compare-and-swap on the threaded head says only that the head
1190
+ // is stale; the retry below re-reads under the pin with its whole
1191
+ // budget. Every other failure is the caller's.
1192
+ if (!(err instanceof WebvhLogConflictError)) {
1193
+ throw err;
1194
+ }
1195
+ }
1196
+ }
1197
+ return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(rest));
1039
1198
  }
1040
1199
  /**
1041
1200
  * One attempt of {@link enrollClientAnnexTransientClient}, re-invoked by the
1042
1201
  * conflict retry (with the same signing key -- static rung 0 has no
1043
- * advanced-rung retry shape).
1202
+ * advanced-rung retry shape) and with no threaded head after the first.
1044
1203
  *
1045
1204
  * @param options {object} see {@link enrollClientAnnexTransientClient}
1046
1205
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
1047
1206
  */
1048
- async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId }) {
1207
+ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, published: alreadyRead }) {
1049
1208
  assertGenerationId(generationId);
1050
- const published = await readClientAnnexLogOrThrow({
1051
- store,
1052
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1053
- ...(pinStore !== undefined ? { pinStore } : {}),
1054
- ...(logId !== undefined ? { logId } : {})
1055
- });
1209
+ const published = alreadyRead !== undefined
1210
+ ? assertPublishedLogDid({
1211
+ published: alreadyRead,
1212
+ ...(expectedDid !== undefined ? { expectedDid } : {})
1213
+ })
1214
+ : await readClientAnnexLogOrThrow({
1215
+ store,
1216
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1217
+ ...(pinStore !== undefined ? { pinStore } : {}),
1218
+ ...(logId !== undefined ? { logId } : {})
1219
+ });
1056
1220
  const { did, doc } = published;
1057
1221
  const vmId = `${did}#${transientKeyMultibase}`;
1058
1222
  // Already enrolled (a completed earlier run): the VM and everything the
@@ -1062,14 +1226,15 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1062
1226
  if (existingMethods.some(method => method.id === vmId)) {
1063
1227
  return { did, doc, log: published.log };
1064
1228
  }
1065
- const rung = await clientAnnexRung({ ladderSeed, generationId });
1066
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
1067
- const revealed = published.updateKeys.includes(rung.keyMultibase);
1068
- if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
1069
- throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
1229
+ const rung = await admitActingRung({
1230
+ ladderSeed,
1231
+ generationId,
1232
+ updateKeys: published.updateKeys,
1233
+ nextKeyHashes: published.nextKeyHashes,
1234
+ message: "client annex: the log commits neither this credential's rung-0 key nor " +
1070
1235
  'its hash; a credential bound mid-generation cannot write the ' +
1071
- 'annex until a writer commits its hash or the next GC swap does.');
1072
- }
1236
+ 'annex until a writer commits its hash or the next GC swap does.'
1237
+ });
1073
1238
  // A non-rotating entry re-states `updateKeys`, which the resolver checks
1074
1239
  // against the previous entry's commitments -- genesis enforces the
1075
1240
  // carry-over convention, and this refuses a log that lost it anyway.
@@ -1125,6 +1290,13 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1125
1290
  // The log only -- an annex has no did:web projection -- conditional on
1126
1291
  // the read this entry was built on.
1127
1292
  await putLogResource({ store, log: updated.log, ifMatch: published.etag });
1293
+ // Advance the pin to what this entry just published, so a host serving the
1294
+ // pre-entry log straight afterwards is refused as a rollback on the next
1295
+ // read (equal-to-pin would otherwise be accepted, and a later stage built on
1296
+ // the stale head would miss this entry).
1297
+ if (pinStore && logId !== undefined) {
1298
+ await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1299
+ }
1128
1300
  return { did: updated.did, doc: updated.doc, log: updated.log };
1129
1301
  }
1130
1302
  /**
@@ -1140,16 +1312,19 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1140
1312
  * entry is appended ({@link delegatedClientsServiceEntry}). Every other
1141
1313
  * service entry, the verification methods, and the relationship arrays are
1142
1314
  * preserved untouched. Idempotent: a document already pointing at the DID is
1143
- * a no-op on the log (it still heals a lagging `did.json`).
1315
+ * a no-op on the log (and, unless `logOnly`, it republishes `did.json` from
1316
+ * the resolved log, which the enrolled-client caller has the authority to
1317
+ * do).
1144
1318
  *
1145
1319
  * @param options {object}
1146
1320
  * @param options.idStore {WebvhIdStore} the ACCOUNT log's store; with
1147
1321
  * `logOnly`, only its log read and `did.jsonl` PUT are used, so the narrow
1148
1322
  * delegated seam satisfies it
1149
- * @param options.updateKeys {ClientWebvhUpdateKeys} this enrolled client's
1150
- * update-key seeds -- or the ladder-rung idiom on a ladder-anchored
1151
- * account (`{ updateSeed: rung0.seed, stagedSeed: rung1.seed }`), as the
1152
- * credential-anchored genesis and the transient-recovery continuation pass
1323
+ * @param options.signer {AccountLogSigner} who signs the pointer entry:
1324
+ * this enrolled client's update-key seeds -- or the ladder-rung idiom on a
1325
+ * ladder-anchored account (`{ updateSeed: rung0.seed, stagedSeed:
1326
+ * rung1.seed }`), as the credential-anchored genesis and the
1327
+ * transient-recovery continuation pass
1153
1328
  * @param options.clientAnnexDid {string} the generation to point at
1154
1329
  * @param [options.expectedDid] {string} the account DID the log must
1155
1330
  * resolve to, from the account pointer
@@ -1161,24 +1336,56 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1161
1336
  * @param [options.logOnly] {boolean} publish `did.jsonl` only, never the
1162
1337
  * `did.json` projection -- the transient-recovery continuation writing
1163
1338
  * through the record's bridge delegation, whose narrow scope covers nothing
1164
- * but the log. The projection heals at the next authorized write (the log
1165
- * is the source of truth)
1166
- * @returns {Promise<{ did: string, doc: DIDDoc }>}
1339
+ * but the log. The projection then lags until some caller holding an
1340
+ * `id`-collection writer runs `ensureDidWebProjection` over the resolved
1341
+ * log; on a client-less account that is a transient visit under its
1342
+ * generation delegation. The log stays the source of truth meanwhile, and
1343
+ * the server reads it rather than the projection, so the lag is a `did:web`
1344
+ * verifier's concern alone
1345
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1346
+ * read and verified under this same pin slot, so the pointer entry builds
1347
+ * on it instead of spending a second round trip on the log the caller just
1348
+ * read or published (the establishment's one-read composition). The FIRST
1349
+ * attempt alone rides it: a lost compare-and-swap means the head is stale
1350
+ * by definition, so the conflict retry re-reads under the pin. That
1351
+ * threaded attempt is EXTRA rather than one of the retry's three: a caller
1352
+ * who saved a read is left with the same conflict budget as one who did not
1353
+ * @returns {Promise<{ did: string, doc: DIDDoc,
1354
+ * published: PublishedWebvhLog }>} `published` is the head this call
1355
+ * leaves standing: the post-entry one, paired with its publish's own ETag,
1356
+ * when the entry was appended; the head it stood on verbatim when the
1357
+ * document already pointed at the DID
1167
1358
  */
1168
- export async function setDelegatedClientsPointer(options) {
1169
- return withLogConflictRetry(() => setDelegatedClientsPointerOnce(options));
1359
+ export async function setDelegatedClientsPointer({ published: threadedHead, ...rest }) {
1360
+ if (threadedHead !== undefined) {
1361
+ try {
1362
+ return await setDelegatedClientsPointerOnce({
1363
+ ...rest,
1364
+ published: threadedHead
1365
+ });
1366
+ }
1367
+ catch (err) {
1368
+ // A lost compare-and-swap on the threaded head says only that the head
1369
+ // is stale; the retry below re-reads under the pin with its whole
1370
+ // budget. Every other failure is the caller's.
1371
+ if (!(err instanceof WebvhLogConflictError)) {
1372
+ throw err;
1373
+ }
1374
+ }
1375
+ }
1376
+ return withLogConflictRetry(() => setDelegatedClientsPointerOnce(rest));
1170
1377
  }
1171
1378
  /**
1172
1379
  * ONE attempt of {@link setDelegatedClientsPointer}, re-invoked by that
1173
1380
  * function's conflict retry and exported for a caller that runs its own.
1174
1381
  * A caller whose retry loop also attributes the signing rung must use this
1175
1382
  * form: the wrapper's inner retry would re-invoke the attempt with the SAME
1176
- * `updateKeys`, and a rung a racing ceremony consumed meanwhile can never
1383
+ * signer, and a rung a racing ceremony consumed meanwhile can never
1177
1384
  * become authorized by re-reading, so the attempt would fail on the plain
1178
1385
  * not-authorized refusal instead of surfacing the conflict the caller's loop
1179
1386
  * knows how to re-attribute from.
1180
1387
  *
1181
- * A caller that already read the head its `updateKeys` were attributed
1388
+ * A caller that already read the head its signer was attributed
1182
1389
  * against passes it as `published`, and the entry is built on exactly that
1183
1390
  * head. A racing entry landing in between then loses the CAS on the PUT and
1184
1391
  * surfaces as a {@link WebvhLogConflictError}, which is what a re-attributing
@@ -1189,72 +1396,64 @@ export async function setDelegatedClientsPointer(options) {
1189
1396
  * @param [options.published] {PublishedWebvhLog} the verified head to build
1190
1397
  * this entry on, when the caller has already read it under the same pin;
1191
1398
  * absent, the attempt reads the head itself
1192
- * @returns {Promise<{ did: string, doc: DIDDoc }>}
1399
+ * @returns {Promise<{ did: string, doc: DIDDoc,
1400
+ * published: PublishedWebvhLog }>}
1193
1401
  */
1194
- export async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false, published: alreadyRead }) {
1402
+ export async function setDelegatedClientsPointerOnce({ idStore, signer, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false, published: alreadyRead }) {
1195
1403
  // Refuses a malformed target before anything is read or written.
1196
1404
  clientAnnexDidParts({ did: clientAnnexDid });
1197
- const published = alreadyRead ??
1198
- (await readPublishedLog({
1199
- idStore,
1200
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1201
- ...(pinStore !== undefined ? { pinStore } : {}),
1202
- ...(logId !== undefined ? { logId } : {})
1203
- }));
1204
- if (!published) {
1205
- throw new Error('did:webvh: did.jsonl is missing; nothing to point at a client annex.');
1206
- }
1207
- const { did, doc } = published;
1208
- if (delegatedClientsPointer({ doc }) === clientAnnexDid) {
1209
- if (!logOnly) {
1210
- await concludeWithPublishedLog({ idStore, published });
1211
- }
1212
- return { did, doc };
1213
- }
1214
- // The entry is signed by this client's active update key; a log that does
1215
- // not authorize it (a rotation torn elsewhere) must heal first.
1216
- const activeKey = await updateKeyMultibase({ seed: updateKeys.updateSeed });
1217
- if (!published.updateKeys.includes(activeKey)) {
1218
- throw new Error("did:webvh: the published log does not authorize this client's active " +
1219
- 'update key; finalize the pending rotation before re-pointing the ' +
1220
- 'delegated-clients entry.');
1221
- }
1222
- await assertCarryOverCommitments({ published });
1223
- const services = servicesPointedAtClientAnnex({
1224
- doc,
1225
- accountDid: did,
1226
- clientAnnexDid
1227
- });
1228
- const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
1229
- const updated = await updateDID({
1230
- log: published.log,
1405
+ let settled;
1406
+ const outcome = await signAccountEntry({
1407
+ idStore,
1231
1408
  signer,
1232
- alsoKnownAsWeb: true,
1233
- // Re-stated unchanged (the library requires them explicitly while
1234
- // prerotation is active); the carry-over commitments are what make the
1235
- // re-statement resolvable.
1236
- updateKeys: published.updateKeys,
1237
- nextKeyHashes: published.nextKeyHashes,
1238
- services
1409
+ ...(alreadyRead !== undefined ? { published: alreadyRead } : {}),
1410
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1411
+ ...(pinStore ? { pinStore } : {}),
1412
+ ...(logId !== undefined ? { logId } : {}),
1413
+ missingMessage: 'did:webvh: did.jsonl is missing; nothing to point at a client annex.',
1414
+ verb: 're-pointing the delegated-clients entry',
1415
+ logOnly,
1416
+ build: async ({ published }) => {
1417
+ const { did, doc } = published;
1418
+ if (delegatedClientsPointer({ doc }) === clientAnnexDid) {
1419
+ if (!logOnly && signer.kind === 'client') {
1420
+ await concludeWithPublishedLog({ idStore, published });
1421
+ }
1422
+ // The head verbatim: the projection PUT touches no log, so this
1423
+ // read's own ETag is still the log's validator.
1424
+ settled = { did, doc, published };
1425
+ return undefined;
1426
+ }
1427
+ return {
1428
+ services: servicesPointedAtClientAnnex({
1429
+ doc,
1430
+ accountDid: did,
1431
+ clientAnnexDid
1432
+ })
1433
+ };
1434
+ }
1239
1435
  });
1240
- if (logOnly) {
1241
- await putLogResource({
1242
- store: idStore,
1243
- log: updated.log,
1244
- ifMatch: published.etag
1245
- });
1246
- }
1247
- else {
1248
- await publishUpdatedLog({ idStore, updated, ifMatch: published.etag });
1436
+ if (settled) {
1437
+ return settled;
1249
1438
  }
1250
- // Advance the pin to what this entry just published, so a host serving the
1251
- // pre-entry log straight afterwards is refused as a rollback on the next
1252
- // read (equal-to-pin would otherwise be accepted, and a composition built
1253
- // on the stale head would re-heal and mint litter).
1254
- if (pinStore && logId !== undefined) {
1255
- await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1256
- }
1257
- return { did: updated.did, doc: updated.doc };
1439
+ const updated = outcome.updated;
1440
+ return {
1441
+ did: updated.did,
1442
+ doc: updated.doc,
1443
+ // The post-entry head, from what `updateDID` already resolved plus this
1444
+ // publish's own validator: the update-key parameters are the ones the
1445
+ // entry re-stated unchanged above, so nothing is re-resolved or re-read.
1446
+ published: {
1447
+ log: updated.log,
1448
+ did: updated.did,
1449
+ // Detached from the entry's own `state`, as every other producer of
1450
+ // this type is, so a consumer editing the document cannot edit the log.
1451
+ doc: structuredClone(updated.doc),
1452
+ updateKeys: updated.meta.updateKeys,
1453
+ nextKeyHashes: updated.meta.nextKeyHashes,
1454
+ ...(outcome.etag !== undefined ? { etag: outcome.etag } : {})
1455
+ }
1456
+ };
1258
1457
  }
1259
1458
  /**
1260
1459
  * The whole transient-enrollment ceremony as the enrollee runs it: resolve
@@ -1291,9 +1490,15 @@ export async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clie
1291
1490
  * @param [options.maxRounds] {number} how many pointer moves to chase
1292
1491
  * before giving up (a GC pass is quarterly, so more than one mid-ceremony
1293
1492
  * move means something else is wrong)
1493
+ * @param [options.published] {PublishedWebvhLog} a generation head the
1494
+ * caller already read and verified under this same pin store (the readiness
1495
+ * stage's, when it published nothing to that log). Round 0 alone rides it,
1496
+ * and only when it is the generation the account document points at NOW: a
1497
+ * head for any other generation is ignored and the round reads fresh, as
1498
+ * every later round does
1294
1499
  * @returns {Promise<{ clientAnnexDid: string, doc: DIDDoc, log: DIDLog }>}
1295
1500
  */
1296
- export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3 }) {
1501
+ export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3, published: threadedHead }) {
1297
1502
  let accountDoc = await readAccountDocument();
1298
1503
  for (let round = 0; round < maxRounds; round++) {
1299
1504
  const clientAnnexDid = delegatedClientsPointer({ doc: accountDoc });
@@ -1304,12 +1509,19 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1304
1509
  const { spaceId, generationId } = clientAnnexDidParts({
1305
1510
  did: clientAnnexDid
1306
1511
  });
1512
+ // Round 0's threaded head, and only for the generation the pointer names:
1513
+ // a head read before a GC swap belongs to the abandoned generation and
1514
+ // says nothing about the one this round enrolls into.
1515
+ const head = round === 0 && threadedHead?.did === clientAnnexDid
1516
+ ? threadedHead
1517
+ : undefined;
1307
1518
  const enrolled = await enrollClientAnnexTransientClient({
1308
1519
  store: storeForGenerationId(generationId),
1309
1520
  ladderSeed,
1310
1521
  generationId,
1311
1522
  transientKeyMultibase,
1312
1523
  expectedDid: clientAnnexDid,
1524
+ ...(head !== undefined ? { published: head } : {}),
1313
1525
  ...(mintDelegation !== undefined
1314
1526
  ? { mintGenerationDelegation: mintDelegation }
1315
1527
  : {}),
@@ -1328,12 +1540,98 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1328
1540
  throw new Error('client annex: the delegated-clients pointer kept moving across ' +
1329
1541
  `${String(maxRounds)} enrollment rounds; giving up.`);
1330
1542
  }
1543
+ /**
1544
+ * The fresh-generation block every separate-pointer-entry caller runs: mint
1545
+ * a generation in the annex Space, install its generation delegation, then
1546
+ * append the account document's pointer entry naming it. The pointer write is
1547
+ * the injected step (`point`): the establishment signs it as a client under
1548
+ * a rung the caller attributed, the transient visit moves it as the ladder
1549
+ * with its reveal-and-retry form. A caller with a step that must land
1550
+ * between the install and the pointer entry (the establishment's bootstrap
1551
+ * arm, whose Space still answers to its creation controller until it flips
1552
+ * it to the account DID) supplies `beforePointerEntry`.
1553
+ *
1554
+ * The install stands on the head the mint just published rather than
1555
+ * re-reading the log this run wrote a moment ago -- but only when that head
1556
+ * carries the PUT's own ETag, since the install's entry publishes under a
1557
+ * compare-and-swap and a head with no validator would degrade that to an
1558
+ * unconditional write. With no ETag the install reads for itself. Either way
1559
+ * its own publish advances the pin, so this generation's pin slot is
1560
+ * established by this run.
1561
+ *
1562
+ * @param options {object}
1563
+ * @param options.was {WasClient} the storage client the mint and the
1564
+ * install write through
1565
+ * @param options.wasServerUrl {string}
1566
+ * @param options.spaceId {string} the auxiliary annex Space's id
1567
+ * @param options.controller {string} the Space's creation controller (see
1568
+ * {@link mintCredentialClientAnnexGeneration})
1569
+ * @param options.ladderSeed {Uint8Array} the minting credential's ladder
1570
+ * seed
1571
+ * @param options.mintGenerationDelegation {Function}
1572
+ * `({ clientAnnexDid }) => Promise<IZcap>`
1573
+ * @param options.point {Function} `(clientAnnexDid) => Promise<T>` -- the
1574
+ * pointer entry, whose result rides the return verbatim
1575
+ * @param [options.capability] {IZcap} the sibling delegation the annex
1576
+ * writes ride, for a caller holding a standing invocation authority
1577
+ * @param [options.beforePointerEntry] {Function}
1578
+ * `({ minted }) => Promise<void>` -- runs after the install and before the
1579
+ * pointer entry, with the mint's published head (its `spaceDescription`
1580
+ * included)
1581
+ * @param [options.pinStore] {ResourceLogPinStore} chain-head pins; the
1582
+ * generation's slot is derived here
1583
+ * @param [options.now] {number} epoch milliseconds, for tests
1584
+ * @returns {Promise<{ clientAnnexDid: string, generationDelegation: IZcap,
1585
+ * pointed: T }>}
1586
+ */
1587
+ export async function mintPointedClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, ladderSeed, mintGenerationDelegation, point, capability, beforePointerEntry, pinStore, now }) {
1588
+ const minted = await mintCredentialClientAnnexGeneration({
1589
+ was,
1590
+ wasServerUrl,
1591
+ spaceId,
1592
+ controller,
1593
+ ladderSeed,
1594
+ ...(capability !== undefined ? { capability } : {})
1595
+ });
1596
+ const ensured = await ensureGenerationDelegationCurrent({
1597
+ store: clientAnnexLogStore({
1598
+ was,
1599
+ spaceId,
1600
+ generationId: minted.generationId,
1601
+ ...(capability !== undefined ? { capability } : {})
1602
+ }),
1603
+ ladderSeed,
1604
+ generationId: minted.generationId,
1605
+ mintGenerationDelegation,
1606
+ expectedDid: minted.did,
1607
+ ...(minted.etag !== undefined ? { published: minted } : {}),
1608
+ ...(pinStore !== undefined
1609
+ ? {
1610
+ pinStore,
1611
+ logId: clientAnnexLogPinId({
1612
+ spaceId,
1613
+ generationId: minted.generationId
1614
+ })
1615
+ }
1616
+ : {}),
1617
+ ...(now !== undefined ? { now } : {})
1618
+ });
1619
+ if (beforePointerEntry !== undefined) {
1620
+ await beforePointerEntry({ minted });
1621
+ }
1622
+ const pointed = await point(minted.did);
1623
+ return {
1624
+ clientAnnexDid: minted.did,
1625
+ generationDelegation: ensured.delegation,
1626
+ pointed
1627
+ };
1628
+ }
1331
1629
  /**
1332
1630
  * RENEW PRECEDES MINT: the blocking pre-mint stage a transient App Connect
1333
1631
  * approval runs before delegating any grant. Reads the annex document
1334
1632
  * and hands back its embedded generation delegation -- renewing it first
1335
- * when it is expired or inside the 30-day renewal window ({@link
1336
- * zcapExpiring}): a fresh delegation is minted through the caller's closure
1633
+ * when it is stale under the house policy ({@link standingZcapStale}): a
1634
+ * fresh delegation is minted through the caller's closure
1337
1635
  * (ladder-signed -- the renewal must not depend on the very delegation it
1338
1636
  * replaces; published through the store, which in a transient session is
1339
1637
  * the credential's sibling delegation, so even a hard-expired delegation is
@@ -1344,14 +1642,16 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1344
1642
  * same way (the GC ceremony's own install stage and the first-VM install
1345
1643
  * make this rare; a heal, not a policy).
1346
1644
  *
1347
- * Beside the expiry axis, an `accountDoc` adds the SIGNER-DEATH axis: a
1348
- * standing delegation whose proof key is no longer in the supplied verified
1349
- * account document has rotted under the current-key-set rule (the enrolled
1350
- * client that minted it was revoked, or the ladder VM that signed it left
1351
- * with the first self-enrollment) and is replaced the same way. No
1352
- * revocation POST accompanies the replacement: a rotted chain no longer
1353
- * verifies at the revocation endpoint, and the expiry-renewal path never
1354
- * revoked either.
1645
+ * That policy is three axes. Expiry -- past, or inside the 30-day renewal
1646
+ * window. SIGNER DEATH, which an `accountDoc` adds: a standing delegation
1647
+ * whose proof key is no longer in the supplied verified account document has
1648
+ * rotted under the current-key-set rule (the enrolled client that minted it
1649
+ * was revoked, or the ladder VM that signed it left with the first
1650
+ * self-enrollment). And RETIREMENT, the projected post-edit reading a caller
1651
+ * asks for with `retiringKeyMultibases`, for a key still listed whose
1652
+ * authority the ceremony is about to end. No revocation POST accompanies the
1653
+ * replacement: a rotted chain no longer verifies at the revocation endpoint,
1654
+ * and the expiry-renewal path never revoked either.
1355
1655
  *
1356
1656
  * Failure is the caller's failure: a renewal that cannot complete throws,
1357
1657
  * and the App Connect approval fails with the standard retryable-ceremony
@@ -1380,64 +1680,96 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1380
1680
  * @param [options.accountDoc] {PublishedKeyDocument} the locally VERIFIED
1381
1681
  * account document; supplied, a standing delegation whose proof key it no
1382
1682
  * longer lists is replaced (the signer-death axis above)
1383
- * @param [options.force] {boolean} replace the embedded delegation
1384
- * unconditionally, however healthy it looks -- the last-client
1385
- * forget's replacement stage, where the standing delegation has just been
1386
- * revoked server-side (a state no client-side predicate can read)
1683
+ * @param [options.retiringKeyMultibases] {string[]} keys whose authority is
1684
+ * about to end, read as a projected post-edit document: a standing
1685
+ * delegation one of them signed is replaced even though the served document
1686
+ * still lists the key. The last-client transition's replacement stage names
1687
+ * its own ladder VM (whose delegations it revokes server-side in the next
1688
+ * breath, a state no client-side predicate can read) and the client its
1689
+ * removal entry has yet to strike
1387
1690
  * @param [options.now] {number} epoch milliseconds, for tests
1388
- * @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
1691
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1692
+ * read and verified under this same pin slot, so the stage builds on it
1693
+ * instead of spending a second round trip on the same log (the transient
1694
+ * visit's one-read composition). The FIRST attempt alone rides it: a lost
1695
+ * compare-and-swap means the head is stale by definition, so the conflict
1696
+ * retry re-reads under the pin. That threaded attempt is EXTRA rather than
1697
+ * one of the retry's three: a caller who saved a read is left with the same
1698
+ * conflict budget as one who did not
1699
+ * @returns {Promise<{ delegation: IZcap, renewed: boolean,
1700
+ * published?: PublishedWebvhLog }>} `published` is the verified head this
1701
+ * pass stood on -- the one read, or the one handed in -- and is present
1702
+ * ONLY when `renewed` is false. The two store implementations differ on
1703
+ * what a renewal's publish hands back: the controller-tier store forwards
1704
+ * the PUT's own ETag, while the delegated store discards the response and
1705
+ * yields none. This stage takes either, and cannot tell which from here, so
1706
+ * it stays conservative and passes no post-renewal head on; a caller
1707
+ * wanting one reads for itself
1389
1708
  */
1390
- export async function ensureGenerationDelegationCurrent(options) {
1391
- return withLogConflictRetry(() => ensureGenerationDelegationCurrentOnce(options));
1709
+ export async function ensureGenerationDelegationCurrent({ published: threadedHead, ...rest }) {
1710
+ if (threadedHead !== undefined) {
1711
+ try {
1712
+ return await ensureGenerationDelegationCurrentOnce({
1713
+ ...rest,
1714
+ published: threadedHead
1715
+ });
1716
+ }
1717
+ catch (err) {
1718
+ // A lost compare-and-swap on the threaded head says only that the head
1719
+ // is stale; the retry below re-reads under the pin with its whole
1720
+ // budget. Every other failure is the caller's.
1721
+ if (!(err instanceof WebvhLogConflictError)) {
1722
+ throw err;
1723
+ }
1724
+ }
1725
+ }
1726
+ return withLogConflictRetry(() => ensureGenerationDelegationCurrentOnce(rest));
1392
1727
  }
1393
1728
  /**
1394
1729
  * One attempt of {@link ensureGenerationDelegationCurrent}, re-invoked by the
1395
1730
  * conflict retry (with the same signing key -- static rung 0 has no
1396
- * advanced-rung retry shape).
1731
+ * advanced-rung retry shape) and with no threaded head after the first.
1397
1732
  *
1398
1733
  * @param options {object} see {@link ensureGenerationDelegationCurrent}
1399
- * @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
1734
+ * @returns {Promise<{ delegation: IZcap, renewed: boolean,
1735
+ * published?: PublishedWebvhLog }>}
1400
1736
  */
1401
- async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, force = false, now }) {
1737
+ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, retiringKeyMultibases = [], now, published: alreadyRead }) {
1402
1738
  assertGenerationId(generationId);
1403
- const published = await readClientAnnexLogOrThrow({
1404
- store,
1405
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1406
- ...(pinStore !== undefined ? { pinStore } : {}),
1407
- ...(logId !== undefined ? { logId } : {})
1408
- });
1739
+ const published = alreadyRead !== undefined
1740
+ ? assertPublishedLogDid({
1741
+ published: alreadyRead,
1742
+ ...(expectedDid !== undefined ? { expectedDid } : {})
1743
+ })
1744
+ : await readClientAnnexLogOrThrow({
1745
+ store,
1746
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1747
+ ...(pinStore !== undefined ? { pinStore } : {}),
1748
+ ...(logId !== undefined ? { logId } : {})
1749
+ });
1409
1750
  const { did, doc } = published;
1410
1751
  const standing = embeddedGenerationDelegation({ doc });
1411
- const signerRotted = standing !== undefined &&
1412
- accountDoc !== undefined &&
1413
- !delegationKeyInDocument({
1414
- doc: accountDoc,
1415
- ...(delegationProofKeyId(standing) !== undefined
1416
- ? { delegationKeyId: delegationProofKeyId(standing) }
1417
- : {})
1418
- });
1419
- if (!force &&
1420
- standing !== undefined &&
1421
- !signerRotted &&
1422
- !zcapExpiring({
1423
- ...(standing.expires !== undefined
1424
- ? { expires: standing.expires }
1425
- : {}),
1752
+ if (standing !== undefined &&
1753
+ !standingZcapStale({
1754
+ zcap: standing,
1755
+ ...(accountDoc !== undefined ? { doc: accountDoc } : {}),
1756
+ retiringKeyMultibases,
1426
1757
  ...(now !== undefined ? { now } : {})
1427
1758
  })) {
1428
- return { delegation: standing, renewed: false };
1759
+ return { delegation: standing, renewed: false, published };
1429
1760
  }
1430
1761
  // The rung refusal precedes the mint: nothing is delegated for a writer
1431
1762
  // who cannot publish the entry that would carry it.
1432
- const rung = await clientAnnexRung({ ladderSeed, generationId });
1433
- const rungHash = await deriveNextKeyHash(rung.keyMultibase);
1434
- const revealed = published.updateKeys.includes(rung.keyMultibase);
1435
- if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
1436
- throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
1763
+ const rung = await admitActingRung({
1764
+ ladderSeed,
1765
+ generationId,
1766
+ updateKeys: published.updateKeys,
1767
+ nextKeyHashes: published.nextKeyHashes,
1768
+ message: "client annex: the log commits neither this credential's rung-0 key nor " +
1437
1769
  'its hash; a credential bound mid-generation cannot renew the ' +
1438
1770
  'generation delegation until a writer commits its hash or the next ' +
1439
- 'GC swap does.');
1440
- }
1771
+ 'GC swap does.'
1772
+ });
1441
1773
  await assertCarryOverCommitments({ published });
1442
1774
  const fresh = await mintDelegation({ clientAnnexDid: did });
1443
1775
  const signer = await updateKeySigner({ seed: rung.seed });
@@ -1457,6 +1789,13 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, genera
1457
1789
  })
1458
1790
  });
1459
1791
  await putLogResource({ store, log: updated.log, ifMatch: published.etag });
1792
+ // Advance the pin to what this entry just published, so a host serving the
1793
+ // pre-entry log straight afterwards is refused as a rollback on the next
1794
+ // read (equal-to-pin would otherwise be accepted, and a later stage built on
1795
+ // the stale head would miss this entry).
1796
+ if (pinStore && logId !== undefined) {
1797
+ await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1798
+ }
1460
1799
  return { delegation: fresh, renewed: true };
1461
1800
  }
1462
1801
  /**
@@ -1527,17 +1866,15 @@ async function retireClientAnnexRungOnce({ store, retiredLadderSeed, actingLadde
1527
1866
  }
1528
1867
  // The acting rung must be committed AFTER the retired members are
1529
1868
  // excluded, so the retired credential can never sign its own strike.
1530
- const acting = await clientAnnexRung({
1869
+ const acting = await admitActingRung({
1531
1870
  ladderSeed: actingLadderSeed,
1532
- generationId
1533
- });
1534
- const actingHash = await deriveNextKeyHash(acting.keyMultibase);
1535
- const revealed = remainingKeys.includes(acting.keyMultibase);
1536
- if (!revealed && !remainingHashes.includes(actingHash)) {
1537
- throw new ClientAnnexRungUncommittedError("client annex: the log commits neither the acting credential's rung-0 " +
1871
+ generationId,
1872
+ updateKeys: remainingKeys,
1873
+ nextKeyHashes: remainingHashes,
1874
+ message: "client annex: the log commits neither the acting credential's rung-0 " +
1538
1875
  'key nor its hash (or it is the retired rung itself); the strike ' +
1539
- 'needs a distinct committed writer -- swap the generation instead.');
1540
- }
1876
+ 'needs a distinct committed writer -- swap the generation instead.'
1877
+ });
1541
1878
  await assertCarryOverCommitments({ published });
1542
1879
  const signer = await updateKeySigner({ seed: acting.seed });
1543
1880
  const updated = await updateDID({
@@ -1617,17 +1954,15 @@ async function commitClientAnnexRungOnce({ store, boundLadderSeed, actingLadderS
1617
1954
  // generation the bound credential itself minted.
1618
1955
  return { committed: false };
1619
1956
  }
1620
- const acting = await clientAnnexRung({
1957
+ const acting = await admitActingRung({
1621
1958
  ladderSeed: actingLadderSeed,
1622
- generationId
1623
- });
1624
- const actingHash = await deriveNextKeyHash(acting.keyMultibase);
1625
- const revealed = published.updateKeys.includes(acting.keyMultibase);
1626
- if (!revealed && !published.nextKeyHashes.includes(actingHash)) {
1627
- throw new ClientAnnexRungUncommittedError("client annex: the log commits neither the acting credential's rung-0 " +
1959
+ generationId,
1960
+ updateKeys: published.updateKeys,
1961
+ nextKeyHashes: published.nextKeyHashes,
1962
+ message: "client annex: the log commits neither the acting credential's rung-0 " +
1628
1963
  'key nor its hash; a commit entry needs a committed writer -- the ' +
1629
- 'bound credential stays locked out until the next GC swap.');
1630
- }
1964
+ 'bound credential stays locked out until the next GC swap.'
1965
+ });
1631
1966
  await assertCarryOverCommitments({ published });
1632
1967
  const signer = await updateKeySigner({ seed: acting.seed });
1633
1968
  const updated = await updateDID({