mindoodb 0.0.93 → 0.0.95

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 (146) 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 +39 -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 +65 -2
  17. package/dist/core/BaseMindooDB.js.map +1 -1
  18. package/dist/core/BaseMindooTenant.d.ts +75 -1
  19. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  20. package/dist/core/BaseMindooTenant.js +229 -7
  21. package/dist/core/BaseMindooTenant.js.map +1 -1
  22. package/dist/core/BaseMindooTenantDirectory.d.ts +165 -2
  23. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  24. package/dist/core/BaseMindooTenantDirectory.js +648 -112
  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 +34 -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 +33 -2
  50. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  51. package/dist/core/accesscontrol/types.js +32 -0
  52. package/dist/core/accesscontrol/types.js.map +1 -1
  53. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  54. package/dist/core/appendonlystores/network/AuthenticationService.js +8 -0
  55. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  56. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +16 -1
  57. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  58. package/dist/core/appendonlystores/network/types.d.ts +13 -0
  59. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  60. package/dist/core/appendonlystores/network/types.js.map +1 -1
  61. package/dist/core/builtinDbInvariants.d.ts +18 -2
  62. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  63. package/dist/core/builtinDbInvariants.js +41 -5
  64. package/dist/core/builtinDbInvariants.js.map +1 -1
  65. package/dist/core/index.d.ts +5 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +28 -5
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/live/LiveChannel.d.ts +41 -0
  70. package/dist/core/live/LiveChannel.d.ts.map +1 -0
  71. package/dist/core/live/LiveChannel.js +151 -0
  72. package/dist/core/live/LiveChannel.js.map +1 -0
  73. package/dist/core/types.d.ts +182 -0
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/dist/core/types.js.map +1 -1
  76. package/dist/core/uri/MindooURI.d.ts +1 -1
  77. package/dist/core/uri/MindooURI.d.ts.map +1 -1
  78. package/dist/core/uri/MindooURI.js +2 -0
  79. package/dist/core/uri/MindooURI.js.map +1 -1
  80. package/dist/core/uri/adminUri.d.ts +52 -0
  81. package/dist/core/uri/adminUri.d.ts.map +1 -0
  82. package/dist/core/uri/adminUri.js +100 -0
  83. package/dist/core/uri/adminUri.js.map +1 -0
  84. package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
  85. package/dist/core/uri/joinRequestUri.js +14 -1
  86. package/dist/core/uri/joinRequestUri.js.map +1 -1
  87. package/dist/core/userid/index.d.ts +9 -0
  88. package/dist/core/userid/index.d.ts.map +1 -1
  89. package/dist/core/userkeys/UserKeyDocument.d.ts +7 -1
  90. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -1
  91. package/dist/core/userkeys/UserKeyDocument.js +14 -3
  92. package/dist/core/userkeys/UserKeyDocument.js.map +1 -1
  93. package/dist/core/userkeys/UserKeyManager.d.ts +95 -4
  94. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -1
  95. package/dist/core/userkeys/UserKeyManager.js +606 -59
  96. package/dist/core/userkeys/UserKeyManager.js.map +1 -1
  97. package/dist/core/userkeys/deviceCertificate.d.ts +46 -0
  98. package/dist/core/userkeys/deviceCertificate.d.ts.map +1 -0
  99. package/dist/core/userkeys/deviceCertificate.js +86 -0
  100. package/dist/core/userkeys/deviceCertificate.js.map +1 -0
  101. package/dist/core/userkeys/generationSignature.d.ts +32 -0
  102. package/dist/core/userkeys/generationSignature.d.ts.map +1 -0
  103. package/dist/core/userkeys/generationSignature.js +136 -0
  104. package/dist/core/userkeys/generationSignature.js.map +1 -0
  105. package/dist/core/userkeys/generationValidity.d.ts +75 -0
  106. package/dist/core/userkeys/generationValidity.d.ts.map +1 -0
  107. package/dist/core/userkeys/generationValidity.js +175 -0
  108. package/dist/core/userkeys/generationValidity.js.map +1 -0
  109. package/dist/core/userkeys/index.d.ts +5 -2
  110. package/dist/core/userkeys/index.d.ts.map +1 -1
  111. package/dist/core/userkeys/index.js +21 -1
  112. package/dist/core/userkeys/index.js.map +1 -1
  113. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +16 -0
  114. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -1
  115. package/dist/core/userkeys/resolveUserKeyDocument.js +42 -1
  116. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -1
  117. package/dist/core/userkeys/types.d.ts +26 -0
  118. package/dist/core/userkeys/types.d.ts.map +1 -1
  119. package/dist/core/userkeys/types.js +3 -1
  120. package/dist/core/userkeys/types.js.map +1 -1
  121. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -1
  122. package/dist/core/userkeys/validateUserKeyDocument.js +7 -0
  123. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -1
  124. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  125. package/dist/node/server/MindooDBServer.js +11 -0
  126. package/dist/node/server/MindooDBServer.js.map +1 -1
  127. package/dist/server/SyncEventBus.d.ts +22 -0
  128. package/dist/server/SyncEventBus.d.ts.map +1 -1
  129. package/dist/server/SyncEventBus.js +21 -0
  130. package/dist/server/SyncEventBus.js.map +1 -1
  131. package/dist/server/TenantManager.d.ts +9 -0
  132. package/dist/server/TenantManager.d.ts.map +1 -1
  133. package/dist/server/TenantManager.js +96 -0
  134. package/dist/server/TenantManager.js.map +1 -1
  135. package/dist/server/http/MindooDBServerCore.d.ts +39 -1
  136. package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
  137. package/dist/server/http/MindooDBServerCore.js +121 -2
  138. package/dist/server/http/MindooDBServerCore.js.map +1 -1
  139. package/dist/server/http/types.d.ts +10 -1
  140. package/dist/server/http/types.d.ts.map +1 -1
  141. package/dist/server/http/types.js.map +1 -1
  142. package/dist/server/index.d.ts +2 -2
  143. package/dist/server/index.d.ts.map +1 -1
  144. package/dist/server/index.js +2 -1
  145. package/dist/server/index.js.map +1 -1
  146. 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,254 @@ 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
+ * Read every `dcert_*` certificate from the `userdirectory` store without
1526
+ * materializing that database (whose own trust checks call back into this
1527
+ * index). Only `doc_create` entries carry a certificate; deletes withdraw
1528
+ * it. Rebuilt when the store head moves, at most every two seconds for
1529
+ * stores that cannot report a head.
1530
+ */
1531
+ async loadDeviceCertificateIndex() {
1532
+ if (this.buildingDeviceCertIndex) {
1533
+ // Re-entry while building: answer from what is known.
1534
+ return this.deviceCertIndex?.byKey ?? new Map();
1535
+ }
1536
+ const baseTenant = this.tenant;
1537
+ if (typeof baseTenant.getDocStoreForDb !== "function")
1538
+ return new Map();
1539
+ let store;
1540
+ try {
1541
+ store = baseTenant.getDocStoreForDb(types_1.USER_DIRECTORY_DB_ID);
1542
+ }
1543
+ catch {
1544
+ return new Map();
1545
+ }
1546
+ const head = await store.getStoreHead?.().catch(() => undefined);
1547
+ const stamp = head ? `${head.epoch}:${head.maxReceiptOrder}` : null;
1548
+ const cached = this.deviceCertIndex;
1549
+ if (cached && stamp !== null && cached.stamp === stamp)
1550
+ return cached.byKey;
1551
+ if (cached && stamp === null && Date.now() - cached.at < 2000)
1552
+ return cached.byKey;
1553
+ const build = (async () => {
1554
+ const byKey = new Map();
1555
+ const isCert = (m) => m.docId.startsWith(deviceCertificate_1.DEVICE_CERT_DOC_ID_PREFIX);
1556
+ const creates = (await store.findEntries("doc_create", null, null)).filter(isCert);
1557
+ if (creates.length === 0)
1558
+ return byKey;
1559
+ const deletes = (await store.findEntries("doc_delete", null, null)).filter(isCert);
1560
+ const entries = await store.getEntries(creates.map((m) => m.id));
1561
+ const crypto = this.tenant.getCryptoAdapter();
1562
+ for (const entry of entries) {
1563
+ try {
1564
+ const plaintext = await baseTenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
1565
+ const cert = (0, deviceCertificate_1.asDeviceCertificate)((0, builtinDbInvariants_1.recordFromCreateChangeBytes)(plaintext));
1566
+ if (!cert || !(await (0, deviceCertificate_1.verifyDeviceCertificate)(crypto, cert)))
1567
+ continue;
1568
+ const createMeta = creates.find((m) => m.id === entry.id) ?? entry;
1569
+ const key = (0, fingerprint_1.normalizePublicKeyPem)(cert.deviceSigningPublicKey);
1570
+ const list = byKey.get(key) ?? [];
1571
+ list.push({
1572
+ cert,
1573
+ docId: entry.docId,
1574
+ createMeta,
1575
+ deleteMetas: deletes.filter((m) => m.docId === entry.docId),
1576
+ });
1577
+ byKey.set(key, list);
1578
+ }
1579
+ catch {
1580
+ // Not readable here (no $publicinfos) or not a certificate.
1581
+ }
1582
+ }
1583
+ return byKey;
1584
+ })();
1585
+ this.buildingDeviceCertIndex = build;
1586
+ try {
1587
+ const byKey = await build;
1588
+ this.deviceCertIndex = { stamp, at: Date.now(), byKey };
1589
+ return byKey;
1590
+ }
1591
+ finally {
1592
+ this.buildingDeviceCertIndex = null;
1593
+ }
1594
+ }
1595
+ /**
1596
+ * The person's device self-grant delegation at the head state
1597
+ * (docs/userkeys.md §9), or `null` when the admin never granted it.
1598
+ */
1599
+ async getDeviceSelfGrantForUsernameHash(usernameHash) {
1600
+ const head = await this.getDirectoryStateHead();
1601
+ return head.usersByHash.get(usernameHash)?.deviceSelfGrant ?? null;
1602
+ }
1603
+ /** {@link getDeviceSelfGrantForUsernameHash} by username (newest grant wins). */
1604
+ async getDeviceSelfGrant(username) {
1605
+ const grants = await this.findGrantAccessDocuments(username);
1606
+ return (0, grantKeys_1.newestDeviceSelfGrant)(grants.map((g) => (0, grantKeys_1.extractDeviceSelfGrant)(g.getData())));
1607
+ }
1608
+ /**
1609
+ * Grant or revoke the person's right to certify their own devices
1610
+ * (docs/userkeys.md §9). `signingPublicKey` is the public half of their
1611
+ * device self-grant key (from the join request, or
1612
+ * `tenant.getDeviceSelfGrantPublicKey()` on one of their devices). `null`
1613
+ * revokes: the key stays on the grant with `revokedAt`, so certificates and
1614
+ * generations signed before stay valid; certificates issued later do not.
1615
+ */
1616
+ async setDeviceSelfGrant(username, signingPublicKey, administrationPrivateKey, administrationPrivateKeyPassword) {
1617
+ await this.updateUserGrant(username, { deviceSelfGrant: signingPublicKey }, administrationPrivateKey, administrationPrivateKeyPassword);
1618
+ }
1619
+ /**
1620
+ * Trusted time from which new User-Key generations must be signed
1621
+ * (`requireUserKeyGenerationSignatureSince`, docs/userkeys.md §9.3), or
1622
+ * `undefined`. Read from the `acl_defaultpolicy` head regardless of the
1623
+ * master switch, like {@link getRequireMetadataSignatureSince}.
1624
+ */
1625
+ async getRequireUserKeyGenerationSignatureSince() {
1626
+ const head = await this.getDirectoryStateHead();
1627
+ const since = head.defaultPolicy?.requireUserKeyGenerationSignatureSince;
1628
+ return typeof since === "number" && Number.isFinite(since) ? since : undefined;
1629
+ }
1630
+ /**
1631
+ * Require signed User-Key generations from now on (docs/userkeys.md §9.3).
1632
+ * Idempotent: an existing cutoff is kept. Run it once every device of the
1633
+ * tenant runs a client that signs generations; unsigned generations
1634
+ * introduced before stay valid.
1635
+ */
1636
+ async requireUserKeyGenerationSignatures(administrationPrivateKey, administrationPrivateKeyPassword) {
1637
+ const head = await this.getDirectoryStateHead();
1638
+ const existing = head.defaultPolicy?.requireUserKeyGenerationSignatureSince;
1639
+ if (typeof existing === "number" && Number.isFinite(existing)) {
1640
+ return { since: existing, changed: false };
1641
+ }
1642
+ const since = (0, timeSource_1.semanticNow)();
1643
+ await this.setDefaultAccessPolicy(head.defaultPolicy
1644
+ ? { requireUserKeyGenerationSignatureSince: since }
1645
+ : { disableAllAccessChecksAndPolicies: true, requireUserKeyGenerationSignatureSince: since }, administrationPrivateKey, administrationPrivateKeyPassword);
1646
+ return { since, changed: true };
1144
1647
  }
1145
1648
  /**
1146
1649
  * Whether the active policy has any Tier 2 (`withfields`) content rule for
@@ -1242,8 +1745,7 @@ class BaseMindooTenantDirectory {
1242
1745
  */
1243
1746
  async canUseLlm() {
1244
1747
  const currentUser = await this.tenant.getCurrentUserId();
1245
- const adminKey = this.tenant.getAdministrationPublicKey();
1246
- if (currentUser.userSigningPublicKey === adminKey) {
1748
+ if (await this.isAdminKeyNow(currentUser.userSigningPublicKey)) {
1247
1749
  return { allowed: true, reason: "tenant admin", tier: "tier1" };
1248
1750
  }
1249
1751
  // Head selector, NOT Date.now() — see canDo.
@@ -1310,8 +1812,7 @@ class BaseMindooTenantDirectory {
1310
1812
  if (allowedDbIds.includes(dbid))
1311
1813
  return true;
1312
1814
  if (opts?.signingKey !== undefined) {
1313
- const adminKey = this.tenant.getAdministrationPublicKey();
1314
- if (opts.signingKey === adminKey)
1815
+ if (await this.isAdminKeyNow(opts.signingKey))
1315
1816
  return true;
1316
1817
  }
1317
1818
  return false;
@@ -1335,9 +1836,8 @@ class BaseMindooTenantDirectory {
1335
1836
  // policy that defines the read gate can be read at all).
1336
1837
  if (input.dbid === types_1.DIRECTORY_DB_ID || input.dbid === types_1.USER_DIRECTORY_DB_ID)
1337
1838
  return true;
1338
- // The tenant admin is exempt from the read gate.
1339
- const adminKey = this.tenant.getAdministrationPublicKey();
1340
- if (input.signingKey === adminKey)
1839
+ // Tenant admins (root and active delegated admins, §6.13) are exempt from the read gate.
1840
+ if (await this.isAdminKeyNow(input.signingKey))
1341
1841
  return true;
1342
1842
  // Gate 2 — per-user/group database read gate (`doc_read`, §6.6). Evaluated
1343
1843
  // server-side (Tier 1) from the principal's signing key at acceptance time.
@@ -1363,8 +1863,7 @@ class BaseMindooTenantDirectory {
1363
1863
  async evaluateReadScopeForSigningKey(input) {
1364
1864
  if (input.dbid === types_1.DIRECTORY_DB_ID || input.dbid === types_1.USER_DIRECTORY_DB_ID)
1365
1865
  return "all";
1366
- const adminKey = this.tenant.getAdministrationPublicKey();
1367
- if (input.signingKey === adminKey)
1866
+ if (await this.isAdminKeyNow(input.signingKey))
1368
1867
  return "all";
1369
1868
  const decision = await this.evaluateAccessForSigningKey({
1370
1869
  op: "doc_read_all",
@@ -1397,7 +1896,7 @@ class BaseMindooTenantDirectory {
1397
1896
  if (personal) {
1398
1897
  if (personal.mode === "none")
1399
1898
  return personal;
1400
- if ((0, DirectoryStateNode_1.signingKeysEqual)(input.signingKey, this.tenant.getAdministrationPublicKey())) {
1899
+ if (await this.isAdminKeyNow(input.signingKey)) {
1401
1900
  return personal;
1402
1901
  }
1403
1902
  if (personal.mode === "read") {
@@ -1453,7 +1952,7 @@ class BaseMindooTenantDirectory {
1453
1952
  const owner = (0, builtinDbInvariants_1.personalDatabaseOwner)(input.dbid, head.defaultPolicy?.personalDatabasePrefixes ?? []);
1454
1953
  if (!owner)
1455
1954
  return null;
1456
- const isAdmin = (0, DirectoryStateNode_1.signingKeysEqual)(input.signingKey, this.tenant.getAdministrationPublicKey());
1955
+ const isAdmin = await this.isAdminKeyNow(input.signingKey);
1457
1956
  if (isAdmin && owner.prefix !== "mbusy_") {
1458
1957
  return { mode: "read", reason: "tenant admin" };
1459
1958
  }
@@ -1461,6 +1960,12 @@ class BaseMindooTenantDirectory {
1461
1960
  if (grant && (0, grantKeys_1.extractActiveKeyPairs)(grant).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, input.signingKey))) {
1462
1961
  return { mode: "read", reason: "a device of the owner" };
1463
1962
  }
1963
+ if (grant && typeof grant.username_hash === "string") {
1964
+ const certified = await this.resolveSignerInNode(head, input.signingKey, Number.MAX_SAFE_INTEGER);
1965
+ if (certified?.via === "certificate" && certified.grant.usernameHash === grant.username_hash) {
1966
+ return { mode: "read", reason: "a certified device of the owner" };
1967
+ }
1968
+ }
1464
1969
  // Free/busy (§6.11): the tenant admin reads every person's busy times but,
1465
1970
  // like every other member, does not write them (unless the admin key is a
1466
1971
  // device of the owner, checked above).
@@ -1559,10 +2064,17 @@ class BaseMindooTenantDirectory {
1559
2064
  if (!lookup)
1560
2065
  return [];
1561
2066
  const grants = await this.findGrantAccessDocuments(lookup.username);
1562
- return grants
2067
+ const live = grants
1563
2068
  .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)));
2069
+ .map((doc) => doc.getData());
2070
+ const listed = live.filter((data) => (0, grantKeys_1.extractActiveKeyPairs)(data).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, signingKey)));
2071
+ if (listed.length > 0)
2072
+ return listed;
2073
+ // A certified device acts under its person's grants (userkeys.md §9.6).
2074
+ const certified = await this.resolveSignerToGrant(signingKey, Number.MAX_SAFE_INTEGER);
2075
+ if (certified?.via !== "certificate")
2076
+ return [];
2077
+ return live.filter((data) => data.username_hash === certified.grant.usernameHash);
1566
2078
  }
1567
2079
  /**
1568
2080
  * Let an identity deposit into personal databases with these prefixes
@@ -1597,10 +2109,7 @@ class BaseMindooTenantDirectory {
1597
2109
  if (grants.length === 0)
1598
2110
  throw new Error(`No grant for ${username}`);
1599
2111
  const directoryDB = await this.getDirectoryDB();
1600
- const signingKeyPair = {
1601
- publicKey: this.tenant.getAdministrationPublicKey(),
1602
- privateKey: administrationPrivateKey,
1603
- };
2112
+ const signingKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1604
2113
  for (const grant of grants) {
1605
2114
  await directoryDB.changeDoc(grant, (d) => {
1606
2115
  const data = d.getData();
@@ -1649,8 +2158,7 @@ class BaseMindooTenantDirectory {
1649
2158
  if (dbid === types_1.DIRECTORY_DB_ID || dbid === types_1.USER_DIRECTORY_DB_ID)
1650
2159
  return true;
1651
2160
  const currentUser = await this.tenant.getCurrentUserId();
1652
- const adminKey = this.tenant.getAdministrationPublicKey();
1653
- if (currentUser.userSigningPublicKey === adminKey)
2161
+ if (await this.isAdminKeyNow(currentUser.userSigningPublicKey))
1654
2162
  return true;
1655
2163
  // The personal-database invariant (§6.11) decides first, policy on top.
1656
2164
  const personal = await this.evaluatePersonalDatabaseAccess({ dbid, signingKey: currentUser.userSigningPublicKey });
@@ -1707,10 +2215,7 @@ class BaseMindooTenantDirectory {
1707
2215
  async writePolicyDoc(docId, policy, administrationPrivateKey, administrationPrivateKeyPassword) {
1708
2216
  const baseTenant = this.tenant;
1709
2217
  const directoryDB = await this.getDirectoryDB();
1710
- const adminSigningKeyPair = {
1711
- publicKey: baseTenant.getAdministrationPublicKey(),
1712
- privateKey: administrationPrivateKey,
1713
- };
2218
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1714
2219
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
1715
2220
  await directoryDB.changeDoc(doc, async (d) => {
1716
2221
  const data = d.getData();
@@ -1756,10 +2261,7 @@ class BaseMindooTenantDirectory {
1756
2261
  (0, types_3.validateAclRule)(ruleDoc);
1757
2262
  const baseTenant = this.tenant;
1758
2263
  const directoryDB = await this.getDirectoryDB();
1759
- const adminSigningKeyPair = {
1760
- publicKey: baseTenant.getAdministrationPublicKey(),
1761
- privateKey: administrationPrivateKey,
1762
- };
2264
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1763
2265
  const doc = await this.getOrCreateAclDoc(directoryDB, (0, types_3.aclRuleDocId)(rule.ruleId), adminSigningKeyPair, administrationPrivateKeyPassword);
1764
2266
  await directoryDB.changeDoc(doc, async (d) => {
1765
2267
  const data = d.getData();
@@ -1811,10 +2313,7 @@ class BaseMindooTenantDirectory {
1811
2313
  async deleteRule(ruleId, administrationPrivateKey, administrationPrivateKeyPassword) {
1812
2314
  const baseTenant = this.tenant;
1813
2315
  const directoryDB = await this.getDirectoryDB();
1814
- const adminSigningKeyPair = {
1815
- publicKey: baseTenant.getAdministrationPublicKey(),
1816
- privateKey: administrationPrivateKey,
1817
- };
2316
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
1818
2317
  try {
1819
2318
  await directoryDB.deleteDocument((0, types_3.aclRuleDocId)(ruleId), {
1820
2319
  signingKeyPair: adminSigningKeyPair,
@@ -1833,10 +2332,9 @@ class BaseMindooTenantDirectory {
1833
2332
  async addTrustedWitness(witness, administrationPrivateKey, administrationPrivateKeyPassword) {
1834
2333
  const baseTenant = this.tenant;
1835
2334
  const directoryDB = await this.getDirectoryDB();
1836
- const adminSigningKeyPair = {
1837
- publicKey: baseTenant.getAdministrationPublicKey(),
1838
- privateKey: administrationPrivateKey,
1839
- };
2335
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2336
+ // Trusted witnesses decide whose receipt times count (§6.13): root only.
2337
+ await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "add trusted witnesses");
1840
2338
  const fingerprint = await this.sha256Hex(witness.witnessPublicKey);
1841
2339
  const doc = await this.getOrCreateAclDoc(directoryDB, (0, types_2.aclTrustedWitnessDocId)(fingerprint), adminSigningKeyPair, administrationPrivateKeyPassword);
1842
2340
  await directoryDB.changeDoc(doc, async (d) => {
@@ -1853,10 +2351,8 @@ class BaseMindooTenantDirectory {
1853
2351
  async removeTrustedWitness(witnessPublicKey, administrationPrivateKey, administrationPrivateKeyPassword) {
1854
2352
  const baseTenant = this.tenant;
1855
2353
  const directoryDB = await this.getDirectoryDB();
1856
- const adminSigningKeyPair = {
1857
- publicKey: baseTenant.getAdministrationPublicKey(),
1858
- privateKey: administrationPrivateKey,
1859
- };
2354
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2355
+ await this.rootKeyPair(administrationPrivateKey, administrationPrivateKeyPassword, "remove trusted witnesses");
1860
2356
  const fingerprint = await this.sha256Hex(witnessPublicKey);
1861
2357
  try {
1862
2358
  await directoryDB.deleteDocument((0, types_2.aclTrustedWitnessDocId)(fingerprint), {
@@ -2076,6 +2572,15 @@ class BaseMindooTenantDirectory {
2076
2572
  ? await this.encryptUserDetailsForTenant(this.buildUserDetailsPayload(username, changes.details))
2077
2573
  : null;
2078
2574
  const identityHashes = await this.computeIdentityHashes(username);
2575
+ // Devices this person certified (userkeys.md §9.6) can be revoked and
2576
+ // wiped here like listed ones, although they are not on the grant.
2577
+ const certifiedByHash = new Map();
2578
+ for (const grant of await this.findGrantAccessDocuments(username)) {
2579
+ const hash = grant.getData().username_hash;
2580
+ if (typeof hash === "string" && !certifiedByHash.has(hash)) {
2581
+ certifiedByHash.set(hash, await this.certificatesOfPerson(hash));
2582
+ }
2583
+ }
2079
2584
  await this.editGrantArrays(username, administrationPrivateKey, administrationPrivateKeyPassword, (data) => {
2080
2585
  if (userDetailsEncrypted !== null) {
2081
2586
  data.user_details_encrypted = userDetailsEncrypted;
@@ -2107,12 +2612,52 @@ class BaseMindooTenantDirectory {
2107
2612
  }
2108
2613
  return p;
2109
2614
  });
2110
- (0, grantKeys_1.applyKeyPairFields)(data, next);
2615
+ const certified = typeof data.username_hash === "string" ? certifiedByHash.get(data.username_hash) ?? [] : [];
2616
+ const isCertifiedOnly = (key) => certified.some((c) => (0, DirectoryStateNode_1.signingKeysEqual)(c.deviceSigningPublicKey, key)) &&
2617
+ !(0, grantKeys_1.extractActiveKeyPairs)(data).some((pair) => (0, DirectoryStateNode_1.signingKeysEqual)(pair.signingPublicKey, key));
2618
+ for (const cert of certified) {
2619
+ const key = cert.deviceSigningPublicKey;
2620
+ if (!revokeSet.has(key) || next.some((p) => (0, DirectoryStateNode_1.signingKeysEqual)(p.signingPublicKey, key)))
2621
+ continue;
2622
+ next.push({
2623
+ signingPublicKey: key,
2624
+ encryptionPublicKey: cert.deviceEncryptionPublicKey,
2625
+ ...(cert.label ? { label: cert.label } : {}),
2626
+ revoked: true,
2627
+ revokedAt: now,
2628
+ });
2629
+ }
2630
+ // Restoring a certified device only lifts the revocation; it does not
2631
+ // turn it into a device listed on the grant.
2632
+ const kept = next.filter((p) => !(restoreSet.has(p.signingPublicKey) && isCertifiedOnly(p.signingPublicKey) && !p.revoked));
2633
+ (0, grantKeys_1.applyKeyPairFields)(data, kept);
2111
2634
  // 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));
2635
+ // restricted to keys present on this grant doc or certified by its
2636
+ // person, and never for restored keys.
2637
+ const present = new Set([
2638
+ ...kept.map((p) => p.signingPublicKey),
2639
+ ...certified.map((c) => c.deviceSigningPublicKey),
2640
+ ]);
2114
2641
  const desiredWipe = changes.wipeSigningKeys ?? (0, grantKeys_1.extractWipeRequestedSigningKeys)(data);
2115
2642
  data.wipeRequestedForSigningKeys = Array.from(new Set(desiredWipe.filter((k) => present.has(k) && !restoreSet.has(k))));
2643
+ if (changes.deviceSelfGrant !== undefined) {
2644
+ const existing = (0, grantKeys_1.extractDeviceSelfGrant)(data);
2645
+ if (changes.deviceSelfGrant === null) {
2646
+ if (existing && existing.revokedAt === undefined) {
2647
+ data.deviceSelfGrant = { ...existing, revokedAt: now };
2648
+ }
2649
+ }
2650
+ else {
2651
+ const key = changes.deviceSelfGrant.trim();
2652
+ if (!key)
2653
+ throw new Error("deviceSelfGrant must be a PEM public key or null");
2654
+ if (!existing ||
2655
+ existing.revokedAt !== undefined ||
2656
+ existing.signingPublicKey.replace(/\s+/g, "") !== key.replace(/\s+/g, "")) {
2657
+ data.deviceSelfGrant = { signingPublicKey: key, grantedAt: now };
2658
+ }
2659
+ }
2660
+ }
2116
2661
  });
2117
2662
  }
2118
2663
  /**
@@ -2398,10 +2943,7 @@ class BaseMindooTenantDirectory {
2398
2943
  const docId = (0, types_3.aclKeyDistributionDocId)(keyId);
2399
2944
  const baseTenant = this.tenant;
2400
2945
  const directoryDB = await this.getDirectoryDB();
2401
- const adminSigningKeyPair = {
2402
- publicKey: baseTenant.getAdministrationPublicKey(),
2403
- privateKey: administrationPrivateKey,
2404
- };
2946
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2405
2947
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
2406
2948
  await directoryDB.changeDoc(doc, async (d) => {
2407
2949
  const data = d.getData();
@@ -2518,10 +3060,7 @@ class BaseMindooTenantDirectory {
2518
3060
  async deleteKeyDistribution(keyId, administrationPrivateKey, administrationPrivateKeyPassword) {
2519
3061
  const baseTenant = this.tenant;
2520
3062
  const directoryDB = await this.getDirectoryDB();
2521
- const adminSigningKeyPair = {
2522
- publicKey: baseTenant.getAdministrationPublicKey(),
2523
- privateKey: administrationPrivateKey,
2524
- };
3063
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2525
3064
  try {
2526
3065
  await directoryDB.deleteDocument((0, types_3.aclKeyDistributionDocId)(keyId), {
2527
3066
  signingKeyPair: adminSigningKeyPair,
@@ -2778,10 +3317,7 @@ class BaseMindooTenantDirectory {
2778
3317
  const docId = (0, types_3.aclAppDistributionDocId)(appId);
2779
3318
  const baseTenant = this.tenant;
2780
3319
  const directoryDB = await this.getDirectoryDB();
2781
- const adminSigningKeyPair = {
2782
- publicKey: baseTenant.getAdministrationPublicKey(),
2783
- privateKey: administrationPrivateKey,
2784
- };
3320
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2785
3321
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
2786
3322
  await directoryDB.changeDoc(doc, async (d) => {
2787
3323
  const data = d.getData();
@@ -2890,10 +3426,7 @@ class BaseMindooTenantDirectory {
2890
3426
  async deleteAppDistribution(appId, administrationPrivateKey, administrationPrivateKeyPassword) {
2891
3427
  const baseTenant = this.tenant;
2892
3428
  const directoryDB = await this.getDirectoryDB();
2893
- const adminSigningKeyPair = {
2894
- publicKey: baseTenant.getAdministrationPublicKey(),
2895
- privateKey: administrationPrivateKey,
2896
- };
3429
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
2897
3430
  try {
2898
3431
  await directoryDB.deleteDocument((0, types_3.aclAppDistributionDocId)(appId), {
2899
3432
  signingKeyPair: adminSigningKeyPair,
@@ -3017,10 +3550,7 @@ class BaseMindooTenantDirectory {
3017
3550
  const docId = (0, types_3.aclSyncSetupPolicyDocId)(policyId);
3018
3551
  const baseTenant = this.tenant;
3019
3552
  const directoryDB = await this.getDirectoryDB();
3020
- const adminSigningKeyPair = {
3021
- publicKey: baseTenant.getAdministrationPublicKey(),
3022
- privateKey: administrationPrivateKey,
3023
- };
3553
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3024
3554
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
3025
3555
  await directoryDB.changeDoc(doc, async (d) => {
3026
3556
  const data = d.getData();
@@ -3130,10 +3660,7 @@ class BaseMindooTenantDirectory {
3130
3660
  async deleteSyncSetupPolicy(policyId, administrationPrivateKey, administrationPrivateKeyPassword) {
3131
3661
  const baseTenant = this.tenant;
3132
3662
  const directoryDB = await this.getDirectoryDB();
3133
- const adminSigningKeyPair = {
3134
- publicKey: baseTenant.getAdministrationPublicKey(),
3135
- privateKey: administrationPrivateKey,
3136
- };
3663
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3137
3664
  try {
3138
3665
  await directoryDB.deleteDocument((0, types_3.aclSyncSetupPolicyDocId)(policyId), {
3139
3666
  signingKeyPair: adminSigningKeyPair,
@@ -3212,7 +3739,7 @@ class BaseMindooTenantDirectory {
3212
3739
  * server-readable and carries every variant.
3213
3740
  */
3214
3741
  async getRevokedDecryptionKeyIdsForSigningKey(signingKey) {
3215
- if (signingKey === this.tenant.getAdministrationPublicKey())
3742
+ if (await this.isAdminKeyNow(signingKey))
3216
3743
  return [];
3217
3744
  const lookup = await this.getUserBySigningPublicKey(signingKey);
3218
3745
  if (!lookup)
@@ -3290,10 +3817,7 @@ class BaseMindooTenantDirectory {
3290
3817
  }
3291
3818
  const baseTenant = this.tenant;
3292
3819
  const directoryDB = await this.getDirectoryDB();
3293
- const adminSigningKeyPair = {
3294
- publicKey: baseTenant.getAdministrationPublicKey(),
3295
- privateKey: administrationPrivateKey,
3296
- };
3820
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3297
3821
  for (const grant of grants) {
3298
3822
  const doc = await directoryDB.getDocument(grant.getId());
3299
3823
  await directoryDB.changeDoc(doc, async (d) => {
@@ -3325,9 +3849,9 @@ class BaseMindooTenantDirectory {
3325
3849
  */
3326
3850
  async identitySetForSigningKey(signingKey, node, opts) {
3327
3851
  // 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;
3852
+ // grant document (it authors the directory itself). Delegated admin keys
3853
+ // (§6.13) count while their window covers the evaluation time.
3854
+ 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
3855
  // Audit #4 (revocation lag): resolve the acting identity strictly from the
3332
3856
  // directory state at the entry's trusted time `T`. A signing key that was
3333
3857
  // not an ACTIVE grant at `T` carries only the pseudo-tokens
@@ -3337,7 +3861,8 @@ class BaseMindooTenantDirectory {
3337
3861
  // allow-rules until the trust cache refreshed (up to
3338
3862
  // DIRECTORY_SYNC_INTERVAL_MS later). The `node` is the time-`T` snapshot
3339
3863
  // whose `bySigningKey` only contains keys whose grant is active at `T`.
3340
- const grantedAtT = !!(0, DirectoryStateNode_1.grantForSigningKey)(node, signingKey);
3864
+ // A certified device (userkeys.md §9.6) acts for its person like a listed one.
3865
+ const grantedAtT = !!(await this.resolveSignerInNode(node, signingKey, opts.trustedTime ?? node.trustedTimeUpperBound));
3341
3866
  // Resolve the author's username from their signing key. Trusted non-user
3342
3867
  // keys (e.g. server-to-server sync identities) have no grant; treat them as
3343
3868
  // an empty identity that still carries `$everyone`/`$author`. Keys that were
@@ -3560,6 +4085,30 @@ class BaseMindooTenantDirectory {
3560
4085
  if (key.replace(/\s+/g, "") === needle)
3561
4086
  return lookup;
3562
4087
  }
4088
+ // A device the person certified (userkeys.md §9.6). Like the cache above,
4089
+ // this keeps naming the person after the device was revoked; callers that
4090
+ // need "active" check the signing-key universe or resolveSignerToGrant.
4091
+ const certified = await this.certifiedGrantInNode(await this.getDirectoryStateHead(), publicKey, Number.MAX_SAFE_INTEGER, { enforceRevocation: false });
4092
+ if (!certified)
4093
+ return null;
4094
+ const personKeys = [
4095
+ ...certified.grant.signingKeys,
4096
+ ...(certified.grant.revokedSigningKeys ?? []),
4097
+ ];
4098
+ for (const personKey of personKeys) {
4099
+ const lookup = this.userLookupCache.get(personKey) ??
4100
+ [...this.userLookupCache.entries()].find(([k]) => (0, DirectoryStateNode_1.signingKeysEqual)(k, personKey))?.[1];
4101
+ if (lookup) {
4102
+ // Device-specific fields come from the certificate; a join response
4103
+ // wrapped for another device of the person must not be handed out.
4104
+ const { joinResponseWrapped: _other, ...personLookup } = lookup;
4105
+ return {
4106
+ ...personLookup,
4107
+ signingPublicKey: certified.cert.deviceSigningPublicKey,
4108
+ encryptionPublicKey: certified.cert.deviceEncryptionPublicKey,
4109
+ };
4110
+ }
4111
+ }
3563
4112
  return null;
3564
4113
  }
3565
4114
  /**
@@ -3577,12 +4126,20 @@ class BaseMindooTenantDirectory {
3577
4126
  const grants = await this.findGrantAccessDocuments(username);
3578
4127
  const active = new Set();
3579
4128
  const wipeRequested = new Set();
4129
+ const hashes = new Set();
3580
4130
  for (const grant of grants) {
3581
4131
  const data = grant.getData();
3582
4132
  for (const key of (0, grantKeys_1.extractSigningPublicKeys)(data))
3583
4133
  active.add(key);
3584
4134
  for (const key of (0, grantKeys_1.extractWipeRequestedSigningKeys)(data))
3585
4135
  wipeRequested.add(key);
4136
+ if (typeof data.username_hash === "string")
4137
+ hashes.add(data.username_hash);
4138
+ }
4139
+ // Devices the person certified count as active (userkeys.md §9.6).
4140
+ for (const hash of hashes) {
4141
+ for (const cert of await this.listCertifiedDevices(hash))
4142
+ active.add(cert.deviceSigningPublicKey);
3586
4143
  }
3587
4144
  return { active: Array.from(active), wipeRequested: Array.from(wipeRequested) };
3588
4145
  }
@@ -3666,10 +4223,7 @@ class BaseMindooTenantDirectory {
3666
4223
  const requestedAt = (0, timeSource_1.semanticNow)();
3667
4224
  const baseTenant = this.tenant;
3668
4225
  const directoryDB = await this.getDirectoryDB();
3669
- const adminSigningKeyPair = {
3670
- publicKey: baseTenant.getAdministrationPublicKey(),
3671
- privateKey: administrationPrivateKey,
3672
- };
4226
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3673
4227
  const doc = await this.getOrCreateAclDoc(directoryDB, docId, adminSigningKeyPair, administrationPrivateKeyPassword);
3674
4228
  await directoryDB.changeDoc(doc, async (d) => {
3675
4229
  const data = d.getData();
@@ -3748,10 +4302,7 @@ class BaseMindooTenantDirectory {
3748
4302
  async deleteDocHistoryPurge(requestId, administrationPrivateKey, administrationPrivateKeyPassword) {
3749
4303
  const baseTenant = this.tenant;
3750
4304
  const directoryDB = await this.getDirectoryDB();
3751
- const adminSigningKeyPair = {
3752
- publicKey: baseTenant.getAdministrationPublicKey(),
3753
- privateKey: administrationPrivateKey,
3754
- };
4305
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3755
4306
  try {
3756
4307
  await directoryDB.deleteDocument((0, types_3.aclDocHistoryPurgeDocId)(requestId), {
3757
4308
  signingKeyPair: adminSigningKeyPair,
@@ -3800,10 +4351,7 @@ class BaseMindooTenantDirectory {
3800
4351
  async changeTenantSettings(changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
3801
4352
  const baseTenant = this.tenant;
3802
4353
  const directoryDB = await this.getDirectoryDB();
3803
- const adminSigningKeyPair = {
3804
- publicKey: baseTenant.getAdministrationPublicKey(),
3805
- privateKey: administrationPrivateKey,
3806
- };
4354
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3807
4355
  // Get cached settings doc or create new one
3808
4356
  let settingsDoc = await this.getTenantSettings();
3809
4357
  if (!settingsDoc) {
@@ -3827,10 +4375,7 @@ class BaseMindooTenantDirectory {
3827
4375
  async changeDBSettings(dbId, changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
3828
4376
  const baseTenant = this.tenant;
3829
4377
  const directoryDB = await this.getDirectoryDB();
3830
- const adminSigningKeyPair = {
3831
- publicKey: baseTenant.getAdministrationPublicKey(),
3832
- privateKey: administrationPrivateKey,
3833
- };
4378
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3834
4379
  // Get cached settings doc for this dbId or create new one
3835
4380
  let settingsDoc = await this.getDBSettings(dbId);
3836
4381
  if (!settingsDoc) {
@@ -3885,10 +4430,7 @@ class BaseMindooTenantDirectory {
3885
4430
  const normalizedGroupName = this.normalizeGroupName(groupName);
3886
4431
  const baseTenant = this.tenant;
3887
4432
  const directoryDB = await this.getDirectoryDB();
3888
- const adminSigningKeyPair = {
3889
- publicKey: baseTenant.getAdministrationPublicKey(),
3890
- privateKey: administrationPrivateKey,
3891
- };
4433
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3892
4434
  // Compute hashes (salted v2 for new writes) and encrypted values for all
3893
4435
  // new usernames. Reads match both v1 and v2 forms (docs/accesscontrol.md §6.5).
3894
4436
  const newMembersHashes = await Promise.all(username.map(u => this.hashUsernameForWrite(u)));
@@ -3958,10 +4500,7 @@ class BaseMindooTenantDirectory {
3958
4500
  // Group doesn't exist, nothing to do
3959
4501
  return;
3960
4502
  }
3961
- const adminSigningKeyPair = {
3962
- publicKey: baseTenant.getAdministrationPublicKey(),
3963
- privateKey: administrationPrivateKey,
3964
- };
4503
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
3965
4504
  // Compute hashes of usernames to remove. Include both legacy (v1) and
3966
4505
  // salted (v2) forms so members stored under either scheme are removed.
3967
4506
  const hashesToRemove = new Set((await Promise.all(username.map(u => this.usernameHashCandidates(u)))).flat());
@@ -4020,10 +4559,7 @@ class BaseMindooTenantDirectory {
4020
4559
  // Group doesn't exist, nothing to do
4021
4560
  return;
4022
4561
  }
4023
- const adminSigningKeyPair = {
4024
- publicKey: baseTenant.getAdministrationPublicKey(),
4025
- privateKey: administrationPrivateKey,
4026
- };
4562
+ const adminSigningKeyPair = await this.adminKeyPair(administrationPrivateKey, administrationPrivateKeyPassword);
4027
4563
  // Delete the document using the proper API
4028
4564
  await directoryDB.deleteDocument(cachedGroup.docId, {
4029
4565
  signingKeyPair: adminSigningKeyPair,