mindoodb 0.0.94 → 0.0.96

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 (197) hide show
  1. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -1
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +63 -4
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  5. package/dist/appendonlystores/network/HttpTransport.d.ts +10 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  7. package/dist/appendonlystores/network/HttpTransport.js +45 -3
  8. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  9. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +22 -0
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +33 -0
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  13. package/dist/browser/index.mjs +42 -0
  14. package/dist/core/BaseMindooDB.d.ts +9 -0
  15. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  16. package/dist/core/BaseMindooDB.js +75 -16
  17. package/dist/core/BaseMindooDB.js.map +1 -1
  18. package/dist/core/BaseMindooTenant.d.ts +82 -1
  19. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  20. package/dist/core/BaseMindooTenant.js +263 -7
  21. package/dist/core/BaseMindooTenant.js.map +1 -1
  22. package/dist/core/BaseMindooTenantDirectory.d.ts +182 -2
  23. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  24. package/dist/core/BaseMindooTenantDirectory.js +683 -113
  25. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  26. package/dist/core/BaseMindooTenantFactory.d.ts +30 -0
  27. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  28. package/dist/core/BaseMindooTenantFactory.js +95 -0
  29. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  30. package/dist/core/accesscontrol/DirectoryStateNode.d.ts +45 -0
  31. package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
  32. package/dist/core/accesscontrol/DirectoryStateNode.js +58 -0
  33. package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
  34. package/dist/core/accesscontrol/adminKeys.d.ts +130 -0
  35. package/dist/core/accesscontrol/adminKeys.d.ts.map +1 -0
  36. package/dist/core/accesscontrol/adminKeys.js +164 -0
  37. package/dist/core/accesscontrol/adminKeys.js.map +1 -0
  38. package/dist/core/accesscontrol/directoryAdminResolver.d.ts +54 -0
  39. package/dist/core/accesscontrol/directoryAdminResolver.d.ts.map +1 -0
  40. package/dist/core/accesscontrol/directoryAdminResolver.js +217 -0
  41. package/dist/core/accesscontrol/directoryAdminResolver.js.map +1 -0
  42. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  43. package/dist/core/accesscontrol/directoryProjection.js +25 -0
  44. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  45. package/dist/core/accesscontrol/grantKeys.d.ts +16 -0
  46. package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
  47. package/dist/core/accesscontrol/grantKeys.js +37 -0
  48. package/dist/core/accesscontrol/grantKeys.js.map +1 -1
  49. package/dist/core/accesscontrol/types.d.ts +11 -2
  50. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  51. package/dist/core/accesscontrol/types.js +7 -0
  52. package/dist/core/accesscontrol/types.js.map +1 -1
  53. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
  54. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  55. package/dist/core/appendonlystores/network/AuthenticationService.js +33 -0
  56. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  57. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +16 -1
  58. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  59. package/dist/core/appendonlystores/network/types.d.ts +13 -0
  60. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  61. package/dist/core/appendonlystores/network/types.js.map +1 -1
  62. package/dist/core/builtinDbInvariants.d.ts +18 -2
  63. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  64. package/dist/core/builtinDbInvariants.js +41 -5
  65. package/dist/core/builtinDbInvariants.js.map +1 -1
  66. package/dist/core/errors.d.ts +23 -0
  67. package/dist/core/errors.d.ts.map +1 -1
  68. package/dist/core/errors.js +34 -1
  69. package/dist/core/errors.js.map +1 -1
  70. package/dist/core/index.d.ts +7 -4
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/core/index.js +31 -5
  73. package/dist/core/index.js.map +1 -1
  74. package/dist/core/indexing/createViewDataProvider.d.ts +10 -1
  75. package/dist/core/indexing/createViewDataProvider.d.ts.map +1 -1
  76. package/dist/core/indexing/createViewDataProvider.js +11 -0
  77. package/dist/core/indexing/createViewDataProvider.js.map +1 -1
  78. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +13 -0
  79. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
  80. package/dist/core/indexing/summary/DocumentSummaryStore.js +46 -5
  81. package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
  82. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts +13 -0
  83. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts.map +1 -1
  84. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js +21 -0
  85. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js.map +1 -1
  86. package/dist/core/indexing/summary/index.d.ts +1 -1
  87. package/dist/core/indexing/summary/index.d.ts.map +1 -1
  88. package/dist/core/indexing/summary/index.js +2 -1
  89. package/dist/core/indexing/summary/index.js.map +1 -1
  90. package/dist/core/indexing/summary/types.d.ts +28 -0
  91. package/dist/core/indexing/summary/types.d.ts.map +1 -1
  92. package/dist/core/indexing/summary/types.js +22 -0
  93. package/dist/core/indexing/summary/types.js.map +1 -1
  94. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +19 -0
  95. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
  96. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +34 -3
  97. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  98. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +7 -0
  99. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -1
  100. package/dist/core/indexing/virtualviews/VirtualViewFactory.js +1 -0
  101. package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -1
  102. package/dist/core/live/LiveChannel.d.ts +41 -0
  103. package/dist/core/live/LiveChannel.d.ts.map +1 -0
  104. package/dist/core/live/LiveChannel.js +151 -0
  105. package/dist/core/live/LiveChannel.js.map +1 -0
  106. package/dist/core/query/executeQuery.d.ts.map +1 -1
  107. package/dist/core/query/executeQuery.js +30 -17
  108. package/dist/core/query/executeQuery.js.map +1 -1
  109. package/dist/core/query/queryView.d.ts +14 -0
  110. package/dist/core/query/queryView.d.ts.map +1 -1
  111. package/dist/core/query/queryView.js +22 -12
  112. package/dist/core/query/queryView.js.map +1 -1
  113. package/dist/core/query/types.d.ts +9 -0
  114. package/dist/core/query/types.d.ts.map +1 -1
  115. package/dist/core/query/types.js.map +1 -1
  116. package/dist/core/types.d.ts +193 -6
  117. package/dist/core/types.d.ts.map +1 -1
  118. package/dist/core/types.js.map +1 -1
  119. package/dist/core/uri/MindooURI.d.ts +1 -1
  120. package/dist/core/uri/MindooURI.d.ts.map +1 -1
  121. package/dist/core/uri/MindooURI.js +2 -0
  122. package/dist/core/uri/MindooURI.js.map +1 -1
  123. package/dist/core/uri/adminUri.d.ts +52 -0
  124. package/dist/core/uri/adminUri.d.ts.map +1 -0
  125. package/dist/core/uri/adminUri.js +100 -0
  126. package/dist/core/uri/adminUri.js.map +1 -0
  127. package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
  128. package/dist/core/uri/joinRequestUri.js +14 -1
  129. package/dist/core/uri/joinRequestUri.js.map +1 -1
  130. package/dist/core/userid/index.d.ts +9 -0
  131. package/dist/core/userid/index.d.ts.map +1 -1
  132. package/dist/core/userkeys/UserKeyDocument.d.ts +7 -1
  133. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -1
  134. package/dist/core/userkeys/UserKeyDocument.js +14 -3
  135. package/dist/core/userkeys/UserKeyDocument.js.map +1 -1
  136. package/dist/core/userkeys/UserKeyManager.d.ts +95 -4
  137. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -1
  138. package/dist/core/userkeys/UserKeyManager.js +606 -59
  139. package/dist/core/userkeys/UserKeyManager.js.map +1 -1
  140. package/dist/core/userkeys/deviceCertificate.d.ts +46 -0
  141. package/dist/core/userkeys/deviceCertificate.d.ts.map +1 -0
  142. package/dist/core/userkeys/deviceCertificate.js +86 -0
  143. package/dist/core/userkeys/deviceCertificate.js.map +1 -0
  144. package/dist/core/userkeys/generationSignature.d.ts +32 -0
  145. package/dist/core/userkeys/generationSignature.d.ts.map +1 -0
  146. package/dist/core/userkeys/generationSignature.js +136 -0
  147. package/dist/core/userkeys/generationSignature.js.map +1 -0
  148. package/dist/core/userkeys/generationValidity.d.ts +75 -0
  149. package/dist/core/userkeys/generationValidity.d.ts.map +1 -0
  150. package/dist/core/userkeys/generationValidity.js +175 -0
  151. package/dist/core/userkeys/generationValidity.js.map +1 -0
  152. package/dist/core/userkeys/index.d.ts +5 -2
  153. package/dist/core/userkeys/index.d.ts.map +1 -1
  154. package/dist/core/userkeys/index.js +21 -1
  155. package/dist/core/userkeys/index.js.map +1 -1
  156. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +16 -0
  157. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -1
  158. package/dist/core/userkeys/resolveUserKeyDocument.js +42 -1
  159. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -1
  160. package/dist/core/userkeys/types.d.ts +26 -0
  161. package/dist/core/userkeys/types.d.ts.map +1 -1
  162. package/dist/core/userkeys/types.js +3 -1
  163. package/dist/core/userkeys/types.js.map +1 -1
  164. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -1
  165. package/dist/core/userkeys/validateUserKeyDocument.js +7 -0
  166. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -1
  167. package/dist/core/utils/idGeneration.d.ts +15 -0
  168. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  169. package/dist/core/utils/idGeneration.js +45 -0
  170. package/dist/core/utils/idGeneration.js.map +1 -1
  171. package/dist/core/utils/index.d.ts +1 -1
  172. package/dist/core/utils/index.d.ts.map +1 -1
  173. package/dist/core/utils/index.js +3 -1
  174. package/dist/core/utils/index.js.map +1 -1
  175. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  176. package/dist/node/server/MindooDBServer.js +11 -0
  177. package/dist/node/server/MindooDBServer.js.map +1 -1
  178. package/dist/server/SyncEventBus.d.ts +22 -0
  179. package/dist/server/SyncEventBus.d.ts.map +1 -1
  180. package/dist/server/SyncEventBus.js +21 -0
  181. package/dist/server/SyncEventBus.js.map +1 -1
  182. package/dist/server/TenantManager.d.ts +9 -0
  183. package/dist/server/TenantManager.d.ts.map +1 -1
  184. package/dist/server/TenantManager.js +101 -1
  185. package/dist/server/TenantManager.js.map +1 -1
  186. package/dist/server/http/MindooDBServerCore.d.ts +39 -1
  187. package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
  188. package/dist/server/http/MindooDBServerCore.js +121 -2
  189. package/dist/server/http/MindooDBServerCore.js.map +1 -1
  190. package/dist/server/http/types.d.ts +10 -1
  191. package/dist/server/http/types.d.ts.map +1 -1
  192. package/dist/server/http/types.js.map +1 -1
  193. package/dist/server/index.d.ts +2 -2
  194. package/dist/server/index.d.ts.map +1 -1
  195. package/dist/server/index.js +2 -1
  196. package/dist/server/index.js.map +1 -1
  197. package/package.json +1 -1
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseMindooTenantDirectory = void 0;
4
4
  const types_1 = require("./types");
5
5
  const errors_1 = require("./errors");
6
+ const storeEntryTime_1 = require("./storeEntryTime");
7
+ const fingerprint_1 = require("./userkeys/fingerprint");
8
+ const deviceCertificate_1 = require("./userkeys/deviceCertificate");
6
9
  const timeSource_1 = require("./utils/timeSource");
7
10
  const singleFlight_1 = require("./utils/singleFlight");
8
11
  const logging_1 = require("./logging");
@@ -16,6 +19,8 @@ const types_3 = require("./accesscontrol/types");
16
19
  const evaluate_1 = require("./accesscontrol/evaluate");
17
20
  const RSAEncryption_1 = require("./crypto/RSAEncryption");
18
21
  const encryptedFields_1 = require("./crypto/encryptedFields");
22
+ const tenantSetup_1 = require("./tenantSetup");
23
+ const adminKeys_1 = require("./accesscontrol/adminKeys");
19
24
  const DIRECTORY_SYNC_INTERVAL_MS = 5 * 60 * 1000;
20
25
  const MAX_UNKNOWN_KEYS_TRACKED = 1000;
21
26
  /** De-duplicated union of two string lists, preserving first-seen order. */
@@ -87,6 +92,10 @@ class BaseMindooTenantDirectory {
87
92
  // Best-effort reverse lookup cache for public signing key -> user details.
88
93
  // Entries remain available even after revocation so historical UIs can still show identity labels.
89
94
  this.userLookupCache = new Map();
95
+ // Device certificates read from `userdirectory` (docs/userkeys.md §9.6),
96
+ // keyed by the certified device's normalized signing key.
97
+ this.deviceCertIndex = null;
98
+ this.buildingDeviceCertIndex = null;
90
99
  // Remote-wipe directive index (docs/accesscontrol.md §6.5): signing public key
91
100
  // targeted for wipe -> id of the admin-signed grant document carrying the
92
101
  // directive. Self-contained key values survive key removal, so this stays
@@ -242,10 +251,7 @@ class BaseMindooTenantDirectory {
242
251
  // Cast to BaseMindooTenant to access protected methods
243
252
  const baseTenant = this.tenant;
244
253
  // Create SigningKeyPair for the administration key
245
- const adminSigningKeyPair = {
246
- publicKey: baseTenant.getAdministrationPublicKey(),
247
- privateKey: administrationPrivateKey,
248
- };
254
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
249
255
  // Compute username hash (salted v2 for new documents) and encrypted user
250
256
  // details before creating the document.
251
257
  const usernameHash = await this.hashUsernameForWrite(userId.username);
@@ -329,8 +335,7 @@ class BaseMindooTenantDirectory {
329
335
  ];
330
336
  if (filtered.length === 0)
331
337
  return;
332
- const baseTenant = this.tenant;
333
- const preparedBy = baseTenant.getAdministrationPublicKey();
338
+ const preparedBy = (await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword)).publicKey;
334
339
  for (const keyId of filtered) {
335
340
  const keyVersions = await this.getKeyVersionManifest(keyId);
336
341
  if (keyVersions.length === 0) {
@@ -530,6 +535,21 @@ class BaseMindooTenantDirectory {
530
535
  return additionalResult;
531
536
  }
532
537
  }
538
+ // A delegated admin key (docs/accesscontrol.md §6.13) holds no grant; it is
539
+ // trusted while its admin window covers now. Checked before the poll below:
540
+ // the admin-key resolver reads the store directly, while the poll may wait
541
+ // for a directory cache pass that is itself verifying this key's entries.
542
+ if (this.trustedKeysCache.get(publicKey) !== true) {
543
+ if (await this.isAdminKeyNow(publicKey)) {
544
+ this.logger.debug(`Public key is an active delegated admin key, trusted`);
545
+ return true;
546
+ }
547
+ // An ended delegated admin key holds no grant either: nothing to poll for.
548
+ if (await this.tenant.isKnownDelegatedAdminKey(publicKey)) {
549
+ this.logger.debug(`Public key is a delegated admin key outside its window, not trusted now`);
550
+ return false;
551
+ }
552
+ }
533
553
  // Store poll: discovers entries this process has not seen yet, so it is
534
554
  // throttled to once per interval — unless the caller forces it (audit #4):
535
555
  // the server push path must observe a freshly pushed revocation
@@ -562,10 +582,252 @@ class BaseMindooTenantDirectory {
562
582
  this.logger.debug(`Public key validation result (from cache): ${cachedResult}`);
563
583
  return cachedResult;
564
584
  }
585
+ // Not on any grant: it may be a device the person certified (§9.6 of userkeys.md).
586
+ if (await this.resolveSignerToGrant(publicKey, Number.MAX_SAFE_INTEGER)) {
587
+ return true;
588
+ }
565
589
  // Key not found in cache means it was never granted access
566
590
  this.logger.debug(`Public key not found in cache, returning false`);
567
591
  return false;
568
592
  }
593
+ // ==================== Delegated administrators (§6.13) ====================
594
+ /** The signing key pair for an admin private key: the root's, or a delegated admin's. */
595
+ async adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword) {
596
+ const pair = await this.tenant.resolveAdminSigningKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
597
+ if (!(0, DirectoryStateNode_1.signingKeysEqual)(pair.publicKey, this.getRootAdminPublicKey()) &&
598
+ !(await this.isAdminKeyNow(pair.publicKey))) {
599
+ throw new Error("Signing key pair mismatch: the private key belongs to neither the root admin key nor an active delegated admin of this tenant");
600
+ }
601
+ return pair;
602
+ }
603
+ /** Like {@link adminKeyPair}, but refuses anything but the root admin key. */
604
+ async rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, action) {
605
+ const pair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
606
+ if (!(0, DirectoryStateNode_1.signingKeysEqual)(pair.publicKey, this.tenant.getAdministrationPublicKey())) {
607
+ throw new Error(`Only the root admin key may ${action}`);
608
+ }
609
+ return pair;
610
+ }
611
+ /**
612
+ * What the trust cache already knows about `publicKey` (true: an active
613
+ * grant key), without polling. Lets entry verification try the store-level
614
+ * admin check first for keys that are not grant keys (§6.13).
615
+ */
616
+ peekTrustedKey(publicKey) {
617
+ return this.trustedKeysCache.get(publicKey);
618
+ }
619
+ /** The tenant's root admin key (the fixed trust anchor). */
620
+ getRootAdminPublicKey() {
621
+ return this.tenant.getAdministrationPublicKey();
622
+ }
623
+ /**
624
+ * Live admin check (auth, read gate, push check): root, or a delegated admin
625
+ * key whose window covers now. Reads the root-signed admin-key documents from
626
+ * the directory store, so it is safe on the directory's own load path.
627
+ */
628
+ async isAdminKeyNow(signingKey) {
629
+ return this.tenant.isAdminSigningKeyAt(signingKey, (0, timeSource_1.semanticNow)());
630
+ }
631
+ /**
632
+ * Whether `signingKey` is a tenant admin at trusted time `T` (§6.13): the
633
+ * root always; a delegated admin key while the directory state at `T` holds
634
+ * its admin-key document with `validFrom ≤ T < validUntil`. Pass
635
+ * `Number.MAX_SAFE_INTEGER` for the directory head judged at "now".
636
+ */
637
+ async isAdminAt(signingKey, T) {
638
+ const root = this.getRootAdminPublicKey();
639
+ if ((0, DirectoryStateNode_1.signingKeysEqual)(signingKey, root))
640
+ return true;
641
+ const node = await this.getDirectoryStateAt(T);
642
+ return (0, DirectoryStateNode_1.isAdminInNode)(node, root, signingKey, (0, adminKeys_1.adminDecisionTime)(T, (0, timeSource_1.semanticNow)()));
643
+ }
644
+ /**
645
+ * Like {@link isAdminAt}, but read from the root-signed admin-key documents in
646
+ * the directory store rather than the time-travel chain (§6.13). This is the
647
+ * form the sync server uses for push and auth decisions: it never waits for a
648
+ * chain rebuild and is safe to call while the directory itself loads.
649
+ */
650
+ async isAdminSigningKeyAt(signingKey, T) {
651
+ return this.tenant.isAdminSigningKeyAt(signingKey, (0, adminKeys_1.adminDecisionTime)(T, (0, timeSource_1.semanticNow)()));
652
+ }
653
+ /**
654
+ * The delegated admin key `signingKey` as valid at `T` (its encryption key and
655
+ * window), or null when it is not a delegated admin then. Store-level like
656
+ * {@link isAdminSigningKeyAt}; the sync server uses it to let an admin key
657
+ * authenticate without a grant.
658
+ */
659
+ async getDelegatedAdminKeyAt(signingKey, T) {
660
+ const tenant = this.tenant;
661
+ if (!(await tenant.isAdminSigningKeyAt(signingKey, (0, adminKeys_1.adminDecisionTime)(T, (0, timeSource_1.semanticNow)()))))
662
+ return null;
663
+ const directoryDB = await this.getDirectoryDB();
664
+ return tenant
665
+ .getDirectoryAdminResolver(directoryDB.getStore())
666
+ .delegatedAdminKeyAt(signingKey, (0, adminKeys_1.adminDecisionTime)(T, (0, timeSource_1.semanticNow)()));
667
+ }
668
+ /**
669
+ * The delegated admin keys at the directory head, with their windows, names
670
+ * (when this replica holds `default`) and status at "now". `pending` is true
671
+ * while the latest revision of a key's document carries no witness receipt
672
+ * yet: other replicas do not see it until a trusted witness stamped it.
673
+ */
674
+ async listAdminKeys() {
675
+ return this.adminKeysFromNode(await this.getDirectoryStateHead(), (0, timeSource_1.semanticNow)(), true);
676
+ }
677
+ /** The delegated admin keys as the directory knew them at trusted time `T`. */
678
+ async getAdminKeysAt(T) {
679
+ return this.adminKeysFromNode(await this.getDirectoryStateAt(T), (0, adminKeys_1.adminDecisionTime)(T, (0, timeSource_1.semanticNow)()), false);
680
+ }
681
+ async adminKeysFromNode(node, at, withPending) {
682
+ const directoryDB = await this.getDirectoryDB();
683
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
684
+ const out = [];
685
+ for (const snapshot of node.adminKeys?.values() ?? []) {
686
+ const docId = await (0, adminKeys_1.aclAdminKeyDocIdForKey)(snapshot.signingPublicKey, subtle);
687
+ let name;
688
+ try {
689
+ const doc = await directoryDB.getDocument(docId);
690
+ const decrypted = await (0, encryptedFields_1.decryptEncryptedField)(this.tenant, doc.getData(), "name_encrypted");
691
+ if (typeof decrypted === "string" && decrypted.trim())
692
+ name = decrypted.trim();
693
+ }
694
+ catch {
695
+ // No `default` key here, or the document is not materialized.
696
+ }
697
+ let pending = false;
698
+ if (withPending) {
699
+ try {
700
+ const entries = await directoryDB.getStore().findNewEntriesForDoc([], docId);
701
+ const latest = entries
702
+ .filter((e) => (0, DirectoryStateNode_1.signingKeysEqual)(e.createdByPublicKey, this.getRootAdminPublicKey()))
703
+ .sort((a, b) => a.createdAt - b.createdAt)
704
+ .pop();
705
+ pending = !!latest && latest.receivedAt === undefined;
706
+ }
707
+ catch {
708
+ pending = false;
709
+ }
710
+ }
711
+ out.push({
712
+ ...snapshot,
713
+ ...(name ? { name } : {}),
714
+ status: (0, adminKeys_1.adminKeyStatusAt)(snapshot, at),
715
+ active: (0, adminKeys_1.isAdminKeySnapshotValidAt)(snapshot, at),
716
+ pending,
717
+ });
718
+ }
719
+ return out.sort((a, b) => a.validFrom - b.validFrom);
720
+ }
721
+ /**
722
+ * Make `signingPublicKey` a delegated admin (§6.13). Root only. Writes the
723
+ * `acl_adminkey_<fingerprint>` document (under `$publicinfos`) and records the
724
+ * admin in `tenantsetup.administrators` for display. Adding a key whose
725
+ * document already exists starts a new window (reactivation): entries signed
726
+ * between the old end and the new start stay invalid.
727
+ *
728
+ * The key becomes effective at the witness time of this write (or at
729
+ * `validFrom`, if later). Other replicas honor it once a trusted witness has
730
+ * stamped the document.
731
+ */
732
+ async addAdminKey(admin, administrationPrivateKey, administrationPrivateKeyPassword) {
733
+ const rootPair = await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "add administrators");
734
+ const name = admin.name.trim();
735
+ if (!name)
736
+ throw new Error("addAdminKey: name is required");
737
+ if (!admin.signingPublicKey.trim() || !admin.encryptionPublicKey.trim()) {
738
+ throw new Error("addAdminKey: signingPublicKey and encryptionPublicKey are required");
739
+ }
740
+ if ((0, DirectoryStateNode_1.signingKeysEqual)(admin.signingPublicKey, rootPair.publicKey)) {
741
+ throw new Error("addAdminKey: the root admin key is always an administrator");
742
+ }
743
+ const directoryDB = await this.getDirectoryDB();
744
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
745
+ const docId = await (0, adminKeys_1.aclAdminKeyDocIdForKey)(admin.signingPublicKey, subtle);
746
+ const nameEncrypted = await (0, encryptedFields_1.encryptEncryptedField)(this.tenant, name);
747
+ const label = admin.label?.trim();
748
+ const doc = await this.getOrCreateAclDoc(directoryDB, docId, rootPair, administrationPrivateKeyPassword);
749
+ await directoryDB.changeDoc(doc, async (d) => {
750
+ const data = d.getData();
751
+ data.form = types_3.ACCESS_CONTROL_FORM;
752
+ data.type = adminKeys_1.ADMIN_KEY_TYPE;
753
+ data.signingPublicKey = admin.signingPublicKey;
754
+ data.encryptionPublicKey = admin.encryptionPublicKey;
755
+ data.name_encrypted = nameEncrypted;
756
+ data.name_encrypted_key = types_1.DEFAULT_TENANT_KEY_ID;
757
+ if (label)
758
+ data.label = label;
759
+ else
760
+ delete data.label;
761
+ if (typeof admin.validFrom === "number" && Number.isFinite(admin.validFrom)) {
762
+ data.validFrom = admin.validFrom;
763
+ }
764
+ else {
765
+ delete data.validFrom;
766
+ }
767
+ delete data.validUntil;
768
+ }, { signingKeyPair: rootPair, signingKeyPassword: administrationPrivateKeyPassword });
769
+ try {
770
+ await (0, tenantSetup_1.ensureTenantSetupAdministrator)(directoryDB, {
771
+ username: name,
772
+ keyPairs: [
773
+ {
774
+ signingPublicKey: admin.signingPublicKey,
775
+ encryptionPublicKey: admin.encryptionPublicKey,
776
+ ...(label ? { label } : {}),
777
+ addedAt: (0, timeSource_1.semanticNow)(),
778
+ },
779
+ ],
780
+ }, rootPair, administrationPrivateKeyPassword, this.tenant);
781
+ }
782
+ catch (error) {
783
+ this.logger.warn(`addAdminKey: recording the administrator in tenantsetup failed: ${error}`);
784
+ }
785
+ await this.updateUnifiedCache();
786
+ }
787
+ /**
788
+ * Change the window of a delegated admin key (§6.13). Root only.
789
+ *
790
+ * - `validUntil: <ms>` deactivates the key as of that witness time; a future
791
+ * date schedules the end. A date before the witness time of this write is
792
+ * raised to it — deactivation never reaches back.
793
+ * - `validUntil: null` removes the end. On a key that has already ended this
794
+ * reactivates it from the witness time of this write on.
795
+ * - `validFrom: <ms>` / `null` sets or clears a later start.
796
+ */
797
+ async setAdminKeyValidity(signingPublicKey, validity, administrationPrivateKey, administrationPrivateKeyPassword) {
798
+ const rootPair = await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "change administrators");
799
+ const directoryDB = await this.getDirectoryDB();
800
+ const docId = await (0, adminKeys_1.aclAdminKeyDocIdForKey)(signingPublicKey, this.tenant.getCryptoAdapter().getSubtle());
801
+ let doc;
802
+ try {
803
+ doc = await directoryDB.getDocument(docId);
804
+ }
805
+ catch {
806
+ throw new Error("setAdminKeyValidity: this key is not a delegated administrator");
807
+ }
808
+ const ended = (() => {
809
+ const data = doc.getData();
810
+ return typeof data.validUntil === "number" && data.validUntil <= (0, timeSource_1.semanticNow)();
811
+ })();
812
+ await directoryDB.changeDoc(doc, async (d) => {
813
+ const data = d.getData();
814
+ if (validity.validUntil === null)
815
+ delete data.validUntil;
816
+ else if (typeof validity.validUntil === "number" && Number.isFinite(validity.validUntil)) {
817
+ data.validUntil = validity.validUntil;
818
+ }
819
+ if (validity.validFrom === null)
820
+ delete data.validFrom;
821
+ else if (typeof validity.validFrom === "number" && Number.isFinite(validity.validFrom)) {
822
+ data.validFrom = validity.validFrom;
823
+ }
824
+ else if (validity.validUntil === null && ended) {
825
+ // Reactivation: a new window starting at this revision's witness time.
826
+ delete data.validFrom;
827
+ }
828
+ }, { signingKeyPair: rootPair, signingKeyPassword: administrationPrivateKeyPassword });
829
+ await this.updateUnifiedCache();
830
+ }
569
831
  /**
570
832
  * Whether a cache miss for `publicKey` may poll the store. A key that missed
571
833
  * within the last DIRECTORY_SYNC_INTERVAL_MS does not poll again; it still
@@ -1093,6 +1355,7 @@ class BaseMindooTenantDirectory {
1093
1355
  const node = await this.getDirectoryStateAt(input.trustedTime);
1094
1356
  const identity = await this.identitySetForSigningKey(input.signingKey, node, {
1095
1357
  isAuthor: input.op === "doc_create" || !!input.isAuthor,
1358
+ trustedTime: input.trustedTime,
1096
1359
  });
1097
1360
  return (0, evaluate_1.evaluateAccess)({
1098
1361
  op: input.op,
@@ -1112,6 +1375,7 @@ class BaseMindooTenantDirectory {
1112
1375
  const node = await this.getDirectoryStateAt(input.trustedTime);
1113
1376
  const identity = await this.identitySetForSigningKey(input.signingKey, node, {
1114
1377
  isAuthor: input.op === "doc_create" || input.isAuthor,
1378
+ trustedTime: input.trustedTime,
1115
1379
  });
1116
1380
  return (0, evaluate_1.evaluateAccess)({
1117
1381
  op: input.op,
@@ -1132,15 +1396,283 @@ class BaseMindooTenantDirectory {
1132
1396
  const signerNode = input.signerTrustedTime === input.creatorTrustedTime
1133
1397
  ? creatorNode
1134
1398
  : await this.getDirectoryStateAt(input.signerTrustedTime);
1135
- return (0, DirectoryStateNode_1.isSamePerson)(creatorNode, signerNode, input.creatorSigningKey, input.signerSigningKey);
1399
+ if ((0, DirectoryStateNode_1.isSamePerson)(creatorNode, signerNode, input.creatorSigningKey, input.signerSigningKey)) {
1400
+ return true;
1401
+ }
1402
+ // Certified devices (userkeys.md §9.6): compare the persons they act for.
1403
+ // The creator is looked up without revocation, like a retired grant device.
1404
+ const creatorHash = (0, DirectoryStateNode_1.grantAtOrBefore)(creatorNode, input.creatorSigningKey)?.usernameHash ??
1405
+ (await this.certifiedGrantInNode(creatorNode, input.creatorSigningKey, input.creatorTrustedTime, {
1406
+ enforceRevocation: false,
1407
+ }))?.grant.usernameHash;
1408
+ const signerHash = (await this.resolveSignerInNode(signerNode, input.signerSigningKey, input.signerTrustedTime))?.grant.usernameHash;
1409
+ return !!creatorHash && creatorHash === signerHash;
1136
1410
  }
1137
1411
  /**
1138
1412
  * The `username_hash` of the active grant that contained `signingKey` at
1139
1413
  * trusted time `T`, or `null` when the key was not a granted device then.
1140
1414
  */
1141
1415
  async resolveUsernameHashForSigningKey(signingKey, trustedTime) {
1416
+ return (await this.resolveSignerToGrant(signingKey, trustedTime))?.grant.usernameHash ?? null;
1417
+ }
1418
+ /**
1419
+ * The grant a signing key acts under at trusted time `T`
1420
+ * (docs/userkeys.md §9.6): a device listed on the grant, or a device with a
1421
+ * valid certificate. A certificate counts when
1422
+ *
1423
+ * - it verifies with the self-grant key that the person's grant names at `T`,
1424
+ * - its `doc_create` was accepted (trusted time) at or before `T`, and
1425
+ * before the delegation's `revokedAt`,
1426
+ * - the person did not delete it at or before `T`, and
1427
+ * - the device is not in the grant's `revokedUserKeyPairs` at `T`.
1428
+ *
1429
+ * The one place that answers "which person is this key"; server auth, ACL
1430
+ * evaluation, ownership and attribution go through it.
1431
+ */
1432
+ async resolveSignerToGrant(signingKey, trustedTime) {
1142
1433
  const node = await this.getDirectoryStateAt(trustedTime);
1143
- return (0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey)?.usernameHash ?? null;
1434
+ return this.resolveSignerInNode(node, signingKey, trustedTime);
1435
+ }
1436
+ async resolveSignerInNode(node, signingKey, trustedTime) {
1437
+ const listed = (0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey);
1438
+ if (listed)
1439
+ return { grant: listed, via: "grant" };
1440
+ const certified = await this.certifiedGrantInNode(node, signingKey, trustedTime, {
1441
+ enforceRevocation: true,
1442
+ });
1443
+ return certified ? { grant: certified.grant, via: "certificate", certificate: certified.cert } : null;
1444
+ }
1445
+ /** Device certificates valid for `signingKey` in `node` (see {@link resolveSignerToGrant}). */
1446
+ async certifiedGrantInNode(node, signingKey, trustedTime, options) {
1447
+ if (!signingKey)
1448
+ return null;
1449
+ const records = (await this.loadDeviceCertificateIndex()).get((0, fingerprint_1.normalizePublicKeyPem)(signingKey));
1450
+ if (!records?.length)
1451
+ return null;
1452
+ const now = Date.now();
1453
+ for (const record of records) {
1454
+ const grant = node.usersByHash.get(record.cert.username_hash);
1455
+ if (!grant)
1456
+ continue;
1457
+ const delegation = grant.deviceSelfGrant;
1458
+ if (!delegation ||
1459
+ (0, fingerprint_1.normalizePublicKeyPem)(delegation.signingPublicKey) !==
1460
+ (0, fingerprint_1.normalizePublicKeyPem)(record.cert.selfGrantPublicKey)) {
1461
+ continue;
1462
+ }
1463
+ const issuedAt = (0, storeEntryTime_1.entryTrustedTime)(record.createMeta, now);
1464
+ if (delegation.revokedAt !== undefined && issuedAt >= delegation.revokedAt)
1465
+ continue;
1466
+ if (options.enforceRevocation) {
1467
+ if (!grant.active)
1468
+ continue;
1469
+ if (issuedAt > trustedTime)
1470
+ continue;
1471
+ const deletedAt = record.deleteMetas.length
1472
+ ? Math.min(...record.deleteMetas.map((m) => (0, storeEntryTime_1.entryTrustedTime)(m, now)))
1473
+ : undefined;
1474
+ if (deletedAt !== undefined && deletedAt <= trustedTime)
1475
+ continue;
1476
+ if ((grant.revokedSigningKeys ?? []).some((key) => (0, DirectoryStateNode_1.signingKeysEqual)(key, signingKey)))
1477
+ continue;
1478
+ }
1479
+ return { grant, cert: record.cert };
1480
+ }
1481
+ return null;
1482
+ }
1483
+ /**
1484
+ * Every certificate of the person with `usernameHash` that their current
1485
+ * self-grant key signed, including revoked devices (for revoke and wipe).
1486
+ */
1487
+ async certificatesOfPerson(usernameHash) {
1488
+ const head = await this.getDirectoryStateHead();
1489
+ const result = [];
1490
+ for (const records of (await this.loadDeviceCertificateIndex()).values()) {
1491
+ const record = records.find((r) => r.cert.username_hash === usernameHash);
1492
+ if (!record)
1493
+ continue;
1494
+ const hit = await this.certifiedGrantInNode(head, record.cert.deviceSigningPublicKey, Number.MAX_SAFE_INTEGER, {
1495
+ enforceRevocation: false,
1496
+ });
1497
+ if (hit?.grant.usernameHash === usernameHash)
1498
+ result.push(hit.cert);
1499
+ }
1500
+ return result;
1501
+ }
1502
+ /**
1503
+ * Certified devices of the person with `usernameHash` that count at the head
1504
+ * state, with their certificates.
1505
+ */
1506
+ async listCertifiedDevices(usernameHash) {
1507
+ const head = await this.getDirectoryStateHead();
1508
+ const index = await this.loadDeviceCertificateIndex();
1509
+ const result = [];
1510
+ const seen = new Set();
1511
+ for (const [key, records] of index) {
1512
+ if (!records.some((r) => r.cert.username_hash === usernameHash))
1513
+ continue;
1514
+ const hit = await this.certifiedGrantInNode(head, records[0].cert.deviceSigningPublicKey, Number.MAX_SAFE_INTEGER, {
1515
+ enforceRevocation: true,
1516
+ });
1517
+ if (hit && hit.grant.usernameHash === usernameHash && !seen.has(key)) {
1518
+ seen.add(key);
1519
+ result.push(hit.cert);
1520
+ }
1521
+ }
1522
+ return result;
1523
+ }
1524
+ /**
1525
+ * Every device certificate in `userdirectory`, with the trusted time its
1526
+ * `doc_create` was accepted (`issuedAt`) and, when the person withdrew it,
1527
+ * the trusted time of the first delete (`deletedAt`) (docs/userkeys.md §9.6).
1528
+ *
1529
+ * Unlike {@link listCertifiedDevices} nothing is filtered: revoked devices,
1530
+ * withdrawn certificates and certificates of revoked delegations are listed
1531
+ * too, so audit views can work out which certified devices a person had at
1532
+ * any point in time. Whether a certificate is valid at `T` is still decided
1533
+ * by {@link resolveSignerToGrant}.
1534
+ */
1535
+ async listDeviceCertificates() {
1536
+ const index = await this.loadDeviceCertificateIndex();
1537
+ const now = Date.now();
1538
+ const result = [];
1539
+ for (const records of index.values()) {
1540
+ for (const record of records) {
1541
+ const deletedAt = record.deleteMetas.length
1542
+ ? Math.min(...record.deleteMetas.map((meta) => (0, storeEntryTime_1.entryTrustedTime)(meta, now)))
1543
+ : undefined;
1544
+ result.push({
1545
+ certificate: record.cert,
1546
+ issuedAt: (0, storeEntryTime_1.entryTrustedTime)(record.createMeta, now),
1547
+ ...(deletedAt !== undefined ? { deletedAt } : {}),
1548
+ });
1549
+ }
1550
+ }
1551
+ return result.sort((a, b) => a.issuedAt - b.issuedAt);
1552
+ }
1553
+ /**
1554
+ * Read every `dcert_*` certificate from the `userdirectory` store without
1555
+ * materializing that database (whose own trust checks call back into this
1556
+ * index). Only `doc_create` entries carry a certificate; deletes withdraw
1557
+ * it. Rebuilt when the store head moves, at most every two seconds for
1558
+ * stores that cannot report a head.
1559
+ */
1560
+ async loadDeviceCertificateIndex() {
1561
+ if (this.buildingDeviceCertIndex) {
1562
+ // Re-entry while building: answer from what is known.
1563
+ return this.deviceCertIndex?.byKey ?? new Map();
1564
+ }
1565
+ const baseTenant = this.tenant;
1566
+ if (typeof baseTenant.getDocStoreForDb !== "function")
1567
+ return new Map();
1568
+ let store;
1569
+ try {
1570
+ store = baseTenant.getDocStoreForDb(types_1.USER_DIRECTORY_DB_ID);
1571
+ }
1572
+ catch {
1573
+ return new Map();
1574
+ }
1575
+ const head = await store.getStoreHead?.().catch(() => undefined);
1576
+ const stamp = head ? `${head.epoch}:${head.maxReceiptOrder}` : null;
1577
+ const cached = this.deviceCertIndex;
1578
+ if (cached && stamp !== null && cached.stamp === stamp)
1579
+ return cached.byKey;
1580
+ if (cached && stamp === null && Date.now() - cached.at < 2000)
1581
+ return cached.byKey;
1582
+ const build = (async () => {
1583
+ const byKey = new Map();
1584
+ const isCert = (m) => m.docId.startsWith(deviceCertificate_1.DEVICE_CERT_DOC_ID_PREFIX);
1585
+ const creates = (await store.findEntries("doc_create", null, null)).filter(isCert);
1586
+ if (creates.length === 0)
1587
+ return byKey;
1588
+ const deletes = (await store.findEntries("doc_delete", null, null)).filter(isCert);
1589
+ const entries = await store.getEntries(creates.map((m) => m.id));
1590
+ const crypto = this.tenant.getCryptoAdapter();
1591
+ for (const entry of entries) {
1592
+ try {
1593
+ const plaintext = await baseTenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
1594
+ const cert = (0, deviceCertificate_1.asDeviceCertificate)((0, builtinDbInvariants_1.recordFromCreateChangeBytes)(plaintext));
1595
+ if (!cert || !(await (0, deviceCertificate_1.verifyDeviceCertificate)(crypto, cert)))
1596
+ continue;
1597
+ const createMeta = creates.find((m) => m.id === entry.id) ?? entry;
1598
+ const key = (0, fingerprint_1.normalizePublicKeyPem)(cert.deviceSigningPublicKey);
1599
+ const list = byKey.get(key) ?? [];
1600
+ list.push({
1601
+ cert,
1602
+ docId: entry.docId,
1603
+ createMeta,
1604
+ deleteMetas: deletes.filter((m) => m.docId === entry.docId),
1605
+ });
1606
+ byKey.set(key, list);
1607
+ }
1608
+ catch {
1609
+ // Not readable here (no $publicinfos) or not a certificate.
1610
+ }
1611
+ }
1612
+ return byKey;
1613
+ })();
1614
+ this.buildingDeviceCertIndex = build;
1615
+ try {
1616
+ const byKey = await build;
1617
+ this.deviceCertIndex = { stamp, at: Date.now(), byKey };
1618
+ return byKey;
1619
+ }
1620
+ finally {
1621
+ this.buildingDeviceCertIndex = null;
1622
+ }
1623
+ }
1624
+ /**
1625
+ * The person's device self-grant delegation at the head state
1626
+ * (docs/userkeys.md §9), or `null` when the admin never granted it.
1627
+ */
1628
+ async getDeviceSelfGrantForUsernameHash(usernameHash) {
1629
+ const head = await this.getDirectoryStateHead();
1630
+ return head.usersByHash.get(usernameHash)?.deviceSelfGrant ?? null;
1631
+ }
1632
+ /** {@link getDeviceSelfGrantForUsernameHash} by username (newest grant wins). */
1633
+ async getDeviceSelfGrant(username) {
1634
+ const grants = await this.findGrantAccessDocuments(username);
1635
+ return (0, grantKeys_1.newestDeviceSelfGrant)(grants.map((g) => (0, grantKeys_1.extractDeviceSelfGrant)(g.getData())));
1636
+ }
1637
+ /**
1638
+ * Grant or revoke the person's right to certify their own devices
1639
+ * (docs/userkeys.md §9). `signingPublicKey` is the public half of their
1640
+ * device self-grant key (from the join request, or
1641
+ * `tenant.getDeviceSelfGrantPublicKey()` on one of their devices). `null`
1642
+ * revokes: the key stays on the grant with `revokedAt`, so certificates and
1643
+ * generations signed before stay valid; certificates issued later do not.
1644
+ */
1645
+ async setDeviceSelfGrant(username, signingPublicKey, administrationPrivateKey, administrationPrivateKeyPassword) {
1646
+ await this.updateUserGrant(username, { deviceSelfGrant: signingPublicKey }, administrationPrivateKey, administrationPrivateKeyPassword);
1647
+ }
1648
+ /**
1649
+ * Trusted time from which new User-Key generations must be signed
1650
+ * (`requireUserKeyGenerationSignatureSince`, docs/userkeys.md §9.3), or
1651
+ * `undefined`. Read from the `acl_defaultpolicy` head regardless of the
1652
+ * master switch, like {@link getRequireMetadataSignatureSince}.
1653
+ */
1654
+ async getRequireUserKeyGenerationSignatureSince() {
1655
+ const head = await this.getDirectoryStateHead();
1656
+ const since = head.defaultPolicy?.requireUserKeyGenerationSignatureSince;
1657
+ return typeof since === "number" && Number.isFinite(since) ? since : undefined;
1658
+ }
1659
+ /**
1660
+ * Require signed User-Key generations from now on (docs/userkeys.md §9.3).
1661
+ * Idempotent: an existing cutoff is kept. Run it once every device of the
1662
+ * tenant runs a client that signs generations; unsigned generations
1663
+ * introduced before stay valid.
1664
+ */
1665
+ async requireUserKeyGenerationSignatures(administrationPrivateKey, administrationPrivateKeyPassword) {
1666
+ const head = await this.getDirectoryStateHead();
1667
+ const existing = head.defaultPolicy?.requireUserKeyGenerationSignatureSince;
1668
+ if (typeof existing === "number" && Number.isFinite(existing)) {
1669
+ return { since: existing, changed: false };
1670
+ }
1671
+ const since = (0, timeSource_1.semanticNow)();
1672
+ await this.setDefaultAccessPolicy(head.defaultPolicy
1673
+ ? { requireUserKeyGenerationSignatureSince: since }
1674
+ : { disableAllAccessChecksAndPolicies: true, requireUserKeyGenerationSignatureSince: since }, administrationPrivateKey, administrationPrivateKeyPassword);
1675
+ return { since, changed: true };
1144
1676
  }
1145
1677
  /**
1146
1678
  * Whether the active policy has any Tier 2 (`withfields`) content rule for
@@ -1242,8 +1774,7 @@ class BaseMindooTenantDirectory {
1242
1774
  */
1243
1775
  async canUseLlm() {
1244
1776
  const currentUser = await this.tenant.getCurrentUserId();
1245
- const adminKey = this.tenant.getAdministrationPublicKey();
1246
- if (currentUser.userSigningPublicKey === adminKey) {
1777
+ if (await this.isAdminKeyNow(currentUser.userSigningPublicKey)) {
1247
1778
  return { allowed: true, reason: "tenant admin", tier: "tier1" };
1248
1779
  }
1249
1780
  // Head selector, NOT Date.now() — see canDo.
@@ -1310,8 +1841,7 @@ class BaseMindooTenantDirectory {
1310
1841
  if (allowedDbIds.includes(dbid))
1311
1842
  return true;
1312
1843
  if (opts?.signingKey !== undefined) {
1313
- const adminKey = this.tenant.getAdministrationPublicKey();
1314
- if (opts.signingKey === adminKey)
1844
+ if (await this.isAdminKeyNow(opts.signingKey))
1315
1845
  return true;
1316
1846
  }
1317
1847
  return false;
@@ -1335,9 +1865,8 @@ class BaseMindooTenantDirectory {
1335
1865
  // policy that defines the read gate can be read at all).
1336
1866
  if (input.dbid === types_1.DIRECTORY_DB_ID || input.dbid === types_1.USER_DIRECTORY_DB_ID)
1337
1867
  return true;
1338
- // The tenant admin is exempt from the read gate.
1339
- const adminKey = this.tenant.getAdministrationPublicKey();
1340
- if (input.signingKey === adminKey)
1868
+ // Tenant admins (root and active delegated admins, §6.13) are exempt from the read gate.
1869
+ if (await this.isAdminKeyNow(input.signingKey))
1341
1870
  return true;
1342
1871
  // Gate 2 — per-user/group database read gate (`doc_read`, §6.6). Evaluated
1343
1872
  // server-side (Tier 1) from the principal's signing key at acceptance time.
@@ -1363,8 +1892,7 @@ class BaseMindooTenantDirectory {
1363
1892
  async evaluateReadScopeForSigningKey(input) {
1364
1893
  if (input.dbid === types_1.DIRECTORY_DB_ID || input.dbid === types_1.USER_DIRECTORY_DB_ID)
1365
1894
  return "all";
1366
- const adminKey = this.tenant.getAdministrationPublicKey();
1367
- if (input.signingKey === adminKey)
1895
+ if (await this.isAdminKeyNow(input.signingKey))
1368
1896
  return "all";
1369
1897
  const decision = await this.evaluateAccessForSigningKey({
1370
1898
  op: "doc_read_all",
@@ -1397,7 +1925,7 @@ class BaseMindooTenantDirectory {
1397
1925
  if (personal) {
1398
1926
  if (personal.mode === "none")
1399
1927
  return personal;
1400
- if ((0, DirectoryStateNode_1.signingKeysEqual)(input.signingKey, this.tenant.getAdministrationPublicKey())) {
1928
+ if (await this.isAdminKeyNow(input.signingKey)) {
1401
1929
  return personal;
1402
1930
  }
1403
1931
  if (personal.mode === "read") {
@@ -1453,7 +1981,7 @@ class BaseMindooTenantDirectory {
1453
1981
  const owner = (0, builtinDbInvariants_1.personalDatabaseOwner)(input.dbid, head.defaultPolicy?.personalDatabasePrefixes ?? []);
1454
1982
  if (!owner)
1455
1983
  return null;
1456
- const isAdmin = (0, DirectoryStateNode_1.signingKeysEqual)(input.signingKey, this.tenant.getAdministrationPublicKey());
1984
+ const isAdmin = await this.isAdminKeyNow(input.signingKey);
1457
1985
  if (isAdmin && owner.prefix !== "mbusy_") {
1458
1986
  return { mode: "read", reason: "tenant admin" };
1459
1987
  }
@@ -1461,6 +1989,12 @@ class BaseMindooTenantDirectory {
1461
1989
  if (grant && (0, grantKeys_1.extractActiveKeyPairs)(grant).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, input.signingKey))) {
1462
1990
  return { mode: "read", reason: "a device of the owner" };
1463
1991
  }
1992
+ if (grant && typeof grant.username_hash === "string") {
1993
+ const certified = await this.resolveSignerInNode(head, input.signingKey, Number.MAX_SAFE_INTEGER);
1994
+ if (certified?.via === "certificate" && certified.grant.usernameHash === grant.username_hash) {
1995
+ return { mode: "read", reason: "a certified device of the owner" };
1996
+ }
1997
+ }
1464
1998
  // Free/busy (§6.11): the tenant admin reads every person's busy times but,
1465
1999
  // like every other member, does not write them (unless the admin key is a
1466
2000
  // device of the owner, checked above).
@@ -1559,10 +2093,17 @@ class BaseMindooTenantDirectory {
1559
2093
  if (!lookup)
1560
2094
  return [];
1561
2095
  const grants = await this.findGrantAccessDocuments(lookup.username);
1562
- return grants
2096
+ const live = grants
1563
2097
  .filter((doc) => !doc.isDeleted())
1564
- .map((doc) => doc.getData())
1565
- .filter((data) => (0, grantKeys_1.extractActiveKeyPairs)(data).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, signingKey)));
2098
+ .map((doc) => doc.getData());
2099
+ const listed = live.filter((data) => (0, grantKeys_1.extractActiveKeyPairs)(data).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, signingKey)));
2100
+ if (listed.length > 0)
2101
+ return listed;
2102
+ // A certified device acts under its person's grants (userkeys.md §9.6).
2103
+ const certified = await this.resolveSignerToGrant(signingKey, Number.MAX_SAFE_INTEGER);
2104
+ if (certified?.via !== "certificate")
2105
+ return [];
2106
+ return live.filter((data) => data.username_hash === certified.grant.usernameHash);
1566
2107
  }
1567
2108
  /**
1568
2109
  * Let an identity deposit into personal databases with these prefixes
@@ -1597,10 +2138,7 @@ class BaseMindooTenantDirectory {
1597
2138
  if (grants.length === 0)
1598
2139
  throw new Error(`No grant for ${username}`);
1599
2140
  const directoryDB = await this.getDirectoryDB();
1600
- const signingKeyPair = {
1601
- publicKey: this.tenant.getAdministrationPublicKey(),
1602
- privateKey: administrationPrivateKey,
1603
- };
2141
+ const signingKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1604
2142
  for (const grant of grants) {
1605
2143
  await directoryDB.changeDoc(grant, (d) => {
1606
2144
  const data = d.getData();
@@ -1649,8 +2187,7 @@ class BaseMindooTenantDirectory {
1649
2187
  if (dbid === types_1.DIRECTORY_DB_ID || dbid === types_1.USER_DIRECTORY_DB_ID)
1650
2188
  return true;
1651
2189
  const currentUser = await this.tenant.getCurrentUserId();
1652
- const adminKey = this.tenant.getAdministrationPublicKey();
1653
- if (currentUser.userSigningPublicKey === adminKey)
2190
+ if (await this.isAdminKeyNow(currentUser.userSigningPublicKey))
1654
2191
  return true;
1655
2192
  // The personal-database invariant (§6.11) decides first, policy on top.
1656
2193
  const personal = await this.evaluatePersonalDatabaseAccess({ dbid, signingKey: currentUser.userSigningPublicKey });
@@ -1707,10 +2244,7 @@ class BaseMindooTenantDirectory {
1707
2244
  async writePolicyDoc(docId, policy, administrationPrivateKey, administrationPrivateKeyPassword) {
1708
2245
  const baseTenant = this.tenant;
1709
2246
  const directoryDB = await this.getDirectoryDB();
1710
- const adminSigningKeyPair = {
1711
- publicKey: baseTenant.getAdministrationPublicKey(),
1712
- privateKey: administrationPrivateKey,
1713
- };
2247
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1714
2248
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
1715
2249
  await directoryDB.changeDoc(doc, async (d) => {
1716
2250
  const data = d.getData();
@@ -1756,10 +2290,7 @@ class BaseMindooTenantDirectory {
1756
2290
  (0, types_3.validateAclRule)(ruleDoc);
1757
2291
  const baseTenant = this.tenant;
1758
2292
  const directoryDB = await this.getDirectoryDB();
1759
- const adminSigningKeyPair = {
1760
- publicKey: baseTenant.getAdministrationPublicKey(),
1761
- privateKey: administrationPrivateKey,
1762
- };
2293
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1763
2294
  const doc = await this.getOrCreateAclDoc(directoryDB, (0, types_3.aclRuleDocId)(rule.ruleId), adminSigningKeyPair, administrationPrivateKeyPassword);
1764
2295
  await directoryDB.changeDoc(doc, async (d) => {
1765
2296
  const data = d.getData();
@@ -1811,10 +2342,7 @@ class BaseMindooTenantDirectory {
1811
2342
  async deleteRule(ruleId, administrationPrivateKey, administrationPrivateKeyPassword) {
1812
2343
  const baseTenant = this.tenant;
1813
2344
  const directoryDB = await this.getDirectoryDB();
1814
- const adminSigningKeyPair = {
1815
- publicKey: baseTenant.getAdministrationPublicKey(),
1816
- privateKey: administrationPrivateKey,
1817
- };
2345
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1818
2346
  try {
1819
2347
  await directoryDB.deleteDocument((0, types_3.aclRuleDocId)(ruleId), {
1820
2348
  signingKeyPair: adminSigningKeyPair,
@@ -1833,10 +2361,9 @@ class BaseMindooTenantDirectory {
1833
2361
  async addTrustedWitness(witness, administrationPrivateKey, administrationPrivateKeyPassword) {
1834
2362
  const baseTenant = this.tenant;
1835
2363
  const directoryDB = await this.getDirectoryDB();
1836
- const adminSigningKeyPair = {
1837
- publicKey: baseTenant.getAdministrationPublicKey(),
1838
- privateKey: administrationPrivateKey,
1839
- };
2364
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2365
+ // Trusted witnesses decide whose receipt times count (§6.13): root only.
2366
+ await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "add trusted witnesses");
1840
2367
  const fingerprint = await this.sha256Hex(witness.witnessPublicKey);
1841
2368
  const doc = await this.getOrCreateAclDoc(directoryDB, (0, types_2.aclTrustedWitnessDocId)(fingerprint), adminSigningKeyPair, administrationPrivateKeyPassword);
1842
2369
  await directoryDB.changeDoc(doc, async (d) => {
@@ -1853,10 +2380,8 @@ class BaseMindooTenantDirectory {
1853
2380
  async removeTrustedWitness(witnessPublicKey, administrationPrivateKey, administrationPrivateKeyPassword) {
1854
2381
  const baseTenant = this.tenant;
1855
2382
  const directoryDB = await this.getDirectoryDB();
1856
- const adminSigningKeyPair = {
1857
- publicKey: baseTenant.getAdministrationPublicKey(),
1858
- privateKey: administrationPrivateKey,
1859
- };
2383
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2384
+ await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "remove trusted witnesses");
1860
2385
  const fingerprint = await this.sha256Hex(witnessPublicKey);
1861
2386
  try {
1862
2387
  await directoryDB.deleteDocument((0, types_2.aclTrustedWitnessDocId)(fingerprint), {
@@ -2076,6 +2601,15 @@ class BaseMindooTenantDirectory {
2076
2601
  ? await this.encryptUserDetailsForTenant(this.buildUserDetailsPayload(username, changes.details))
2077
2602
  : null;
2078
2603
  const identityHashes = await this.computeIdentityHashes(username);
2604
+ // Devices this person certified (userkeys.md §9.6) can be revoked and
2605
+ // wiped here like listed ones, although they are not on the grant.
2606
+ const certifiedByHash = new Map();
2607
+ for (const grant of await this.findGrantAccessDocuments(username)) {
2608
+ const hash = grant.getData().username_hash;
2609
+ if (typeof hash === "string" && !certifiedByHash.has(hash)) {
2610
+ certifiedByHash.set(hash, await this.certificatesOfPerson(hash));
2611
+ }
2612
+ }
2079
2613
  await this.editGrantArrays(username, administrationPrivateKey, administrationPrivateKeyPassword, (data) => {
2080
2614
  if (userDetailsEncrypted !== null) {
2081
2615
  data.user_details_encrypted = userDetailsEncrypted;
@@ -2107,12 +2641,52 @@ class BaseMindooTenantDirectory {
2107
2641
  }
2108
2642
  return p;
2109
2643
  });
2110
- (0, grantKeys_1.applyKeyPairFields)(data, next);
2644
+ const certified = typeof data.username_hash === "string" ? certifiedByHash.get(data.username_hash) ?? [] : [];
2645
+ const isCertifiedOnly = (key) => certified.some((c) => (0, DirectoryStateNode_1.signingKeysEqual)(c.deviceSigningPublicKey, key)) &&
2646
+ !(0, grantKeys_1.extractActiveKeyPairs)(data).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, key));
2647
+ for (const cert of certified) {
2648
+ const key = cert.deviceSigningPublicKey;
2649
+ if (!revokeSet.has(key) || next.some((p) => (0, DirectoryStateNode_1.signingKeysEqual)(p.signingPublicKey, key)))
2650
+ continue;
2651
+ next.push({
2652
+ signingPublicKey: key,
2653
+ encryptionPublicKey: cert.deviceEncryptionPublicKey,
2654
+ ...(cert.label ? { label: cert.label } : {}),
2655
+ revoked: true,
2656
+ revokedAt: now,
2657
+ });
2658
+ }
2659
+ // Restoring a certified device only lifts the revocation; it does not
2660
+ // turn it into a device listed on the grant.
2661
+ const kept = next.filter((p) => !(restoreSet.has(p.signingPublicKey) && isCertifiedOnly(p.signingPublicKey) && !p.revoked));
2662
+ (0, grantKeys_1.applyKeyPairFields)(data, kept);
2111
2663
  // Remote-wipe set: authoritative when supplied (else preserve existing),
2112
- // restricted to keys present on this grant doc and never for restored keys.
2113
- const present = new Set(next.map((p) => p.signingPublicKey));
2664
+ // restricted to keys present on this grant doc or certified by its
2665
+ // person, and never for restored keys.
2666
+ const present = new Set([
2667
+ ...kept.map((p) => p.signingPublicKey),
2668
+ ...certified.map((c) => c.deviceSigningPublicKey),
2669
+ ]);
2114
2670
  const desiredWipe = changes.wipeSigningKeys ?? (0, grantKeys_1.extractWipeRequestedSigningKeys)(data);
2115
2671
  data.wipeRequestedForSigningKeys = Array.from(new Set(desiredWipe.filter((k) => present.has(k) && !restoreSet.has(k))));
2672
+ if (changes.deviceSelfGrant !== undefined) {
2673
+ const existing = (0, grantKeys_1.extractDeviceSelfGrant)(data);
2674
+ if (changes.deviceSelfGrant === null) {
2675
+ if (existing && existing.revokedAt === undefined) {
2676
+ data.deviceSelfGrant = { ...existing, revokedAt: now };
2677
+ }
2678
+ }
2679
+ else {
2680
+ const key = changes.deviceSelfGrant.trim();
2681
+ if (!key)
2682
+ throw new Error("deviceSelfGrant must be a PEM public key or null");
2683
+ if (!existing ||
2684
+ existing.revokedAt !== undefined ||
2685
+ existing.signingPublicKey.replace(/\s+/g, "") !== key.replace(/\s+/g, "")) {
2686
+ data.deviceSelfGrant = { signingPublicKey: key, grantedAt: now };
2687
+ }
2688
+ }
2689
+ }
2116
2690
  });
2117
2691
  }
2118
2692
  /**
@@ -2398,10 +2972,7 @@ class BaseMindooTenantDirectory {
2398
2972
  const docId = (0, types_3.aclKeyDistributionDocId)(keyId);
2399
2973
  const baseTenant = this.tenant;
2400
2974
  const directoryDB = await this.getDirectoryDB();
2401
- const adminSigningKeyPair = {
2402
- publicKey: baseTenant.getAdministrationPublicKey(),
2403
- privateKey: administrationPrivateKey,
2404
- };
2975
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2405
2976
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
2406
2977
  await directoryDB.changeDoc(doc, async (d) => {
2407
2978
  const data = d.getData();
@@ -2518,10 +3089,7 @@ class BaseMindooTenantDirectory {
2518
3089
  async deleteKeyDistribution(keyId, administrationPrivateKey, administrationPrivateKeyPassword) {
2519
3090
  const baseTenant = this.tenant;
2520
3091
  const directoryDB = await this.getDirectoryDB();
2521
- const adminSigningKeyPair = {
2522
- publicKey: baseTenant.getAdministrationPublicKey(),
2523
- privateKey: administrationPrivateKey,
2524
- };
3092
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2525
3093
  try {
2526
3094
  await directoryDB.deleteDocument((0, types_3.aclKeyDistributionDocId)(keyId), {
2527
3095
  signingKeyPair: adminSigningKeyPair,
@@ -2778,10 +3346,7 @@ class BaseMindooTenantDirectory {
2778
3346
  const docId = (0, types_3.aclAppDistributionDocId)(appId);
2779
3347
  const baseTenant = this.tenant;
2780
3348
  const directoryDB = await this.getDirectoryDB();
2781
- const adminSigningKeyPair = {
2782
- publicKey: baseTenant.getAdministrationPublicKey(),
2783
- privateKey: administrationPrivateKey,
2784
- };
3349
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2785
3350
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
2786
3351
  await directoryDB.changeDoc(doc, async (d) => {
2787
3352
  const data = d.getData();
@@ -2890,10 +3455,7 @@ class BaseMindooTenantDirectory {
2890
3455
  async deleteAppDistribution(appId, administrationPrivateKey, administrationPrivateKeyPassword) {
2891
3456
  const baseTenant = this.tenant;
2892
3457
  const directoryDB = await this.getDirectoryDB();
2893
- const adminSigningKeyPair = {
2894
- publicKey: baseTenant.getAdministrationPublicKey(),
2895
- privateKey: administrationPrivateKey,
2896
- };
3458
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2897
3459
  try {
2898
3460
  await directoryDB.deleteDocument((0, types_3.aclAppDistributionDocId)(appId), {
2899
3461
  signingKeyPair: adminSigningKeyPair,
@@ -3017,10 +3579,7 @@ class BaseMindooTenantDirectory {
3017
3579
  const docId = (0, types_3.aclSyncSetupPolicyDocId)(policyId);
3018
3580
  const baseTenant = this.tenant;
3019
3581
  const directoryDB = await this.getDirectoryDB();
3020
- const adminSigningKeyPair = {
3021
- publicKey: baseTenant.getAdministrationPublicKey(),
3022
- privateKey: administrationPrivateKey,
3023
- };
3582
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3024
3583
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
3025
3584
  await directoryDB.changeDoc(doc, async (d) => {
3026
3585
  const data = d.getData();
@@ -3130,10 +3689,7 @@ class BaseMindooTenantDirectory {
3130
3689
  async deleteSyncSetupPolicy(policyId, administrationPrivateKey, administrationPrivateKeyPassword) {
3131
3690
  const baseTenant = this.tenant;
3132
3691
  const directoryDB = await this.getDirectoryDB();
3133
- const adminSigningKeyPair = {
3134
- publicKey: baseTenant.getAdministrationPublicKey(),
3135
- privateKey: administrationPrivateKey,
3136
- };
3692
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3137
3693
  try {
3138
3694
  await directoryDB.deleteDocument((0, types_3.aclSyncSetupPolicyDocId)(policyId), {
3139
3695
  signingKeyPair: adminSigningKeyPair,
@@ -3212,7 +3768,7 @@ class BaseMindooTenantDirectory {
3212
3768
  * server-readable and carries every variant.
3213
3769
  */
3214
3770
  async getRevokedDecryptionKeyIdsForSigningKey(signingKey) {
3215
- if (signingKey === this.tenant.getAdministrationPublicKey())
3771
+ if (await this.isAdminKeyNow(signingKey))
3216
3772
  return [];
3217
3773
  const lookup = await this.getUserBySigningPublicKey(signingKey);
3218
3774
  if (!lookup)
@@ -3290,10 +3846,7 @@ class BaseMindooTenantDirectory {
3290
3846
  }
3291
3847
  const baseTenant = this.tenant;
3292
3848
  const directoryDB = await this.getDirectoryDB();
3293
- const adminSigningKeyPair = {
3294
- publicKey: baseTenant.getAdministrationPublicKey(),
3295
- privateKey: administrationPrivateKey,
3296
- };
3849
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3297
3850
  for (const grant of grants) {
3298
3851
  const doc = await directoryDB.getDocument(grant.getId());
3299
3852
  await directoryDB.changeDoc(doc, async (d) => {
@@ -3325,9 +3878,9 @@ class BaseMindooTenantDirectory {
3325
3878
  */
3326
3879
  async identitySetForSigningKey(signingKey, node, opts) {
3327
3880
  // The tenant admin key is the root of trust and is resolved even without a
3328
- // grant document (it authors the directory itself).
3329
- const adminKey = this.tenant.getAdministrationPublicKey();
3330
- const isAdmin = signingKey === adminKey;
3881
+ // grant document (it authors the directory itself). Delegated admin keys
3882
+ // (§6.13) count while their window covers the evaluation time.
3883
+ const isAdmin = (0, DirectoryStateNode_1.isAdminInNode)(node, this.getRootAdminPublicKey(), signingKey, (0, adminKeys_1.adminDecisionTime)(opts.trustedTime ?? Number.MAX_SAFE_INTEGER, (0, timeSource_1.semanticNow)()));
3331
3884
  // Audit #4 (revocation lag): resolve the acting identity strictly from the
3332
3885
  // directory state at the entry's trusted time `T`. A signing key that was
3333
3886
  // not an ACTIVE grant at `T` carries only the pseudo-tokens
@@ -3337,7 +3890,8 @@ class BaseMindooTenantDirectory {
3337
3890
  // allow-rules until the trust cache refreshed (up to
3338
3891
  // DIRECTORY_SYNC_INTERVAL_MS later). The `node` is the time-`T` snapshot
3339
3892
  // whose `bySigningKey` only contains keys whose grant is active at `T`.
3340
- const grantedAtT = !!(0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey);
3893
+ // A certified device (userkeys.md §9.6) acts for its person like a listed one.
3894
+ const grantedAtT = !!(await this.resolveSignerInNode(node, signingKey, opts.trustedTime ?? node.trustedTimeUpperBound));
3341
3895
  // Resolve the author's username from their signing key. Trusted non-user
3342
3896
  // keys (e.g. server-to-server sync identities) have no grant; treat them as
3343
3897
  // an empty identity that still carries `$everyone`/`$author`. Keys that were
@@ -3560,6 +4114,30 @@ class BaseMindooTenantDirectory {
3560
4114
  if (key.replace(/\s+/g, "") === needle)
3561
4115
  return lookup;
3562
4116
  }
4117
+ // A device the person certified (userkeys.md §9.6). Like the cache above,
4118
+ // this keeps naming the person after the device was revoked; callers that
4119
+ // need "active" check the signing-key universe or resolveSignerToGrant.
4120
+ const certified = await this.certifiedGrantInNode(await this.getDirectoryStateHead(), publicKey, Number.MAX_SAFE_INTEGER, { enforceRevocation: false });
4121
+ if (!certified)
4122
+ return null;
4123
+ const personKeys = [
4124
+ ...certified.grant.signingKeys,
4125
+ ...(certified.grant.revokedSigningKeys ?? []),
4126
+ ];
4127
+ for (const personKey of personKeys) {
4128
+ const lookup = this.userLookupCache.get(personKey) ??
4129
+ [...this.userLookupCache.entries()].find(([k]) => (0, DirectoryStateNode_1.signingKeysEqual)(k, personKey))?.[1];
4130
+ if (lookup) {
4131
+ // Device-specific fields come from the certificate; a join response
4132
+ // wrapped for another device of the person must not be handed out.
4133
+ const { joinResponseWrapped: _other, ...personLookup } = lookup;
4134
+ return {
4135
+ ...personLookup,
4136
+ signingPublicKey: certified.cert.deviceSigningPublicKey,
4137
+ encryptionPublicKey: certified.cert.deviceEncryptionPublicKey,
4138
+ };
4139
+ }
4140
+ }
3563
4141
  return null;
3564
4142
  }
3565
4143
  /**
@@ -3577,14 +4155,27 @@ class BaseMindooTenantDirectory {
3577
4155
  const grants = await this.findGrantAccessDocuments(username);
3578
4156
  const active = new Set();
3579
4157
  const wipeRequested = new Set();
4158
+ const revokedPairs = new Set();
4159
+ const hashes = new Set();
3580
4160
  for (const grant of grants) {
3581
4161
  const data = grant.getData();
3582
4162
  for (const key of (0, grantKeys_1.extractSigningPublicKeys)(data))
3583
4163
  active.add(key);
3584
4164
  for (const key of (0, grantKeys_1.extractWipeRequestedSigningKeys)(data))
3585
4165
  wipeRequested.add(key);
4166
+ for (const pair of (0, grantKeys_1.extractRevokedKeyPairs)(data))
4167
+ revokedPairs.add(pair.signingPublicKey);
4168
+ if (typeof data.username_hash === "string")
4169
+ hashes.add(data.username_hash);
3586
4170
  }
3587
- return { active: Array.from(active), wipeRequested: Array.from(wipeRequested) };
4171
+ // Devices the person certified count as active (userkeys.md §9.6).
4172
+ for (const hash of hashes) {
4173
+ for (const cert of await this.listCertifiedDevices(hash))
4174
+ active.add(cert.deviceSigningPublicKey);
4175
+ }
4176
+ // A key revoked on one grant but active on another (or restored) is active.
4177
+ const revoked = [...revokedPairs].filter((key) => ![...active].some((other) => (0, DirectoryStateNode_1.signingKeysEqual)(other, key)));
4178
+ return { active: Array.from(active), wipeRequested: Array.from(wipeRequested), revoked };
3588
4179
  }
3589
4180
  /**
3590
4181
  * List a user's currently-granted device key pairs (§6.5), each with its
@@ -3666,10 +4257,7 @@ class BaseMindooTenantDirectory {
3666
4257
  const requestedAt = (0, timeSource_1.semanticNow)();
3667
4258
  const baseTenant = this.tenant;
3668
4259
  const directoryDB = await this.getDirectoryDB();
3669
- const adminSigningKeyPair = {
3670
- publicKey: baseTenant.getAdministrationPublicKey(),
3671
- privateKey: administrationPrivateKey,
3672
- };
4260
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3673
4261
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
3674
4262
  await directoryDB.changeDoc(doc, async (d) => {
3675
4263
  const data = d.getData();
@@ -3748,10 +4336,7 @@ class BaseMindooTenantDirectory {
3748
4336
  async deleteDocHistoryPurge(requestId, administrationPrivateKey, administrationPrivateKeyPassword) {
3749
4337
  const baseTenant = this.tenant;
3750
4338
  const directoryDB = await this.getDirectoryDB();
3751
- const adminSigningKeyPair = {
3752
- publicKey: baseTenant.getAdministrationPublicKey(),
3753
- privateKey: administrationPrivateKey,
3754
- };
4339
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3755
4340
  try {
3756
4341
  await directoryDB.deleteDocument((0, types_3.aclDocHistoryPurgeDocId)(requestId), {
3757
4342
  signingKeyPair: adminSigningKeyPair,
@@ -3800,10 +4385,7 @@ class BaseMindooTenantDirectory {
3800
4385
  async changeTenantSettings(changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
3801
4386
  const baseTenant = this.tenant;
3802
4387
  const directoryDB = await this.getDirectoryDB();
3803
- const adminSigningKeyPair = {
3804
- publicKey: baseTenant.getAdministrationPublicKey(),
3805
- privateKey: administrationPrivateKey,
3806
- };
4388
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3807
4389
  // Get cached settings doc or create new one
3808
4390
  let settingsDoc = await this.getTenantSettings();
3809
4391
  if (!settingsDoc) {
@@ -3827,10 +4409,7 @@ class BaseMindooTenantDirectory {
3827
4409
  async changeDBSettings(dbId, changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
3828
4410
  const baseTenant = this.tenant;
3829
4411
  const directoryDB = await this.getDirectoryDB();
3830
- const adminSigningKeyPair = {
3831
- publicKey: baseTenant.getAdministrationPublicKey(),
3832
- privateKey: administrationPrivateKey,
3833
- };
4412
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3834
4413
  // Get cached settings doc for this dbId or create new one
3835
4414
  let settingsDoc = await this.getDBSettings(dbId);
3836
4415
  if (!settingsDoc) {
@@ -3885,10 +4464,7 @@ class BaseMindooTenantDirectory {
3885
4464
  const normalizedGroupName = this.normalizeGroupName(groupName);
3886
4465
  const baseTenant = this.tenant;
3887
4466
  const directoryDB = await this.getDirectoryDB();
3888
- const adminSigningKeyPair = {
3889
- publicKey: baseTenant.getAdministrationPublicKey(),
3890
- privateKey: administrationPrivateKey,
3891
- };
4467
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3892
4468
  // Compute hashes (salted v2 for new writes) and encrypted values for all
3893
4469
  // new usernames. Reads match both v1 and v2 forms (docs/accesscontrol.md §6.5).
3894
4470
  const newMembersHashes = await Promise.all(username.map(u => this.hashUsernameForWrite(u)));
@@ -3958,10 +4534,7 @@ class BaseMindooTenantDirectory {
3958
4534
  // Group doesn't exist, nothing to do
3959
4535
  return;
3960
4536
  }
3961
- const adminSigningKeyPair = {
3962
- publicKey: baseTenant.getAdministrationPublicKey(),
3963
- privateKey: administrationPrivateKey,
3964
- };
4537
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3965
4538
  // Compute hashes of usernames to remove. Include both legacy (v1) and
3966
4539
  // salted (v2) forms so members stored under either scheme are removed.
3967
4540
  const hashesToRemove = new Set((await Promise.all(username.map(u => this.usernameHashCandidates(u)))).flat());
@@ -4020,10 +4593,7 @@ class BaseMindooTenantDirectory {
4020
4593
  // Group doesn't exist, nothing to do
4021
4594
  return;
4022
4595
  }
4023
- const adminSigningKeyPair = {
4024
- publicKey: baseTenant.getAdministrationPublicKey(),
4025
- privateKey: administrationPrivateKey,
4026
- };
4596
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
4027
4597
  // Delete the document using the proper API
4028
4598
  await directoryDB.deleteDocument(cachedGroup.docId, {
4029
4599
  signingKeyPair: adminSigningKeyPair,