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
@@ -6,6 +6,7 @@ const timeSource_1 = require("./utils/timeSource");
6
6
  const logging_1 = require("./logging");
7
7
  const grantKeys_1 = require("./accesscontrol/grantKeys");
8
8
  const types_2 = require("./accesscontrol/types");
9
+ const DirectoryStateNode_1 = require("./accesscontrol/DirectoryStateNode");
9
10
  const DirectoryTimeTravelIndex_1 = require("./accesscontrol/DirectoryTimeTravelIndex");
10
11
  const directoryProjection_1 = require("./accesscontrol/directoryProjection");
11
12
  const types_3 = require("./accesscontrol/types");
@@ -113,7 +114,7 @@ class BaseMindooTenantDirectory {
113
114
  }
114
115
  return this.directoryDB;
115
116
  }
116
- async registerUser(userId, administrationPrivateKey, administrationPrivateKeyPassword, userDetails, label) {
117
+ async registerUser(userId, administrationPrivateKey, administrationPrivateKeyPassword, userDetails, label, options) {
117
118
  this.logger.info(`Registering user: ${userId.username}`);
118
119
  // Check if user with same username (case-insensitive) already has ACTIVE
119
120
  // access. A grant whose keys were removed (revocation, §6.5) is ignored
@@ -131,7 +132,7 @@ class BaseMindooTenantDirectory {
131
132
  const keysMatch = (0, grantKeys_1.extractSigningPublicKeys)(existingData).includes(userId.userSigningPublicKey) &&
132
133
  (0, grantKeys_1.extractEncryptionPublicKeys)(existingData).includes(userId.userEncryptionPublicKey);
133
134
  if (keysMatch) {
134
- // Same user with same keys - skip re-registration
135
+ // Same user with same keys - skip re-registration and re-wrap
135
136
  this.logger.debug(`User ${userId.username} already registered with same keys, skipping`);
136
137
  return;
137
138
  }
@@ -141,10 +142,11 @@ class BaseMindooTenantDirectory {
141
142
  const newPair = {
142
143
  signingPublicKey: userId.userSigningPublicKey,
143
144
  encryptionPublicKey: userId.userEncryptionPublicKey,
145
+ addedAt: (0, timeSource_1.semanticNow)(),
144
146
  };
145
147
  if (trimmedLabel.length > 0)
146
148
  newPair.label = trimmedLabel;
147
- await this.addUserKeys(userId.username, [newPair], administrationPrivateKey, administrationPrivateKeyPassword);
149
+ await this.addUserKeys(userId.username, [newPair], administrationPrivateKey, administrationPrivateKeyPassword, options?.distributeKeyIds);
148
150
  return;
149
151
  }
150
152
  // Cast to BaseMindooTenant to access protected methods
@@ -202,6 +204,7 @@ class BaseMindooTenantDirectory {
202
204
  const initialPair = {
203
205
  signingPublicKey: userId.userSigningPublicKey,
204
206
  encryptionPublicKey: userId.userEncryptionPublicKey,
207
+ addedAt: (0, timeSource_1.semanticNow)(),
205
208
  };
206
209
  if (trimmedLabel.length > 0)
207
210
  initialPair.label = trimmedLabel;
@@ -216,6 +219,91 @@ class BaseMindooTenantDirectory {
216
219
  throw error;
217
220
  }
218
221
  this.logger.info(`Registered user: ${userId.username}`);
222
+ // Push selected keys (default: tenant default) via key distribution so the
223
+ // joining device can bootstrap without a join-response URI once the
224
+ // server delivers `$publicinfos` (docs/accesscontrol.md §13).
225
+ const distributeKeyIds = options?.distributeKeyIds ?? [types_1.DEFAULT_TENANT_KEY_ID];
226
+ await this.autoDistributeKeysToUser(userId.username, distributeKeyIds, administrationPrivateKey, administrationPrivateKeyPassword);
227
+ }
228
+ /**
229
+ * Merge `username` into `acl_keydistribution_<keyId>` for each distributable
230
+ * key id (wraps to the person's published User-Key). Skips protected ids and
231
+ * keys the caller's KeyBag does not hold. Used after grant so
232
+ * discover-bootstrap can import `default` without a join-response URI.
233
+ */
234
+ async autoDistributeKeysToUser(username, keyIds, administrationPrivateKey, administrationPrivateKeyPassword, publicKeyOverride) {
235
+ const filtered = [
236
+ ...new Set(keyIds
237
+ .map((id) => (typeof id === "string" ? id.trim() : ""))
238
+ .filter((id) => id.length > 0 && !types_3.PROTECTED_DISTRIBUTION_KEY_IDS.includes(id))),
239
+ ];
240
+ if (filtered.length === 0)
241
+ return;
242
+ const baseTenant = this.tenant;
243
+ const preparedBy = baseTenant.getAdministrationPublicKey();
244
+ for (const keyId of filtered) {
245
+ const keyVersions = await this.getKeyVersionManifest(keyId);
246
+ if (keyVersions.length === 0) {
247
+ this.logger.debug(`autoDistributeKeysToUser: skipping "${keyId}" — caller KeyBag does not hold it`);
248
+ continue;
249
+ }
250
+ const existing = (await this.listKeyDistributions()).find((v) => v.keyId === keyId);
251
+ const pushtoNames = new Set((existing?.pushtoUsernames ?? [])
252
+ .map((u) => u.trim())
253
+ .filter((u) => u.length > 0));
254
+ pushtoNames.add(username.trim());
255
+ const usernameLower = username.trim().toLowerCase();
256
+ const pullfromNames = (existing?.pullfromUsernames ?? [])
257
+ .map((u) => u.trim())
258
+ .filter((u) => u.length > 0 && u.toLowerCase() !== usernameLower);
259
+ const pushto = [];
260
+ for (const pushtoUsername of pushtoNames) {
261
+ const override = pushtoUsername.trim() === username.trim() ? publicKeyOverride : undefined;
262
+ const wrapped = await this.wrapKeyForUser(keyId, pushtoUsername, override);
263
+ if (wrapped)
264
+ pushto.push(wrapped);
265
+ }
266
+ if (pushto.length === 0) {
267
+ this.logger.warn(`autoDistributeKeysToUser: skipping "${keyId}" — no published User-Key wrap yet for "${username}"`);
268
+ continue;
269
+ }
270
+ const pullfrom = [];
271
+ for (const pullUsername of pullfromNames) {
272
+ pullfrom.push({
273
+ username: pullUsername,
274
+ username_hash: await this.hashUsernameForWrite(pullUsername),
275
+ });
276
+ }
277
+ const title = (typeof existing?.title === "string" && existing.title.trim()) || keyId;
278
+ const comment = typeof existing?.comment === "string" && existing.comment.trim()
279
+ ? existing.comment.trim()
280
+ : undefined;
281
+ await this.publishKeyDistribution({
282
+ v: 1,
283
+ keyId,
284
+ keyVersions,
285
+ title,
286
+ ...(comment ? { comment } : {}),
287
+ preparedByPublicKey: preparedBy,
288
+ pushto,
289
+ pullfrom,
290
+ }, administrationPrivateKey, administrationPrivateKeyPassword);
291
+ this.logger.info(`autoDistributeKeysToUser: merged "${username}" into distribution for "${keyId}"`);
292
+ }
293
+ }
294
+ /**
295
+ * Key ids that already list `username` in pushto (plus `default`), so a newly
296
+ * added device gets re-wrapped material. Protected ids are excluded.
297
+ */
298
+ async keyIdsNeedingDeviceWrap(username) {
299
+ const lower = username.trim().toLowerCase();
300
+ const ids = new Set([types_1.DEFAULT_TENANT_KEY_ID]);
301
+ for (const view of await this.listKeyDistributions()) {
302
+ if (view.pushtoUsernames?.some((u) => u.trim().toLowerCase() === lower)) {
303
+ ids.add(view.keyId);
304
+ }
305
+ }
306
+ return [...ids].filter((id) => !types_3.PROTECTED_DISTRIBUTION_KEY_IDS.includes(id));
219
307
  }
220
308
  async findGrantAccessDocuments(username) {
221
309
  this.logger.debug(`Finding grant access documents for username: ${username}`);
@@ -421,6 +509,7 @@ class BaseMindooTenantDirectory {
421
509
  }
422
510
  // Track group documents by name for merging (handles offline sync scenarios)
423
511
  const groupDocsByName = new Map();
512
+ let sawGrantAccessChange = false;
424
513
  // Process changes (documents are returned in order by lastModified, oldest to newest)
425
514
  for await (const { doc, cursor } of directoryDB.iterateChangesSince(startCursor)) {
426
515
  const data = doc.getData();
@@ -440,6 +529,7 @@ class BaseMindooTenantDirectory {
440
529
  }
441
530
  }
442
531
  if (data.type === "grantaccess") {
532
+ sawGrantAccessChange = true;
443
533
  // Record this grant document's CURRENT signing keys. Because grant
444
534
  // documents are Automerge-merged in place, removing keys (the only
445
535
  // revocation mechanism, §6.5) is reflected by a smaller array here.
@@ -580,6 +670,9 @@ class BaseMindooTenantDirectory {
580
670
  if (newlyTrustedKeys.size > 0) {
581
671
  void this.tenant.reconcileAuthorTrustChangesSafe(newlyTrustedKeys);
582
672
  }
673
+ if (sawGrantAccessChange) {
674
+ this.tenant.scheduleUserKeyReconcile?.();
675
+ }
583
676
  // Merge group documents with the same name
584
677
  for (const [normalizedGroupName, docs] of groupDocsByName.entries()) {
585
678
  // Collect all member hashes and encrypted values from all documents with this group name
@@ -823,6 +916,25 @@ class BaseMindooTenantDirectory {
823
916
  isServer: false,
824
917
  });
825
918
  }
919
+ /**
920
+ * Grant-level `$author` resolver: creator at `creatorTrustedTime`, signer at
921
+ * `signerTrustedTime`. See {@link isSamePerson}.
922
+ */
923
+ async isSamePerson(input) {
924
+ const creatorNode = await this.getDirectoryStateAt(input.creatorTrustedTime);
925
+ const signerNode = input.signerTrustedTime === input.creatorTrustedTime
926
+ ? creatorNode
927
+ : await this.getDirectoryStateAt(input.signerTrustedTime);
928
+ return (0, DirectoryStateNode_1.isSamePerson)(creatorNode, signerNode, input.creatorSigningKey, input.signerSigningKey);
929
+ }
930
+ /**
931
+ * The `username_hash` of the active grant that contained `signingKey` at
932
+ * trusted time `T`, or `null` when the key was not a granted device then.
933
+ */
934
+ async resolveUsernameHashForSigningKey(signingKey, trustedTime) {
935
+ const node = await this.getDirectoryStateAt(trustedTime);
936
+ return (0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey)?.usernameHash ?? null;
937
+ }
826
938
  /**
827
939
  * Whether the active policy has any Tier 2 (`withfields`) content rule for
828
940
  * operation `op` in database `dbid` at the current head state. Lets the
@@ -927,7 +1039,7 @@ class BaseMindooTenantDirectory {
927
1039
  * bypass).
928
1040
  */
929
1041
  async isDatabaseAllowed(dbid, opts) {
930
- if (dbid === "directory")
1042
+ if (dbid === types_1.DIRECTORY_DB_ID || dbid === types_1.USER_DIRECTORY_DB_ID)
931
1043
  return true;
932
1044
  const { mode, allowedDbIds } = await this.getDatabaseCreationPolicy();
933
1045
  if (mode === "open")
@@ -954,7 +1066,7 @@ class BaseMindooTenantDirectory {
954
1066
  }
955
1067
  // The directory database is never read-gated (it must always sync so the
956
1068
  // policy that defines the read gate can be read at all).
957
- if (input.dbid === "directory")
1069
+ if (input.dbid === types_1.DIRECTORY_DB_ID || input.dbid === types_1.USER_DIRECTORY_DB_ID)
958
1070
  return true;
959
1071
  // The tenant admin is exempt from the read gate.
960
1072
  const adminKey = this.tenant.getAdministrationPublicKey();
@@ -984,7 +1096,7 @@ class BaseMindooTenantDirectory {
984
1096
  * ({@link isDatabaseAllowed}); callers enforce both gates.
985
1097
  */
986
1098
  async canReadDatabase(dbid) {
987
- if (dbid === "directory")
1099
+ if (dbid === types_1.DIRECTORY_DB_ID || dbid === types_1.USER_DIRECTORY_DB_ID)
988
1100
  return true;
989
1101
  const currentUser = await this.tenant.getCurrentUserId();
990
1102
  const adminKey = this.tenant.getAdministrationPublicKey();
@@ -1177,16 +1289,61 @@ class BaseMindooTenantDirectory {
1177
1289
  * Pairs are merged by signing key (a pair with an already-present signing key
1178
1290
  * updates its encryption key/label). See {@link mergeKeyPairs}.
1179
1291
  */
1180
- async addUserKeys(username, keyPairs, administrationPrivateKey, administrationPrivateKeyPassword) {
1292
+ async addUserKeys(username, keyPairs, administrationPrivateKey, administrationPrivateKeyPassword, distributeKeyIds) {
1293
+ const now = (0, timeSource_1.semanticNow)();
1181
1294
  const additions = keyPairs.map((pair) => {
1182
1295
  const trimmedLabel = typeof pair.label === "string" ? pair.label.trim() : "";
1183
- return trimmedLabel.length > 0
1184
- ? { ...pair, label: trimmedLabel }
1185
- : { signingPublicKey: pair.signingPublicKey, encryptionPublicKey: pair.encryptionPublicKey };
1296
+ const next = {
1297
+ signingPublicKey: pair.signingPublicKey,
1298
+ encryptionPublicKey: pair.encryptionPublicKey,
1299
+ addedAt: pair.addedAt ?? now,
1300
+ };
1301
+ if (trimmedLabel.length > 0)
1302
+ next.label = trimmedLabel;
1303
+ if (pair.revoked) {
1304
+ next.revoked = true;
1305
+ if (typeof pair.revokedAt === "number")
1306
+ next.revokedAt = pair.revokedAt;
1307
+ }
1308
+ const wrapped = typeof pair.joinResponseWrapped === "string" ? pair.joinResponseWrapped.trim() : "";
1309
+ if (wrapped)
1310
+ next.joinResponseWrapped = wrapped;
1311
+ return next;
1186
1312
  });
1187
1313
  await this.editGrantArrays(username, administrationPrivateKey, administrationPrivateKeyPassword, (data) => {
1188
1314
  (0, grantKeys_1.applyKeyPairFields)(data, (0, grantKeys_1.mergeKeyPairs)((0, grantKeys_1.extractKeyPairs)(data), additions));
1189
1315
  });
1316
+ // Device enrollment no longer re-wraps named keys: wraps target User-Keys,
1317
+ // which do not change when a device is added. An existing approved device
1318
+ // wraps the User-Key for the new device instead.
1319
+ if (distributeKeyIds !== undefined && distributeKeyIds.length > 0) {
1320
+ await this.autoDistributeKeysToUser(username, distributeKeyIds, administrationPrivateKey, administrationPrivateKeyPassword);
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Store a per-device RSA-hybrid join bootstrap (`username` + optional
1325
+ * `tenantLabel`) on the matching `userKeyPairs` entry. The blob is
1326
+ * `$publicinfos`-readable so device discovery can forward it; only the
1327
+ * device's encryption private key can unwrap it.
1328
+ */
1329
+ async attachDeviceJoinBootstrap(username, signingPublicKey, wrappedJoinResponse, administrationPrivateKey, administrationPrivateKeyPassword) {
1330
+ const wrapped = wrappedJoinResponse.trim();
1331
+ if (!wrapped) {
1332
+ throw new Error("Cannot attach device join bootstrap: wrappedJoinResponse is empty");
1333
+ }
1334
+ let found = false;
1335
+ await this.editGrantArrays(username, administrationPrivateKey, administrationPrivateKeyPassword, (data) => {
1336
+ const next = (0, grantKeys_1.extractKeyPairs)(data).map((pair) => {
1337
+ if (!(0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, signingPublicKey))
1338
+ return pair;
1339
+ found = true;
1340
+ return { ...pair, joinResponseWrapped: wrapped };
1341
+ });
1342
+ (0, grantKeys_1.applyKeyPairFields)(data, next);
1343
+ });
1344
+ if (!found) {
1345
+ throw new Error(`Cannot attach device join bootstrap: no granted device matches the signing key for "${username}"`);
1346
+ }
1190
1347
  }
1191
1348
  /**
1192
1349
  * Revoke keys from a user's grant (§6.5 revoke a device/key). A device is
@@ -1498,6 +1655,61 @@ class BaseMindooTenantDirectory {
1498
1655
  devices,
1499
1656
  };
1500
1657
  }
1658
+ /**
1659
+ * Wrap every stored version of `keyId` to the recipient's published User-Key
1660
+ * (one wrap per person, not per device). Throws if the caller does not hold
1661
+ * the key. Returns `null` when the recipient has no published (non-pending)
1662
+ * User-Key — callers treat that as a pending state, not an error.
1663
+ *
1664
+ * `publicKeyOverride` is the join-bootstrap exception: the admin wraps
1665
+ * `default` to the User-Key from the join request before the person has
1666
+ * written a device wrap.
1667
+ */
1668
+ async wrapKeyForUser(keyId, username, publicKeyOverride) {
1669
+ const baseTenant = this.tenant;
1670
+ const versions = await baseTenant.fingerprintKeyVersions(keyId);
1671
+ if (versions.length === 0) {
1672
+ throw new Error(`Cannot wrap key: key "${keyId}" is not in your KeyBag`);
1673
+ }
1674
+ let publicKey = publicKeyOverride;
1675
+ let fingerprint;
1676
+ if (!publicKey) {
1677
+ const published = await baseTenant.getUserKeyManager().publishedUserKeyFor(username);
1678
+ // Wrap even while the User-Key document is still pending (no device
1679
+ // wraps yet). The public key is already the person's; later devices
1680
+ // unwrap `default` from this document after they import the User-Key.
1681
+ if (!published) {
1682
+ this.logger.warn(`wrapKeyForUser: no published User-Key for "${username}" — cannot wrap "${keyId}"`);
1683
+ return null;
1684
+ }
1685
+ publicKey = published.publicKey;
1686
+ // Derive the map key from the PEM we actually encrypt to, never from the
1687
+ // document's fingerprint field. If the two ever disagree, the recipient
1688
+ // finds a wrap under its own fingerprint that its private key cannot
1689
+ // open — an undebuggable "wrong User-Key or corrupt wrap".
1690
+ const derived = await this.encryptionKeyFingerprint(publicKey);
1691
+ if (published.fingerprint && published.fingerprint !== derived) {
1692
+ this.logger.warn(`wrapKeyForUser: User-Key document for "${username}" is inconsistent — ` +
1693
+ `fingerprint field says ${published.fingerprint} but its publicKey hashes to ${derived}; ` +
1694
+ `wrapping "${keyId}" under ${derived}`);
1695
+ }
1696
+ fingerprint = derived;
1697
+ }
1698
+ if (!fingerprint) {
1699
+ fingerprint = await this.encryptionKeyFingerprint(publicKey);
1700
+ }
1701
+ const rsa = new RSAEncryption_1.RSAEncryption(this.tenant.getCryptoAdapter(), this.logger.createChild("RSAEncryption"));
1702
+ const wrappedByVersion = {};
1703
+ for (const version of versions) {
1704
+ const wrapped = await rsa.encrypt(version.bytes, publicKey);
1705
+ wrappedByVersion[version.fingerprint] = this.bytesToBase64(wrapped);
1706
+ }
1707
+ return {
1708
+ username,
1709
+ username_hash: await this.hashUsernameForWrite(username),
1710
+ devices: { [fingerprint]: wrappedByVersion },
1711
+ };
1712
+ }
1501
1713
  /**
1502
1714
  * The canonical write-time hash for `username` (the value stored in
1503
1715
  * `pushto_users_hashes` / `pullfrom_users_hashes`). Exposed so callers (the
@@ -1507,6 +1719,13 @@ class BaseMindooTenantDirectory {
1507
1719
  async getUsernameHash(username) {
1508
1720
  return this.hashUsernameForWrite(username);
1509
1721
  }
1722
+ /**
1723
+ * All hash forms a given username could be stored as (v1/v2/v3). Public so
1724
+ * User-Key lookup can scan `userdirectory` the same way grants are found.
1725
+ */
1726
+ async getUsernameHashCandidates(username) {
1727
+ return this.usernameHashCandidates(username);
1728
+ }
1510
1729
  /**
1511
1730
  * The version manifest (`{createdAt, fingerprint}`) of `keyId` from the
1512
1731
  * caller's KeyBag. The single source of truth for the distribution's
@@ -1547,15 +1766,31 @@ class BaseMindooTenantDirectory {
1547
1766
  pullfrom_users_hashes: pullfromHashes,
1548
1767
  pushto_users_keys: pushtoUsersKeys,
1549
1768
  });
1550
- // Directory-level validation: every pushto user must have an active grant,
1551
- // and every active encryption device must have wrapped material (otherwise a
1552
- // newly-added device could not decrypt — re-wrap is required).
1769
+ // Directory-level validation: every pushto user must have an active grant.
1770
+ // Coverage is per published User-Key epoch (or per device for service
1771
+ // accounts that have no User-Key).
1553
1772
  const manifestFps = new Set(request.keyVersions.map((v) => v.fingerprint));
1554
1773
  for (const p of request.pushto) {
1555
1774
  const userKeys = await this.getUserPublicKeys(p.username);
1556
1775
  if (!userKeys) {
1557
1776
  throw new Error(`Cannot publish: pushto user "${p.username}" has no active grant`);
1558
1777
  }
1778
+ const baseTenant = this.tenant;
1779
+ const published = await baseTenant.getUserKeyManager().publishedUserKeyFor(p.username);
1780
+ if (published) {
1781
+ // Pending documents are the join-bootstrap case: the admin wraps
1782
+ // `default` to the join-request User-Key before the person has written
1783
+ // a device wrap. Coverage is still per User-Key fingerprint.
1784
+ const wrapped = p.devices[published.fingerprint];
1785
+ if (!wrapped) {
1786
+ throw new Error(`Cannot publish: pushto user "${p.username}" has no wrap for their User-Key`);
1787
+ }
1788
+ const covered = Object.keys(wrapped);
1789
+ if (covered.length !== manifestFps.size || covered.some((fp) => !manifestFps.has(fp))) {
1790
+ throw new Error(`Cannot publish: User-Key coverage for "${p.username}" does not match the version manifest`);
1791
+ }
1792
+ continue;
1793
+ }
1559
1794
  const activePems = await this.getUserEncryptionPublicKeys(p.username);
1560
1795
  for (const pem of activePems) {
1561
1796
  const deviceFp = await this.encryptionKeyFingerprint(pem);
@@ -1722,13 +1957,41 @@ class BaseMindooTenantDirectory {
1722
1957
  */
1723
1958
  async reconcileImportedKeysForCurrentUser(username) {
1724
1959
  const baseTenant = this.tenant;
1725
- const key = await baseTenant.getEncryptionPrivateKeyForReconcile();
1726
- if (!key) {
1960
+ const keys = [];
1961
+ const userKeys = await baseTenant.getUserKeyManager().getUserKeyCryptoKeysForReconcile();
1962
+ keys.push(...userKeys);
1963
+ const deviceKey = await baseTenant.getEncryptionPrivateKeyForReconcile();
1964
+ if (deviceKey)
1965
+ keys.push(deviceKey);
1966
+ if (keys.length === 0) {
1727
1967
  this.logger.debug("reconcileImportedKeysForCurrentUser: no encryption key available; " +
1728
1968
  "skipping import pass (revoke pass still ran)");
1729
1969
  return { imported: [], removed: [] };
1730
1970
  }
1731
- return this.reconcileKeyDistributionsWithKey(username, key);
1971
+ let imported = [];
1972
+ let removed = [];
1973
+ // A wrap is addressed to exactly one of these keys, so every other candidate
1974
+ // fails on it by construction. Collect the misses and judge them only once
1975
+ // the whole pass is done, otherwise the losing candidates cry wolf on a key
1976
+ // that was delivered fine.
1977
+ const unopened = new Map();
1978
+ for (const key of keys) {
1979
+ const result = await this.reconcileKeyDistributionsWithKey(username, key);
1980
+ imported = Array.from(new Set([...imported, ...result.imported]));
1981
+ removed = Array.from(new Set([...removed, ...result.removed]));
1982
+ for (const [keyId, reason] of result.unopened ?? []) {
1983
+ unopened.set(keyId, [...(unopened.get(keyId) ?? []), reason]);
1984
+ }
1985
+ }
1986
+ for (const [keyId, reasons] of unopened) {
1987
+ if (imported.includes(keyId))
1988
+ continue;
1989
+ if (await baseTenant.hasDecryptionKey(keyId))
1990
+ continue;
1991
+ this.logger.warn(`reconcileKeyDistributions: ${keyId} not unwrapped by any of the ${keys.length} ` +
1992
+ `available encryption key(s) (wrong User-Key or corrupt wrap): ${reasons.join(" || ")}`);
1993
+ }
1994
+ return { imported, removed };
1732
1995
  }
1733
1996
  /**
1734
1997
  * Reconcile the local KeyBag against the directory head for `username`
@@ -1755,14 +2018,35 @@ class BaseMindooTenantDirectory {
1755
2018
  const directoryDB = await this.getDirectoryDB();
1756
2019
  await directoryDB.syncStoreChanges();
1757
2020
  await this.updateUnifiedCache();
1758
- if (this.keyDistributionCache.size === 0)
2021
+ if (this.keyDistributionCache.size === 0) {
2022
+ this.logger.warn("reconcileKeyDistributionsWithKey: directory has no key-distribution documents in cache");
1759
2023
  return { imported: [], removed: [] };
2024
+ }
2025
+ if (!this.keyDistributionCache.has(types_1.DEFAULT_TENANT_KEY_ID)) {
2026
+ this.logger.warn("reconcileKeyDistributionsWithKey: no acl_keydistribution_default in directory cache");
2027
+ }
1760
2028
  const myHashes = new Set(await this.usernameHashCandidates(username));
2029
+ // Nameless / pre-adoption devices: also match via the grant's
2030
+ // identity_hashes / username_hash looked up by signing public key.
2031
+ try {
2032
+ const signingKey = (await this.tenant.getCurrentUserId()).userSigningPublicKey;
2033
+ const lookup = await this.getUserBySigningPublicKey(signingKey);
2034
+ if (lookup) {
2035
+ for (const h of await this.identityHashesForLookup(lookup)) {
2036
+ myHashes.add(h);
2037
+ }
2038
+ }
2039
+ }
2040
+ catch (error) {
2041
+ this.logger.debug(`reconcileKeyDistributionsWithKey: signing-key hash supplement failed: ${error}`);
2042
+ }
1761
2043
  const baseTenant = this.tenant;
1762
2044
  const privateKey = encryptionPrivateCryptoKey;
1763
2045
  const rsa = new RSAEncryption_1.RSAEncryption(this.tenant.getCryptoAdapter(), this.logger.createChild("RSAEncryption"));
1764
2046
  const imported = [];
1765
2047
  const removed = [];
2048
+ // keyId -> why this candidate key could not open any of its wraps.
2049
+ const unopened = new Map();
1766
2050
  for (const [keyId, entry] of this.keyDistributionCache.entries()) {
1767
2051
  if (types_3.PROTECTED_DISTRIBUTION_KEY_IDS.includes(keyId))
1768
2052
  continue;
@@ -1778,8 +2062,13 @@ class BaseMindooTenantDirectory {
1778
2062
  removed.push(keyId);
1779
2063
  continue;
1780
2064
  }
1781
- if (!meInPush)
2065
+ if (!meInPush) {
2066
+ if (keyId === types_1.DEFAULT_TENANT_KEY_ID) {
2067
+ this.logger.warn(`reconcileKeyDistributions: "${keyId}" exists but current user is not in pushto ` +
2068
+ `(hashes=${entry.pushto_users_hashes.length})`);
2069
+ }
1782
2070
  continue;
2071
+ }
1783
2072
  const wrappedMap = entry.pushto_users_keys;
1784
2073
  // Collect device entries addressed to me (by hash prefix). We do not know
1785
2074
  // which device is ours without trying, so attempt to unwrap each; only our
@@ -1788,39 +2077,63 @@ class BaseMindooTenantDirectory {
1788
2077
  const hash = entryKey.split("|")[0];
1789
2078
  return myHashes.has(hash);
1790
2079
  });
1791
- if (myEntries.length === 0)
2080
+ if (myEntries.length === 0) {
2081
+ this.logger.warn(`reconcileKeyDistributions: "${keyId}" lists this user in pushto but has no wrap entries for their hashes`);
1792
2082
  continue;
2083
+ }
1793
2084
  const collected = new Map();
1794
- for (const [, wrappedByVersion] of myEntries) {
2085
+ // Why each candidate failed, so a "none unwrapped" warning can name the
2086
+ // cause instead of lumping a missing version in with a decrypt failure.
2087
+ const attempts = [];
2088
+ for (const [entryKey, wrappedByVersion] of myEntries) {
1795
2089
  for (const ref of manifest) {
1796
2090
  if (collected.has(ref.fingerprint))
1797
2091
  continue;
1798
2092
  const wrappedB64 = wrappedByVersion[ref.fingerprint];
1799
- if (typeof wrappedB64 !== "string")
2093
+ if (typeof wrappedB64 !== "string") {
2094
+ attempts.push(`${entryKey}: no wrap for version ${ref.fingerprint}; entry carries [${Object.keys(wrappedByVersion).join(", ")}]`);
1800
2095
  continue;
2096
+ }
1801
2097
  try {
1802
2098
  const bytes = await rsa.decrypt(this.base64ToBytes(wrappedB64), privateKey);
1803
2099
  const actualFp = await baseTenant.fingerprintKeyBytes(bytes);
1804
2100
  if (actualFp !== ref.fingerprint) {
1805
2101
  this.logger.warn(`reconcileKeyDistributions: fingerprint mismatch for ${keyId}@${ref.fingerprint} (got ${actualFp}); rejecting`);
2102
+ attempts.push(`${entryKey}: unwrapped ${ref.fingerprint} but got key ${actualFp}`);
1806
2103
  continue;
1807
2104
  }
1808
2105
  collected.set(ref.fingerprint, { bytes, keyVersionCreatedAt: ref.createdAt });
1809
2106
  }
1810
- catch {
1811
- // Not our device entry (or corrupt); try the next one.
2107
+ catch (error) {
2108
+ // Byte length separates a truncated/corrupt wrap from a genuine
2109
+ // key mismatch: hybrid format is 2 + 384 (RSA-3072) + 12 + AES.
2110
+ const wrappedBytes = this.base64ToBytes(wrappedB64);
2111
+ const rsaBlockLength = (wrappedBytes[0] << 8) | wrappedBytes[1];
2112
+ attempts.push(`${entryKey}: RSA decrypt of version ${ref.fingerprint} failed ` +
2113
+ `(${error instanceof Error ? error.message || error.name : error}); ` +
2114
+ `wrap b64=${wrappedB64.length} bytes=${wrappedBytes.length} rsaBlock=${rsaBlockLength}`);
1812
2115
  }
1813
2116
  }
1814
2117
  if (collected.size === manifest.length)
1815
2118
  break;
1816
2119
  }
1817
- if (collected.size === 0)
2120
+ if (collected.size === 0) {
2121
+ // Not an error yet: the caller runs this pass once per candidate key
2122
+ // (every User-Key epoch, then the device key), so a wrap addressed to
2123
+ // the User-Key can never open with the device key. Only the caller knows
2124
+ // whether some other candidate succeeded, so report instead of warning.
2125
+ const addressedTo = myEntries.map(([entryKey]) => entryKey.split("|")[1] ?? "?");
2126
+ unopened.set(keyId, `${myEntries.length} wrap(s) for this user, none unwrapped; ` +
2127
+ `wraps addressed to [${addressedTo.join(", ")}], ` +
2128
+ `manifest versions [${manifest.map((ref) => ref.fingerprint).join(", ")}]; ` +
2129
+ `attempts: ${attempts.join(" | ")}`);
1818
2130
  continue;
2131
+ }
1819
2132
  const count = await baseTenant.importDeliveredDecryptionKeyVersions(keyId, Array.from(collected.values()));
1820
2133
  if (count > 0)
1821
2134
  imported.push(keyId);
1822
2135
  }
1823
- return { imported, removed };
2136
+ return { imported, removed, unopened };
1824
2137
  }
1825
2138
  /**
1826
2139
  * Whether `keyId` is managed by a distribution at the directory head AND the
@@ -2345,13 +2658,13 @@ class BaseMindooTenantDirectory {
2345
2658
  return hashes;
2346
2659
  }
2347
2660
  /**
2348
- * Hash-space core of the revoked-key blacklist (docs/accesscontrol.md §13):
2349
- * every `acl_keydistribution_<keyId>` whose `pullfrom_users_hashes` intersects
2350
- * `myHashes`. Protected ids (the tenant default / public-infos keys) are never
2351
- * reported. Reads straight from {@link keyDistributionCache} (O(cache size),
2352
- * no doc scan); the caller must have refreshed it first (both entry points do,
2353
- * via `updateUnifiedCache` / `getUserBySigningPublicKey`).
2354
- */
2661
+ * Hash-space core of the revoked-key blacklist (docs/accesscontrol.md §13):
2662
+ * every `acl_keydistribution_<keyId>` whose `pullfrom_users_hashes` intersects
2663
+ * `myHashes`. Protected id `$publicinfos` is never reported. Reads straight from
2664
+ * {@link keyDistributionCache} (O(cache size), no doc scan); the caller must have
2665
+ * refreshed it first (both entry points do, via `updateUnifiedCache` /
2666
+ * `getUserBySigningPublicKey`).
2667
+ */
2355
2668
  collectRevokedKeyIdsForHashes(myHashes) {
2356
2669
  const revoked = [];
2357
2670
  for (const [keyId, entry] of this.keyDistributionCache.entries()) {
@@ -2439,7 +2752,7 @@ class BaseMindooTenantDirectory {
2439
2752
  // allow-rules until the trust cache refreshed (up to
2440
2753
  // DIRECTORY_SYNC_INTERVAL_MS later). The `node` is the time-`T` snapshot
2441
2754
  // whose `bySigningKey` only contains keys whose grant is active at `T`.
2442
- const grantedAtT = node.bySigningKey.has(signingKey);
2755
+ const grantedAtT = !!(0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey);
2443
2756
  // Resolve the author's username from their signing key. Trusted non-user
2444
2757
  // keys (e.g. server-to-server sync identities) have no grant; treat them as
2445
2758
  // an empty identity that still carries `$everyone`/`$author`. Keys that were
@@ -2650,7 +2963,19 @@ class BaseMindooTenantDirectory {
2650
2963
  const directoryDB = await this.getDirectoryDB();
2651
2964
  await directoryDB.syncStoreChanges();
2652
2965
  await this.updateUnifiedCache();
2653
- return this.userLookupCache.get(publicKey) ?? null;
2966
+ const exact = this.userLookupCache.get(publicKey);
2967
+ if (exact)
2968
+ return exact;
2969
+ // Auth/JWT/client transport may re-wrap PEM line endings; match the grant
2970
+ // entry whose signing key is the same key material.
2971
+ const needle = publicKey.replace(/\s+/g, "");
2972
+ if (!needle)
2973
+ return null;
2974
+ for (const [key, lookup] of this.userLookupCache) {
2975
+ if (key.replace(/\s+/g, "") === needle)
2976
+ return lookup;
2977
+ }
2978
+ return null;
2654
2979
  }
2655
2980
  /**
2656
2981
  * Resolve the full set of signing keys relevant to authenticating a device for
@@ -2881,7 +3206,7 @@ class BaseMindooTenantDirectory {
2881
3206
  const directoryDB = await this.getDirectoryDB();
2882
3207
  await directoryDB.syncStoreChanges();
2883
3208
  await this.updateUnifiedCache();
2884
- const known = new Set(["directory", "main"]);
3209
+ const known = new Set([types_1.DIRECTORY_DB_ID, types_1.USER_DIRECTORY_DB_ID]);
2885
3210
  for (const dbId of this.dbSettingsCache.keys()) {
2886
3211
  known.add(dbId);
2887
3212
  }
@@ -3282,6 +3607,9 @@ class BaseMindooTenantDirectory {
3282
3607
  details,
3283
3608
  identityHashes,
3284
3609
  identityHashesV,
3610
+ ...(pair.joinResponseWrapped
3611
+ ? { joinResponseWrapped: pair.joinResponseWrapped }
3612
+ : {}),
3285
3613
  }));
3286
3614
  }
3287
3615
  async encryptGroupMemberForTenant(plaintext) {