mindoodb 0.0.51 → 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 (185) 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 +57 -0
  15. package/dist/core/BaseMindooDB.d.ts +85 -10
  16. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  17. package/dist/core/BaseMindooDB.js +687 -196
  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 +407 -86
  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 +286 -17
  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 +2 -1
  43. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  44. package/dist/core/accesscontrol/types.js +3 -2
  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 +6 -1
  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 +7 -0
  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/index.d.ts +7 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js +30 -4
  91. package/dist/core/index.js.map +1 -1
  92. package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
  93. package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
  94. package/dist/core/join/deviceJoinBootstrap.js +37 -0
  95. package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
  96. package/dist/core/keys/KeyBag.d.ts +20 -2
  97. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  98. package/dist/core/keys/KeyBag.js +22 -10
  99. package/dist/core/keys/KeyBag.js.map +1 -1
  100. package/dist/core/logging/MindooLogger.d.ts.map +1 -1
  101. package/dist/core/logging/MindooLogger.js +65 -40
  102. package/dist/core/logging/MindooLogger.js.map +1 -1
  103. package/dist/core/tenantIdValidation.d.ts.map +1 -1
  104. package/dist/core/tenantIdValidation.js +1 -1
  105. package/dist/core/tenantIdValidation.js.map +1 -1
  106. package/dist/core/tenantSetup.d.ts +94 -9
  107. package/dist/core/tenantSetup.d.ts.map +1 -1
  108. package/dist/core/tenantSetup.js +208 -20
  109. package/dist/core/tenantSetup.js.map +1 -1
  110. package/dist/core/types.d.ts +295 -33
  111. package/dist/core/types.d.ts.map +1 -1
  112. package/dist/core/types.js +14 -1
  113. package/dist/core/types.js.map +1 -1
  114. package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
  115. package/dist/core/uri/joinRequestUri.js +12 -3
  116. package/dist/core/uri/joinRequestUri.js.map +1 -1
  117. package/dist/core/userid/canonicalUsername.d.ts +66 -0
  118. package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
  119. package/dist/core/userid/canonicalUsername.js +235 -0
  120. package/dist/core/userid/canonicalUsername.js.map +1 -0
  121. package/dist/core/userid/index.d.ts +9 -0
  122. package/dist/core/userid/index.d.ts.map +1 -1
  123. package/dist/core/userid/index.js +14 -0
  124. package/dist/core/userid/index.js.map +1 -1
  125. package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
  126. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
  127. package/dist/core/userkeys/UserKeyDocument.js +145 -0
  128. package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
  129. package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
  130. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
  131. package/dist/core/userkeys/UserKeyManager.js +1277 -0
  132. package/dist/core/userkeys/UserKeyManager.js.map +1 -0
  133. package/dist/core/userkeys/fingerprint.d.ts +17 -0
  134. package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
  135. package/dist/core/userkeys/fingerprint.js +59 -0
  136. package/dist/core/userkeys/fingerprint.js.map +1 -0
  137. package/dist/core/userkeys/index.d.ts +11 -0
  138. package/dist/core/userkeys/index.d.ts.map +1 -0
  139. package/dist/core/userkeys/index.js +49 -0
  140. package/dist/core/userkeys/index.js.map +1 -0
  141. package/dist/core/userkeys/recipients.d.ts +51 -0
  142. package/dist/core/userkeys/recipients.d.ts.map +1 -0
  143. package/dist/core/userkeys/recipients.js +292 -0
  144. package/dist/core/userkeys/recipients.js.map +1 -0
  145. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
  146. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
  147. package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
  148. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
  149. package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
  150. package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
  151. package/dist/core/userkeys/sealedCrypto.js +79 -0
  152. package/dist/core/userkeys/sealedCrypto.js.map +1 -0
  153. package/dist/core/userkeys/sealedTypes.d.ts +72 -0
  154. package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
  155. package/dist/core/userkeys/sealedTypes.js +14 -0
  156. package/dist/core/userkeys/sealedTypes.js.map +1 -0
  157. package/dist/core/userkeys/types.d.ts +70 -0
  158. package/dist/core/userkeys/types.d.ts.map +1 -0
  159. package/dist/core/userkeys/types.js +48 -0
  160. package/dist/core/userkeys/types.js.map +1 -0
  161. package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
  162. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
  163. package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
  164. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
  165. package/dist/node/server/CapabilityMatcher.d.ts +5 -0
  166. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
  167. package/dist/node/server/CapabilityMatcher.js +10 -1
  168. package/dist/node/server/CapabilityMatcher.js.map +1 -1
  169. package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
  170. package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
  171. package/dist/node/server/DeviceDiscoveryService.js +165 -0
  172. package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
  173. package/dist/node/server/MindooDBServer.d.ts +13 -0
  174. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  175. package/dist/node/server/MindooDBServer.js +99 -61
  176. package/dist/node/server/MindooDBServer.js.map +1 -1
  177. package/dist/node/server/SystemAdminAuth.d.ts +7 -0
  178. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
  179. package/dist/node/server/SystemAdminAuth.js +4 -1
  180. package/dist/node/server/SystemAdminAuth.js.map +1 -1
  181. package/dist/node/server/TenantManager.d.ts +29 -0
  182. package/dist/node/server/TenantManager.d.ts.map +1 -1
  183. package/dist/node/server/TenantManager.js +146 -7
  184. package/dist/node/server/TenantManager.js.map +1 -1
  185. package/package.json +5 -5
@@ -2,11 +2,19 @@
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");
@@ -48,6 +56,7 @@ class BaseMindooTenant {
48
56
  */
49
57
  preDecryptedMaterial) {
50
58
  this.databaseCache = new Map();
59
+ this.databaseOpenInFlight = new Map();
51
60
  this.directoryCache = null;
52
61
  this.remoteStoreCache = new Map();
53
62
  /**
@@ -89,6 +98,9 @@ class BaseMindooTenant {
89
98
  // recursing or overlapping. Reconcile is idempotent, so a skipped overlap is
90
99
  // harmless — the next trigger re-runs it.
91
100
  this.reconcileInFlight = false;
101
+ this.userKeyReconcileScheduled = false;
102
+ this.userKeys = new UserKeyManager_1.UserKeyManager(this);
103
+ this.sealedGenerations = new Map();
92
104
  // Local cache support
93
105
  this.cacheManager = null;
94
106
  this.factory = factory;
@@ -134,6 +146,9 @@ class BaseMindooTenant {
134
146
  if (preDecryptedMaterial?.encryptionKey) {
135
147
  this.decryptedUserEncryptionKeyCache = preDecryptedMaterial.encryptionKey;
136
148
  }
149
+ if (preDecryptedMaterial?.userKeyPrivateBytes) {
150
+ this.userKeys.seedDecryptedUserKeyBytes(preDecryptedMaterial.userKeyPrivateBytes);
151
+ }
137
152
  // Set up cache if a local cache store is provided. When a pre-derived
138
153
  // cache encryption key is available, prefer that to skip the password
139
154
  // path and avoid retaining the plaintext password inside the encrypted
@@ -193,6 +208,148 @@ class BaseMindooTenant {
193
208
  }
194
209
  return timestamp;
195
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
+ }
196
353
  getFactory() {
197
354
  return this.factory;
198
355
  }
@@ -225,6 +382,9 @@ class BaseMindooTenant {
225
382
  */
226
383
  async importAesKeyCached(symmetricKey, usage) {
227
384
  let cacheKey = usage + ":";
385
+ if (symmetricKey.length !== 32) {
386
+ throw new Error(`AES-256 key must be 32 bytes, got ${symmetricKey.length}`);
387
+ }
228
388
  for (let i = 0; i < symmetricKey.length; i++) {
229
389
  cacheKey += symmetricKey[i].toString(16).padStart(2, "0");
230
390
  }
@@ -233,9 +393,8 @@ class BaseMindooTenant {
233
393
  return cached;
234
394
  }
235
395
  const subtle = this.cryptoAdapter.getSubtle();
236
- // Create a new Uint8Array to ensure we have a proper ArrayBuffer
237
- const keyArray = new Uint8Array(symmetricKey);
238
- 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
239
398
  [usage]);
240
399
  if (this.importedAesKeyCache.size >= BaseMindooTenant.MAX_IMPORTED_KEY_CACHE_ENTRIES) {
241
400
  this.importedAesKeyCache.clear();
@@ -254,7 +413,14 @@ class BaseMindooTenant {
254
413
  // Get the symmetric key for this key ID
255
414
  let symmetricKey;
256
415
  try {
257
- 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") {
258
424
  // Use cached tenant encryption key if available
259
425
  if (this.decryptedTenantKeyCache) {
260
426
  symmetricKey = this.decryptedTenantKeyCache;
@@ -388,7 +554,9 @@ class BaseMindooTenant {
388
554
  }
389
555
  }
390
556
  this.logger.debug(`Decrypt failed against all ${candidates.length} version(s) of key "${decryptionKeyId}"`);
391
- throw lastError ?? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
557
+ throw lastError instanceof Error
558
+ ? new errors_1.SymmetricKeyNotFoundError(decryptionKeyId)
559
+ : new errors_1.SymmetricKeyNotFoundError(decryptionKeyId);
392
560
  }
393
561
  /**
394
562
  * Return every stored version of the symmetric key for `decryptionKeyId`,
@@ -400,6 +568,9 @@ class BaseMindooTenant {
400
568
  * tenant-default version so encryption keeps using the latest key.
401
569
  */
402
570
  async getDecryptionKeyCandidates(decryptionKeyId) {
571
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
572
+ return this.sealedGenerations.get(decryptionKeyId) ?? [];
573
+ }
403
574
  const id = decryptionKeyId === "default" ? types_1.DEFAULT_TENANT_KEY_ID : decryptionKeyId;
404
575
  const versions = await this.keyBag.getAllKeys("doc", this.tenantId, id);
405
576
  if (decryptionKeyId === "default" && versions.length > 0) {
@@ -415,7 +586,7 @@ class BaseMindooTenant {
415
586
  // Sign the payload
416
587
  const signature = await subtle.sign({
417
588
  name: "Ed25519",
418
- }, signingKey, payload.buffer);
589
+ }, signingKey, payload.slice());
419
590
  this.logger.debug(`Signed payload (signature: ${signature.byteLength} bytes)`);
420
591
  return new Uint8Array(signature);
421
592
  }
@@ -467,11 +638,23 @@ class BaseMindooTenant {
467
638
  }, false, // not extractable
468
639
  ["sign"]);
469
640
  // Sign the payload
470
- const signature = await subtle.sign({
641
+ const signatureBuffer = await subtle.sign({
471
642
  name: "Ed25519",
472
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
+ }
473
656
  this.logger.debug(`Signed payload with provided key (signature: ${signature.byteLength} bytes)`);
474
- return new Uint8Array(signature);
657
+ return signature;
475
658
  }
476
659
  async verifySignature(payload, signature, publicKey) {
477
660
  this.logger.debug(`Verifying signature`);
@@ -645,7 +828,8 @@ class BaseMindooTenant {
645
828
  // Every local database belonging to this tenant: the directory plus any
646
829
  // opened/known data databases. Wipe is a whole-tenant operation.
647
830
  const dbIds = new Set([
648
- "directory",
831
+ types_1.DIRECTORY_DB_ID,
832
+ types_1.USER_DIRECTORY_DB_ID,
649
833
  ...this.databaseCache.keys(),
650
834
  ...(additionalDbIds ?? []),
651
835
  ]);
@@ -701,7 +885,7 @@ class BaseMindooTenant {
701
885
  }
702
886
  }
703
887
  async assertCurrentUserCanOpenDB(id) {
704
- if (id === "directory") {
888
+ if (id === types_1.DIRECTORY_DB_ID || id === types_1.USER_DIRECTORY_DB_ID) {
705
889
  return;
706
890
  }
707
891
  const currentUser = await this.getCurrentUserId();
@@ -757,7 +941,7 @@ class BaseMindooTenant {
757
941
  const validDbId = (0, databaseIdValidation_1.validateDatabaseId)(id, "dbId");
758
942
  // Enforce admin-only mode for directory database - this is a security invariant
759
943
  // The directory database must only accept entries signed by the admin key
760
- const effectiveOptions = validDbId === "directory"
944
+ const effectiveOptions = validDbId === types_1.DIRECTORY_DB_ID
761
945
  ? { ...options, adminOnlyDb: true }
762
946
  : options ?? {};
763
947
  const normalizedTimeTravelDate = this.normalizeTimeTravelDate(effectiveOptions.timeTravelDate);
@@ -765,22 +949,41 @@ class BaseMindooTenant {
765
949
  ? validDbId
766
950
  : `${validDbId}::tt:${normalizedTimeTravelDate}`;
767
951
  await this.assertCurrentUserCanOpenDB(validDbId);
768
- // Return cached database if it exists
769
952
  const cached = this.databaseCache.get(databaseCacheKey);
770
953
  if (cached) {
771
- // For directory DB, verify admin-only flag matches (defensive check)
772
- if (validDbId === "directory" && !cached.isAdminOnlyDb()) {
954
+ if (validDbId === types_1.DIRECTORY_DB_ID && !cached.isAdminOnlyDb()) {
773
955
  throw new Error("Directory database was cached without adminOnlyDb - this should never happen");
774
956
  }
775
- // Live databases must reflect the current KeyBag state before they
776
- // are handed back to callers. Time-travel snapshots are immutable
777
- // historical views and intentionally do not participate in
778
- // visibility reconciliation, so we skip the call for them.
779
957
  if (normalizedTimeTravelDate == null) {
780
958
  await this.reconcileKeyBagChanges();
781
959
  }
782
960
  return cached;
783
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) {
784
987
  // Extract store options and DB-specific options
785
988
  const { adminOnlyDb, timeTravelDate: _timeTravelDate, persistTimeTravelCache, autoIndexing, attachmentConfig, documentCacheConfig, snapshotConfig, performanceCallback, ...storeOptions } = effectiveOptions;
786
989
  // Snapshot opens can share an already-open live store instance. This keeps
@@ -1056,50 +1259,23 @@ class BaseMindooTenant {
1056
1259
  const signerLogger = this.logger.createChild("MindooDocSigner");
1057
1260
  return new MindooDocSigner_1.MindooDocSigner(this, signKey, signerLogger);
1058
1261
  }
1059
- async exportJoinResponseDocKeyVersions(keyId, sharePassword) {
1060
- const scopedKeyId = `doc:${this.tenantId}:${keyId}`;
1061
- const details = (await this.keyBag.listKeyDetails())
1062
- .filter((detail) => detail.scopedKeyId === scopedKeyId)
1063
- .sort((a, b) => a.versionIndex - b.versionIndex);
1064
- 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) {
1065
1265
  throw new Error(`Failed to export document key "${keyId}" for tenant "${this.tenantId}"`);
1066
1266
  }
1267
+ const rsa = new RSAEncryption_1.RSAEncryption(this.cryptoAdapter, this.logger.createChild("RSAEncryption"));
1067
1268
  const versions = [];
1068
- for (const detail of details) {
1069
- const encryptedKey = await this.keyBag.encryptAndExportKeyVersion("doc", this.tenantId, keyId, detail.versionIndex, sharePassword);
1070
- if (!encryptedKey) {
1071
- throw new Error(`Failed to export document key "${keyId}" version ${detail.versionIndex} for tenant "${this.tenantId}"`);
1072
- }
1073
- const { createdAt: encryptedKeyCreatedAt, ...encryptedKeyPayload } = encryptedKey;
1269
+ for (const version of keyVersions) {
1074
1270
  versions.push({
1075
- createdAt: detail.createdAt ?? encryptedKeyCreatedAt,
1076
- encryptedKey: encryptedKeyPayload,
1271
+ createdAt: version.createdAt,
1272
+ wrappedKey: await rsa.wrapKeyToBase64(version.key, recipientEncryptionPublicKey),
1077
1273
  });
1078
1274
  }
1079
1275
  return { keyId, versions };
1080
1276
  }
1081
- resolveJoinResponseDocKeyIds(options) {
1082
- const requestedKeyIds = options.sharedDocKeyIds ?? [types_1.PUBLIC_INFOS_KEY_ID, types_1.DEFAULT_TENANT_KEY_ID];
1083
- const keyIds = new Set();
1084
- keyIds.add(types_1.PUBLIC_INFOS_KEY_ID);
1085
- for (const keyId of requestedKeyIds) {
1086
- const normalizedKeyId = keyId.trim();
1087
- if (normalizedKeyId) {
1088
- keyIds.add(normalizedKeyId);
1089
- }
1090
- }
1091
- return [
1092
- types_1.PUBLIC_INFOS_KEY_ID,
1093
- ...Array.from(keyIds)
1094
- .filter((keyId) => keyId !== types_1.PUBLIC_INFOS_KEY_ID)
1095
- .sort((a, b) => {
1096
- if (a === types_1.DEFAULT_TENANT_KEY_ID)
1097
- return -1;
1098
- if (b === types_1.DEFAULT_TENANT_KEY_ID)
1099
- return 1;
1100
- return a.localeCompare(b);
1101
- }),
1102
- ];
1277
+ resolveJoinResponseDocKeyIds(_options) {
1278
+ return [types_1.PUBLIC_INFOS_KEY_ID];
1103
1279
  }
1104
1280
  async approveJoinRequest(joinRequest, options) {
1105
1281
  // Parse the join request if it's a URI string
@@ -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,
@@ -1168,7 +1408,7 @@ class BaseMindooTenant {
1168
1408
  else {
1169
1409
  try {
1170
1410
  const directoryDb = await this.openDB("directory", { adminOnlyDb: true });
1171
- const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb);
1411
+ const setupLabel = await (0, tenantSetup_1.readTenantSetupLabel)(directoryDb, this);
1172
1412
  if (setupLabel) {
1173
1413
  joinResponse.tenantLabel = setupLabel;
1174
1414
  }
@@ -1177,6 +1417,36 @@ class BaseMindooTenant {
1177
1417
  // Best-effort: older tenants or missing setup doc.
1178
1418
  }
1179
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
+ }
1180
1450
  console.log(`[approveJoinRequest] ✓ Join request approved for user "${username}"`);
1181
1451
  this.logger.info(`Join request approved for user: ${username}`);
1182
1452
  // Return as URI string or object depending on format option
@@ -1342,8 +1612,9 @@ class BaseMindooTenant {
1342
1612
  const signingKey = await this.getDecryptedSigningKey();
1343
1613
  // Get the current user's decrypted RSA encryption private key (for decrypting entries)
1344
1614
  const decryptedEncryptionKey = await this.getDecryptedEncryptionKey();
1345
- // Create the client network store
1346
- 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);
1347
1618
  console.log(`[connectToServer] ✓ Connected to server for db "${validDbId}"`);
1348
1619
  this.logger.info(`Connected to server for db "${validDbId}"`);
1349
1620
  return store;
@@ -1507,6 +1778,9 @@ class BaseMindooTenant {
1507
1778
  * @returns true if the key can be resolved, false otherwise
1508
1779
  */
1509
1780
  async hasDecryptionKey(decryptionKeyId) {
1781
+ if ((0, sealedTypes_1.isSealedKeyId)(decryptionKeyId)) {
1782
+ return (this.sealedGenerations.get(decryptionKeyId)?.length ?? 0) > 0;
1783
+ }
1510
1784
  try {
1511
1785
  await this.getSymmetricKey(decryptionKeyId);
1512
1786
  return true;
@@ -1529,14 +1803,14 @@ class BaseMindooTenant {
1529
1803
  return this.keyBag.getDocKeyFingerprint(this.tenantId);
1530
1804
  }
1531
1805
  /**
1532
- * Remove a **named** document key from the local KeyBag. Refuses to delete the
1533
- * shared tenant default key (`"default"` / {@link DEFAULT_TENANT_KEY_ID}),
1534
- * which is required for the tenant to function. Returns true if a key was
1535
- * 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.
1536
1810
  */
1537
1811
  async removeNamedDecryptionKey(decryptionKeyId) {
1538
- if (decryptionKeyId === "default" || decryptionKeyId === types_1.DEFAULT_TENANT_KEY_ID) {
1539
- 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`);
1540
1814
  return false;
1541
1815
  }
1542
1816
  try {
@@ -1573,19 +1847,24 @@ class BaseMindooTenant {
1573
1847
  const directory = await this.openDirectory();
1574
1848
  const removed = [];
1575
1849
  const imported = [];
1576
- // Revoke pass — no key, no comparison, idempotent.
1577
- if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
1578
- let revokedIds = [];
1579
- try {
1580
- 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);
1581
1857
  }
1582
- catch (error) {
1583
- this.logger.warn(`reconcileKeyDistributionsForCurrentUser: revoked-id lookup failed: ${error}`);
1858
+ else if (typeof directory.getRevokedDecryptionKeyIdsForUser === "function") {
1859
+ revokedIds = await directory.getRevokedDecryptionKeyIdsForUser(username);
1584
1860
  }
1585
- for (const keyId of revokedIds) {
1586
- if (await this.removeNamedDecryptionKey(keyId)) {
1587
- removed.push(keyId);
1588
- }
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);
1589
1868
  }
1590
1869
  }
1591
1870
  // Import pass — sources the encryption private key from this tenant. The
@@ -1605,7 +1884,41 @@ class BaseMindooTenant {
1605
1884
  this.logger.warn(`reconcileKeyDistributionsForCurrentUser: import pass failed: ${error}`);
1606
1885
  }
1607
1886
  }
1608
- 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
+ }
1609
1922
  }
1610
1923
  /**
1611
1924
  * Single-flight, best-effort wrapper around
@@ -1717,13 +2030,14 @@ class BaseMindooTenant {
1717
2030
  /**
1718
2031
  * Remove exactly the key versions of `keyId` whose raw-bytes fingerprint is in
1719
2032
  * `fingerprints` (version-scoped `pullfrom` revocation). Versions obtained
1720
- * elsewhere — i.e. not in the distribution manifest — survive. Refuses the
1721
- * protected tenant default / `$publicinfos` keys. The KeyBag change feed
1722
- * 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).
1723
2037
  * Returns the number of versions removed.
1724
2038
  */
1725
2039
  async removeDecryptionKeyVersionsByFingerprint(keyId, fingerprints) {
1726
- 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) {
1727
2041
  this.logger.debug(`removeDecryptionKeyVersionsByFingerprint: refusing protected key ${keyId}`);
1728
2042
  return 0;
1729
2043
  }
@@ -1768,6 +2082,13 @@ class BaseMindooTenant {
1768
2082
  * @returns The decrypted symmetric key bytes
1769
2083
  */
1770
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
+ }
1771
2092
  if (decryptionKeyId === "default") {
1772
2093
  // Use cached tenant encryption key if available
1773
2094
  if (this.decryptedTenantKeyCache) {