mindoodb 0.0.50 → 0.0.53

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 (198) hide show
  1. package/README-server.md +11 -2
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +13 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +45 -20
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
  7. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/HttpTransport.js +6 -62
  9. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +24 -0
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +75 -24
  13. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  14. package/dist/browser/index.mjs +72 -0
  15. package/dist/core/BaseMindooDB.d.ts +89 -14
  16. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  17. package/dist/core/BaseMindooDB.js +692 -201
  18. package/dist/core/BaseMindooDB.js.map +1 -1
  19. package/dist/core/BaseMindooTenant.d.ts +59 -7
  20. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  21. package/dist/core/BaseMindooTenant.js +427 -90
  22. package/dist/core/BaseMindooTenant.js.map +1 -1
  23. package/dist/core/BaseMindooTenantDirectory.d.ts +61 -9
  24. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  25. package/dist/core/BaseMindooTenantDirectory.js +363 -35
  26. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  27. package/dist/core/BaseMindooTenantFactory.d.ts +17 -1
  28. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  29. package/dist/core/BaseMindooTenantFactory.js +292 -16
  30. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  31. package/dist/core/accesscontrol/DirectoryStateNode.d.ts +68 -1
  32. package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
  33. package/dist/core/accesscontrol/DirectoryStateNode.js +157 -4
  34. package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
  35. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  36. package/dist/core/accesscontrol/directoryProjection.js +14 -5
  37. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  38. package/dist/core/accesscontrol/grantKeys.d.ts +17 -14
  39. package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
  40. package/dist/core/accesscontrol/grantKeys.js +51 -21
  41. package/dist/core/accesscontrol/grantKeys.js.map +1 -1
  42. package/dist/core/accesscontrol/types.d.ts +13 -7
  43. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  44. package/dist/core/accesscontrol/types.js +16 -10
  45. package/dist/core/accesscontrol/types.js.map +1 -1
  46. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
  47. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  48. package/dist/core/appendonlystores/network/AuthenticationService.js +70 -14
  49. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  50. package/dist/core/appendonlystores/network/entryWireCodec.d.ts +51 -0
  51. package/dist/core/appendonlystores/network/entryWireCodec.d.ts.map +1 -0
  52. package/dist/core/appendonlystores/network/entryWireCodec.js +93 -0
  53. package/dist/core/appendonlystores/network/entryWireCodec.js.map +1 -0
  54. package/dist/core/builtinDbInvariants.d.ts +53 -0
  55. package/dist/core/builtinDbInvariants.d.ts.map +1 -0
  56. package/dist/core/builtinDbInvariants.js +147 -0
  57. package/dist/core/builtinDbInvariants.js.map +1 -0
  58. package/dist/core/copy/feasibility.d.ts.map +1 -1
  59. package/dist/core/copy/feasibility.js +3 -0
  60. package/dist/core/copy/feasibility.js.map +1 -1
  61. package/dist/core/copy/flatten.d.ts.map +1 -1
  62. package/dist/core/copy/flatten.js +7 -2
  63. package/dist/core/copy/flatten.js.map +1 -1
  64. package/dist/core/copy/host.d.ts +5 -0
  65. package/dist/core/copy/host.d.ts.map +1 -1
  66. package/dist/core/copy/preflight.d.ts.map +1 -1
  67. package/dist/core/copy/preflight.js +7 -1
  68. package/dist/core/copy/preflight.js.map +1 -1
  69. package/dist/core/copy/types.d.ts +8 -1
  70. package/dist/core/copy/types.d.ts.map +1 -1
  71. package/dist/core/crypto/EntrySignature.d.ts +8 -2
  72. package/dist/core/crypto/EntrySignature.d.ts.map +1 -1
  73. package/dist/core/crypto/EntrySignature.js +14 -5
  74. package/dist/core/crypto/EntrySignature.js.map +1 -1
  75. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
  76. package/dist/core/crypto/MindooDocSigner.js +11 -2
  77. package/dist/core/crypto/MindooDocSigner.js.map +1 -1
  78. package/dist/core/crypto/RSAEncryption.d.ts +19 -0
  79. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
  80. package/dist/core/crypto/RSAEncryption.js +36 -3
  81. package/dist/core/crypto/RSAEncryption.js.map +1 -1
  82. package/dist/core/crypto/privateKeyEncryption.js +1 -1
  83. package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
  84. package/dist/core/crypto/rsaOaep3072.d.ts +12 -0
  85. package/dist/core/crypto/rsaOaep3072.d.ts.map +1 -0
  86. package/dist/core/crypto/rsaOaep3072.js +30 -0
  87. package/dist/core/crypto/rsaOaep3072.js.map +1 -0
  88. package/dist/core/databaseIdValidation.d.ts +23 -0
  89. package/dist/core/databaseIdValidation.d.ts.map +1 -1
  90. package/dist/core/databaseIdValidation.js +59 -1
  91. package/dist/core/databaseIdValidation.js.map +1 -1
  92. package/dist/core/index.d.ts +9 -2
  93. package/dist/core/index.d.ts.map +1 -1
  94. package/dist/core/index.js +40 -4
  95. package/dist/core/index.js.map +1 -1
  96. package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
  97. package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
  98. package/dist/core/join/deviceJoinBootstrap.js +37 -0
  99. package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
  100. package/dist/core/keys/KeyBag.d.ts +20 -2
  101. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  102. package/dist/core/keys/KeyBag.js +22 -10
  103. package/dist/core/keys/KeyBag.js.map +1 -1
  104. package/dist/core/logging/MindooLogger.d.ts.map +1 -1
  105. package/dist/core/logging/MindooLogger.js +65 -40
  106. package/dist/core/logging/MindooLogger.js.map +1 -1
  107. package/dist/core/tenantIdValidation.d.ts.map +1 -1
  108. package/dist/core/tenantIdValidation.js +1 -1
  109. package/dist/core/tenantIdValidation.js.map +1 -1
  110. package/dist/core/tenantSetup.d.ts +109 -0
  111. package/dist/core/tenantSetup.d.ts.map +1 -0
  112. package/dist/core/tenantSetup.js +250 -0
  113. package/dist/core/tenantSetup.js.map +1 -0
  114. package/dist/core/types.d.ts +333 -43
  115. package/dist/core/types.d.ts.map +1 -1
  116. package/dist/core/types.js +27 -9
  117. package/dist/core/types.js.map +1 -1
  118. package/dist/core/uri/joinRequestUri.d.ts +37 -0
  119. package/dist/core/uri/joinRequestUri.d.ts.map +1 -0
  120. package/dist/core/uri/joinRequestUri.js +168 -0
  121. package/dist/core/uri/joinRequestUri.js.map +1 -0
  122. package/dist/core/userid/canonicalUsername.d.ts +66 -0
  123. package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
  124. package/dist/core/userid/canonicalUsername.js +235 -0
  125. package/dist/core/userid/canonicalUsername.js.map +1 -0
  126. package/dist/core/userid/index.d.ts +9 -0
  127. package/dist/core/userid/index.d.ts.map +1 -1
  128. package/dist/core/userid/index.js +14 -0
  129. package/dist/core/userid/index.js.map +1 -1
  130. package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
  131. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
  132. package/dist/core/userkeys/UserKeyDocument.js +145 -0
  133. package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
  134. package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
  135. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
  136. package/dist/core/userkeys/UserKeyManager.js +1277 -0
  137. package/dist/core/userkeys/UserKeyManager.js.map +1 -0
  138. package/dist/core/userkeys/fingerprint.d.ts +17 -0
  139. package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
  140. package/dist/core/userkeys/fingerprint.js +59 -0
  141. package/dist/core/userkeys/fingerprint.js.map +1 -0
  142. package/dist/core/userkeys/index.d.ts +11 -0
  143. package/dist/core/userkeys/index.d.ts.map +1 -0
  144. package/dist/core/userkeys/index.js +49 -0
  145. package/dist/core/userkeys/index.js.map +1 -0
  146. package/dist/core/userkeys/recipients.d.ts +51 -0
  147. package/dist/core/userkeys/recipients.d.ts.map +1 -0
  148. package/dist/core/userkeys/recipients.js +292 -0
  149. package/dist/core/userkeys/recipients.js.map +1 -0
  150. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
  151. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
  152. package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
  153. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
  154. package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
  155. package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
  156. package/dist/core/userkeys/sealedCrypto.js +79 -0
  157. package/dist/core/userkeys/sealedCrypto.js.map +1 -0
  158. package/dist/core/userkeys/sealedTypes.d.ts +72 -0
  159. package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
  160. package/dist/core/userkeys/sealedTypes.js +14 -0
  161. package/dist/core/userkeys/sealedTypes.js.map +1 -0
  162. package/dist/core/userkeys/types.d.ts +70 -0
  163. package/dist/core/userkeys/types.d.ts.map +1 -0
  164. package/dist/core/userkeys/types.js +48 -0
  165. package/dist/core/userkeys/types.js.map +1 -0
  166. package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
  167. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
  168. package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
  169. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
  170. package/dist/core/utils/idGeneration.d.ts +39 -20
  171. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  172. package/dist/core/utils/idGeneration.js +82 -62
  173. package/dist/core/utils/idGeneration.js.map +1 -1
  174. package/dist/core/utils/index.d.ts +1 -1
  175. package/dist/core/utils/index.d.ts.map +1 -1
  176. package/dist/core/utils/index.js +4 -1
  177. package/dist/core/utils/index.js.map +1 -1
  178. package/dist/node/server/CapabilityMatcher.d.ts +5 -0
  179. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
  180. package/dist/node/server/CapabilityMatcher.js +10 -1
  181. package/dist/node/server/CapabilityMatcher.js.map +1 -1
  182. package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
  183. package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
  184. package/dist/node/server/DeviceDiscoveryService.js +165 -0
  185. package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
  186. package/dist/node/server/MindooDBServer.d.ts +13 -0
  187. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  188. package/dist/node/server/MindooDBServer.js +99 -61
  189. package/dist/node/server/MindooDBServer.js.map +1 -1
  190. package/dist/node/server/SystemAdminAuth.d.ts +7 -0
  191. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
  192. package/dist/node/server/SystemAdminAuth.js +4 -1
  193. package/dist/node/server/SystemAdminAuth.js.map +1 -1
  194. package/dist/node/server/TenantManager.d.ts +29 -0
  195. package/dist/node/server/TenantManager.d.ts.map +1 -1
  196. package/dist/node/server/TenantManager.js +146 -7
  197. package/dist/node/server/TenantManager.js.map +1 -1
  198. package/package.json +5 -5
@@ -2,20 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseMindooTenant = void 0;
4
4
  const types_1 = require("./types");
5
+ const userid_1 = require("./userid");
5
6
  const BaseMindooDB_1 = require("./BaseMindooDB");
6
7
  const BaseMindooTenantDirectory_1 = require("./BaseMindooTenantDirectory");
7
8
  const grantKeys_1 = require("./accesscontrol/grantKeys");
9
+ const UserKeyManager_1 = require("./userkeys/UserKeyManager");
10
+ const types_2 = require("./userkeys/types");
11
+ const validateUserKeyDocument_1 = require("./userkeys/validateUserKeyDocument");
12
+ const sealedTypes_1 = require("./userkeys/sealedTypes");
13
+ const recipients_1 = require("./userkeys/recipients");
14
+ const sealedCrypto_1 = require("./userkeys/sealedCrypto");
8
15
  const MindooDocSigner_1 = require("./crypto/MindooDocSigner");
9
16
  const RSAEncryption_1 = require("./crypto/RSAEncryption");
17
+ const deviceJoinBootstrap_1 = require("./join/deviceJoinBootstrap");
10
18
  const privateKeyEncryption_1 = require("./crypto/privateKeyEncryption");
11
19
  const EntrySignature_1 = require("./crypto/EntrySignature");
12
20
  const DomainStatement_1 = require("./crypto/DomainStatement");
13
21
  const storeEntryTime_1 = require("./storeEntryTime");
14
22
  const idGeneration_1 = require("./utils/idGeneration");
15
23
  const timeSource_1 = require("./utils/timeSource");
24
+ const tenantSetup_1 = require("./tenantSetup");
16
25
  const errors_1 = require("./errors");
17
26
  const logging_1 = require("./logging");
18
27
  const MindooURI_1 = require("./uri/MindooURI");
28
+ const joinRequestUri_1 = require("./uri/joinRequestUri");
19
29
  const EncryptedLocalCacheStore_1 = require("./cache/EncryptedLocalCacheStore");
20
30
  const CacheManager_1 = require("./cache/CacheManager");
21
31
  const databaseIdValidation_1 = require("./databaseIdValidation");
@@ -46,6 +56,7 @@ class BaseMindooTenant {
46
56
  */
47
57
  preDecryptedMaterial) {
48
58
  this.databaseCache = new Map();
59
+ this.databaseOpenInFlight = new Map();
49
60
  this.directoryCache = null;
50
61
  this.remoteStoreCache = new Map();
51
62
  /**
@@ -87,6 +98,9 @@ class BaseMindooTenant {
87
98
  // recursing or overlapping. Reconcile is idempotent, so a skipped overlap is
88
99
  // harmless — the next trigger re-runs it.
89
100
  this.reconcileInFlight = false;
101
+ this.userKeyReconcileScheduled = false;
102
+ this.userKeys = new UserKeyManager_1.UserKeyManager(this);
103
+ this.sealedGenerations = new Map();
90
104
  // Local cache support
91
105
  this.cacheManager = null;
92
106
  this.factory = factory;
@@ -132,6 +146,9 @@ class BaseMindooTenant {
132
146
  if (preDecryptedMaterial?.encryptionKey) {
133
147
  this.decryptedUserEncryptionKeyCache = preDecryptedMaterial.encryptionKey;
134
148
  }
149
+ if (preDecryptedMaterial?.userKeyPrivateBytes) {
150
+ this.userKeys.seedDecryptedUserKeyBytes(preDecryptedMaterial.userKeyPrivateBytes);
151
+ }
135
152
  // Set up cache if a local cache store is provided. When a pre-derived
136
153
  // cache encryption key is available, prefer that to skip the password
137
154
  // path and avoid retaining the plaintext password inside the encrypted
@@ -191,6 +208,148 @@ class BaseMindooTenant {
191
208
  }
192
209
  return timestamp;
193
210
  }
211
+ getCurrentPrivateUser() {
212
+ return this.currentUser;
213
+ }
214
+ getCurrentUserPassword() {
215
+ return this.currentUserPassword;
216
+ }
217
+ getUserKeyManager() {
218
+ return this.userKeys;
219
+ }
220
+ isDatabaseOpen(dbId) {
221
+ return this.databaseCache.has(dbId) || this.databaseOpenInFlight.has(dbId);
222
+ }
223
+ /** Fully initialized (not merely in-flight). See UserKeyManager.canReconcileAgainstUserDirectory. */
224
+ isDatabaseReady(dbId) {
225
+ return this.databaseCache.has(dbId);
226
+ }
227
+ /**
228
+ * Coalesce grant-driven user-key compares onto one run after the current
229
+ * `updateUnifiedCache` stack unwinds. Calling reconcile synchronously from
230
+ * the cache pass re-enters the cache via `findGrantAccessDocuments` and
231
+ * unbounded-allocates.
232
+ */
233
+ scheduleUserKeyReconcile() {
234
+ if (this.userKeys.isReconciling() || this.userKeyReconcileScheduled)
235
+ return;
236
+ this.userKeyReconcileScheduled = true;
237
+ queueMicrotask(() => {
238
+ this.userKeyReconcileScheduled = false;
239
+ if (this.userKeys.isReconciling())
240
+ return;
241
+ void this.reconcileUserKeysSafe();
242
+ });
243
+ }
244
+ noteUserDirectoryFetched() {
245
+ this.userKeys.noteUserDirectoryFetched();
246
+ }
247
+ noteUserDirectoryFetchFailed(error) {
248
+ this.userKeys.noteUserDirectoryFetchFailed(error);
249
+ }
250
+ async listPendingUserKeyDevices() {
251
+ return this.userKeys.listPendingUserKeyDevices();
252
+ }
253
+ async listUserKeyDevices() {
254
+ return this.userKeys.listUserKeyDevices();
255
+ }
256
+ async approveUserKeyDevice(fingerprint) {
257
+ return this.userKeys.approveUserKeyDevice(fingerprint);
258
+ }
259
+ async declineUserKeyDevice(fingerprint) {
260
+ return this.userKeys.declineUserKeyDevice(fingerprint);
261
+ }
262
+ async undoDeclineUserKeyDevice(fingerprint) {
263
+ return this.userKeys.undoDeclineUserKeyDevice(fingerprint);
264
+ }
265
+ async rotateUserKey() {
266
+ return this.userKeys.rotateUserKey();
267
+ }
268
+ async reconcileUserKeys(options) {
269
+ return this.userKeys.reconcile(options);
270
+ }
271
+ async reconcileUserKeysSafe() {
272
+ try {
273
+ await this.userKeys.reconcile();
274
+ }
275
+ catch (error) {
276
+ this.logger.warn(`reconcileUserKeysSafe: ${error}`);
277
+ }
278
+ }
279
+ async getUserKeyEnrollmentStatus() {
280
+ return this.userKeys.getEnrollmentStatus();
281
+ }
282
+ async ensureLocalUserKeyPair() {
283
+ await this.userKeys.ensureLocalUserKeyPair(this.currentUser, this.currentUserPassword);
284
+ }
285
+ rememberSealedGenerations(keyId, generations) {
286
+ this.sealedGenerations.set(keyId, generations);
287
+ }
288
+ getSealedGenerations(keyId) {
289
+ return this.sealedGenerations.get(keyId) ?? [];
290
+ }
291
+ async ingestSealedRecipients(keyId, recipients) {
292
+ const cached = this.sealedGenerations.get(keyId);
293
+ const epoch = typeof recipients.epoch === "number" ? recipients.epoch : 0;
294
+ if (cached && cached.length >= epoch && epoch > 0) {
295
+ console.log("[sealed] ingest: cache already has generations", {
296
+ keyId,
297
+ cached: cached.length,
298
+ epoch,
299
+ });
300
+ return;
301
+ }
302
+ const keys = [
303
+ ...(await this.userKeys.getUserKeyCryptoKeysForReconcile()),
304
+ await this.getEncryptionPrivateKeyForReconcile(),
305
+ ].filter((k) => !!k);
306
+ console.log("[sealed] ingest: trying unwrap", {
307
+ keyId,
308
+ wrapCount: recipients.wraps?.length ?? 0,
309
+ wrapKinds: (recipients.wraps ?? []).map((wrap) => wrap.kind),
310
+ epoch,
311
+ keyCount: keys.length,
312
+ cached: cached?.length ?? 0,
313
+ });
314
+ if (keys.length === 0) {
315
+ // Keep a create-time session DEK. Without unwrap keys we cannot prove
316
+ // the wrap list excludes us, and deleting would make the author unable
317
+ // to read a document they just sealed.
318
+ return;
319
+ }
320
+ for (const key of keys) {
321
+ try {
322
+ const gens = await (0, sealedCrypto_1.openBundle)({
323
+ crypto: this.cryptoAdapter,
324
+ recipients,
325
+ privateKey: key,
326
+ });
327
+ this.sealedGenerations.set(keyId, gens);
328
+ console.log("[sealed] ingest: unwrapped", { keyId, generations: gens.length });
329
+ return;
330
+ }
331
+ catch {
332
+ // try next key
333
+ }
334
+ }
335
+ // Newest wrap list is not for us (removed, or never added). Drop any
336
+ // session DEK so hasDecryptionKey / visibility reconcile hide the
337
+ // document instead of leaving the last readable plaintext editable.
338
+ console.log("[sealed] ingest: unwrap failed, dropping session DEK", {
339
+ keyId,
340
+ cached: cached?.length ?? 0,
341
+ epoch,
342
+ });
343
+ this.sealedGenerations.delete(keyId);
344
+ }
345
+ async ingestSealedFromEntries(keyId, entries) {
346
+ if (!(0, sealedTypes_1.isSealedKeyId)(keyId))
347
+ return;
348
+ const newest = (0, recipients_1.newestRecipientBlock)(entries);
349
+ if (!newest)
350
+ return;
351
+ await this.ingestSealedRecipients(keyId, newest);
352
+ }
194
353
  getFactory() {
195
354
  return this.factory;
196
355
  }
@@ -223,6 +382,9 @@ class BaseMindooTenant {
223
382
  */
224
383
  async importAesKeyCached(symmetricKey, usage) {
225
384
  let cacheKey = usage + ":";
385
+ if (symmetricKey.length !== 32) {
386
+ throw new Error(`AES-256 key must be 32 bytes, got ${symmetricKey.length}`);
387
+ }
226
388
  for (let i = 0; i < symmetricKey.length; i++) {
227
389
  cacheKey += symmetricKey[i].toString(16).padStart(2, "0");
228
390
  }
@@ -231,9 +393,8 @@ class BaseMindooTenant {
231
393
  return cached;
232
394
  }
233
395
  const subtle = this.cryptoAdapter.getSubtle();
234
- // Create a new Uint8Array to ensure we have a proper ArrayBuffer
235
- const keyArray = new Uint8Array(symmetricKey);
236
- const cryptoKey = await subtle.importKey("raw", keyArray.buffer, { name: "AES-GCM" }, false, // not extractable
396
+ const keyArray = symmetricKey.slice();
397
+ const cryptoKey = await subtle.importKey("raw", keyArray, { name: "AES-GCM" }, false, // not extractable
237
398
  [usage]);
238
399
  if (this.importedAesKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
239
400
  this.importedAesKeyCache.clear();
@@ -252,7 +413,14 @@ class BaseMindooTenant {
252
413
  // Get the symmetric key for this key ID
253
414
  let symmetricKey;
254
415
  try {
255
- if (decryptionKeyId === "default") {
416
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
417
+ const generations = this.sealedGenerations.get(decryptionKeyId);
418
+ if (!generations?.length) {
419
+ throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
420
+ }
421
+ symmetricKey = generations[0];
422
+ }
423
+ else if (decryptionKeyId === "default") {
256
424
  // Use cached tenant encryption key if available
257
425
  if (this.decryptedTenantKeyCache) {
258
426
  symmetricKey = this.decryptedTenantKeyCache;
@@ -386,7 +554,9 @@ class BaseMindooTenant {
386
554
  }
387
555
  }
388
556
  this.logger.debug(`Decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
389
- throw lastError ?? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
557
+ throw lastError instanceof Error
558
+ ? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId)
559
+ : new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
390
560
  }
391
561
  /**
392
562
  * Return every stored version of the symmetric key for `decryptionKeyId`,
@@ -398,6 +568,9 @@ class BaseMindooTenant {
398
568
  * tenant-default version so encryption keeps using the latest key.
399
569
  */
400
570
  async getDecryptionKeyCandidates(decryptionKeyId) {
571
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
572
+ return this.sealedGenerations.get(decryptionKeyId) ?? [];
573
+ }
401
574
  const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
402
575
  const versions = await this.keyBag.getAllKeys("doc", this.tenantId, id);
403
576
  if (decryptionKeyId === "default" && versions.length > 0) {
@@ -413,7 +586,7 @@ class BaseMindooTenant {
413
586
  // Sign the payload
414
587
  const signature = await subtle.sign({
415
588
  name: "Ed25519",
416
- }, signingKey, payload.buffer);
589
+ }, signingKey, payload.slice());
417
590
  this.logger.debug(`Signed payload (signature: ${signature.byteLength} bytes)`);
418
591
  return new Uint8Array(signature);
419
592
  }
@@ -465,11 +638,23 @@ class BaseMindooTenant {
465
638
  }, false, // not extractable
466
639
  ["sign"]);
467
640
  // Sign the payload
468
- const signature = await subtle.sign({
641
+ const signatureBuffer = await subtle.sign({
469
642
  name: "Ed25519",
470
643
  }, signingKey, payload.buffer);
644
+ const signature = new Uint8Array(signatureBuffer);
645
+ // Prove the decrypted private key belongs to the claimed public key.
646
+ // Directory helpers historically paired `getAdministrationPublicKey()` with
647
+ // whatever private key the UI passed; admin-only then compared only the
648
+ // claimed PEM and accepted the write into local memory. Verifying here
649
+ // closes that gap before any store entry is attributed to that public key.
650
+ const verifyKey = await this.importVerifyKeyCached(signingKeyPair.publicKey);
651
+ const matches = await subtle.verify({ name: "Ed25519" }, verifyKey, signature.buffer, payload.buffer);
652
+ if (!matches) {
653
+ throw new Error("Signing key pair mismatch: the decrypted private key does not correspond to the claimed public key. "
654
+ + "For admin-only databases this usually means a non-admin identity was used as the tenant admin.");
655
+ }
471
656
  this.logger.debug(`Signed payload with provided key (signature: ${signature.byteLength} bytes)`);
472
- return new Uint8Array(signature);
657
+ return signature;
473
658
  }
474
659
  async verifySignature(payload, signature, publicKey) {
475
660
  this.logger.debug(`Verifying signature`);
@@ -643,7 +828,8 @@ class BaseMindooTenant {
643
828
  // Every local database belonging to this tenant: the directory plus any
644
829
  // opened/known data databases. Wipe is a whole-tenant operation.
645
830
  const dbIds = new Set([
646
- "directory",
831
+ types_1.DIRECTORY_DB_ID,
832
+ types_1.USER_DIRECTORY_DB_ID,
647
833
  ...this.databaseCache.keys(),
648
834
  ...(additionalDbIds ?? []),
649
835
  ]);
@@ -699,7 +885,7 @@ class BaseMindooTenant {
699
885
  }
700
886
  }
701
887
  async assertCurrentUserCanOpenDB(id) {
702
- if (id === "directory") {
888
+ if (id === types_1.DIRECTORY_DB_ID || id === types_1.USER_DIRECTORY_DB_ID) {
703
889
  return;
704
890
  }
705
891
  const currentUser = await this.getCurrentUserId();
@@ -755,7 +941,7 @@ class BaseMindooTenant {
755
941
  const validDbId = (0, databaseIdValidation_1.validateDatabaseId)(id, "dbId");
756
942
  // Enforce admin-only mode for directory database - this is a security invariant
757
943
  // The directory database must only accept entries signed by the admin key
758
- const effectiveOptions = validDbId === "directory"
944
+ const effectiveOptions = validDbId === types_1.DIRECTORY_DB_ID
759
945
  ? { ...options, adminOnlyDb: true }
760
946
  : options ?? {};
761
947
  const normalizedTimeTravelDate = this.normalizeTimeTravelDate(effectiveOptions.timeTravelDate);
@@ -763,22 +949,41 @@ class BaseMindooTenant {
763
949
  ? validDbId
764
950
  : `${validDbId}::tt:${normalizedTimeTravelDate}`;
765
951
  await this.assertCurrentUserCanOpenDB(validDbId);
766
- // Return cached database if it exists
767
952
  const cached = this.databaseCache.get(databaseCacheKey);
768
953
  if (cached) {
769
- // For directory DB, verify admin-only flag matches (defensive check)
770
- if (validDbId === "directory" && !cached.isAdminOnlyDb()) {
954
+ if (validDbId === types_1.DIRECTORY_DB_ID && !cached.isAdminOnlyDb()) {
771
955
  throw new Error("Directory database was cached without adminOnlyDb - this should never happen");
772
956
  }
773
- // Live databases must reflect the current KeyBag state before they
774
- // are handed back to callers. Time-travel snapshots are immutable
775
- // historical views and intentionally do not participate in
776
- // visibility reconciliation, so we skip the call for them.
777
957
  if (normalizedTimeTravelDate == null) {
778
958
  await this.reconcileKeyBagChanges();
779
959
  }
780
960
  return cached;
781
961
  }
962
+ const inFlight = this.databaseOpenInFlight.get(databaseCacheKey);
963
+ if (inFlight) {
964
+ return inFlight;
965
+ }
966
+ let resolveOpen;
967
+ let rejectOpen;
968
+ const opening = new Promise((resolve, reject) => {
969
+ resolveOpen = resolve;
970
+ rejectOpen = reject;
971
+ });
972
+ this.databaseOpenInFlight.set(databaseCacheKey, opening);
973
+ try {
974
+ const db = await this.openDBUncached(validDbId, databaseCacheKey, effectiveOptions, normalizedTimeTravelDate);
975
+ resolveOpen(db);
976
+ return db;
977
+ }
978
+ catch (error) {
979
+ rejectOpen(error);
980
+ throw error;
981
+ }
982
+ finally {
983
+ this.databaseOpenInFlight.delete(databaseCacheKey);
984
+ }
985
+ }
986
+ async openDBUncached(validDbId, databaseCacheKey, effectiveOptions, normalizedTimeTravelDate) {
782
987
  // Extract store options and DB-specific options
783
988
  const { adminOnlyDb, timeTravelDate: _timeTravelDate, persistTimeTravelCache, autoIndexing, attachmentConfig, documentCacheConfig, snapshotConfig, performanceCallback, ...storeOptions } = effectiveOptions;
784
989
  // Snapshot opens can share an already-open live store instance. This keeps
@@ -1054,50 +1259,23 @@ class BaseMindooTenant {
1054
1259
  const signerLogger = this.logger.createChild("MindooDocSigner");
1055
1260
  return new MindooDocSigner_1.MindooDocSigner(this, signKey, signerLogger);
1056
1261
  }
1057
- async exportJoinResponseDocKeyVersions(keyId, sharePassword) {
1058
- const scopedKeyId = `doc:${this.tenantId}:${keyId}`;
1059
- const details = (await this.keyBag.listKeyDetails())
1060
- .filter((detail) => detail.scopedKeyId === scopedKeyId)
1061
- .sort((a, b) => a.versionIndex - b.versionIndex);
1062
- if (details.length === 0) {
1262
+ async exportJoinResponseDocKeyVersions(keyId, recipientEncryptionPublicKey) {
1263
+ const keyVersions = await this.keyBag.getAllKeyVersions("doc", this.tenantId, keyId);
1264
+ if (keyVersions.length === 0) {
1063
1265
  throw new Error(`Failed to export document key "${keyId}" for tenant "${this.tenantId}"`);
1064
1266
  }
1267
+ const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
1065
1268
  const versions = [];
1066
- for (const detail of details) {
1067
- const encryptedKey = await this.keyBag.encryptAndExportKeyVersion("doc", this.tenantId, keyId, detail.versionIndex, sharePassword);
1068
- if (!encryptedKey) {
1069
- throw new Error(`Failed to export document key "${keyId}" version ${detail.versionIndex} for tenant "${this.tenantId}"`);
1070
- }
1071
- const { createdAt: encryptedKeyCreatedAt, ...encryptedKeyPayload } = encryptedKey;
1269
+ for (const version of keyVersions) {
1072
1270
  versions.push({
1073
- createdAt: detail.createdAt ?? encryptedKeyCreatedAt,
1074
- encryptedKey: encryptedKeyPayload,
1271
+ createdAt: version.createdAt,
1272
+ wrappedKey: await rsa.wrapKeyToBase64(version.key, recipientEncryptionPublicKey),
1075
1273
  });
1076
1274
  }
1077
1275
  return { keyId, versions };
1078
1276
  }
1079
- resolveJoinResponseDocKeyIds(options) {
1080
- const requestedKeyIds = options.sharedDocKeyIds ?? [types_1.PUBLIC_INFOS_KEY_ID, types_1.DEFAULT_TENANT_KEY_ID];
1081
- const keyIds = new Set();
1082
- keyIds.add(types_1.PUBLIC_INFOS_KEY_ID);
1083
- for (const keyId of requestedKeyIds) {
1084
- const normalizedKeyId = keyId.trim();
1085
- if (normalizedKeyId) {
1086
- keyIds.add(normalizedKeyId);
1087
- }
1088
- }
1089
- return [
1090
- types_1.PUBLIC_INFOS_KEY_ID,
1091
- ...Array.from(keyIds)
1092
- .filter((keyId) => keyId !== types_1.PUBLIC_INFOS_KEY_ID)
1093
- .sort((a, b) => {
1094
- if (a === types_1.DEFAULT_TENANT_KEY_ID)
1095
- return -1;
1096
- if (b === types_1.DEFAULT_TENANT_KEY_ID)
1097
- return 1;
1098
- return a.localeCompare(b);
1099
- }),
1100
- ];
1277
+ resolveJoinResponseDocKeyIds(_options) {
1278
+ return [types_1.PUBLIC_INFOS_KEY_ID];
1101
1279
  }
1102
1280
  async approveJoinRequest(joinRequest, options) {
1103
1281
  // Parse the join request if it's a URI string
@@ -1106,11 +1284,9 @@ class BaseMindooTenant {
1106
1284
  if (!(0, MindooURI_1.isMindooURI)(joinRequest)) {
1107
1285
  throw new Error("Invalid join request: expected a JoinRequest object or a mdb://join-request/... URI string");
1108
1286
  }
1109
- const decoded = (0, MindooURI_1.decodeMindooURI)(joinRequest);
1110
- if (decoded.type !== "join-request") {
1111
- throw new Error(`Invalid URI type: expected "join-request", got "${decoded.type}"`);
1112
- }
1113
- request = decoded.payload;
1287
+ // Normalizes the compact v3 transport back to PEM-armored keys; older
1288
+ // verbose requests pass through unchanged.
1289
+ request = (0, joinRequestUri_1.decodeJoinRequestUri)(joinRequest);
1114
1290
  }
1115
1291
  else {
1116
1292
  request = joinRequest;
@@ -1138,17 +1314,81 @@ class BaseMindooTenant {
1138
1314
  // overrides the one suggested by the joining user in the request.
1139
1315
  const deviceLabel = typeof options.label === "string" && options.label.trim().length > 0
1140
1316
  ? options.label.trim()
1141
- : request.label;
1142
- await directory.registerUser(publicUserId, options.adminSigningKey, options.adminPassword, undefined, deviceLabel);
1143
- // 2. Export selected document keys encrypted with the share password.
1144
- // `$publicinfos` is mandatory because the joining user needs it for
1145
- // directory access; all selected key ids include every rotated version.
1146
- const encryptedDocKeys = await Promise.all(this.resolveJoinResponseDocKeyIds(options).map((keyId) => this.exportJoinResponseDocKeyVersions(keyId, options.sharePassword)));
1317
+ : typeof request.label === "string"
1318
+ ? request.label.trim()
1319
+ : "";
1320
+ if (!deviceLabel) {
1321
+ throw new Error("Device label is required when granting access");
1322
+ }
1323
+ await directory.registerUser(publicUserId, options.adminSigningKey, options.adminPassword, undefined, deviceLabel, {
1324
+ distributeKeyIds: [],
1325
+ });
1326
+ const userPublicKey = typeof request.userPublicKey === "string" ? request.userPublicKey.trim() : "";
1327
+ if (userPublicKey) {
1328
+ const userKeyDoc = await this.userKeys.createPendingFromJoin({
1329
+ username,
1330
+ userPublicKey,
1331
+ signingKeyPair: {
1332
+ publicKey: this.administrationPublicKey,
1333
+ privateKey: options.adminSigningKey,
1334
+ },
1335
+ signingKeyPassword: options.adminPassword,
1336
+ });
1337
+ const payload = (0, validateUserKeyDocument_1.asUserKeyPayload)(userKeyDoc.getData());
1338
+ const epoch = payload ? (0, types_2.currentUserKeyEpoch)(payload) : null;
1339
+ const publishedPublicKey = epoch ? payload?.userKeys[epoch]?.publicKey : undefined;
1340
+ // First device: wrap `default` to the join-request User-Key (pending is
1341
+ // OK). Additional device: wrap to the already-published User-Key — never
1342
+ // to this device's freshly generated pair.
1343
+ if (publishedPublicKey === userPublicKey) {
1344
+ await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword, userPublicKey);
1345
+ }
1346
+ else if (publishedPublicKey) {
1347
+ await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword);
1348
+ }
1349
+ }
1350
+ else {
1351
+ const existingUserKey = await this.userKeys.publishedUserKeyFor(username);
1352
+ if (!existingUserKey) {
1353
+ throw new Error("Cannot approve join request: it carries no userPublicKey and the person has no published User-Key. Generate a User-Key on the joining device first.");
1354
+ }
1355
+ await directory.autoDistributeKeysToUser(username, [types_1.DEFAULT_TENANT_KEY_ID], options.adminSigningKey, options.adminPassword);
1356
+ }
1357
+ // Same person: wrap the published User-Key to this device's encryption
1358
+ // key (from the grant). A different existing person stays pending until
1359
+ // one of *their* devices writes the wrap — this replica cannot.
1360
+ const current = await this.getCurrentUserId();
1361
+ const samePerson = (0, userid_1.usernamesEqual)(current.username, username);
1362
+ console.log("[approveJoinRequest] User-Key wrap decision", {
1363
+ joiningUsername: username,
1364
+ currentUsername: current.username,
1365
+ samePerson,
1366
+ hasEncryptionPublicKey: !!request.encryptionPublicKey,
1367
+ });
1368
+ if (samePerson) {
1369
+ try {
1370
+ await this.userKeys.wrapUserKeyForGrantDevice(request.encryptionPublicKey, deviceLabel);
1371
+ console.log(`[approveJoinRequest] ✓ wrapped User-Key for joining device of "${username}"`);
1372
+ this.logger.info(`approveJoinRequest: wrapped User-Key for joining device of "${username}"`);
1373
+ }
1374
+ catch (error) {
1375
+ console.error("[approveJoinRequest] User-Key wrap for joining device failed", error);
1376
+ this.logger.error(`approveJoinRequest: User-Key wrap for joining device failed (${error instanceof Error ? error.message : error})`);
1377
+ }
1378
+ }
1379
+ else {
1380
+ this.logger.info(`approveJoinRequest: User-Key wrap deferred; joining "${username}" is not the current user "${current.username}"`);
1381
+ }
1382
+ // 2. Export selected document keys RSA-OAEP-wrapped to the requester's
1383
+ // encryption public key. `$publicinfos` is mandatory because the joining
1384
+ // user needs it for directory access; all selected key ids include every
1385
+ // rotated version.
1386
+ const encryptedDocKeys = await Promise.all(this.resolveJoinResponseDocKeyIds(options).map((keyId) => this.exportJoinResponseDocKeyVersions(keyId, request.encryptionPublicKey)));
1147
1387
  // 3. Build the join response. It echoes the registered username so the
1148
1388
  // joining device can adopt it instead of keeping a name the directory
1149
1389
  // does not hold.
1150
1390
  const joinResponse = {
1151
- v: 2,
1391
+ v: 3,
1152
1392
  tenantId: this.tenantId,
1153
1393
  adminSigningPublicKey: this.administrationPublicKey,
1154
1394
  adminEncryptionPublicKey: this.administrationEncryptionPublicKey,
@@ -1161,6 +1401,52 @@ class BaseMindooTenant {
1161
1401
  if (options.adminUsername) {
1162
1402
  joinResponse.adminUsername = options.adminUsername;
1163
1403
  }
1404
+ const explicitTenantLabel = typeof options.tenantLabel === "string" ? options.tenantLabel.trim() : "";
1405
+ if (explicitTenantLabel) {
1406
+ joinResponse.tenantLabel = explicitTenantLabel;
1407
+ }
1408
+ else {
1409
+ try {
1410
+ const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
1411
+ const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb, this);
1412
+ if (setupLabel) {
1413
+ joinResponse.tenantLabel = setupLabel;
1414
+ }
1415
+ }
1416
+ catch {
1417
+ // Best-effort: older tenants or missing setup doc.
1418
+ }
1419
+ }
1420
+ const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
1421
+ const wrappedJoinResponse = await (0, deviceJoinBootstrap_1.wrapDeviceJoinBootstrap)(rsa, {
1422
+ username,
1423
+ tenantLabel: joinResponse.tenantLabel,
1424
+ adminUsername: joinResponse.adminUsername,
1425
+ }, request.encryptionPublicKey);
1426
+ await directory.attachDeviceJoinBootstrap(username, request.signingPublicKey, wrappedJoinResponse, options.adminSigningKey, options.adminPassword);
1427
+ // Tenants created before `tenantsetup` recorded administrators have no
1428
+ // entry, leaving members with a fingerprint instead of a name. A grant is
1429
+ // where the admin supplies both its name and its keys, so heal it here.
1430
+ if (options.adminUsername) {
1431
+ try {
1432
+ const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
1433
+ const written = await (0, tenantSetup_1.ensureTenantSetupAdministrator)(directoryDb, {
1434
+ username: options.adminUsername,
1435
+ keyPairs: [
1436
+ {
1437
+ signingPublicKey: this.administrationPublicKey,
1438
+ encryptionPublicKey: this.administrationEncryptionPublicKey,
1439
+ },
1440
+ ],
1441
+ }, { publicKey: this.administrationPublicKey, privateKey: options.adminSigningKey }, options.adminPassword, this);
1442
+ if (written) {
1443
+ this.logger.info(`approveJoinRequest: recorded administrator "${options.adminUsername}" in tenantsetup`);
1444
+ }
1445
+ }
1446
+ catch (error) {
1447
+ this.logger.warn(`approveJoinRequest: recording the administrator in tenantsetup failed: ${error instanceof Error ? error.message : error}`);
1448
+ }
1449
+ }
1164
1450
  console.log(`[approveJoinRequest] ✓ Join request approved for user "${username}"`);
1165
1451
  this.logger.info(`Join request approved for user: ${username}`);
1166
1452
  // Return as URI string or object depending on format option
@@ -1326,8 +1612,9 @@ class BaseMindooTenant {
1326
1612
  const signingKey = await this.getDecryptedSigningKey();
1327
1613
  // Get the current user's decrypted RSA encryption private key (for decrypting entries)
1328
1614
  const decryptedEncryptionKey = await this.getDecryptedEncryptionKey();
1329
- // Create the client network store
1330
- const store = new ClientNetworkContentAddressedStore(validDbId, storeKind, transport, this.cryptoAdapter, this.currentUser.username, signingKey, decryptedEncryptionKey, this.logger.createChild(`ClientNetworkStore:${validDbId}:${storeKind}`));
1615
+ // Create the client network store. Pass the device signing public key so
1616
+ // auth challenges can identify this specific device on multi-device grants.
1617
+ const store = new ClientNetworkContentAddressedStore(validDbId, storeKind, transport, this.cryptoAdapter, this.currentUser.username, signingKey, decryptedEncryptionKey, this.logger.createChild(`ClientNetworkStore:${validDbId}:${storeKind}`), this.currentUser.userSigningKeyPair.publicKey);
1331
1618
  console.log(`[connectToServer] ✓ Connected to server for db "${validDbId}"`);
1332
1619
  this.logger.info(`Connected to server for db "${validDbId}"`);
1333
1620
  return store;
@@ -1491,6 +1778,9 @@ class BaseMindooTenant {
1491
1778
  * @returns true if the key can be resolved, false otherwise
1492
1779
  */
1493
1780
  async hasDecryptionKey(decryptionKeyId) {
1781
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
1782
+ return (this.sealedGenerations.get(decryptionKeyId)?.length ?? 0) > 0;
1783
+ }
1494
1784
  try {
1495
1785
  await this.getSymmetricKey(decryptionKeyId);
1496
1786
  return true;
@@ -1513,14 +1803,14 @@ class BaseMindooTenant {
1513
1803
  return this.keyBag.getDocKeyFingerprint(this.tenantId);
1514
1804
  }
1515
1805
  /**
1516
- * Remove a **named** document key from the local KeyBag. Refuses to delete the
1517
- * shared tenant default key (`"default"` / {@link DEFAULT_TENANT_KEY_ID}),
1518
- * which is required for the tenant to function. Returns true if a key was
1519
- * removed.
1806
+ * Remove a **named** document key from the local KeyBag. Refuses to delete
1807
+ * `$publicinfos` (required for directory access). The tenant `default` key
1808
+ * may be removed when a key-distribution `pullfrom` revokes content access.
1809
+ * Returns true if a key was removed.
1520
1810
  */
1521
1811
  async removeNamedDecryptionKey(decryptionKeyId) {
1522
- if (decryptionKeyId === "default" || decryptionKeyId === types_1.DEFAULT_TENANT_KEY_ID) {
1523
- this.logger.debug(`removeNamedDecryptionKey: refusing to delete the tenant default key`);
1812
+ if (decryptionKeyId === types_1.PUBLIC_INFOS_KEY_ID) {
1813
+ this.logger.debug(`removeNamedDecryptionKey: refusing to delete the public-infos key`);
1524
1814
  return false;
1525
1815
  }
1526
1816
  try {
@@ -1557,19 +1847,24 @@ class BaseMindooTenant {
1557
1847
  const directory = await this.openDirectory();
1558
1848
  const removed = [];
1559
1849
  const imported = [];
1560
- // Revoke pass — no key, no comparison, idempotent.
1561
- if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
1562
- let revokedIds = [];
1563
- try {
1564
- revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
1850
+ // Revoke pass — no key, no comparison, idempotent. Prefer signing-key
1851
+ // lookup when the local username is empty (nameless device bootstrap).
1852
+ let revokedIds = [];
1853
+ try {
1854
+ if (!username.trim() &&
1855
+ typeof directory.getRevokedDecryptionKeyIdsForSigningKey === "function") {
1856
+ revokedIds = await directory.getRevokedDecryptionKeyIdsForSigningKey(this.currentUser.userSigningKeyPair.publicKey);
1565
1857
  }
1566
- catch (error) {
1567
- this.logger.warn(`reconcileKeyDistributionsForCurrentUser: revoked-id lookup failed: ${error}`);
1858
+ else if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
1859
+ revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
1568
1860
  }
1569
- for (const keyId of revokedIds) {
1570
- if (await this.removeNamedDecryptionKey(keyId)) {
1571
- removed.push(keyId);
1572
- }
1861
+ }
1862
+ catch (error) {
1863
+ this.logger.warn(`reconcileKeyDistributionsForCurrentUser: revoked-id lookup failed: ${error}`);
1864
+ }
1865
+ for (const keyId of revokedIds) {
1866
+ if (await this.removeNamedDecryptionKey(keyId)) {
1867
+ removed.push(keyId);
1573
1868
  }
1574
1869
  }
1575
1870
  // Import pass — sources the encryption private key from this tenant. The
@@ -1589,7 +1884,41 @@ class BaseMindooTenant {
1589
1884
  this.logger.warn(`reconcileKeyDistributionsForCurrentUser: import pass failed: ${error}`);
1590
1885
  }
1591
1886
  }
1592
- return { imported, removed };
1887
+ let adoptedUsername;
1888
+ if (imported.includes(types_1.DEFAULT_TENANT_KEY_ID) ||
1889
+ imported.includes("default")) {
1890
+ adoptedUsername = await this.adoptRegisteredUsernameFromGrant(directory);
1891
+ }
1892
+ return { imported, removed, ...(adoptedUsername ? { adoptedUsername } : {}) };
1893
+ }
1894
+ /**
1895
+ * After importing the tenant `default` key, decrypt grant `user_details` and
1896
+ * adopt the registered username so local identity matches the directory
1897
+ * (same outcome as join-response `username`).
1898
+ */
1899
+ async adoptRegisteredUsernameFromGrant(directory) {
1900
+ try {
1901
+ if (typeof directory.getUserBySigningPublicKey !== "function")
1902
+ return undefined;
1903
+ const lookup = await directory.getUserBySigningPublicKey(this.currentUser.userSigningKeyPair.publicKey);
1904
+ const registered = (typeof lookup?.details?.username === "string" && lookup.details.username.trim()) ||
1905
+ (typeof lookup?.username === "string" &&
1906
+ lookup.username.trim() &&
1907
+ // Server-side lookups return username_hash when details are opaque;
1908
+ // only adopt cleartext names (contain '/' for DN form, or non-hex).
1909
+ (lookup.username.includes("/") || !/^[a-f0-9]{64}$/i.test(lookup.username))
1910
+ ? lookup.username.trim()
1911
+ : "");
1912
+ if (!registered || registered === this.currentUser.username)
1913
+ return undefined;
1914
+ this.logger.info(`Adopting registered username "${registered}" (was "${this.currentUser.username}")`);
1915
+ this.currentUser = { ...this.currentUser, username: registered };
1916
+ return registered;
1917
+ }
1918
+ catch (error) {
1919
+ this.logger.debug(`adoptRegisteredUsernameFromGrant failed: ${error}`);
1920
+ return undefined;
1921
+ }
1593
1922
  }
1594
1923
  /**
1595
1924
  * Single-flight, best-effort wrapper around
@@ -1701,13 +2030,14 @@ class BaseMindooTenant {
1701
2030
  /**
1702
2031
  * Remove exactly the key versions of `keyId` whose raw-bytes fingerprint is in
1703
2032
  * `fingerprints` (version-scoped `pullfrom` revocation). Versions obtained
1704
- * elsewhere — i.e. not in the distribution manifest — survive. Refuses the
1705
- * protected tenant default / `$publicinfos` keys. The KeyBag change feed
1706
- * triggers visibility reconciliation (scope purge when nothing remains).
2033
+ * elsewhere — i.e. not in the distribution manifest — survive. Refuses
2034
+ * `$publicinfos` (directory access). The tenant `default` key may be version-
2035
+ * revoked via key distribution. The KeyBag change feed triggers visibility
2036
+ * reconciliation (scope purge when nothing remains).
1707
2037
  * Returns the number of versions removed.
1708
2038
  */
1709
2039
  async removeDecryptionKeyVersionsByFingerprint(keyId, fingerprints) {
1710
- if (keyId === "default" || keyId === types_1.DEFAULT_TENANT_KEY_ID || keyId === types_1.PUBLIC_INFOS_KEY_ID) {
2040
+ if (keyId === types_1.PUBLIC_INFOS_KEY_ID) {
1711
2041
  this.logger.debug(`removeDecryptionKeyVersionsByFingerprint: refusing protected key ${keyId}`);
1712
2042
  return 0;
1713
2043
  }
@@ -1752,6 +2082,13 @@ class BaseMindooTenant {
1752
2082
  * @returns The decrypted symmetric key bytes
1753
2083
  */
1754
2084
  async getSymmetricKey(decryptionKeyId) {
2085
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
2086
+ const generations = this.sealedGenerations.get(decryptionKeyId);
2087
+ if (!generations?.length) {
2088
+ throw new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
2089
+ }
2090
+ return generations[0];
2091
+ }
1755
2092
  if (decryptionKeyId === "default") {
1756
2093
  // Use cached tenant encryption key if available
1757
2094
  if (this.decryptedTenantKeyCache) {