@interop/wallet-core 0.62.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +41 -16
  2. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts +46 -26
  3. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts.map +1 -1
  4. package/dist/clientAnnex/credentialAnchoredGenesis.js +91 -39
  5. package/dist/clientAnnex/credentialAnchoredGenesis.js.map +1 -1
  6. package/dist/clientAnnex/establish.d.ts +55 -34
  7. package/dist/clientAnnex/establish.d.ts.map +1 -1
  8. package/dist/clientAnnex/establish.js +102 -56
  9. package/dist/clientAnnex/establish.js.map +1 -1
  10. package/dist/clientAnnex/forget.d.ts +16 -2
  11. package/dist/clientAnnex/forget.d.ts.map +1 -1
  12. package/dist/clientAnnex/forget.js +11 -9
  13. package/dist/clientAnnex/forget.js.map +1 -1
  14. package/dist/clientAnnex/forgetLast.d.ts +62 -18
  15. package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
  16. package/dist/clientAnnex/forgetLast.js +92 -26
  17. package/dist/clientAnnex/forgetLast.js.map +1 -1
  18. package/dist/clientAnnex/gc.d.ts +3 -2
  19. package/dist/clientAnnex/gc.d.ts.map +1 -1
  20. package/dist/clientAnnex/gc.js +7 -7
  21. package/dist/clientAnnex/gc.js.map +1 -1
  22. package/dist/clientAnnex/heal.d.ts +38 -5
  23. package/dist/clientAnnex/heal.d.ts.map +1 -1
  24. package/dist/clientAnnex/heal.js +346 -236
  25. package/dist/clientAnnex/heal.js.map +1 -1
  26. package/dist/clientAnnex/index.d.ts +21 -5
  27. package/dist/clientAnnex/index.d.ts.map +1 -1
  28. package/dist/clientAnnex/index.js +24 -5
  29. package/dist/clientAnnex/index.js.map +1 -1
  30. package/dist/clientAnnex/ladder.d.ts +58 -0
  31. package/dist/clientAnnex/ladder.d.ts.map +1 -1
  32. package/dist/clientAnnex/ladder.js +222 -33
  33. package/dist/clientAnnex/ladder.js.map +1 -1
  34. package/dist/clientAnnex/ladderAnchored.d.ts +148 -22
  35. package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
  36. package/dist/clientAnnex/ladderAnchored.js +391 -346
  37. package/dist/clientAnnex/ladderAnchored.js.map +1 -1
  38. package/dist/clientAnnex/log.d.ts +162 -43
  39. package/dist/clientAnnex/log.d.ts.map +1 -1
  40. package/dist/clientAnnex/log.js +423 -192
  41. package/dist/clientAnnex/log.js.map +1 -1
  42. package/dist/clientAnnex/mend.d.ts +12 -6
  43. package/dist/clientAnnex/mend.d.ts.map +1 -1
  44. package/dist/clientAnnex/mend.js +26 -12
  45. package/dist/clientAnnex/mend.js.map +1 -1
  46. package/dist/clientAnnex/recoveryLadderAnchored.d.ts +6 -4
  47. package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
  48. package/dist/clientAnnex/recoveryLadderAnchored.js +56 -35
  49. package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -1
  50. package/dist/clientAnnex/spaceCapability.d.ts +123 -0
  51. package/dist/clientAnnex/spaceCapability.d.ts.map +1 -0
  52. package/dist/clientAnnex/spaceCapability.js +152 -0
  53. package/dist/clientAnnex/spaceCapability.js.map +1 -0
  54. package/dist/clientAnnex/stages.d.ts +63 -0
  55. package/dist/clientAnnex/stages.d.ts.map +1 -0
  56. package/dist/clientAnnex/stages.js +64 -0
  57. package/dist/clientAnnex/stages.js.map +1 -0
  58. package/dist/clientAnnex/zcap.d.ts +1 -1
  59. package/dist/clientAnnex/zcap.d.ts.map +1 -1
  60. package/dist/clientAnnex/zcap.js +42 -35
  61. package/dist/clientAnnex/zcap.js.map +1 -1
  62. package/dist/clients/policy.d.ts +15 -1
  63. package/dist/clients/policy.d.ts.map +1 -1
  64. package/dist/clients/policy.js +12 -6
  65. package/dist/clients/policy.js.map +1 -1
  66. package/dist/clients/revocation.d.ts +18 -10
  67. package/dist/clients/revocation.d.ts.map +1 -1
  68. package/dist/clients/revocation.js +13 -5
  69. package/dist/clients/revocation.js.map +1 -1
  70. package/dist/clients/rosterPolicy.d.ts +10 -2
  71. package/dist/clients/rosterPolicy.d.ts.map +1 -1
  72. package/dist/clients/rosterPolicy.js +41 -24
  73. package/dist/clients/rosterPolicy.js.map +1 -1
  74. package/dist/descriptors/acquire.d.ts.map +1 -1
  75. package/dist/descriptors/acquire.js +6 -23
  76. package/dist/descriptors/acquire.js.map +1 -1
  77. package/dist/descriptors/cipher.d.ts.map +1 -1
  78. package/dist/descriptors/cipher.js +5 -0
  79. package/dist/descriptors/cipher.js.map +1 -1
  80. package/dist/descriptors/errors.d.ts +43 -0
  81. package/dist/descriptors/errors.d.ts.map +1 -0
  82. package/dist/descriptors/errors.js +45 -0
  83. package/dist/descriptors/errors.js.map +1 -0
  84. package/dist/descriptors/index.d.ts +5 -0
  85. package/dist/descriptors/index.d.ts.map +1 -1
  86. package/dist/descriptors/index.js +5 -0
  87. package/dist/descriptors/index.js.map +1 -1
  88. package/dist/enrollment/enrollment.d.ts +37 -10
  89. package/dist/enrollment/enrollment.d.ts.map +1 -1
  90. package/dist/enrollment/enrollment.js +52 -15
  91. package/dist/enrollment/enrollment.js.map +1 -1
  92. package/dist/genesis/accountGenesis.d.ts +42 -11
  93. package/dist/genesis/accountGenesis.d.ts.map +1 -1
  94. package/dist/genesis/accountGenesis.js +80 -22
  95. package/dist/genesis/accountGenesis.js.map +1 -1
  96. package/dist/genesis/index.d.ts +3 -1
  97. package/dist/genesis/index.d.ts.map +1 -1
  98. package/dist/genesis/index.js +3 -1
  99. package/dist/genesis/index.js.map +1 -1
  100. package/dist/identity/agents.d.ts +17 -1
  101. package/dist/identity/agents.d.ts.map +1 -1
  102. package/dist/identity/agents.js +23 -7
  103. package/dist/identity/agents.js.map +1 -1
  104. package/dist/identity/index.d.ts +3 -1
  105. package/dist/identity/index.d.ts.map +1 -1
  106. package/dist/identity/index.js +3 -1
  107. package/dist/identity/index.js.map +1 -1
  108. package/dist/index.d.ts +6 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/keyring/index.d.ts +2 -1
  113. package/dist/keyring/index.d.ts.map +1 -1
  114. package/dist/keyring/index.js +2 -1
  115. package/dist/keyring/index.js.map +1 -1
  116. package/dist/keyring/record.d.ts.map +1 -1
  117. package/dist/keyring/record.js +3 -6
  118. package/dist/keyring/record.js.map +1 -1
  119. package/dist/keyring/unlockSpace.d.ts +14 -5
  120. package/dist/keyring/unlockSpace.d.ts.map +1 -1
  121. package/dist/keyring/unlockSpace.js +31 -36
  122. package/dist/keyring/unlockSpace.js.map +1 -1
  123. package/dist/keys/index.d.ts +10 -6
  124. package/dist/keys/index.d.ts.map +1 -1
  125. package/dist/keys/index.js +9 -5
  126. package/dist/keys/index.js.map +1 -1
  127. package/dist/keys/rosterLogStore.d.ts +12 -11
  128. package/dist/keys/rosterLogStore.d.ts.map +1 -1
  129. package/dist/keys/rosterLogStore.js +15 -13
  130. package/dist/keys/rosterLogStore.js.map +1 -1
  131. package/dist/keys/rosterStore.d.ts +3 -6
  132. package/dist/keys/rosterStore.d.ts.map +1 -1
  133. package/dist/keys/rosterStore.js +10 -9
  134. package/dist/keys/rosterStore.js.map +1 -1
  135. package/dist/keys/spaceEpochs.d.ts.map +1 -1
  136. package/dist/keys/spaceEpochs.js +2 -3
  137. package/dist/keys/spaceEpochs.js.map +1 -1
  138. package/dist/keys/userKeyRoster.d.ts +57 -7
  139. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  140. package/dist/keys/userKeyRoster.js +222 -17
  141. package/dist/keys/userKeyRoster.js.map +1 -1
  142. package/dist/keys/userKeyRosterCascade.d.ts +7 -6
  143. package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
  144. package/dist/keys/userKeyRosterCascade.js +7 -6
  145. package/dist/keys/userKeyRosterCascade.js.map +1 -1
  146. package/dist/keys/wasLabelsStore.d.ts +9 -2
  147. package/dist/keys/wasLabelsStore.d.ts.map +1 -1
  148. package/dist/keys/wasLabelsStore.js +14 -6
  149. package/dist/keys/wasLabelsStore.js.map +1 -1
  150. package/dist/log.d.ts +7 -2
  151. package/dist/log.d.ts.map +1 -1
  152. package/dist/log.js +6 -1
  153. package/dist/log.js.map +1 -1
  154. package/dist/recovery/index.d.ts +12 -8
  155. package/dist/recovery/index.d.ts.map +1 -1
  156. package/dist/recovery/index.js +11 -7
  157. package/dist/recovery/index.js.map +1 -1
  158. package/dist/recovery/recoveryCode.d.ts +27 -7
  159. package/dist/recovery/recoveryCode.d.ts.map +1 -1
  160. package/dist/recovery/recoveryCode.js +18 -6
  161. package/dist/recovery/recoveryCode.js.map +1 -1
  162. package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
  163. package/dist/recovery/recoveryDelegation.js +21 -34
  164. package/dist/recovery/recoveryDelegation.js.map +1 -1
  165. package/dist/recovery/recoveryWebvh.d.ts +75 -63
  166. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  167. package/dist/recovery/recoveryWebvh.js +127 -116
  168. package/dist/recovery/recoveryWebvh.js.map +1 -1
  169. package/dist/request/classify.d.ts +32 -8
  170. package/dist/request/classify.d.ts.map +1 -1
  171. package/dist/request/classify.js +39 -14
  172. package/dist/request/classify.js.map +1 -1
  173. package/dist/request/ephemeralExchange.d.ts +1 -6
  174. package/dist/request/ephemeralExchange.d.ts.map +1 -1
  175. package/dist/request/ephemeralExchange.js.map +1 -1
  176. package/dist/request/onboarding.d.ts.map +1 -1
  177. package/dist/request/onboarding.js +2 -2
  178. package/dist/request/onboarding.js.map +1 -1
  179. package/dist/request/parse.d.ts.map +1 -1
  180. package/dist/request/parse.js +2 -3
  181. package/dist/request/parse.js.map +1 -1
  182. package/dist/resourceLog/controller.d.ts +34 -12
  183. package/dist/resourceLog/controller.d.ts.map +1 -1
  184. package/dist/resourceLog/controller.js +79 -86
  185. package/dist/resourceLog/controller.js.map +1 -1
  186. package/dist/resourceLog/document.d.ts +182 -0
  187. package/dist/resourceLog/document.d.ts.map +1 -0
  188. package/dist/resourceLog/document.js +159 -0
  189. package/dist/resourceLog/document.js.map +1 -0
  190. package/dist/resourceLog/errors.d.ts +47 -8
  191. package/dist/resourceLog/errors.d.ts.map +1 -1
  192. package/dist/resourceLog/errors.js +54 -8
  193. package/dist/resourceLog/errors.js.map +1 -1
  194. package/dist/resourceLog/index.d.ts +7 -3
  195. package/dist/resourceLog/index.d.ts.map +1 -1
  196. package/dist/resourceLog/index.js +7 -3
  197. package/dist/resourceLog/index.js.map +1 -1
  198. package/dist/resourceLog/ladderRungs.d.ts +35 -0
  199. package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
  200. package/dist/resourceLog/ladderRungs.js +352 -0
  201. package/dist/resourceLog/ladderRungs.js.map +1 -0
  202. package/dist/resourceLog/license.d.ts +42 -17
  203. package/dist/resourceLog/license.d.ts.map +1 -1
  204. package/dist/resourceLog/license.js +38 -24
  205. package/dist/resourceLog/license.js.map +1 -1
  206. package/dist/space/activity.d.ts +15 -15
  207. package/dist/space/activity.d.ts.map +1 -1
  208. package/dist/space/activity.js +15 -15
  209. package/dist/space/activity.js.map +1 -1
  210. package/dist/space/collections.d.ts +11 -0
  211. package/dist/space/collections.d.ts.map +1 -1
  212. package/dist/space/collections.js +13 -0
  213. package/dist/space/collections.js.map +1 -1
  214. package/dist/space/deleteSpace.d.ts +28 -0
  215. package/dist/space/deleteSpace.d.ts.map +1 -0
  216. package/dist/space/deleteSpace.js +44 -0
  217. package/dist/space/deleteSpace.js.map +1 -0
  218. package/dist/space/errors.d.ts.map +1 -1
  219. package/dist/space/errors.js +0 -1
  220. package/dist/space/errors.js.map +1 -1
  221. package/dist/space/index.d.ts +6 -0
  222. package/dist/space/index.d.ts.map +1 -1
  223. package/dist/space/index.js +6 -0
  224. package/dist/space/index.js.map +1 -1
  225. package/dist/space/plaintextCollection.d.ts +43 -0
  226. package/dist/space/plaintextCollection.d.ts.map +1 -0
  227. package/dist/space/plaintextCollection.js +17 -0
  228. package/dist/space/plaintextCollection.js.map +1 -0
  229. package/dist/stages.d.ts +23 -0
  230. package/dist/stages.d.ts.map +1 -0
  231. package/dist/stages.js +23 -0
  232. package/dist/stages.js.map +1 -0
  233. package/dist/sync/index.d.ts +7 -0
  234. package/dist/sync/index.d.ts.map +1 -1
  235. package/dist/sync/index.js +7 -0
  236. package/dist/sync/index.js.map +1 -1
  237. package/dist/sync/push.js +4 -4
  238. package/dist/sync/push.js.map +1 -1
  239. package/dist/sync/remint.js +2 -2
  240. package/dist/sync/remint.js.map +1 -1
  241. package/dist/sync/types.d.ts +41 -1
  242. package/dist/sync/types.d.ts.map +1 -1
  243. package/dist/sync/types.js +47 -1
  244. package/dist/sync/types.js.map +1 -1
  245. package/dist/unlock/index.d.ts +5 -1
  246. package/dist/unlock/index.d.ts.map +1 -1
  247. package/dist/unlock/index.js +5 -1
  248. package/dist/unlock/index.js.map +1 -1
  249. package/dist/unlock/retire.d.ts +35 -7
  250. package/dist/unlock/retire.d.ts.map +1 -1
  251. package/dist/unlock/retire.js +52 -14
  252. package/dist/unlock/retire.js.map +1 -1
  253. package/dist/unlock/standingClient.d.ts.map +1 -1
  254. package/dist/unlock/standingClient.js +5 -1
  255. package/dist/unlock/standingClient.js.map +1 -1
  256. package/dist/unlock/standingWebvh.d.ts +217 -35
  257. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  258. package/dist/unlock/standingWebvh.js +469 -210
  259. package/dist/unlock/standingWebvh.js.map +1 -1
  260. package/dist/webvh/accountEntry.d.ts +146 -0
  261. package/dist/webvh/accountEntry.d.ts.map +1 -0
  262. package/dist/webvh/accountEntry.js +239 -0
  263. package/dist/webvh/accountEntry.js.map +1 -0
  264. package/dist/webvh/didWeb.d.ts +12 -7
  265. package/dist/webvh/didWeb.d.ts.map +1 -1
  266. package/dist/webvh/didWeb.js +2 -2
  267. package/dist/webvh/didWeb.js.map +1 -1
  268. package/dist/webvh/didWebProjection.d.ts +164 -0
  269. package/dist/webvh/didWebProjection.d.ts.map +1 -0
  270. package/dist/webvh/didWebProjection.js +230 -0
  271. package/dist/webvh/didWebProjection.js.map +1 -0
  272. package/dist/webvh/didWebvh.d.ts +275 -136
  273. package/dist/webvh/didWebvh.d.ts.map +1 -1
  274. package/dist/webvh/didWebvh.js +307 -340
  275. package/dist/webvh/didWebvh.js.map +1 -1
  276. package/dist/webvh/enrollClient.d.ts +65 -0
  277. package/dist/webvh/enrollClient.d.ts.map +1 -0
  278. package/dist/webvh/enrollClient.js +172 -0
  279. package/dist/webvh/enrollClient.js.map +1 -0
  280. package/dist/webvh/index.d.ts +37 -12
  281. package/dist/webvh/index.d.ts.map +1 -1
  282. package/dist/webvh/index.js +33 -10
  283. package/dist/webvh/index.js.map +1 -1
  284. package/dist/webvh/listClients.d.ts +1 -35
  285. package/dist/webvh/listClients.d.ts.map +1 -1
  286. package/dist/webvh/listClients.js +2 -30
  287. package/dist/webvh/listClients.js.map +1 -1
  288. package/dist/webvh/revokeClient.d.ts +41 -11
  289. package/dist/webvh/revokeClient.d.ts.map +1 -1
  290. package/dist/webvh/revokeClient.js +99 -48
  291. package/dist/webvh/revokeClient.js.map +1 -1
  292. package/dist/webvh/standingZcap.d.ts +61 -14
  293. package/dist/webvh/standingZcap.d.ts.map +1 -1
  294. package/dist/webvh/standingZcap.js +91 -0
  295. package/dist/webvh/standingZcap.js.map +1 -1
  296. package/dist/webvh/verifyLog.d.ts +59 -5
  297. package/dist/webvh/verifyLog.d.ts.map +1 -1
  298. package/dist/webvh/verifyLog.js +76 -11
  299. package/dist/webvh/verifyLog.js.map +1 -1
  300. package/dist/webvh/wasIdStore.d.ts +8 -8
  301. package/dist/webvh/wasIdStore.d.ts.map +1 -1
  302. package/dist/webvh/wasIdStore.js +39 -14
  303. package/dist/webvh/wasIdStore.js.map +1 -1
  304. package/dist/webvh/zcap.d.ts +14 -1
  305. package/dist/webvh/zcap.d.ts.map +1 -1
  306. package/dist/webvh/zcap.js +3 -27
  307. package/dist/webvh/zcap.js.map +1 -1
  308. package/package.json +2 -2
  309. package/dist/webvh/keyAgreement.d.ts +0 -97
  310. package/dist/webvh/keyAgreement.d.ts.map +0 -1
  311. package/dist/webvh/keyAgreement.js +0 -71
  312. package/dist/webvh/keyAgreement.js.map +0 -1
@@ -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, 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
@@ -308,12 +316,19 @@ export async function ensureClientAnnexSpace({ was, spaceId, controller }) {
308
316
  * rung-0 hash, the minting credential's included
309
317
  * @param options.signer {Signer} the minting credential's rung-0 signer
310
318
  * @returns {Promise<{ did: string; generationId: string; log: DIDLog;
311
- * doc: DIDDoc }>}
319
+ * doc: DIDDoc; spaceDescription?: SpaceDescription }>} `spaceDescription`
320
+ * is the auxiliary Space's Description as the ensure read or wrote it, so a
321
+ * caller flipping the controller afterwards need not re-read it. Always
322
+ * present here, since this minter always runs the ensure.
312
323
  */
313
324
  export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
314
- await ensureClientAnnexSpace({ was, spaceId, controller });
325
+ const spaceDescription = await ensureClientAnnexSpace({
326
+ was,
327
+ spaceId,
328
+ controller
329
+ });
315
330
  const generationId = mintGenerationId();
316
- return publishClientAnnexGenesis({
331
+ const published = await publishClientAnnexGenesis({
317
332
  was,
318
333
  wasServerUrl,
319
334
  spaceId,
@@ -322,6 +337,7 @@ export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, co
322
337
  nextKeyHashes,
323
338
  signer
324
339
  });
340
+ return { ...published, spaceDescription };
325
341
  }
326
342
  /**
327
343
  * The shared genesis-publish tail of both generation minters: creates the
@@ -343,14 +359,17 @@ export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, co
343
359
  */
344
360
  async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer, capability }) {
345
361
  // 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 });
362
+ // fresh random generation id means this is always a create, so the truthful
363
+ // `current: null` skips was-client's pre-merge describe of a Collection
364
+ // that cannot exist. Plaintext on purpose: the server resolves the annex
365
+ // DID out of its own storage, and the collection is capability-gated rather
366
+ // than encrypted.
367
+ await plaintextCollection({
368
+ was,
369
+ spaceId,
370
+ collectionId: generationId,
371
+ capability
372
+ }).configure({ current: null, name: generationId, force: true });
354
373
  const created = await createClientAnnexLog({
355
374
  wasServerUrl,
356
375
  spaceId,
@@ -404,15 +423,18 @@ async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generatio
404
423
  * covers the collections beneath the Space, never the Space Description,
405
424
  * and a standing sibling delegation presupposes the auxiliary Space
406
425
  * @returns {Promise<{ did: string; generationId: string; log: DIDLog;
407
- * doc: DIDDoc }>}
426
+ * doc: DIDDoc; spaceDescription?: SpaceDescription }>} `spaceDescription`
427
+ * is the auxiliary Space's Description as the ensure read or wrote it, so a
428
+ * caller flipping the controller afterwards need not re-read it. Present
429
+ * exactly when the ensure ran, so absent under a supplied `capability`.
408
430
  */
409
431
  export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, ladderSeed, extraNextKeyHashes = [], capability }) {
410
- if (capability === undefined) {
411
- await ensureClientAnnexSpace({ was, spaceId, controller });
412
- }
432
+ const spaceDescription = capability === undefined
433
+ ? await ensureClientAnnexSpace({ was, spaceId, controller })
434
+ : undefined;
413
435
  const generationId = mintGenerationId();
414
436
  const rung = await clientAnnexRung({ ladderSeed, generationId });
415
- return publishClientAnnexGenesis({
437
+ const published = await publishClientAnnexGenesis({
416
438
  was,
417
439
  wasServerUrl,
418
440
  spaceId,
@@ -425,6 +447,10 @@ export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, s
425
447
  signer: await updateKeySigner({ seed: rung.seed }),
426
448
  ...(capability !== undefined ? { capability } : {})
427
449
  });
450
+ return {
451
+ ...published,
452
+ ...(spaceDescription !== undefined ? { spaceDescription } : {})
453
+ };
428
454
  }
429
455
  /**
430
456
  * The type IRI of the account document's delegated-clients service entry --
@@ -481,6 +507,61 @@ export function delegatedClientsPointer({ doc }) {
481
507
  }
482
508
  return undefined;
483
509
  }
510
+ /**
511
+ * Every auxiliary annex Space the account log's `#DelegatedClients` pointer
512
+ * has ever named, oldest first and one entry per Space. A pointer entry is
513
+ * append-only: a superseded value stops being current but its Space survives
514
+ * the move, so an enumeration that reads only the resolved document (the
515
+ * {@link delegatedClientsPointer} case) misses every Space a generation swap
516
+ * has left behind. Several generations of one Space collapse to the entry
517
+ * that named it first.
518
+ *
519
+ * The account deletion ceremony is the reader: it must name each auxiliary
520
+ * Space it is about to destroy, and the log is the only durable record of
521
+ * the superseded ones. Each entry carries the annex DID and its host beside
522
+ * the Space id, because an account that has migrated hosts leaves entries
523
+ * this deployment cannot address: deleting `spaceId` on the CURRENT host
524
+ * would address a Space that is not the one the entry names, and a 404 there
525
+ * would otherwise read as a clean deletion. Such an entry is the caller's to
526
+ * report as a residue.
527
+ *
528
+ * The acting unlock record's own `delegatedClients` sibling target is NOT
529
+ * included here; a caller that wants it unions it in itself, since a torn
530
+ * establishment can converge on a Space no pointer entry ever named.
531
+ *
532
+ * An entry carrying no document state, and an endpoint that does not parse
533
+ * as a client-annex DID, are skipped rather than refused: the walk is an
534
+ * enumeration aid, and a caller cannot act on an id it could not read.
535
+ *
536
+ * @param options {object}
537
+ * @param options.log {DIDLog} the VERIFIED account log
538
+ * @returns {Array<{ did: string, host: string, spaceId: string }>} the
539
+ * Spaces, in log order
540
+ */
541
+ export function delegatedClientsSpaceHistory({ log }) {
542
+ const spaces = [];
543
+ for (const entry of log) {
544
+ if (entry?.state === undefined || entry.state === null) {
545
+ continue;
546
+ }
547
+ const pointed = delegatedClientsPointer({ doc: entry.state });
548
+ if (pointed === undefined) {
549
+ continue;
550
+ }
551
+ let parts;
552
+ try {
553
+ parts = clientAnnexDidParts({ did: pointed });
554
+ }
555
+ catch {
556
+ continue;
557
+ }
558
+ if (spaces.some(space => space.spaceId === parts.spaceId)) {
559
+ continue;
560
+ }
561
+ spaces.push({ did: pointed, host: parts.host, spaceId: parts.spaceId });
562
+ }
563
+ return spaces;
564
+ }
484
565
  /**
485
566
  * The account document's `service` array with the delegated-clients pointer
486
567
  * set to `clientAnnexDid`. An existing pointer entry is re-pointed in place,
@@ -901,33 +982,42 @@ function withGenerationDelegationEntry({ doc, clientAnnexDid, delegation }) {
901
982
  ];
902
983
  }
903
984
  /**
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
985
+ * Parses the host, the auxiliary Space id and the generation id out of an
986
+ * annex DID string. All three are permanent substrings of every annex DID by
906
987
  * construction: the generation id is the final path segment of the annex
907
988
  * DID (`did:webvh:<scid>:<host>:...:space:<spaceId>:<generationId>`), and it
908
989
  * is the generation-identifying half of the annex rung HKDF
909
990
  * labels, so this parse is what lets an enrollee derive its writing key from
910
991
  * the pointer alone -- no log read, no registry.
911
992
  *
993
+ * The host is the method-specific id's second segment, percent-decoded (a
994
+ * port rides as `%3A` inside the one segment). A caller enumerating Spaces
995
+ * out of a log compares it against the deployment it is talking to: an
996
+ * account that has migrated hosts carries entries naming the old one, which
997
+ * this deployment cannot address.
998
+ *
912
999
  * @param options {object}
913
1000
  * @param options.did {string} an annex did:webvh string
914
- * @returns {{ spaceId: string, generationId: string }}
1001
+ * @returns {{ host: string, spaceId: string, generationId: string }}
915
1002
  */
916
1003
  export function clientAnnexDidParts({ did }) {
917
1004
  const parts = did.split(':');
918
1005
  const generationId = parts[parts.length - 1];
919
1006
  const spaceId = parts[parts.length - 2];
1007
+ const host = parts[3];
920
1008
  if (parts.length < 7 ||
921
1009
  parts[0] !== 'did' ||
922
1010
  parts[1] !== 'webvh' ||
923
1011
  parts[parts.length - 3] !== 'space' ||
924
1012
  generationId === undefined ||
925
1013
  spaceId === undefined ||
926
- spaceId.length === 0) {
1014
+ spaceId.length === 0 ||
1015
+ host === undefined ||
1016
+ host.length === 0) {
927
1017
  throw new Error(`Not a client annex did:webvh: "${did}".`);
928
1018
  }
929
1019
  assertGenerationId(generationId);
930
- return { spaceId, generationId };
1020
+ return { host: decodeURIComponent(host), spaceId, generationId };
931
1021
  }
932
1022
  /**
933
1023
  * Thrown when the published annex log commits neither the writing
@@ -944,6 +1034,37 @@ export class ClientAnnexRungUncommittedError extends Error {
944
1034
  this.name = 'ClientAnnexRungUncommittedError';
945
1035
  }
946
1036
  }
1037
+ /**
1038
+ * Derives a generation's rung-0 key for a ladder and admits it as the writer
1039
+ * of the entry about to be built: it must stand revealed in the log's
1040
+ * `updateKeys`, or its hash must stand committed in `nextKeyHashes`.
1041
+ *
1042
+ * The one implementation of the annex's writer-admission rule, which every
1043
+ * entry builder in this module applies before it mints anything. Annex
1044
+ * entries verify against the log's own hash-commitment chain, so a key that
1045
+ * is neither revealed nor committed can never be made to verify mid-log --
1046
+ * the caller supplies only the refusal message, since what a locked-out
1047
+ * credential should do next differs per ceremony.
1048
+ *
1049
+ * @param options {object}
1050
+ * @param options.ladderSeed {Uint8Array} the acting credential's ladder seed
1051
+ * @param options.generationId {string}
1052
+ * @param options.updateKeys {string[]} the keys the entry will be verified
1053
+ * against (the retired members already excluded, where a ceremony excludes
1054
+ * them)
1055
+ * @param options.nextKeyHashes {string[]} likewise
1056
+ * @param options.message {string} the refusal's message
1057
+ * @returns {Promise<{ seed: Uint8Array; keyMultibase: string }>}
1058
+ */
1059
+ async function admitActingRung({ ladderSeed, generationId, updateKeys, nextKeyHashes, message }) {
1060
+ const rung = await clientAnnexRung({ ladderSeed, generationId });
1061
+ const rungHash = await deriveNextKeyHash(rung.keyMultibase);
1062
+ if (!updateKeys.includes(rung.keyMultibase) &&
1063
+ !nextKeyHashes.includes(rungHash)) {
1064
+ throw new ClientAnnexRungUncommittedError(message);
1065
+ }
1066
+ return rung;
1067
+ }
947
1068
  /**
948
1069
  * Reads and resolves the published annex log through the narrow seam, or
949
1070
  * throws when the generation's `did.jsonl` is missing (an unpointed or
@@ -957,18 +1078,14 @@ export class ClientAnnexRungUncommittedError extends Error {
957
1078
  * @returns {Promise<PublishedWebvhLog>}
958
1079
  */
959
1080
  async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }) {
960
- // readPublishedLog only calls getIdResourceRaw, so the narrow seam is safe.
961
- const published = await readPublishedLog({
1081
+ return readPublishedLogOrThrow({
962
1082
  idStore: store,
963
1083
  ...(expectedDid !== undefined ? { expectedDid } : {}),
964
1084
  ...(pinStore !== undefined ? { pinStore } : {}),
965
- ...(logId !== undefined ? { logId } : {})
1085
+ ...(logId !== undefined ? { logId } : {}),
1086
+ missingMessage: 'client annex: did.jsonl is missing; the generation was never minted ' +
1087
+ 'or has been collected.'
966
1088
  });
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
1089
  }
973
1090
  /**
974
1091
  * TRANSIENT ENROLLMENT: publishes one per-visit verification method into a
@@ -1032,27 +1149,56 @@ async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }
1032
1149
  * transient session passes an in-memory store)
1033
1150
  * @param [options.logId] {string} the generation's pin-slot key, from
1034
1151
  * {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
1152
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1153
+ * read and verified under this same pin slot, so the enrollment builds its
1154
+ * entry on it instead of spending a second round trip on the same log (the
1155
+ * transient visit's one-read composition). The FIRST attempt alone rides it:
1156
+ * a lost compare-and-swap means the head is stale by definition, so the
1157
+ * conflict retry re-reads under the pin. That threaded attempt is EXTRA
1158
+ * rather than one of the retry's three: a caller who saved a read is left
1159
+ * with the same conflict budget as one who did not
1035
1160
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
1036
1161
  */
1037
- export async function enrollClientAnnexTransientClient(options) {
1038
- return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(options));
1162
+ export async function enrollClientAnnexTransientClient({ published: threadedHead, ...rest }) {
1163
+ if (threadedHead !== undefined) {
1164
+ try {
1165
+ return await enrollClientAnnexTransientClientOnce({
1166
+ ...rest,
1167
+ published: threadedHead
1168
+ });
1169
+ }
1170
+ catch (err) {
1171
+ // A lost compare-and-swap on the threaded head says only that the head
1172
+ // is stale; the retry below re-reads under the pin with its whole
1173
+ // budget. Every other failure is the caller's.
1174
+ if (!(err instanceof WebvhLogConflictError)) {
1175
+ throw err;
1176
+ }
1177
+ }
1178
+ }
1179
+ return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(rest));
1039
1180
  }
1040
1181
  /**
1041
1182
  * One attempt of {@link enrollClientAnnexTransientClient}, re-invoked by the
1042
1183
  * conflict retry (with the same signing key -- static rung 0 has no
1043
- * advanced-rung retry shape).
1184
+ * advanced-rung retry shape) and with no threaded head after the first.
1044
1185
  *
1045
1186
  * @param options {object} see {@link enrollClientAnnexTransientClient}
1046
1187
  * @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
1047
1188
  */
1048
- async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId }) {
1189
+ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, published: alreadyRead }) {
1049
1190
  assertGenerationId(generationId);
1050
- const published = await readClientAnnexLogOrThrow({
1051
- store,
1052
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1053
- ...(pinStore !== undefined ? { pinStore } : {}),
1054
- ...(logId !== undefined ? { logId } : {})
1055
- });
1191
+ const published = alreadyRead !== undefined
1192
+ ? assertPublishedLogDid({
1193
+ published: alreadyRead,
1194
+ ...(expectedDid !== undefined ? { expectedDid } : {})
1195
+ })
1196
+ : await readClientAnnexLogOrThrow({
1197
+ store,
1198
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1199
+ ...(pinStore !== undefined ? { pinStore } : {}),
1200
+ ...(logId !== undefined ? { logId } : {})
1201
+ });
1056
1202
  const { did, doc } = published;
1057
1203
  const vmId = `${did}#${transientKeyMultibase}`;
1058
1204
  // Already enrolled (a completed earlier run): the VM and everything the
@@ -1062,14 +1208,15 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1062
1208
  if (existingMethods.some(method => method.id === vmId)) {
1063
1209
  return { did, doc, log: published.log };
1064
1210
  }
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 " +
1211
+ const rung = await admitActingRung({
1212
+ ladderSeed,
1213
+ generationId,
1214
+ updateKeys: published.updateKeys,
1215
+ nextKeyHashes: published.nextKeyHashes,
1216
+ message: "client annex: the log commits neither this credential's rung-0 key nor " +
1070
1217
  '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
- }
1218
+ 'annex until a writer commits its hash or the next GC swap does.'
1219
+ });
1073
1220
  // A non-rotating entry re-states `updateKeys`, which the resolver checks
1074
1221
  // against the previous entry's commitments -- genesis enforces the
1075
1222
  // carry-over convention, and this refuses a log that lost it anyway.
@@ -1125,6 +1272,13 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1125
1272
  // The log only -- an annex has no did:web projection -- conditional on
1126
1273
  // the read this entry was built on.
1127
1274
  await putLogResource({ store, log: updated.log, ifMatch: published.etag });
1275
+ // Advance the pin to what this entry just published, so a host serving the
1276
+ // pre-entry log straight afterwards is refused as a rollback on the next
1277
+ // read (equal-to-pin would otherwise be accepted, and a later stage built on
1278
+ // the stale head would miss this entry).
1279
+ if (pinStore && logId !== undefined) {
1280
+ await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1281
+ }
1128
1282
  return { did: updated.did, doc: updated.doc, log: updated.log };
1129
1283
  }
1130
1284
  /**
@@ -1140,16 +1294,19 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1140
1294
  * entry is appended ({@link delegatedClientsServiceEntry}). Every other
1141
1295
  * service entry, the verification methods, and the relationship arrays are
1142
1296
  * 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`).
1297
+ * a no-op on the log (and, unless `logOnly`, it republishes `did.json` from
1298
+ * the resolved log, which the enrolled-client caller has the authority to
1299
+ * do).
1144
1300
  *
1145
1301
  * @param options {object}
1146
1302
  * @param options.idStore {WebvhIdStore} the ACCOUNT log's store; with
1147
1303
  * `logOnly`, only its log read and `did.jsonl` PUT are used, so the narrow
1148
1304
  * 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
1305
+ * @param options.signer {AccountLogSigner} who signs the pointer entry:
1306
+ * this enrolled client's update-key seeds -- or the ladder-rung idiom on a
1307
+ * ladder-anchored account (`{ updateSeed: rung0.seed, stagedSeed:
1308
+ * rung1.seed }`), as the credential-anchored genesis and the
1309
+ * transient-recovery continuation pass
1153
1310
  * @param options.clientAnnexDid {string} the generation to point at
1154
1311
  * @param [options.expectedDid] {string} the account DID the log must
1155
1312
  * resolve to, from the account pointer
@@ -1161,24 +1318,56 @@ async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generat
1161
1318
  * @param [options.logOnly] {boolean} publish `did.jsonl` only, never the
1162
1319
  * `did.json` projection -- the transient-recovery continuation writing
1163
1320
  * 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 }>}
1321
+ * but the log. The projection then lags until some caller holding an
1322
+ * `id`-collection writer runs `ensureDidWebProjection` over the resolved
1323
+ * log; on a client-less account that is a transient visit under its
1324
+ * generation delegation. The log stays the source of truth meanwhile, and
1325
+ * the server reads it rather than the projection, so the lag is a `did:web`
1326
+ * verifier's concern alone
1327
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1328
+ * read and verified under this same pin slot, so the pointer entry builds
1329
+ * on it instead of spending a second round trip on the log the caller just
1330
+ * read or published (the establishment's one-read composition). The FIRST
1331
+ * attempt alone rides it: a lost compare-and-swap means the head is stale
1332
+ * by definition, so the conflict retry re-reads under the pin. That
1333
+ * threaded attempt is EXTRA rather than one of the retry's three: a caller
1334
+ * who saved a read is left with the same conflict budget as one who did not
1335
+ * @returns {Promise<{ did: string, doc: DIDDoc,
1336
+ * published: PublishedWebvhLog }>} `published` is the head this call
1337
+ * leaves standing: the post-entry one, paired with its publish's own ETag,
1338
+ * when the entry was appended; the head it stood on verbatim when the
1339
+ * document already pointed at the DID
1167
1340
  */
1168
- export async function setDelegatedClientsPointer(options) {
1169
- return withLogConflictRetry(() => setDelegatedClientsPointerOnce(options));
1341
+ export async function setDelegatedClientsPointer({ published: threadedHead, ...rest }) {
1342
+ if (threadedHead !== undefined) {
1343
+ try {
1344
+ return await setDelegatedClientsPointerOnce({
1345
+ ...rest,
1346
+ published: threadedHead
1347
+ });
1348
+ }
1349
+ catch (err) {
1350
+ // A lost compare-and-swap on the threaded head says only that the head
1351
+ // is stale; the retry below re-reads under the pin with its whole
1352
+ // budget. Every other failure is the caller's.
1353
+ if (!(err instanceof WebvhLogConflictError)) {
1354
+ throw err;
1355
+ }
1356
+ }
1357
+ }
1358
+ return withLogConflictRetry(() => setDelegatedClientsPointerOnce(rest));
1170
1359
  }
1171
1360
  /**
1172
1361
  * ONE attempt of {@link setDelegatedClientsPointer}, re-invoked by that
1173
1362
  * function's conflict retry and exported for a caller that runs its own.
1174
1363
  * A caller whose retry loop also attributes the signing rung must use this
1175
1364
  * 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
1365
+ * signer, and a rung a racing ceremony consumed meanwhile can never
1177
1366
  * become authorized by re-reading, so the attempt would fail on the plain
1178
1367
  * not-authorized refusal instead of surfacing the conflict the caller's loop
1179
1368
  * knows how to re-attribute from.
1180
1369
  *
1181
- * A caller that already read the head its `updateKeys` were attributed
1370
+ * A caller that already read the head its signer was attributed
1182
1371
  * against passes it as `published`, and the entry is built on exactly that
1183
1372
  * head. A racing entry landing in between then loses the CAS on the PUT and
1184
1373
  * surfaces as a {@link WebvhLogConflictError}, which is what a re-attributing
@@ -1189,72 +1378,64 @@ export async function setDelegatedClientsPointer(options) {
1189
1378
  * @param [options.published] {PublishedWebvhLog} the verified head to build
1190
1379
  * this entry on, when the caller has already read it under the same pin;
1191
1380
  * absent, the attempt reads the head itself
1192
- * @returns {Promise<{ did: string, doc: DIDDoc }>}
1381
+ * @returns {Promise<{ did: string, doc: DIDDoc,
1382
+ * published: PublishedWebvhLog }>}
1193
1383
  */
1194
- export async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false, published: alreadyRead }) {
1384
+ export async function setDelegatedClientsPointerOnce({ idStore, signer, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false, published: alreadyRead }) {
1195
1385
  // Refuses a malformed target before anything is read or written.
1196
1386
  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,
1387
+ let settled;
1388
+ const outcome = await signAccountEntry({
1389
+ idStore,
1231
1390
  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
1391
+ ...(alreadyRead !== undefined ? { published: alreadyRead } : {}),
1392
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1393
+ ...(pinStore ? { pinStore } : {}),
1394
+ ...(logId !== undefined ? { logId } : {}),
1395
+ missingMessage: 'did:webvh: did.jsonl is missing; nothing to point at a client annex.',
1396
+ verb: 're-pointing the delegated-clients entry',
1397
+ logOnly,
1398
+ build: async ({ published }) => {
1399
+ const { did, doc } = published;
1400
+ if (delegatedClientsPointer({ doc }) === clientAnnexDid) {
1401
+ if (!logOnly && signer.kind === 'client') {
1402
+ await concludeWithPublishedLog({ idStore, published });
1403
+ }
1404
+ // The head verbatim: the projection PUT touches no log, so this
1405
+ // read's own ETag is still the log's validator.
1406
+ settled = { did, doc, published };
1407
+ return undefined;
1408
+ }
1409
+ return {
1410
+ services: servicesPointedAtClientAnnex({
1411
+ doc,
1412
+ accountDid: did,
1413
+ clientAnnexDid
1414
+ })
1415
+ };
1416
+ }
1239
1417
  });
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 });
1249
- }
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) });
1418
+ if (settled) {
1419
+ return settled;
1256
1420
  }
1257
- return { did: updated.did, doc: updated.doc };
1421
+ const updated = outcome.updated;
1422
+ return {
1423
+ did: updated.did,
1424
+ doc: updated.doc,
1425
+ // The post-entry head, from what `updateDID` already resolved plus this
1426
+ // publish's own validator: the update-key parameters are the ones the
1427
+ // entry re-stated unchanged above, so nothing is re-resolved or re-read.
1428
+ published: {
1429
+ log: updated.log,
1430
+ did: updated.did,
1431
+ // Detached from the entry's own `state`, as every other producer of
1432
+ // this type is, so a consumer editing the document cannot edit the log.
1433
+ doc: structuredClone(updated.doc),
1434
+ updateKeys: updated.meta.updateKeys,
1435
+ nextKeyHashes: updated.meta.nextKeyHashes,
1436
+ ...(outcome.etag !== undefined ? { etag: outcome.etag } : {})
1437
+ }
1438
+ };
1258
1439
  }
1259
1440
  /**
1260
1441
  * The whole transient-enrollment ceremony as the enrollee runs it: resolve
@@ -1291,9 +1472,15 @@ export async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clie
1291
1472
  * @param [options.maxRounds] {number} how many pointer moves to chase
1292
1473
  * before giving up (a GC pass is quarterly, so more than one mid-ceremony
1293
1474
  * move means something else is wrong)
1475
+ * @param [options.published] {PublishedWebvhLog} a generation head the
1476
+ * caller already read and verified under this same pin store (the readiness
1477
+ * stage's, when it published nothing to that log). Round 0 alone rides it,
1478
+ * and only when it is the generation the account document points at NOW: a
1479
+ * head for any other generation is ignored and the round reads fresh, as
1480
+ * every later round does
1294
1481
  * @returns {Promise<{ clientAnnexDid: string, doc: DIDDoc, log: DIDLog }>}
1295
1482
  */
1296
- export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3 }) {
1483
+ export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3, published: threadedHead }) {
1297
1484
  let accountDoc = await readAccountDocument();
1298
1485
  for (let round = 0; round < maxRounds; round++) {
1299
1486
  const clientAnnexDid = delegatedClientsPointer({ doc: accountDoc });
@@ -1304,12 +1491,19 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1304
1491
  const { spaceId, generationId } = clientAnnexDidParts({
1305
1492
  did: clientAnnexDid
1306
1493
  });
1494
+ // Round 0's threaded head, and only for the generation the pointer names:
1495
+ // a head read before a GC swap belongs to the abandoned generation and
1496
+ // says nothing about the one this round enrolls into.
1497
+ const head = round === 0 && threadedHead?.did === clientAnnexDid
1498
+ ? threadedHead
1499
+ : undefined;
1307
1500
  const enrolled = await enrollClientAnnexTransientClient({
1308
1501
  store: storeForGenerationId(generationId),
1309
1502
  ladderSeed,
1310
1503
  generationId,
1311
1504
  transientKeyMultibase,
1312
1505
  expectedDid: clientAnnexDid,
1506
+ ...(head !== undefined ? { published: head } : {}),
1313
1507
  ...(mintDelegation !== undefined
1314
1508
  ? { mintGenerationDelegation: mintDelegation }
1315
1509
  : {}),
@@ -1332,8 +1526,8 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1332
1526
  * RENEW PRECEDES MINT: the blocking pre-mint stage a transient App Connect
1333
1527
  * approval runs before delegating any grant. Reads the annex document
1334
1528
  * 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
1529
+ * when it is stale under the house policy ({@link standingZcapStale}): a
1530
+ * fresh delegation is minted through the caller's closure
1337
1531
  * (ladder-signed -- the renewal must not depend on the very delegation it
1338
1532
  * replaces; published through the store, which in a transient session is
1339
1533
  * the credential's sibling delegation, so even a hard-expired delegation is
@@ -1344,14 +1538,16 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1344
1538
  * same way (the GC ceremony's own install stage and the first-VM install
1345
1539
  * make this rare; a heal, not a policy).
1346
1540
  *
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.
1541
+ * That policy is three axes. Expiry -- past, or inside the 30-day renewal
1542
+ * window. SIGNER DEATH, which an `accountDoc` adds: a standing delegation
1543
+ * whose proof key is no longer in the supplied verified account document has
1544
+ * rotted under the current-key-set rule (the enrolled client that minted it
1545
+ * was revoked, or the ladder VM that signed it left with the first
1546
+ * self-enrollment). And RETIREMENT, the projected post-edit reading a caller
1547
+ * asks for with `retiringKeyMultibases`, for a key still listed whose
1548
+ * authority the ceremony is about to end. No revocation POST accompanies the
1549
+ * replacement: a rotted chain no longer verifies at the revocation endpoint,
1550
+ * and the expiry-renewal path never revoked either.
1355
1551
  *
1356
1552
  * Failure is the caller's failure: a renewal that cannot complete throws,
1357
1553
  * and the App Connect approval fails with the standard retryable-ceremony
@@ -1380,64 +1576,96 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
1380
1576
  * @param [options.accountDoc] {PublishedKeyDocument} the locally VERIFIED
1381
1577
  * account document; supplied, a standing delegation whose proof key it no
1382
1578
  * 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)
1579
+ * @param [options.retiringKeyMultibases] {string[]} keys whose authority is
1580
+ * about to end, read as a projected post-edit document: a standing
1581
+ * delegation one of them signed is replaced even though the served document
1582
+ * still lists the key. The last-client transition's replacement stage names
1583
+ * its own ladder VM (whose delegations it revokes server-side in the next
1584
+ * breath, a state no client-side predicate can read) and the client its
1585
+ * removal entry has yet to strike
1387
1586
  * @param [options.now] {number} epoch milliseconds, for tests
1388
- * @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
1587
+ * @param [options.published] {PublishedWebvhLog} a head the caller already
1588
+ * read and verified under this same pin slot, so the stage builds on it
1589
+ * instead of spending a second round trip on the same log (the transient
1590
+ * visit's one-read composition). The FIRST attempt alone rides it: a lost
1591
+ * compare-and-swap means the head is stale by definition, so the conflict
1592
+ * retry re-reads under the pin. That threaded attempt is EXTRA rather than
1593
+ * one of the retry's three: a caller who saved a read is left with the same
1594
+ * conflict budget as one who did not
1595
+ * @returns {Promise<{ delegation: IZcap, renewed: boolean,
1596
+ * published?: PublishedWebvhLog }>} `published` is the verified head this
1597
+ * pass stood on -- the one read, or the one handed in -- and is present
1598
+ * ONLY when `renewed` is false. The two store implementations differ on
1599
+ * what a renewal's publish hands back: the controller-tier store forwards
1600
+ * the PUT's own ETag, while the delegated store discards the response and
1601
+ * yields none. This stage takes either, and cannot tell which from here, so
1602
+ * it stays conservative and passes no post-renewal head on; a caller
1603
+ * wanting one reads for itself
1389
1604
  */
1390
- export async function ensureGenerationDelegationCurrent(options) {
1391
- return withLogConflictRetry(() => ensureGenerationDelegationCurrentOnce(options));
1605
+ export async function ensureGenerationDelegationCurrent({ published: threadedHead, ...rest }) {
1606
+ if (threadedHead !== undefined) {
1607
+ try {
1608
+ return await ensureGenerationDelegationCurrentOnce({
1609
+ ...rest,
1610
+ published: threadedHead
1611
+ });
1612
+ }
1613
+ catch (err) {
1614
+ // A lost compare-and-swap on the threaded head says only that the head
1615
+ // is stale; the retry below re-reads under the pin with its whole
1616
+ // budget. Every other failure is the caller's.
1617
+ if (!(err instanceof WebvhLogConflictError)) {
1618
+ throw err;
1619
+ }
1620
+ }
1621
+ }
1622
+ return withLogConflictRetry(() => ensureGenerationDelegationCurrentOnce(rest));
1392
1623
  }
1393
1624
  /**
1394
1625
  * One attempt of {@link ensureGenerationDelegationCurrent}, re-invoked by the
1395
1626
  * conflict retry (with the same signing key -- static rung 0 has no
1396
- * advanced-rung retry shape).
1627
+ * advanced-rung retry shape) and with no threaded head after the first.
1397
1628
  *
1398
1629
  * @param options {object} see {@link ensureGenerationDelegationCurrent}
1399
- * @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
1630
+ * @returns {Promise<{ delegation: IZcap, renewed: boolean,
1631
+ * published?: PublishedWebvhLog }>}
1400
1632
  */
1401
- async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, force = false, now }) {
1633
+ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, retiringKeyMultibases = [], now, published: alreadyRead }) {
1402
1634
  assertGenerationId(generationId);
1403
- const published = await readClientAnnexLogOrThrow({
1404
- store,
1405
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1406
- ...(pinStore !== undefined ? { pinStore } : {}),
1407
- ...(logId !== undefined ? { logId } : {})
1408
- });
1635
+ const published = alreadyRead !== undefined
1636
+ ? assertPublishedLogDid({
1637
+ published: alreadyRead,
1638
+ ...(expectedDid !== undefined ? { expectedDid } : {})
1639
+ })
1640
+ : await readClientAnnexLogOrThrow({
1641
+ store,
1642
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1643
+ ...(pinStore !== undefined ? { pinStore } : {}),
1644
+ ...(logId !== undefined ? { logId } : {})
1645
+ });
1409
1646
  const { did, doc } = published;
1410
1647
  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
- : {}),
1648
+ if (standing !== undefined &&
1649
+ !standingZcapStale({
1650
+ zcap: standing,
1651
+ ...(accountDoc !== undefined ? { doc: accountDoc } : {}),
1652
+ retiringKeyMultibases,
1426
1653
  ...(now !== undefined ? { now } : {})
1427
1654
  })) {
1428
- return { delegation: standing, renewed: false };
1655
+ return { delegation: standing, renewed: false, published };
1429
1656
  }
1430
1657
  // The rung refusal precedes the mint: nothing is delegated for a writer
1431
1658
  // 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 " +
1659
+ const rung = await admitActingRung({
1660
+ ladderSeed,
1661
+ generationId,
1662
+ updateKeys: published.updateKeys,
1663
+ nextKeyHashes: published.nextKeyHashes,
1664
+ message: "client annex: the log commits neither this credential's rung-0 key nor " +
1437
1665
  'its hash; a credential bound mid-generation cannot renew the ' +
1438
1666
  'generation delegation until a writer commits its hash or the next ' +
1439
- 'GC swap does.');
1440
- }
1667
+ 'GC swap does.'
1668
+ });
1441
1669
  await assertCarryOverCommitments({ published });
1442
1670
  const fresh = await mintDelegation({ clientAnnexDid: did });
1443
1671
  const signer = await updateKeySigner({ seed: rung.seed });
@@ -1457,6 +1685,13 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, genera
1457
1685
  })
1458
1686
  });
1459
1687
  await putLogResource({ store, log: updated.log, ifMatch: published.etag });
1688
+ // Advance the pin to what this entry just published, so a host serving the
1689
+ // pre-entry log straight afterwards is refused as a rollback on the next
1690
+ // read (equal-to-pin would otherwise be accepted, and a later stage built on
1691
+ // the stale head would miss this entry).
1692
+ if (pinStore && logId !== undefined) {
1693
+ await pinStore.write({ logId, pin: pinOfLog(updated.log) });
1694
+ }
1460
1695
  return { delegation: fresh, renewed: true };
1461
1696
  }
1462
1697
  /**
@@ -1527,17 +1762,15 @@ async function retireClientAnnexRungOnce({ store, retiredLadderSeed, actingLadde
1527
1762
  }
1528
1763
  // The acting rung must be committed AFTER the retired members are
1529
1764
  // excluded, so the retired credential can never sign its own strike.
1530
- const acting = await clientAnnexRung({
1765
+ const acting = await admitActingRung({
1531
1766
  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 " +
1767
+ generationId,
1768
+ updateKeys: remainingKeys,
1769
+ nextKeyHashes: remainingHashes,
1770
+ message: "client annex: the log commits neither the acting credential's rung-0 " +
1538
1771
  'key nor its hash (or it is the retired rung itself); the strike ' +
1539
- 'needs a distinct committed writer -- swap the generation instead.');
1540
- }
1772
+ 'needs a distinct committed writer -- swap the generation instead.'
1773
+ });
1541
1774
  await assertCarryOverCommitments({ published });
1542
1775
  const signer = await updateKeySigner({ seed: acting.seed });
1543
1776
  const updated = await updateDID({
@@ -1617,17 +1850,15 @@ async function commitClientAnnexRungOnce({ store, boundLadderSeed, actingLadderS
1617
1850
  // generation the bound credential itself minted.
1618
1851
  return { committed: false };
1619
1852
  }
1620
- const acting = await clientAnnexRung({
1853
+ const acting = await admitActingRung({
1621
1854
  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 " +
1855
+ generationId,
1856
+ updateKeys: published.updateKeys,
1857
+ nextKeyHashes: published.nextKeyHashes,
1858
+ message: "client annex: the log commits neither the acting credential's rung-0 " +
1628
1859
  'key nor its hash; a commit entry needs a committed writer -- the ' +
1629
- 'bound credential stays locked out until the next GC swap.');
1630
- }
1860
+ 'bound credential stays locked out until the next GC swap.'
1861
+ });
1631
1862
  await assertCarryOverCommitments({ published });
1632
1863
  const signer = await updateKeySigner({ seed: acting.seed });
1633
1864
  const updated = await updateDID({