@interop/wallet-core 0.61.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +41 -16
  2. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts +46 -26
  3. package/dist/clientAnnex/credentialAnchoredGenesis.d.ts.map +1 -1
  4. package/dist/clientAnnex/credentialAnchoredGenesis.js +91 -39
  5. package/dist/clientAnnex/credentialAnchoredGenesis.js.map +1 -1
  6. package/dist/clientAnnex/establish.d.ts +55 -34
  7. package/dist/clientAnnex/establish.d.ts.map +1 -1
  8. package/dist/clientAnnex/establish.js +102 -56
  9. package/dist/clientAnnex/establish.js.map +1 -1
  10. package/dist/clientAnnex/forget.d.ts +16 -2
  11. package/dist/clientAnnex/forget.d.ts.map +1 -1
  12. package/dist/clientAnnex/forget.js +11 -9
  13. package/dist/clientAnnex/forget.js.map +1 -1
  14. package/dist/clientAnnex/forgetLast.d.ts +126 -35
  15. package/dist/clientAnnex/forgetLast.d.ts.map +1 -1
  16. package/dist/clientAnnex/forgetLast.js +208 -60
  17. package/dist/clientAnnex/forgetLast.js.map +1 -1
  18. package/dist/clientAnnex/gc.d.ts +3 -2
  19. package/dist/clientAnnex/gc.d.ts.map +1 -1
  20. package/dist/clientAnnex/gc.js +7 -7
  21. package/dist/clientAnnex/gc.js.map +1 -1
  22. package/dist/clientAnnex/heal.d.ts +101 -33
  23. package/dist/clientAnnex/heal.d.ts.map +1 -1
  24. package/dist/clientAnnex/heal.js +559 -212
  25. package/dist/clientAnnex/heal.js.map +1 -1
  26. package/dist/clientAnnex/index.d.ts +21 -5
  27. package/dist/clientAnnex/index.d.ts.map +1 -1
  28. package/dist/clientAnnex/index.js +24 -5
  29. package/dist/clientAnnex/index.js.map +1 -1
  30. package/dist/clientAnnex/ladder.d.ts +304 -18
  31. package/dist/clientAnnex/ladder.d.ts.map +1 -1
  32. package/dist/clientAnnex/ladder.js +942 -64
  33. package/dist/clientAnnex/ladder.js.map +1 -1
  34. package/dist/clientAnnex/ladderAnchored.d.ts +261 -28
  35. package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
  36. package/dist/clientAnnex/ladderAnchored.js +521 -295
  37. package/dist/clientAnnex/ladderAnchored.js.map +1 -1
  38. package/dist/clientAnnex/log.d.ts +193 -38
  39. package/dist/clientAnnex/log.d.ts.map +1 -1
  40. package/dist/clientAnnex/log.js +445 -190
  41. package/dist/clientAnnex/log.js.map +1 -1
  42. package/dist/clientAnnex/mend.d.ts +12 -6
  43. package/dist/clientAnnex/mend.d.ts.map +1 -1
  44. package/dist/clientAnnex/mend.js +26 -12
  45. package/dist/clientAnnex/mend.js.map +1 -1
  46. package/dist/clientAnnex/recoveryLadderAnchored.d.ts +38 -11
  47. package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
  48. package/dist/clientAnnex/recoveryLadderAnchored.js +177 -55
  49. package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -1
  50. package/dist/clientAnnex/spaceCapability.d.ts +123 -0
  51. package/dist/clientAnnex/spaceCapability.d.ts.map +1 -0
  52. package/dist/clientAnnex/spaceCapability.js +152 -0
  53. package/dist/clientAnnex/spaceCapability.js.map +1 -0
  54. package/dist/clientAnnex/stages.d.ts +63 -0
  55. package/dist/clientAnnex/stages.d.ts.map +1 -0
  56. package/dist/clientAnnex/stages.js +64 -0
  57. package/dist/clientAnnex/stages.js.map +1 -0
  58. package/dist/clientAnnex/zcap.d.ts +1 -1
  59. package/dist/clientAnnex/zcap.d.ts.map +1 -1
  60. package/dist/clientAnnex/zcap.js +42 -35
  61. package/dist/clientAnnex/zcap.js.map +1 -1
  62. package/dist/clients/policy.d.ts +15 -1
  63. package/dist/clients/policy.d.ts.map +1 -1
  64. package/dist/clients/policy.js +12 -6
  65. package/dist/clients/policy.js.map +1 -1
  66. package/dist/clients/revocation.d.ts +18 -10
  67. package/dist/clients/revocation.d.ts.map +1 -1
  68. package/dist/clients/revocation.js +13 -5
  69. package/dist/clients/revocation.js.map +1 -1
  70. package/dist/clients/rosterPolicy.d.ts +10 -2
  71. package/dist/clients/rosterPolicy.d.ts.map +1 -1
  72. package/dist/clients/rosterPolicy.js +41 -24
  73. package/dist/clients/rosterPolicy.js.map +1 -1
  74. package/dist/descriptors/acquire.d.ts.map +1 -1
  75. package/dist/descriptors/acquire.js +6 -23
  76. package/dist/descriptors/acquire.js.map +1 -1
  77. package/dist/descriptors/cipher.d.ts.map +1 -1
  78. package/dist/descriptors/cipher.js +5 -0
  79. package/dist/descriptors/cipher.js.map +1 -1
  80. package/dist/descriptors/errors.d.ts +43 -0
  81. package/dist/descriptors/errors.d.ts.map +1 -0
  82. package/dist/descriptors/errors.js +45 -0
  83. package/dist/descriptors/errors.js.map +1 -0
  84. package/dist/descriptors/index.d.ts +5 -0
  85. package/dist/descriptors/index.d.ts.map +1 -1
  86. package/dist/descriptors/index.js +5 -0
  87. package/dist/descriptors/index.js.map +1 -1
  88. package/dist/enrollment/enrollment.d.ts +37 -10
  89. package/dist/enrollment/enrollment.d.ts.map +1 -1
  90. package/dist/enrollment/enrollment.js +52 -15
  91. package/dist/enrollment/enrollment.js.map +1 -1
  92. package/dist/genesis/accountGenesis.d.ts +42 -11
  93. package/dist/genesis/accountGenesis.d.ts.map +1 -1
  94. package/dist/genesis/accountGenesis.js +80 -22
  95. package/dist/genesis/accountGenesis.js.map +1 -1
  96. package/dist/genesis/index.d.ts +3 -1
  97. package/dist/genesis/index.d.ts.map +1 -1
  98. package/dist/genesis/index.js +3 -1
  99. package/dist/genesis/index.js.map +1 -1
  100. package/dist/identity/agents.d.ts +17 -1
  101. package/dist/identity/agents.d.ts.map +1 -1
  102. package/dist/identity/agents.js +24 -8
  103. package/dist/identity/agents.js.map +1 -1
  104. package/dist/identity/index.d.ts +3 -1
  105. package/dist/identity/index.d.ts.map +1 -1
  106. package/dist/identity/index.js +3 -1
  107. package/dist/identity/index.js.map +1 -1
  108. package/dist/index.d.ts +6 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/keyring/index.d.ts +2 -1
  113. package/dist/keyring/index.d.ts.map +1 -1
  114. package/dist/keyring/index.js +2 -1
  115. package/dist/keyring/index.js.map +1 -1
  116. package/dist/keyring/record.d.ts.map +1 -1
  117. package/dist/keyring/record.js +3 -6
  118. package/dist/keyring/record.js.map +1 -1
  119. package/dist/keyring/unlockSpace.d.ts +14 -5
  120. package/dist/keyring/unlockSpace.d.ts.map +1 -1
  121. package/dist/keyring/unlockSpace.js +31 -36
  122. package/dist/keyring/unlockSpace.js.map +1 -1
  123. package/dist/keys/index.d.ts +10 -6
  124. package/dist/keys/index.d.ts.map +1 -1
  125. package/dist/keys/index.js +9 -5
  126. package/dist/keys/index.js.map +1 -1
  127. package/dist/keys/rosterLogStore.d.ts +12 -11
  128. package/dist/keys/rosterLogStore.d.ts.map +1 -1
  129. package/dist/keys/rosterLogStore.js +15 -13
  130. package/dist/keys/rosterLogStore.js.map +1 -1
  131. package/dist/keys/rosterStore.d.ts +3 -6
  132. package/dist/keys/rosterStore.d.ts.map +1 -1
  133. package/dist/keys/rosterStore.js +10 -9
  134. package/dist/keys/rosterStore.js.map +1 -1
  135. package/dist/keys/spaceEpochs.d.ts.map +1 -1
  136. package/dist/keys/spaceEpochs.js +2 -3
  137. package/dist/keys/spaceEpochs.js.map +1 -1
  138. package/dist/keys/userKeyRoster.d.ts +89 -9
  139. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  140. package/dist/keys/userKeyRoster.js +262 -19
  141. package/dist/keys/userKeyRoster.js.map +1 -1
  142. package/dist/keys/userKeyRosterCascade.d.ts +7 -6
  143. package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
  144. package/dist/keys/userKeyRosterCascade.js +7 -6
  145. package/dist/keys/userKeyRosterCascade.js.map +1 -1
  146. package/dist/keys/wasLabelsStore.d.ts +9 -2
  147. package/dist/keys/wasLabelsStore.d.ts.map +1 -1
  148. package/dist/keys/wasLabelsStore.js +14 -6
  149. package/dist/keys/wasLabelsStore.js.map +1 -1
  150. package/dist/log.d.ts +7 -2
  151. package/dist/log.d.ts.map +1 -1
  152. package/dist/log.js +6 -1
  153. package/dist/log.js.map +1 -1
  154. package/dist/recovery/index.d.ts +12 -8
  155. package/dist/recovery/index.d.ts.map +1 -1
  156. package/dist/recovery/index.js +11 -7
  157. package/dist/recovery/index.js.map +1 -1
  158. package/dist/recovery/recoveryCode.d.ts +27 -7
  159. package/dist/recovery/recoveryCode.d.ts.map +1 -1
  160. package/dist/recovery/recoveryCode.js +18 -6
  161. package/dist/recovery/recoveryCode.js.map +1 -1
  162. package/dist/recovery/recoveryDelegation.d.ts +4 -1
  163. package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
  164. package/dist/recovery/recoveryDelegation.js +25 -36
  165. package/dist/recovery/recoveryDelegation.js.map +1 -1
  166. package/dist/recovery/recoveryWebvh.d.ts +117 -65
  167. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  168. package/dist/recovery/recoveryWebvh.js +279 -126
  169. package/dist/recovery/recoveryWebvh.js.map +1 -1
  170. package/dist/request/classify.d.ts +32 -8
  171. package/dist/request/classify.d.ts.map +1 -1
  172. package/dist/request/classify.js +39 -14
  173. package/dist/request/classify.js.map +1 -1
  174. package/dist/request/ephemeralExchange.d.ts +1 -6
  175. package/dist/request/ephemeralExchange.d.ts.map +1 -1
  176. package/dist/request/ephemeralExchange.js.map +1 -1
  177. package/dist/request/onboarding.d.ts.map +1 -1
  178. package/dist/request/onboarding.js +2 -2
  179. package/dist/request/onboarding.js.map +1 -1
  180. package/dist/request/parse.d.ts.map +1 -1
  181. package/dist/request/parse.js +2 -3
  182. package/dist/request/parse.js.map +1 -1
  183. package/dist/resourceLog/controller.d.ts +34 -12
  184. package/dist/resourceLog/controller.d.ts.map +1 -1
  185. package/dist/resourceLog/controller.js +79 -86
  186. package/dist/resourceLog/controller.js.map +1 -1
  187. package/dist/resourceLog/document.d.ts +182 -0
  188. package/dist/resourceLog/document.d.ts.map +1 -0
  189. package/dist/resourceLog/document.js +159 -0
  190. package/dist/resourceLog/document.js.map +1 -0
  191. package/dist/resourceLog/errors.d.ts +47 -8
  192. package/dist/resourceLog/errors.d.ts.map +1 -1
  193. package/dist/resourceLog/errors.js +54 -8
  194. package/dist/resourceLog/errors.js.map +1 -1
  195. package/dist/resourceLog/index.d.ts +7 -3
  196. package/dist/resourceLog/index.d.ts.map +1 -1
  197. package/dist/resourceLog/index.js +7 -3
  198. package/dist/resourceLog/index.js.map +1 -1
  199. package/dist/resourceLog/ladderRungs.d.ts +35 -0
  200. package/dist/resourceLog/ladderRungs.d.ts.map +1 -0
  201. package/dist/resourceLog/ladderRungs.js +352 -0
  202. package/dist/resourceLog/ladderRungs.js.map +1 -0
  203. package/dist/resourceLog/license.d.ts +42 -17
  204. package/dist/resourceLog/license.d.ts.map +1 -1
  205. package/dist/resourceLog/license.js +38 -24
  206. package/dist/resourceLog/license.js.map +1 -1
  207. package/dist/space/activity.d.ts +15 -15
  208. package/dist/space/activity.d.ts.map +1 -1
  209. package/dist/space/activity.js +15 -15
  210. package/dist/space/activity.js.map +1 -1
  211. package/dist/space/collections.d.ts +11 -0
  212. package/dist/space/collections.d.ts.map +1 -1
  213. package/dist/space/collections.js +13 -0
  214. package/dist/space/collections.js.map +1 -1
  215. package/dist/space/deleteSpace.d.ts +28 -0
  216. package/dist/space/deleteSpace.d.ts.map +1 -0
  217. package/dist/space/deleteSpace.js +44 -0
  218. package/dist/space/deleteSpace.js.map +1 -0
  219. package/dist/space/errors.d.ts.map +1 -1
  220. package/dist/space/errors.js +0 -1
  221. package/dist/space/errors.js.map +1 -1
  222. package/dist/space/index.d.ts +6 -0
  223. package/dist/space/index.d.ts.map +1 -1
  224. package/dist/space/index.js +6 -0
  225. package/dist/space/index.js.map +1 -1
  226. package/dist/space/plaintextCollection.d.ts +43 -0
  227. package/dist/space/plaintextCollection.d.ts.map +1 -0
  228. package/dist/space/plaintextCollection.js +17 -0
  229. package/dist/space/plaintextCollection.js.map +1 -0
  230. package/dist/stages.d.ts +23 -0
  231. package/dist/stages.d.ts.map +1 -0
  232. package/dist/stages.js +23 -0
  233. package/dist/stages.js.map +1 -0
  234. package/dist/sync/index.d.ts +7 -0
  235. package/dist/sync/index.d.ts.map +1 -1
  236. package/dist/sync/index.js +7 -0
  237. package/dist/sync/index.js.map +1 -1
  238. package/dist/sync/push.js +4 -4
  239. package/dist/sync/push.js.map +1 -1
  240. package/dist/sync/remint.js +2 -2
  241. package/dist/sync/remint.js.map +1 -1
  242. package/dist/sync/types.d.ts +41 -1
  243. package/dist/sync/types.d.ts.map +1 -1
  244. package/dist/sync/types.js +47 -1
  245. package/dist/sync/types.js.map +1 -1
  246. package/dist/unlock/index.d.ts +6 -2
  247. package/dist/unlock/index.d.ts.map +1 -1
  248. package/dist/unlock/index.js +5 -1
  249. package/dist/unlock/index.js.map +1 -1
  250. package/dist/unlock/retire.d.ts +95 -14
  251. package/dist/unlock/retire.d.ts.map +1 -1
  252. package/dist/unlock/retire.js +102 -10
  253. package/dist/unlock/retire.js.map +1 -1
  254. package/dist/unlock/standingClient.d.ts.map +1 -1
  255. package/dist/unlock/standingClient.js +5 -1
  256. package/dist/unlock/standingClient.js.map +1 -1
  257. package/dist/unlock/standingWebvh.d.ts +341 -49
  258. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  259. package/dist/unlock/standingWebvh.js +608 -164
  260. package/dist/unlock/standingWebvh.js.map +1 -1
  261. package/dist/webvh/accountEntry.d.ts +146 -0
  262. package/dist/webvh/accountEntry.d.ts.map +1 -0
  263. package/dist/webvh/accountEntry.js +239 -0
  264. package/dist/webvh/accountEntry.js.map +1 -0
  265. package/dist/webvh/didWeb.d.ts +12 -7
  266. package/dist/webvh/didWeb.d.ts.map +1 -1
  267. package/dist/webvh/didWeb.js +2 -2
  268. package/dist/webvh/didWeb.js.map +1 -1
  269. package/dist/webvh/didWebProjection.d.ts +164 -0
  270. package/dist/webvh/didWebProjection.d.ts.map +1 -0
  271. package/dist/webvh/didWebProjection.js +230 -0
  272. package/dist/webvh/didWebProjection.js.map +1 -0
  273. package/dist/webvh/didWebvh.d.ts +278 -138
  274. package/dist/webvh/didWebvh.d.ts.map +1 -1
  275. package/dist/webvh/didWebvh.js +314 -345
  276. package/dist/webvh/didWebvh.js.map +1 -1
  277. package/dist/webvh/enrollClient.d.ts +65 -0
  278. package/dist/webvh/enrollClient.d.ts.map +1 -0
  279. package/dist/webvh/enrollClient.js +172 -0
  280. package/dist/webvh/enrollClient.js.map +1 -0
  281. package/dist/webvh/index.d.ts +37 -12
  282. package/dist/webvh/index.d.ts.map +1 -1
  283. package/dist/webvh/index.js +33 -10
  284. package/dist/webvh/index.js.map +1 -1
  285. package/dist/webvh/listClients.d.ts +1 -33
  286. package/dist/webvh/listClients.d.ts.map +1 -1
  287. package/dist/webvh/listClients.js +2 -28
  288. package/dist/webvh/listClients.js.map +1 -1
  289. package/dist/webvh/revokeClient.d.ts +89 -11
  290. package/dist/webvh/revokeClient.d.ts.map +1 -1
  291. package/dist/webvh/revokeClient.js +182 -47
  292. package/dist/webvh/revokeClient.js.map +1 -1
  293. package/dist/webvh/standingZcap.d.ts +61 -14
  294. package/dist/webvh/standingZcap.d.ts.map +1 -1
  295. package/dist/webvh/standingZcap.js +91 -0
  296. package/dist/webvh/standingZcap.js.map +1 -1
  297. package/dist/webvh/verifyLog.d.ts +59 -5
  298. package/dist/webvh/verifyLog.d.ts.map +1 -1
  299. package/dist/webvh/verifyLog.js +76 -11
  300. package/dist/webvh/verifyLog.js.map +1 -1
  301. package/dist/webvh/wasIdStore.d.ts +8 -8
  302. package/dist/webvh/wasIdStore.d.ts.map +1 -1
  303. package/dist/webvh/wasIdStore.js +39 -14
  304. package/dist/webvh/wasIdStore.js.map +1 -1
  305. package/dist/webvh/zcap.d.ts +14 -1
  306. package/dist/webvh/zcap.d.ts.map +1 -1
  307. package/dist/webvh/zcap.js +3 -27
  308. package/dist/webvh/zcap.js.map +1 -1
  309. package/package.json +5 -5
  310. package/dist/webvh/keyAgreement.d.ts +0 -72
  311. package/dist/webvh/keyAgreement.d.ts.map +0 -1
  312. package/dist/webvh/keyAgreement.js +0 -47
  313. package/dist/webvh/keyAgreement.js.map +0 -1
@@ -21,10 +21,10 @@
21
21
  * key-agreement key (under `keyAgreement`, the source of record for
22
22
  * user-key-wrap recipient keys). The genesis comes in two flavors: a wallet
23
23
  * that keeps a KMS supplies its `didWebKeys` map and the document gains one
24
- * server-held key, the KMS `authentication` key (a convenience for DIDAuth);
25
- * a wallet with no KMS supplies no map and the document holds client keys
26
- * only (a later log entry can still add the KMS authentication key when the
27
- * first KMS-capable client appears). In either flavor every other relation
24
+ * server-held key, the KMS `authentication` key (a DIDAuth signing key the
25
+ * document publishes); a wallet with no KMS supplies no map and the document
26
+ * holds client keys only, permanently -- no later entry adds the KMS key to a
27
+ * document that published without it. In either flavor every other relation
28
28
  * lists client keys only. In particular no server-held key may appear under
29
29
  * `keyAgreement` (no server key is a wrap target) or under `assertionMethod`
30
30
  * (membership there is what entitles a key to issue assertions as the account
@@ -35,8 +35,7 @@
35
35
  * glue: a `Signer` bridge over a client-held update-key seed, the idempotent,
36
36
  * crash-resumable provisioning flow (`ensureDidWebvh`), the per-client
37
37
  * update-key rotation ceremony (`rotateWebvhUpdateKey`), the two-entry client
38
- * enrollment ceremony (`enrollWebvhClient`), and the lost-`keys.json` recovery
39
- * path for the did:web relationship bindings (`repairKeyBindings`). Update-key
38
+ * enrollment ceremony (`enrollWebvhClient`). Update-key
40
39
  * seeds are minted here but persisted by the caller -- with client-held keys a
41
40
  * lost seed is lost update authority, so every publish is preceded by a
42
41
  * caller-persisted write.
@@ -71,10 +70,11 @@ import { equalBytes } from '@noble/ciphers/utils.js';
71
70
  import { sha256 } from '@noble/hashes/sha2.js';
72
71
  import { base58, base64urlnopad } from '@scure/base';
73
72
  import { VOCAB_CONTEXT_URL } from 'byoe-context';
74
- import { DID_DOCUMENT_RESOURCE, DID_LOG_RESOURCE, ID_COLLECTION } from '../space/collections.js';
73
+ import { DID_LOG_RESOURCE, ID_COLLECTION } from '../space/collections.js';
75
74
  import { ResourceLogContinuityError } from '@interop/vh-resource-log';
75
+ import { putDidWebProjection } from './didWebProjection.js';
76
76
  import { multibaseOf } from './didWeb.js';
77
- import { accountLogPinId, checkAccountLogContinuity } from './verifyLog.js';
77
+ import { accountLogPinId, checkAccountLogContinuity, checkAndAdvanceAccountLogPin } from './verifyLog.js';
78
78
  /**
79
79
  * The Multikey verification-method type the did:webvh data model uses for both
80
80
  * the Ed25519 (authentication/assertionMethod/capability*) and X25519
@@ -326,8 +326,9 @@ export function assertCanonicalClientKeys({ signingKeyMultibase, keyAgreementKey
326
326
  /**
327
327
  * The one builder of the ladder VM's published verification method -- the
328
328
  * STABLE SIBLING key a standing credential derives from its ladder seed
329
- * (`@interop/wallet-core/unlock`, `ladderVmKeyMultibase`), published while
330
- * the account has no enrolled client. The shape is forced, not
329
+ * (`@interop/wallet-core/unlock`, `ladderVmKeyMultibase`), published for as
330
+ * long as that credential stands and co-resident with whatever clients the
331
+ * account has enrolled. The shape is forced, not
331
332
  * preferred: @interop/zcap's `isController` flat-compares the delegating VM's
332
333
  * `controller` string against the parent capability's controller (which the
333
334
  * server synthesizes as the account did:webvh), and only an
@@ -338,9 +339,9 @@ export function assertCanonicalClientKeys({ signingKeyMultibase, keyAgreementKey
338
339
  * The method is listed under `assertionMethod` and `capabilityDelegation`
339
340
  * ONLY -- no `authentication`, no `capabilityInvocation`, no `keyAgreement`
340
341
  * twin, and no marker property. Recognition is by that relation asymmetry
341
- * (`ladderVmIds` in the listings module): a `capabilityDelegation` member
342
- * absent from `capabilityInvocation` is the ladder VM, which also keeps it
343
- * structurally out of every client listing.
342
+ * (`ladderVmIds`, in the shared account-document readers): a
343
+ * `capabilityDelegation` member absent from `capabilityInvocation` is the
344
+ * ladder VM, which also keeps it structurally out of every client listing.
344
345
  *
345
346
  * Because the key is derived, a reinstall republishes the SAME key under the
346
347
  * SAME id, and a still-unexpired delegation it signed resumes verifying the
@@ -543,9 +544,10 @@ function assembleWebvhVerificationMethods({ controllerTemplate, didWebKeys, clie
543
544
  };
544
545
  }
545
546
  if (ladderVm) {
546
- throw new Error('did:webvh: a genesis document lists a founding client or a ladder VM, ' +
547
- 'never both -- the ladder VM exists only while the account has no ' +
548
- 'enrolled client.');
547
+ throw new Error('did:webvh: a genesis document lists either a founding client or a ' +
548
+ 'ladder VM, and this one supplies both. The constraint is ' +
549
+ 'genesis-only: a later entry may publish clients and ladder VMs ' +
550
+ 'side by side.');
549
551
  }
550
552
  const { signingKeyMultibase, keyAgreementKeyMultibase } = clientKeys;
551
553
  const method = (publicKeyMultibase) => ({
@@ -598,7 +600,7 @@ function assembleWebvhVerificationMethods({ controllerTemplate, didWebKeys, clie
598
600
  * @param options.updateKeyPublicKeyMultibase {string}
599
601
  * @param options.nextKeyHashes {string[]}
600
602
  * @param options.signer {Signer}
601
- * @returns {Promise<{ log: DIDLog; webDoc: object; did: string }>}
603
+ * @returns {Promise<CreatedWebvhLog>}
602
604
  */
603
605
  async function createWebvhLog({ wasServerUrl, spaceId, didWebKeys, clientKeys, ladderVm, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
604
606
  const { host } = new URL(wasServerUrl);
@@ -631,7 +633,19 @@ async function createWebvhLog({ wasServerUrl, spaceId, didWebKeys, clientKeys, l
631
633
  if (!result.webDoc) {
632
634
  throw new Error('createDID did not return a webDoc despite alsoKnownAsWeb.');
633
635
  }
634
- return { log: result.log, webDoc: result.webDoc, did: result.did };
636
+ return {
637
+ log: result.log,
638
+ webDoc: result.webDoc,
639
+ did: result.did,
640
+ // Detached: `createDID` hands back the very object the genesis entry
641
+ // carries as its `state`, and a read-side head's document is a copy of
642
+ // the entry's state. Cloning here makes both producers of a
643
+ // {@link PublishedWebvhLog} alias the same way, so a caller that mutates
644
+ // a document it was handed can never edit a log entry through it.
645
+ doc: structuredClone(result.doc),
646
+ updateKeys: result.meta.updateKeys ?? [],
647
+ nextKeyHashes: result.meta.nextKeyHashes ?? []
648
+ };
635
649
  }
636
650
  /**
637
651
  * Builds a genesis entry's `nextKeyHashes`: the active update key's own
@@ -688,7 +702,7 @@ export async function genesisNextKeyHashes({ activeKeyMultibase, stagedKeyMultib
688
702
  * @param options.updateKeyPublicKeyMultibase {string} ladder rung 0's key
689
703
  * @param options.nextKeyHashes {string[]} [hash(rung 0), hash(rung 1)]
690
704
  * @param options.signer {Signer} ladder rung 0's signer
691
- * @returns {Promise<{ log: DIDLog; webDoc: object; did: string }>}
705
+ * @returns {Promise<CreatedWebvhLog>}
692
706
  */
693
707
  export async function createLadderAnchoredWebvhLog({ wasServerUrl, spaceId, didWebKeys, ladderVmKeyMultibase, credentialKeyAgreementMethod, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
694
708
  return createWebvhLog({
@@ -758,17 +772,20 @@ export async function withLogConflictRetry(run) {
758
772
  * @param options.log {DIDLog}
759
773
  * @param [options.ifMatch] {string} publish only if the log is unchanged
760
774
  * @param [options.ifNoneMatch] {boolean} publish only if the log is absent
761
- * @returns {Promise<void>}
775
+ * @returns {Promise<{ etag?: string }>} the new validator of the log just
776
+ * written, for a stage building its entry on this head; absent against a
777
+ * backend that serves no ETags
762
778
  */
763
779
  export async function putLogResource({ store, log, ifMatch, ifNoneMatch }) {
764
780
  try {
765
- await store.putIdResource({
781
+ const written = await store.putIdResource({
766
782
  resourceId: DID_LOG_RESOURCE,
767
783
  content: logToJsonlString(log),
768
784
  contentType: 'text/jsonl',
769
785
  ifMatch,
770
786
  ifNoneMatch
771
787
  });
788
+ return written?.etag !== undefined ? { etag: written.etag } : {};
772
789
  }
773
790
  catch (err) {
774
791
  if (err?.name === 'PreconditionFailedError') {
@@ -777,6 +794,45 @@ export async function putLogResource({ store, log, ifMatch, ifNoneMatch }) {
777
794
  throw err;
778
795
  }
779
796
  }
797
+ /**
798
+ * THE POSTAMBLE: publishes `did.jsonl` -- the log only, never `did.json` (a
799
+ * caller writing through a bridge delegation is authorized for nothing else)
800
+ * -- and advances the caller's chain-head pin to what it just published, so a
801
+ * host rolling the log back straight afterwards is refused on the next read.
802
+ *
803
+ * So an entry published here leaves the `did:web` projection standing at
804
+ * whatever it said before: a ceremony whose entry REMOVES inventory (a
805
+ * removal, a retirement) must republish the projection itself before the
806
+ * entry lands, and a projection left behind by one that did not is mended by
807
+ * `ensureDidWebProjection` at the next visit holding a writer for the `id`
808
+ * collection (a controller-invoking client, or a transient visit under its
809
+ * generation delegation).
810
+ * The publish is conditional on the read the entry was built on; a lost race
811
+ * surfaces as a {@link WebvhLogConflictError} (the mapping lives in
812
+ * {@link putLogResource}).
813
+ *
814
+ * The two halves are one function on purpose. Separating them is what leaves
815
+ * a pin standing behind an entry this client itself published, the gap that
816
+ * already forced `BuiltOnHeadNotReachedError` into existence as a
817
+ * compensating class.
818
+ *
819
+ * @param options {object}
820
+ * @param options.store {object} anything with the seam's `putIdResource`
821
+ * @param options.log {DIDLog} the log this entry produced
822
+ * @param [options.ifMatch] {string} publish only if `did.jsonl` is unchanged
823
+ * @param [options.pinStore] {ResourceLogPinStore} the caller's chain-head
824
+ * pins; the pin advances only when a `logId` names its slot
825
+ * @param [options.logId] {string} the log's pin slot
826
+ * @returns {Promise<{ etag?: string }>} the new validator of the log this
827
+ * entry just published, for a stage building on the post-entry head
828
+ */
829
+ export async function publishEntryPinned({ store, log, ifMatch, pinStore, logId }) {
830
+ const written = await putLogResource({ store, log, ifMatch });
831
+ if (pinStore && logId !== undefined) {
832
+ await pinStore.write({ logId, pin: pinOfLog(log) });
833
+ }
834
+ return written;
835
+ }
780
836
  /**
781
837
  * Publishes an already-created log: PUT `did.jsonl` (`text/jsonl`) then PUT
782
838
  * `did.json` from `webDoc` (`application/did+json`, adopting the webvh
@@ -793,8 +849,11 @@ export async function putLogResource({ store, log, ifMatch, ifNoneMatch }) {
793
849
  * after the log's compare-and-swap was WON, so it is already serialized behind
794
850
  * that win; the log is the source of truth and the projection a derived cache;
795
851
  * and {@link concludeWithPublishedLog} re-derives and republishes the
796
- * projection from the resolved log on every ceremony's no-op path, healing any
797
- * lag a race or a torn publish leaves behind.
852
+ * projection from the resolved log on the no-op path of every ceremony that
853
+ * reaches this function, so a torn publish on one of those paths is healed by
854
+ * the next run of the same ceremony. That reach is the controller-invoking
855
+ * paths alone; `ensureDidWebProjection` is what mends a projection a
856
+ * ladder-signed entry ({@link publishEntryPinned}) left behind.
798
857
  *
799
858
  * @param options {object}
800
859
  * @param options.idStore {WebvhIdStore}
@@ -802,15 +861,19 @@ export async function putLogResource({ store, log, ifMatch, ifNoneMatch }) {
802
861
  * @param options.webDoc {object}
803
862
  * @param [options.ifMatch] {string} publish only if `did.jsonl` is unchanged
804
863
  * @param [options.ifNoneMatch] {boolean} publish only if `did.jsonl` is absent
805
- * @returns {Promise<void>}
864
+ * @returns {Promise<{ etag?: string }>} the LOG's new validator, never the
865
+ * projection's: the projection is a derived cache, and only the log's ETag
866
+ * is a precondition anything is built on
806
867
  */
807
868
  export async function publishWebvhLog({ idStore, log, webDoc, ifMatch, ifNoneMatch }) {
808
- await putLogResource({ store: idStore, log, ifMatch, ifNoneMatch });
809
- await idStore.putIdResource({
810
- resourceId: DID_DOCUMENT_RESOURCE,
811
- content: webDoc,
812
- contentType: 'application/did+json'
869
+ const written = await putLogResource({
870
+ store: idStore,
871
+ log,
872
+ ifMatch,
873
+ ifNoneMatch
813
874
  });
875
+ await putDidWebProjection({ store: idStore, webDoc });
876
+ return written;
814
877
  }
815
878
  /**
816
879
  * The shared guard-and-publish tail of every ceremony that extends the log
@@ -825,13 +888,13 @@ export async function publishWebvhLog({ idStore, log, webDoc, ifMatch, ifNoneMat
825
888
  * @param [options.updated.webDoc] {object}
826
889
  * @param [options.ifMatch] {string} the ETag of the read this entry was built
827
890
  * on
828
- * @returns {Promise<void>}
891
+ * @returns {Promise<{ etag?: string }>} the log's new validator
829
892
  */
830
893
  export async function publishUpdatedLog({ idStore, updated, ifMatch }) {
831
894
  if (!updated.webDoc) {
832
895
  throw new Error('did:webvh: updateDID returned no webDoc despite the did:web alsoKnownAs.');
833
896
  }
834
- await publishWebvhLog({
897
+ return publishWebvhLog({
835
898
  idStore,
836
899
  log: updated.log,
837
900
  webDoc: updated.webDoc,
@@ -839,20 +902,116 @@ export async function publishUpdatedLog({ idStore, updated, ifMatch }) {
839
902
  });
840
903
  }
841
904
  /**
842
- * Writes `keys.json` v2: the did:web relationship map plus the `webvh` block,
843
- * preserving the three did:web relationships. Exported for the ladder-anchored
844
- * ensure, whose create path records the account DID the same way the
845
- * enrolled-client one does.
905
+ * The served `keys.json`, read for the two members this module acts on. A body
906
+ * of any other shape reads as an empty map rather than throwing: the resource
907
+ * is host-held bookkeeping, and the caller's next step is a rewrite either
908
+ * way.
909
+ *
910
+ * @param content {unknown}
911
+ * @returns {object} the members present, as a partial key map
912
+ */
913
+ function servedKeyMap(content) {
914
+ const map = content;
915
+ const { vmId, kmsKeyId } = map?.authentication ?? {};
916
+ const did = map?.webvh?.did;
917
+ return {
918
+ ...(typeof vmId === 'string' && typeof kmsKeyId === 'string'
919
+ ? { authentication: { vmId, kmsKeyId } }
920
+ : {}),
921
+ ...(typeof did === 'string' ? { webvh: { did } } : {})
922
+ };
923
+ }
924
+ /**
925
+ * Writes `keys.json` v2: the KMS binding plus the `webvh` block. Exported for
926
+ * the ladder-anchored ensure, whose create path records the account DID the
927
+ * same way the enrolled-client one does.
928
+ *
929
+ * The body is CONSTRUCTED from the two members the map carries rather than
930
+ * spread from the caller's, so a legacy `keyAgreement` binding a stored map
931
+ * still holds is dropped by this rewrite.
932
+ *
933
+ * This is the genesis' rewrite of the map the KMS stage created one stage
934
+ * earlier, so it carries that write's ETag as its `ifMatch`. A caller holding
935
+ * no ETag (a backend that versions nothing) writes unconditionally.
936
+ *
937
+ * A lost precondition CONVERGES rather than propagating, since the genesis
938
+ * entry has already published by the time this runs and a bookkeeping resource
939
+ * must not fail a ceremony standing behind it: the served map is re-read, a
940
+ * map already naming this DID under this binding is left alone, and anything
941
+ * else is rewritten once under the served ETag. A second lost precondition
942
+ * propagates. Without the store's optional read there is nothing to converge
943
+ * on, and the first failure propagates.
846
944
  *
847
945
  * @param options {object}
848
946
  * @param options.idStore {WebvhIdStore}
849
- * @param options.didWebKeys {DidWebKeyMap}
947
+ * @param options.didWebKeys {DidWebKeyMap} the binding to record, which on
948
+ * the create path is the one this run published in the genesis entry
850
949
  * @param options.webvh {DidWebvhBlock}
950
+ * @param [options.ifMatch] {string} the ETag the KMS stage's own write
951
+ * returned
952
+ * @returns {Promise<void>}
953
+ */
954
+ export async function writeKeysJson({ idStore, didWebKeys, webvh, ifMatch }) {
955
+ const content = {
956
+ authentication: didWebKeys.authentication,
957
+ webvh
958
+ };
959
+ try {
960
+ await idStore.putKeyMap({
961
+ content,
962
+ ...(ifMatch !== undefined && { ifMatch })
963
+ });
964
+ return;
965
+ }
966
+ catch (err) {
967
+ const lostRace = err?.name === 'PreconditionFailedError';
968
+ if (!lostRace || !idStore.getKeyMapRaw) {
969
+ throw err;
970
+ }
971
+ const served = await idStore.getKeyMapRaw();
972
+ const servedMap = servedKeyMap(served?.content);
973
+ if (servedMap.webvh?.did === webvh.did &&
974
+ servedMap.authentication?.vmId === content.authentication.vmId) {
975
+ // The winner recorded this DID under this binding already: done.
976
+ return;
977
+ }
978
+ await idStore.putKeyMap({
979
+ content,
980
+ ...(served?.etag !== undefined && { ifMatch: served.etag })
981
+ });
982
+ }
983
+ }
984
+ /**
985
+ * Records the account DID into a `keys.json` carrying a KMS binding and no (or
986
+ * a stale) `webvh` block -- the state a run torn between its genesis entry and
987
+ * its rewrite leaves behind. It is the adoption path's half of that rewrite,
988
+ * and the binding comes from the SERVED map: the run that published the log
989
+ * recorded it, while this run's own map may name a key that log never
990
+ * published.
991
+ *
992
+ * A no-op when the store offers no read, when the served map carries no
993
+ * binding, and when it already names this DID.
994
+ *
995
+ * @param options {object}
996
+ * @param options.idStore {WebvhIdStore}
997
+ * @param options.did {string} the DID the adopted log resolves to
851
998
  * @returns {Promise<void>}
852
999
  */
853
- export async function writeKeysJson({ idStore, didWebKeys, webvh }) {
854
- const content = { ...didWebKeys, webvh };
855
- await idStore.putKeyMap({ content });
1000
+ export async function backfillKeyMapWebvhBlock({ idStore, did }) {
1001
+ if (!idStore.getKeyMapRaw) {
1002
+ return;
1003
+ }
1004
+ const served = await idStore.getKeyMapRaw();
1005
+ const servedMap = servedKeyMap(served?.content);
1006
+ if (!servedMap.authentication || servedMap.webvh?.did === did) {
1007
+ return;
1008
+ }
1009
+ await writeKeysJson({
1010
+ idStore,
1011
+ didWebKeys: { authentication: servedMap.authentication },
1012
+ webvh: { did },
1013
+ ...(served?.etag !== undefined && { ifMatch: served.etag })
1014
+ });
856
1015
  }
857
1016
  /**
858
1017
  * Reads and resolves the published `did.jsonl`, or returns `undefined` when
@@ -881,7 +1040,9 @@ export async function writeKeysJson({ idStore, didWebKeys, webvh }) {
881
1040
  * refused as a `rollback` instead of read as `undefined`.
882
1041
  *
883
1042
  * @param options {object}
884
- * @param options.idStore {WebvhIdStore}
1043
+ * @param options.idStore {Pick<WebvhIdStore, 'getIdResourceRaw'>} the read
1044
+ * half of the seam alone, so a caller holding a narrower store (a bridge
1045
+ * delegation's read + PUT pair) passes it directly
885
1046
  * @param [options.expectedDid] {string} the DID the log must resolve to
886
1047
  * @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
887
1048
  * pins for the account log
@@ -915,20 +1076,7 @@ export async function readPublishedLog({ idStore, expectedDid, pinStore, logId }
915
1076
  if (resolved.meta.error || !resolved.did || !resolved.doc) {
916
1077
  throw new Error(`did:webvh: existing did.jsonl failed to resolve (${resolved.meta.error}).`);
917
1078
  }
918
- if (expectedDid !== undefined && resolved.did !== expectedDid) {
919
- throw new Error('did:webvh: the published did.jsonl resolves to a different DID ' +
920
- `(${resolved.did}) than expected (${expectedDid}).`);
921
- }
922
- if (pinned) {
923
- const pin = await pinned.store.read({ logId: pinned.logId });
924
- const served = checkAccountLogContinuity({ log, pin });
925
- // Advanced only when the served head is genuinely ahead of the pin: the
926
- // check above has already refused everything that is not.
927
- if (!pin || pin.head !== served.head) {
928
- await pinned.store.write({ logId: pinned.logId, pin: served });
929
- }
930
- }
931
- return {
1079
+ const published = {
932
1080
  log,
933
1081
  did: resolved.did,
934
1082
  doc: resolved.doc,
@@ -936,6 +1084,77 @@ export async function readPublishedLog({ idStore, expectedDid, pinStore, logId }
936
1084
  nextKeyHashes: resolved.meta.nextKeyHashes ?? [],
937
1085
  etag: read.etag
938
1086
  };
1087
+ assertPublishedLogDid({ published, expectedDid });
1088
+ if (pinned) {
1089
+ await checkAndAdvanceAccountLogPin({
1090
+ pinStore: pinned.store,
1091
+ logId: pinned.logId,
1092
+ log
1093
+ });
1094
+ }
1095
+ return published;
1096
+ }
1097
+ /**
1098
+ * The substituted-account refusal on its own: a resolved head that is not the
1099
+ * DID the caller expected is refused rather than built on. Stated once so a
1100
+ * fresh read and a caller-threaded head refuse identically.
1101
+ *
1102
+ * A ceremony that takes an already-read head from its caller (the transient
1103
+ * visit's one-read composition, where the readiness stage hands its verified
1104
+ * head to the enrollment) runs this explicitly: the read that would otherwise
1105
+ * have run it never happened, and a head resolving to another DID must not
1106
+ * become the entry's basis just because it arrived by parameter.
1107
+ *
1108
+ * @param options {object}
1109
+ * @param options.published {PublishedWebvhLog} the resolved head
1110
+ * @param [options.expectedDid] {string} the DID it must resolve to; absent,
1111
+ * the head is accepted (the caller discovering the DID from the log itself)
1112
+ * @returns {PublishedWebvhLog} the head verbatim
1113
+ */
1114
+ export function assertPublishedLogDid({ published, expectedDid }) {
1115
+ if (expectedDid !== undefined && published.did !== expectedDid) {
1116
+ throw new Error('did:webvh: the published did.jsonl resolves to a different DID ' +
1117
+ `(${published.did}) than expected (${expectedDid}).`);
1118
+ }
1119
+ return published;
1120
+ }
1121
+ /**
1122
+ * {@link readPublishedLog} for the ceremonies whose premise is a log that
1123
+ * already exists: an absent `did.jsonl` is a refusal rather than a state to
1124
+ * branch on, so the caller gets a `PublishedWebvhLog` or an error. The
1125
+ * `missingMessage` is the caller's own phrasing of what there is nothing to do
1126
+ * against ("nothing to enroll into", "nothing to recover"), since the read
1127
+ * itself cannot know which ceremony is standing on it.
1128
+ *
1129
+ * Every other option, and every check behind them -- the `expectedDid`
1130
+ * refusal and the chain-head pin's rollback / fork / identity-switch
1131
+ * refusals -- is {@link readPublishedLog}'s verbatim. Each attempt of a
1132
+ * conflict-retried ceremony reads for itself, so the continuity check runs on
1133
+ * the read the compare-and-swap publish is conditioned on rather than only on
1134
+ * an orchestrator's pre-read.
1135
+ *
1136
+ * @param options {object}
1137
+ * @param options.idStore {Pick<WebvhIdStore, 'getIdResourceRaw'>}
1138
+ * @param [options.expectedDid] {string} the DID the log must resolve to
1139
+ * @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
1140
+ * pins for the log being read
1141
+ * @param [options.logId] {string} the log's pin-slot key; required whenever
1142
+ * a `pinStore` is supplied
1143
+ * @param [options.missingMessage] {string} the thrown `Error`'s message when
1144
+ * the log is absent
1145
+ * @returns {Promise<PublishedWebvhLog>}
1146
+ */
1147
+ export async function readPublishedLogOrThrow({ idStore, expectedDid, pinStore, logId, missingMessage = 'did:webvh: did.jsonl is missing.' }) {
1148
+ const published = await readPublishedLog({
1149
+ idStore,
1150
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
1151
+ ...(pinStore ? { pinStore } : {}),
1152
+ ...(logId !== undefined ? { logId } : {})
1153
+ });
1154
+ if (!published) {
1155
+ throw new Error(missingMessage);
1156
+ }
1157
+ return published;
939
1158
  }
940
1159
  /**
941
1160
  * The chain-head pin a log establishes: the genesis entry's method and SCID
@@ -967,16 +1186,22 @@ export function servedHead(log) {
967
1186
  * already-revoked no-op. All of them used to infer completion from `did.jsonl`
968
1187
  * alone, which is a half of the state: {@link publishWebvhLog} writes the log
969
1188
  * and its `did:web` projection in two non-atomic PUTs, so a crash between them
970
- * leaves a `did.jsonl` that is complete beside a `did.json` that lags it
971
- * forever (nothing else republishes the projection).
1189
+ * leaves a `did.jsonl` that is complete beside a `did.json` that lags it.
972
1190
  *
973
1191
  * So the projection is re-derived from the resolved log and re-PUT
974
1192
  * unconditionally rather than compared first: the write is idempotent and one
975
1193
  * request either way, and the resolved log is the source of truth for what the
976
1194
  * projection must say. A ceremony that no-ops on the log therefore still heals
977
- * a torn earlier publish. The projection PUT is deliberately unconditional
978
- * here too: healing it is the whole point, and the log it was derived from is
979
- * the state this call just read and resolved.
1195
+ * a torn earlier publish OF THAT CEREMONY. The projection PUT is deliberately
1196
+ * unconditional here too: healing it is the whole point, and the log it was
1197
+ * derived from is the state this call just read and resolved.
1198
+ *
1199
+ * The reach is what to hold on to: every caller here invokes as the account's
1200
+ * controller, so this heals nothing for the ladder-signed entries that publish
1201
+ * through {@link publishEntryPinned} and write the log alone. Those are mended
1202
+ * by their own ceremony's pre-entry projection PUT and, failing that, by
1203
+ * `ensureDidWebProjection` at the next visit that holds an `id`-collection
1204
+ * writer.
980
1205
  *
981
1206
  * @param options {object}
982
1207
  * @param options.idStore {WebvhIdStore}
@@ -985,10 +1210,9 @@ export function servedHead(log) {
985
1210
  * resolved document
986
1211
  */
987
1212
  export async function concludeWithPublishedLog({ idStore, published }) {
988
- await idStore.putIdResource({
989
- resourceId: DID_DOCUMENT_RESOURCE,
990
- content: generateParallelDidWeb(published.did, published.doc),
991
- contentType: 'application/did+json'
1213
+ await putDidWebProjection({
1214
+ store: idStore,
1215
+ webDoc: generateParallelDidWeb(published.did, published.doc)
992
1216
  });
993
1217
  return { did: published.did, doc: published.doc };
994
1218
  }
@@ -1017,6 +1241,22 @@ export function effectiveParameters(log) {
1017
1241
  }
1018
1242
  return out;
1019
1243
  }
1244
+ /**
1245
+ * The log's current `updateKeys` / `nextKeyHashes` view: the last entry's
1246
+ * effective parameters, or the empty pair for a log with no entries.
1247
+ *
1248
+ * The empty default is a policy choice, not a convenience: it is what makes
1249
+ * an unresolvable log attribute as "no rung standing" rather than throw, so
1250
+ * it is stated here once rather than at each attribution site.
1251
+ *
1252
+ * @param published {object}
1253
+ * @param published.log {DIDLog}
1254
+ * @returns {object}
1255
+ */
1256
+ export function currentLogParameters(published) {
1257
+ const params = effectiveParameters(published.log);
1258
+ return params[params.length - 1] ?? { updateKeys: [], nextKeyHashes: [] };
1259
+ }
1020
1260
  /**
1021
1261
  * The `publicKeyMultibase` of every update-key seed this client holds, in
1022
1262
  * role order (active, staged, pending).
@@ -1103,8 +1343,10 @@ function advancedSeeds({ published, multibases, updateKeys }) {
1103
1343
  * @param options.wasServerUrl {string}
1104
1344
  * @param options.spaceId {string}
1105
1345
  * @param [options.didWebKeys] {DidWebKeyMapV2} the parsed keys.json (with any
1106
- * webvh block) returned by the did:web provisioning; absent on a
1346
+ * webvh block) returned by the KMS-authentication stage; absent on a
1107
1347
  * client-keys-only genesis (no KMS anywhere in the path)
1348
+ * @param [options.keysJsonEtag] {string} the ETag that stage's own write
1349
+ * returned, carried as the `ifMatch` of the rewrite that records the DID
1108
1350
  * @param options.clientKeys {WebvhClientKeys} this client's published keys
1109
1351
  * @param options.updateKeys {ClientWebvhUpdateKeys} already persisted
1110
1352
  * client-local
@@ -1124,7 +1366,7 @@ export async function ensureDidWebvh(options) {
1124
1366
  * @param options {object} see {@link ensureDidWebvh}
1125
1367
  * @returns {Promise<{ did: string }>}
1126
1368
  */
1127
- async function ensureDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, clientKeys, updateKeys, expectedDid, pinStore }) {
1369
+ async function ensureDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys, keysJsonEtag, clientKeys, updateKeys, expectedDid, pinStore }) {
1128
1370
  // The DID this run expects the published log to resolve to: the caller's,
1129
1371
  // else the one keys.json already records. Undefined only on the documented
1130
1372
  // first-contact adoption, which discovers the DID from the log itself.
@@ -1156,10 +1398,13 @@ async function ensureDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys,
1156
1398
  await writeKeysJson({
1157
1399
  idStore,
1158
1400
  didWebKeys,
1159
- webvh: { did: published.did }
1401
+ webvh: { did: published.did },
1402
+ ...(keysJsonEtag !== undefined && { ifMatch: keysJsonEtag })
1160
1403
  });
1161
1404
  }
1162
- // Heals a did.json left lagging by a torn earlier publish.
1405
+ // Heals a did.json left lagging by a torn earlier publish of this
1406
+ // controller-invoking path (a ladder-signed entry's lag is not reached
1407
+ // here; `ensureDidWebProjection` is that mender).
1163
1408
  const { did } = await concludeWithPublishedLog({ idStore, published });
1164
1409
  return { did };
1165
1410
  }
@@ -1194,7 +1439,8 @@ async function ensureDidWebvhOnce({ idStore, wasServerUrl, spaceId, didWebKeys,
1194
1439
  await writeKeysJson({
1195
1440
  idStore,
1196
1441
  didWebKeys,
1197
- webvh: { did: created.did }
1442
+ webvh: { did: created.did },
1443
+ ...(keysJsonEtag !== undefined && { ifMatch: keysJsonEtag })
1198
1444
  });
1199
1445
  }
1200
1446
  return { did: created.did };
@@ -1378,281 +1624,4 @@ export async function assertCarryOverCommitments({ published }) {
1378
1624
  }
1379
1625
  }
1380
1626
  }
1381
- /**
1382
- * The relationship references of a resolved document as verification-method
1383
- * ids, tolerating embedded objects beside string references.
1384
- *
1385
- * @param relation {Array} the relationship array, when present
1386
- * @returns {string[]}
1387
- */
1388
- export function relationIds(relation) {
1389
- const ids = [];
1390
- for (const entry of relation ?? []) {
1391
- const id = typeof entry === 'string' ? entry : entry?.id;
1392
- if (id) {
1393
- ids.push(id);
1394
- }
1395
- }
1396
- return ids;
1397
- }
1398
- /**
1399
- * Enrolls a second wallet client into the published did:webvh document -- the
1400
- * log half of the enrollment ceremony (the user key roster wrap happens first,
1401
- * outside this module). Two entries, forced by prerotation (a new update key
1402
- * must hash into the PREVIOUS entry's `nextKeyHashes`):
1403
- *
1404
- * 1. **Commit**: a sparse entry extending `nextKeyHashes` with the new
1405
- * client's update-key and staged-key hashes (document and `updateKeys`
1406
- * untouched).
1407
- * 2. **Add**: an entry adding the new client's verification methods (its
1408
- * Ed25519 key under the four signing relationships, its X25519 twin under
1409
- * `keyAgreement`) and its update key to `updateKeys`.
1410
- *
1411
- * Both entries are signed by THIS client's active update key (quorum-of-one:
1412
- * any single enrolled client can enroll). The ceremony is resumable from
1413
- * durable state alone: a tear after the commit is detected by its hashes
1414
- * already standing in `nextKeyHashes` (skip to the add entry), and a
1415
- * completed enrollment is detected by the update key already being authorized
1416
- * (no-op). Re-running with the same key set converges without forking the
1417
- * log.
1418
- *
1419
- * Each entry publishes conditionally on the read it was built on (the add
1420
- * entry on the mid-ceremony re-read), so a concurrent ceremony -- a revocation,
1421
- * another enrollment -- can never be erased by this one; a lost race re-runs
1422
- * from the top (see {@link withLogConflictRetry}) and rebases on the new head.
1423
- *
1424
- * @param options {object}
1425
- * @param options.idStore {WebvhIdStore}
1426
- * @param options.updateKeys {ClientWebvhUpdateKeys} THIS client's seeds
1427
- * @param options.newClient {WebvhEnrollmentKeys} the enrollee's public halves
1428
- * @returns {Promise<{ did: string }>}
1429
- */
1430
- export async function enrollWebvhClient(options) {
1431
- return withLogConflictRetry(() => enrollWebvhClientOnce(options));
1432
- }
1433
- /**
1434
- * One attempt of {@link enrollWebvhClient}, re-invoked by the conflict retry.
1435
- *
1436
- * @param options {object} see {@link enrollWebvhClient}
1437
- * @returns {Promise<{ did: string }>}
1438
- */
1439
- async function enrollWebvhClientOnce({ idStore, updateKeys, newClient }) {
1440
- let published = await readPublishedLog({ idStore });
1441
- if (!published) {
1442
- throw new Error('did:webvh: did.jsonl is missing; nothing to enroll into.');
1443
- }
1444
- const multibases = await updateKeyMultibases({ updateKeys });
1445
- // Already enrolled (a completed earlier run): the new client's update key is
1446
- // authorized, which only the add entry writes. Idempotent no-op on the log,
1447
- // but it still heals a did.json the earlier run left lagging.
1448
- if (published.updateKeys.includes(newClient.updateKeyMultibase)) {
1449
- const { did } = await concludeWithPublishedLog({ idStore, published });
1450
- return { did };
1451
- }
1452
- // Both entries are signed by this client's active update key; a log that
1453
- // does not authorize it (a rotation torn elsewhere) must heal first.
1454
- if (!published.updateKeys.includes(multibases.update)) {
1455
- throw new Error("did:webvh: the published log does not authorize this client's active " +
1456
- 'update key; finalize the pending rotation before enrolling.');
1457
- }
1458
- const newUpdateKeyHash = await deriveNextKeyHash(newClient.updateKeyMultibase);
1459
- const newStagedKeyHash = await deriveNextKeyHash(newClient.stagedUpdateKeyMultibase);
1460
- // The commit entry (skipped when a torn earlier run already published it).
1461
- const committed = published.nextKeyHashes.includes(newUpdateKeyHash) &&
1462
- published.nextKeyHashes.includes(newStagedKeyHash);
1463
- if (!committed) {
1464
- // A sparse entry re-states the authorized updateKeys, and the resolver
1465
- // checks each against the PREVIOUS entry's commitments -- so every
1466
- // currently authorized key's hash must already stand in nextKeyHashes
1467
- // (the carry-over convention). A log minted before the convention cannot
1468
- // take a non-rotating entry.
1469
- for (const key of published.updateKeys) {
1470
- if (!published.nextKeyHashes.includes(await deriveNextKeyHash(key))) {
1471
- throw new Error('did:webvh: the published log does not carry the active update ' +
1472
- "keys' own hashes in nextKeyHashes (it predates the carry-over " +
1473
- 'commitment convention); re-provision the account before ' +
1474
- 'enrolling.');
1475
- }
1476
- }
1477
- const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
1478
- const updated = await updateDID({
1479
- log: published.log,
1480
- signer,
1481
- alsoKnownAsWeb: true,
1482
- // Re-stated unchanged (the library requires them explicitly while
1483
- // prerotation is active); the carry-over commitments are what make the
1484
- // re-statement resolvable.
1485
- updateKeys: published.updateKeys,
1486
- nextKeyHashes: [
1487
- ...new Set([
1488
- ...published.nextKeyHashes,
1489
- newUpdateKeyHash,
1490
- newStagedKeyHash
1491
- ])
1492
- ]
1493
- });
1494
- await publishUpdatedLog({ idStore, updated, ifMatch: published.etag });
1495
- // Re-read through the same verifying path the resume case uses, so the
1496
- // add entry below always builds on the published, resolved state. It must
1497
- // still be the same account: the DID the first read resolved to is what
1498
- // the entry just published extends.
1499
- published = await readPublishedLog({ idStore, expectedDid: published.did });
1500
- if (!published) {
1501
- throw new Error('did:webvh: did.jsonl vanished mid-enrollment.');
1502
- }
1503
- }
1504
- // The add entry: the new client's two verification methods and its update
1505
- // key, on top of the full existing document (updateDID replaces the
1506
- // verification-method set and relationship arrays wholesale).
1507
- const { did, doc, log, updateKeys: authorizedKeys, nextKeyHashes, etag } = published;
1508
- const vmId = (publicKeyMultibase) => `${did}#${publicKeyMultibase}`;
1509
- // The signing method is controlled by the account; the key-agreement method
1510
- // alone carries the controller marker, and the pair builder refuses a
1511
- // key-agreement key that is not the signing key's canonical twin.
1512
- const addedMethods = markedVerificationMethodPair({
1513
- controller: did,
1514
- signingKeyMultibase: newClient.signingKeyMultibase,
1515
- keyAgreementKeyMultibase: newClient.keyAgreementKeyMultibase
1516
- });
1517
- const existingMethods = (doc.verificationMethod ?? []);
1518
- const verificationMethods = [
1519
- ...existingMethods.filter(method => !addedMethods.some(added => added.id === method.id)),
1520
- ...addedMethods
1521
- ];
1522
- const withReference = (relation, id) => [...new Set([...relationIds(relation), id])];
1523
- const signingVmId = vmId(newClient.signingKeyMultibase);
1524
- const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
1525
- const updated = await updateDID({
1526
- log,
1527
- signer,
1528
- alsoKnownAsWeb: true,
1529
- updateKeys: [...new Set([...authorizedKeys, newClient.updateKeyMultibase])],
1530
- nextKeyHashes,
1531
- verificationMethods,
1532
- authentication: withReference(doc.authentication, signingVmId),
1533
- assertionMethod: withReference(doc.assertionMethod, signingVmId),
1534
- keyAgreement: withReference(doc.keyAgreement, vmId(newClient.keyAgreementKeyMultibase)),
1535
- capabilityInvocation: withReference(doc.capabilityInvocation, signingVmId),
1536
- capabilityDelegation: withReference(doc.capabilityDelegation, signingVmId)
1537
- });
1538
- // The etag of the read this entry was built on: the mid-ceremony re-read
1539
- // when the commit entry ran here, the original read when it was skipped.
1540
- await publishUpdatedLog({ idStore, updated, ifMatch: etag });
1541
- return { did: updated.did };
1542
- }
1543
- /**
1544
- * Rebuilds `keys.json` from the published artifacts plus a WebKMS key listing
1545
- * -- the recovery path for a lost or rolled-back `keys.json`. List Keys is
1546
- * authorized as `read` against the keystore controller, which only the
1547
- * root-controlled keystore agent can invoke.
1548
- *
1549
- * KMS key local ids are server-generated random and appear in no published
1550
- * artifact, so the bindings are rediscovered by public key material instead.
1551
- * List the keystore once -- each listed description carries `keyUrl`, the
1552
- * key's canonical invocation URL (the signable handle its alias-overridden
1553
- * `id` erases) -- then match `did.json`'s relationship verification methods
1554
- * by `publicKeyMultibase` and rewrite `keys.json` from what matched. The
1555
- * `authentication` and `keyAgreement` bindings are required; `assertionMethod`
1556
- * lists client keys only, so no KMS binding exists there and none is rebuilt.
1557
- * When `did.jsonl` is published, its resolved DID is recorded in the `webvh`
1558
- * block; there is nothing else to repair there, since the log's update keys are
1559
- * client-held seeds that no keystore listing could recover.
1560
- *
1561
- * An unmatchable binding is unrepairable and throws: a published artifact
1562
- * depends on a key the keystore no longer lists.
1563
- *
1564
- * The log read takes the same checks every other ceremony read does, and for
1565
- * the same reason: what it reads is written straight back into the rebuilt
1566
- * `keys.json`, so a substituted log would rewrite the `webvh` block to a wrong
1567
- * DID and a truncated one would be adopted as this account's history. A caller
1568
- * that still holds the account pointer passes `expectedDid` and its
1569
- * `pinStore`; the pure-recovery caller that has lost everything but the
1570
- * keystore has no DID to expect and passes neither, which is the one read here
1571
- * that legitimately discovers the DID from the log itself.
1572
- *
1573
- * @param options {object}
1574
- * @param options.keystoreAgent {KeystoreAgent}
1575
- * @param options.idStore {WebvhIdStore}
1576
- * @param [options.expectedDid] {string} the DID the published log must
1577
- * resolve to
1578
- * @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
1579
- * pins for the account log
1580
- * @param [options.logId] {string} the account log's pin-slot key, built by
1581
- * the caller with `accountLogPinId({ spaceId })`; required whenever a
1582
- * `pinStore` is supplied
1583
- * @returns {Promise<DidWebKeyMapV2>} the rebuilt, persisted keys.json
1584
- */
1585
- export async function repairKeyBindings({ keystoreAgent, idStore, expectedDid, pinStore, logId }) {
1586
- if (pinStore && logId === undefined) {
1587
- throw new TypeError('logId is required when pinStore is supplied');
1588
- }
1589
- const didDoc = (await idStore.getIdResource({
1590
- resourceId: DID_DOCUMENT_RESOURCE
1591
- }));
1592
- if (!didDoc) {
1593
- throw new Error('keys.json repair: did.json is not published; there is nothing to ' +
1594
- 'match key bindings against.');
1595
- }
1596
- // One listing, matched by public key material below. `keyUrl` is the
1597
- // list-only projection field (webkms-client >= 14.7.1 types it; an older
1598
- // server omits it, so entries without one are skipped and simply fail to
1599
- // match).
1600
- const listed = (await keystoreAgent.listKeys());
1601
- const keyUrlByMultibase = new Map();
1602
- for (const description of listed) {
1603
- if (description.publicKeyMultibase && description.keyUrl) {
1604
- keyUrlByMultibase.set(description.publicKeyMultibase, description.keyUrl);
1605
- }
1606
- }
1607
- // A relationship's first keystore-backed verification method. A
1608
- // relationship can name several verification methods now that enrolled
1609
- // clients publish their own keys beside the KMS ones, so every reference is
1610
- // tried and the first keystore-backed one wins; a client-held key simply
1611
- // fails to match and is skipped.
1612
- const findKmsBacked = (relationship) => {
1613
- const tried = [];
1614
- for (const reference of didDoc[relationship] ?? []) {
1615
- const vmId = typeof reference === 'string' ? reference : reference?.id;
1616
- if (!vmId) {
1617
- continue;
1618
- }
1619
- const method = didDoc.verificationMethod?.find(entry => entry.id === vmId);
1620
- const publicKeyMultibase = method?.publicKeyMultibase ?? multibaseOf(vmId);
1621
- tried.push(publicKeyMultibase);
1622
- const kmsKeyId = keyUrlByMultibase.get(publicKeyMultibase);
1623
- if (kmsKeyId) {
1624
- return { bound: { vmId, kmsKeyId }, tried };
1625
- }
1626
- }
1627
- return { tried };
1628
- };
1629
- const bind = (relationship) => {
1630
- if ((didDoc[relationship] ?? []).length === 0) {
1631
- throw new Error(`keys.json repair: did.json declares no ${relationship} verification method.`);
1632
- }
1633
- const { bound, tried } = findKmsBacked(relationship);
1634
- if (!bound) {
1635
- throw new Error(`keys.json repair: no keystore key matches the ${relationship} ` +
1636
- `verification method (${tried.join(', ')}).`);
1637
- }
1638
- return bound;
1639
- };
1640
- const repaired = {
1641
- authentication: bind('authentication'),
1642
- keyAgreement: bind('keyAgreement')
1643
- };
1644
- // The webvh block, recovered from the published log: the DID and nothing
1645
- // else, since the update keys never left the client that minted them.
1646
- const published = await readPublishedLog({
1647
- idStore,
1648
- ...(expectedDid !== undefined ? { expectedDid } : {}),
1649
- ...(pinStore && logId !== undefined ? { pinStore, logId } : {})
1650
- });
1651
- if (published) {
1652
- repaired.webvh = { did: published.did };
1653
- }
1654
- // Persist the rebuilt anchor in one write.
1655
- await idStore.putKeyMap({ content: repaired });
1656
- return repaired;
1657
- }
1658
1627
  //# sourceMappingURL=didWebvh.js.map