mindoodb 0.0.1

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 (170) hide show
  1. package/README.md +263 -0
  2. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +5 -0
  3. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -0
  4. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +17 -0
  5. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -0
  6. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +111 -0
  7. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -0
  8. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +237 -0
  9. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -0
  10. package/dist/appendonlystores/network/HttpTransport.d.ts +78 -0
  11. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -0
  12. package/dist/appendonlystores/network/HttpTransport.js +356 -0
  13. package/dist/appendonlystores/network/HttpTransport.js.map +1 -0
  14. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +131 -0
  15. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -0
  16. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +248 -0
  17. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -0
  18. package/dist/appendonlystores/network/index.d.ts +8 -0
  19. package/dist/appendonlystores/network/index.d.ts.map +1 -0
  20. package/dist/appendonlystores/network/index.js +21 -0
  21. package/dist/appendonlystores/network/index.js.map +1 -0
  22. package/dist/browser/crypto/BrowserCryptoAdapter.d.ts +14 -0
  23. package/dist/browser/crypto/BrowserCryptoAdapter.d.ts.map +1 -0
  24. package/dist/browser/crypto/BrowserCryptoAdapter.js +30 -0
  25. package/dist/browser/crypto/BrowserCryptoAdapter.js.map +1 -0
  26. package/dist/browser/index.d.ts +3 -0
  27. package/dist/browser/index.d.ts.map +1 -0
  28. package/dist/browser/index.js +26 -0
  29. package/dist/browser/index.js.map +1 -0
  30. package/dist/core/BaseMindooDB.d.ts +182 -0
  31. package/dist/core/BaseMindooDB.d.ts.map +1 -0
  32. package/dist/core/BaseMindooDB.js +1701 -0
  33. package/dist/core/BaseMindooDB.js.map +1 -0
  34. package/dist/core/BaseMindooTenant.d.ts +155 -0
  35. package/dist/core/BaseMindooTenant.d.ts.map +1 -0
  36. package/dist/core/BaseMindooTenant.js +673 -0
  37. package/dist/core/BaseMindooTenant.js.map +1 -0
  38. package/dist/core/BaseMindooTenantDirectory.d.ts +99 -0
  39. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -0
  40. package/dist/core/BaseMindooTenantDirectory.js +776 -0
  41. package/dist/core/BaseMindooTenantDirectory.js.map +1 -0
  42. package/dist/core/BaseMindooTenantFactory.d.ts +91 -0
  43. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -0
  44. package/dist/core/BaseMindooTenantFactory.js +297 -0
  45. package/dist/core/BaseMindooTenantFactory.js.map +1 -0
  46. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +111 -0
  47. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -0
  48. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +293 -0
  49. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -0
  50. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +78 -0
  51. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -0
  52. package/dist/core/appendonlystores/network/AuthenticationService.js +297 -0
  53. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -0
  54. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +174 -0
  55. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -0
  56. package/dist/core/appendonlystores/network/NetworkTransport.js +3 -0
  57. package/dist/core/appendonlystores/network/NetworkTransport.js.map +1 -0
  58. package/dist/core/appendonlystores/network/types.d.ts +135 -0
  59. package/dist/core/appendonlystores/network/types.d.ts.map +1 -0
  60. package/dist/core/appendonlystores/network/types.js +35 -0
  61. package/dist/core/appendonlystores/network/types.js.map +1 -0
  62. package/dist/core/appendonlystores/types.d.ts +157 -0
  63. package/dist/core/appendonlystores/types.d.ts.map +1 -0
  64. package/dist/core/appendonlystores/types.js +3 -0
  65. package/dist/core/appendonlystores/types.js.map +1 -0
  66. package/dist/core/crypto/CryptoAdapter.d.ts +19 -0
  67. package/dist/core/crypto/CryptoAdapter.d.ts.map +1 -0
  68. package/dist/core/crypto/CryptoAdapter.js +3 -0
  69. package/dist/core/crypto/CryptoAdapter.js.map +1 -0
  70. package/dist/core/crypto/MindooDocSigner.d.ts +106 -0
  71. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -0
  72. package/dist/core/crypto/MindooDocSigner.js +192 -0
  73. package/dist/core/crypto/MindooDocSigner.js.map +1 -0
  74. package/dist/core/crypto/RSAEncryption.d.ts +62 -0
  75. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -0
  76. package/dist/core/crypto/RSAEncryption.js +167 -0
  77. package/dist/core/crypto/RSAEncryption.js.map +1 -0
  78. package/dist/core/errors.d.ts +10 -0
  79. package/dist/core/errors.d.ts.map +1 -0
  80. package/dist/core/errors.js +17 -0
  81. package/dist/core/errors.js.map +1 -0
  82. package/dist/core/index.d.ts +14 -0
  83. package/dist/core/index.d.ts.map +1 -0
  84. package/dist/core/index.js +53 -0
  85. package/dist/core/index.js.map +1 -0
  86. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts +33 -0
  87. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts.map +1 -0
  88. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js +27 -0
  89. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js.map +1 -0
  90. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts +31 -0
  91. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts.map +1 -0
  92. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js +3 -0
  93. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js.map +1 -0
  94. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +52 -0
  95. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -0
  96. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -0
  97. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -0
  98. package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts +44 -0
  99. package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts.map +1 -0
  100. package/dist/core/indexing/virtualviews/ViewEntrySortKey.js +107 -0
  101. package/dist/core/indexing/virtualviews/ViewEntrySortKey.js.map +1 -0
  102. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +28 -0
  103. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -0
  104. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +133 -0
  105. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -0
  106. package/dist/core/indexing/virtualviews/VirtualView.d.ts +109 -0
  107. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -0
  108. package/dist/core/indexing/virtualviews/VirtualView.js +504 -0
  109. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -0
  110. package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts +59 -0
  111. package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts.map +1 -0
  112. package/dist/core/indexing/virtualviews/VirtualViewColumn.js +70 -0
  113. package/dist/core/indexing/virtualviews/VirtualViewColumn.js.map +1 -0
  114. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts +49 -0
  115. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts.map +1 -0
  116. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js +69 -0
  117. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js.map +1 -0
  118. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +121 -0
  119. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -0
  120. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +365 -0
  121. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -0
  122. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +131 -0
  123. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -0
  124. package/dist/core/indexing/virtualviews/VirtualViewFactory.js +236 -0
  125. package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -0
  126. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +330 -0
  127. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -0
  128. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +1010 -0
  129. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -0
  130. package/dist/core/indexing/virtualviews/index.d.ts +53 -0
  131. package/dist/core/indexing/virtualviews/index.d.ts.map +1 -0
  132. package/dist/core/indexing/virtualviews/index.js +89 -0
  133. package/dist/core/indexing/virtualviews/index.js.map +1 -0
  134. package/dist/core/indexing/virtualviews/types.d.ts +115 -0
  135. package/dist/core/indexing/virtualviews/types.d.ts.map +1 -0
  136. package/dist/core/indexing/virtualviews/types.js +175 -0
  137. package/dist/core/indexing/virtualviews/types.js.map +1 -0
  138. package/dist/core/keys/KeyBag.d.ts +122 -0
  139. package/dist/core/keys/KeyBag.d.ts.map +1 -0
  140. package/dist/core/keys/KeyBag.js +401 -0
  141. package/dist/core/keys/KeyBag.js.map +1 -0
  142. package/dist/core/types.d.ts +1004 -0
  143. package/dist/core/types.d.ts.map +1 -0
  144. package/dist/core/types.js +10 -0
  145. package/dist/core/types.js.map +1 -0
  146. package/dist/core/userid/index.d.ts +53 -0
  147. package/dist/core/userid/index.d.ts.map +1 -0
  148. package/dist/core/userid/index.js +3 -0
  149. package/dist/core/userid/index.js.map +1 -0
  150. package/dist/core/utils/idGeneration.d.ts +109 -0
  151. package/dist/core/utils/idGeneration.d.ts.map +1 -0
  152. package/dist/core/utils/idGeneration.js +182 -0
  153. package/dist/core/utils/idGeneration.js.map +1 -0
  154. package/dist/core/utils/index.d.ts +5 -0
  155. package/dist/core/utils/index.d.ts.map +1 -0
  156. package/dist/core/utils/index.js +19 -0
  157. package/dist/core/utils/index.js.map +1 -0
  158. package/dist/index.d.ts +7 -0
  159. package/dist/index.d.ts.map +1 -0
  160. package/dist/index.js +40 -0
  161. package/dist/index.js.map +1 -0
  162. package/dist/node/crypto/NodeCryptoAdapter.d.ts +16 -0
  163. package/dist/node/crypto/NodeCryptoAdapter.d.ts.map +1 -0
  164. package/dist/node/crypto/NodeCryptoAdapter.js +53 -0
  165. package/dist/node/crypto/NodeCryptoAdapter.js.map +1 -0
  166. package/dist/node/index.d.ts +3 -0
  167. package/dist/node/index.d.ts.map +1 -0
  168. package/dist/node/index.js +26 -0
  169. package/dist/node/index.js.map +1 -0
  170. package/package.json +58 -0
@@ -0,0 +1,776 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseMindooTenantDirectory = void 0;
4
+ const types_1 = require("./types");
5
+ const RSAEncryption_1 = require("./crypto/RSAEncryption");
6
+ class BaseMindooTenantDirectory {
7
+ constructor(tenant) {
8
+ this.directoryDB = null;
9
+ // Cache for trusted public keys: key -> isActive (true = granted, false = revoked)
10
+ // This cache is updated incrementally as changes are processed
11
+ // Note: No recursion guard needed because directory DB is admin-only,
12
+ // so loading entries doesn't trigger validatePublicSigningKey recursively
13
+ this.trustedKeysCache = new Map();
14
+ // Mapping from grant document ID to public key (needed for revocation lookups)
15
+ this.grantDocIdToPublicKey = new Map();
16
+ // Cache for settings documents
17
+ this.tenantSettingsCache = null;
18
+ this.dbSettingsCache = new Map();
19
+ // Cache for groups: key -> merged group data (key: lowercase groupName)
20
+ // We store merged data separately to avoid mutating MindooDoc objects
21
+ // members_hashes are used for lookups, members_encrypted only admin can decrypt
22
+ this.groupsCache = new Map();
23
+ // Unified cache cursor for all document types
24
+ this.unifiedCacheLastCursor = null;
25
+ this.tenant = tenant;
26
+ }
27
+ async getDirectoryDB() {
28
+ if (!this.directoryDB) {
29
+ // Open directory DB with admin-only flag (also enforced at tenant level)
30
+ this.directoryDB = await this.tenant.openDB("directory", { adminOnlyDb: true });
31
+ // Defensive check: verify the DB was opened with admin-only mode
32
+ // This is a security invariant that must always hold
33
+ if (!this.directoryDB.isAdminOnlyDb()) {
34
+ throw new Error("Directory database must be opened with adminOnlyDb=true for security");
35
+ }
36
+ }
37
+ return this.directoryDB;
38
+ }
39
+ async registerUser(userId, administrationPrivateKey, administrationPrivateKeyPassword) {
40
+ console.log(`[BaseMindooTenantDirectory] Registering user: ${userId.username}`);
41
+ // Check if user with same username (case-insensitive) already exists
42
+ const existingDocs = await this.findGrantAccessDocuments(userId.username);
43
+ if (existingDocs.length > 0) {
44
+ // Check if the keys match the existing registration
45
+ const existingDoc = existingDocs[existingDocs.length - 1]; // Use most recent
46
+ const existingData = existingDoc.getData();
47
+ const keysMatch = existingData.userSigningPublicKey === userId.userSigningPublicKey &&
48
+ existingData.userEncryptionPublicKey === userId.userEncryptionPublicKey;
49
+ if (keysMatch) {
50
+ // Same user with same keys - skip re-registration
51
+ console.log(`[BaseMindooTenantDirectory] User ${userId.username} already registered with same keys, skipping`);
52
+ return;
53
+ }
54
+ else {
55
+ // Different keys for same username - this is an error
56
+ throw new Error(`Cannot register user "${userId.username}": a user with the same username (case-insensitive) ` +
57
+ `is already registered with different keys`);
58
+ }
59
+ }
60
+ // Cast to BaseMindooTenant to access protected methods
61
+ const baseTenant = this.tenant;
62
+ // Create SigningKeyPair for the administration key
63
+ const adminSigningKeyPair = {
64
+ publicKey: baseTenant.getAdministrationPublicKey(),
65
+ privateKey: administrationPrivateKey,
66
+ };
67
+ // Compute username hash and encrypted username before creating the document
68
+ // (since the callback to changeDocWithSigningKey cannot be async for automerge reasons)
69
+ const usernameHash = await this.hashUsername(userId.username);
70
+ const usernameEncrypted = await this.encryptForAdmin(userId.username);
71
+ // Add user to directory database
72
+ console.log(`[BaseMindooTenantDirectory] Creating document for user registration`);
73
+ const directoryDB = await this.getDirectoryDB();
74
+ // Create document with admin signing key so the initial entry is trusted
75
+ // Use PUBLIC_INFOS_KEY_ID so servers can validate users without full tenant access
76
+ const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
77
+ console.log(`[BaseMindooTenantDirectory] Document created: ${newDoc.getId()}`);
78
+ try {
79
+ // Set the document data fields - changeDocWithSigningKey handles signing at entry level
80
+ await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
81
+ const data = doc.getData();
82
+ data.form = "useroperation";
83
+ data.type = "grantaccess";
84
+ data.username_hash = usernameHash;
85
+ data.username_encrypted = usernameEncrypted;
86
+ data.userSigningPublicKey = userId.userSigningPublicKey;
87
+ data.userEncryptionPublicKey = userId.userEncryptionPublicKey;
88
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
89
+ }
90
+ catch (error) {
91
+ console.error(`[BaseMindooTenantDirectory] ERROR in changeDocWithSigningKey:`, error);
92
+ console.error(`[BaseMindooTenantDirectory] Error type: ${error instanceof Error ? error.constructor.name : typeof error}`);
93
+ console.error(`[BaseMindooTenantDirectory] Error message: ${error instanceof Error ? error.message : String(error)}`);
94
+ if (error instanceof Error && error.stack) {
95
+ console.error(`[BaseMindooTenantDirectory] Error stack: ${error.stack}`);
96
+ }
97
+ throw error;
98
+ }
99
+ console.log(`[BaseMindooTenantDirectory] Registered user: ${userId.username}`);
100
+ }
101
+ async findGrantAccessDocuments(username) {
102
+ console.log(`[BaseMindooTenantDirectory] Finding grant access documents for username: ${username}`);
103
+ // Sync changes to make sure everything is processed
104
+ const directoryDB = await this.getDirectoryDB();
105
+ await directoryDB.syncStoreChanges();
106
+ // Update unified cache to ensure we have latest data
107
+ await this.updateUnifiedCache();
108
+ // Compute the hash to search for
109
+ const targetHash = await this.hashUsername(username);
110
+ const matchingDocs = [];
111
+ const revokedDocIds = new Set();
112
+ // Use the generator-based iteration API for cleaner code
113
+ // No signature verification needed - DB already enforces admin-only
114
+ for await (const { doc } of directoryDB.iterateChangesSince(null, 100)) {
115
+ const data = doc.getData();
116
+ // Check if this is a grant access document we're looking for (by username_hash)
117
+ if (data.form === "useroperation" &&
118
+ data.type === "grantaccess" &&
119
+ data.username_hash === targetHash) {
120
+ console.log(`[BaseMindooTenantDirectory] Found grant access document for username hash: ${targetHash}`);
121
+ matchingDocs.push(doc);
122
+ }
123
+ // Check if this is a revoke access document for the same username (by username_hash)
124
+ if (data.form === "useroperation" &&
125
+ data.type === "revokeaccess" &&
126
+ data.username_hash === targetHash &&
127
+ data.revokeDocId &&
128
+ typeof data.revokeDocId === "string") {
129
+ console.log(`[BaseMindooTenantDirectory] Found revoke access document for username hash: ${targetHash}, revoking doc ID: ${data.revokeDocId}`);
130
+ revokedDocIds.add(data.revokeDocId);
131
+ }
132
+ }
133
+ // Filter out revoked documents
134
+ const activeDocs = matchingDocs.filter(doc => !revokedDocIds.has(doc.getId()));
135
+ if (activeDocs.length === 0) {
136
+ console.log(`[BaseMindooTenantDirectory] No active grant access documents found for username: ${username} (found ${matchingDocs.length} total, ${revokedDocIds.size} revoked)`);
137
+ }
138
+ else {
139
+ console.log(`[BaseMindooTenantDirectory] Found ${activeDocs.length} active grant access document(s) for username: ${username} (${matchingDocs.length} total, ${revokedDocIds.size} revoked)`);
140
+ }
141
+ return activeDocs;
142
+ }
143
+ async revokeUser(username, requestDataWipe, administrationPrivateKey, administrationPrivateKeyPassword) {
144
+ console.log(`[BaseMindooTenantDirectory] Revoking user: ${username}`);
145
+ // Find all grant access documents for this user
146
+ const grantAccessDocs = await this.findGrantAccessDocuments(username);
147
+ // If no grant access documents found, exit early
148
+ if (grantAccessDocs.length === 0) {
149
+ console.log(`[BaseMindooTenantDirectory] No grant access documents found for ${username}, exiting revocation`);
150
+ return;
151
+ }
152
+ // Cast to BaseMindooTenant to access protected methods
153
+ const baseTenant = this.tenant;
154
+ const directoryDB = await this.getDirectoryDB();
155
+ // Create SigningKeyPair for the administration key
156
+ const adminSigningKeyPair = {
157
+ publicKey: baseTenant.getAdministrationPublicKey(),
158
+ privateKey: administrationPrivateKey,
159
+ };
160
+ // Compute username hash and encrypted username before creating documents
161
+ const usernameHash = await this.hashUsername(username);
162
+ const usernameEncrypted = await this.encryptForAdmin(username);
163
+ // Create revocation documents for each grant access document found
164
+ for (const grantAccessDoc of grantAccessDocs) {
165
+ const revokeDocId = grantAccessDoc.getId();
166
+ // Create document with admin signing key so the initial entry is trusted
167
+ // Use PUBLIC_INFOS_KEY_ID so servers can validate users without full tenant access
168
+ const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
169
+ // Set the document data fields - changeDocWithSigningKey handles signing at entry level
170
+ await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
171
+ const data = doc.getData();
172
+ data.form = "useroperation";
173
+ data.type = "revokeaccess";
174
+ data.username_hash = usernameHash;
175
+ data.username_encrypted = usernameEncrypted;
176
+ data.revokeDocId = revokeDocId;
177
+ data.requestDataWipe = requestDataWipe;
178
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
179
+ console.log(`[BaseMindooTenantDirectory] Created revocation document for grant access doc: ${revokeDocId}`);
180
+ }
181
+ console.log(`[BaseMindooTenantDirectory] Revoked user: ${username} (created ${grantAccessDocs.length} revocation document(s))`);
182
+ }
183
+ async validatePublicSigningKey(publicKey) {
184
+ console.log(`[BaseMindooTenantDirectory] Validating public signing key`);
185
+ // Cast to BaseMindooTenant to access protected methods
186
+ const baseTenant = this.tenant;
187
+ // Get administration public key - this is always trusted as the root of trust
188
+ const administrationPublicKey = baseTenant.getAdministrationPublicKey();
189
+ // The administration key is always trusted - it's the root of trust for the tenant
190
+ if (publicKey === administrationPublicKey) {
191
+ console.log(`[BaseMindooTenantDirectory] Public key is administration key, trusted`);
192
+ return true;
193
+ }
194
+ // Check cache first - if we have a cached result, return it after ensuring we're up to date
195
+ // Sync changes to make sure everything is processed
196
+ const directoryDB = await this.getDirectoryDB();
197
+ await directoryDB.syncStoreChanges();
198
+ // Update unified cache with any new changes since last cursor
199
+ await this.updateUnifiedCache();
200
+ // Now check the cache
201
+ const cachedResult = this.trustedKeysCache.get(publicKey);
202
+ if (cachedResult !== undefined) {
203
+ console.log(`[BaseMindooTenantDirectory] Public key validation result (from cache): ${cachedResult}`);
204
+ return cachedResult;
205
+ }
206
+ // Key not found in cache means it was never granted access
207
+ console.log(`[BaseMindooTenantDirectory] Public key not found in cache, returning false`);
208
+ return false;
209
+ }
210
+ /**
211
+ * Update all caches (trusted keys, groups, settings) by processing new changes since the last cursor.
212
+ * This unified method processes all document types in a single loop for efficiency.
213
+ * No signature verification is needed since the DB already enforces admin-only access.
214
+ */
215
+ async updateUnifiedCache() {
216
+ const directoryDB = await this.getDirectoryDB();
217
+ // Determine starting cursor (null = process all, otherwise incremental)
218
+ const startCursor = this.unifiedCacheLastCursor;
219
+ // If processing from the beginning, clear all caches first
220
+ if (startCursor === null) {
221
+ this.trustedKeysCache.clear();
222
+ this.grantDocIdToPublicKey.clear();
223
+ this.tenantSettingsCache = null;
224
+ this.dbSettingsCache.clear();
225
+ this.groupsCache.clear();
226
+ }
227
+ // Track group documents by name for merging (handles offline sync scenarios)
228
+ const groupDocsByName = new Map();
229
+ // Process changes (documents are returned in order by lastModified, oldest to newest)
230
+ for await (const { doc, cursor } of directoryDB.iterateChangesSince(startCursor, 100)) {
231
+ const data = doc.getData();
232
+ // Process user operation documents (grant/revoke access)
233
+ if (data.form === "useroperation") {
234
+ // Check if this is a grant access document
235
+ if (data.type === "grantaccess" &&
236
+ data.userSigningPublicKey &&
237
+ typeof data.userSigningPublicKey === "string") {
238
+ const userPublicKey = data.userSigningPublicKey;
239
+ console.log(`[BaseMindooTenantDirectory] Cache: adding trusted key from grant access document`);
240
+ // Mark as active (true) unless already revoked
241
+ if (!this.trustedKeysCache.has(userPublicKey) || this.trustedKeysCache.get(userPublicKey) === true) {
242
+ this.trustedKeysCache.set(userPublicKey, true);
243
+ }
244
+ // Store mapping for future revocation lookups (persisted across iterations)
245
+ this.grantDocIdToPublicKey.set(doc.getId(), userPublicKey);
246
+ }
247
+ // Check if this is a revoke access document
248
+ if (data.type === "revokeaccess" &&
249
+ data.revokeDocId &&
250
+ typeof data.revokeDocId === "string") {
251
+ const revokedDocId = data.revokeDocId;
252
+ // Find the public key for this grant doc ID and mark as revoked
253
+ // Use the persistent mapping that survives across iterations
254
+ const revokedPublicKey = this.grantDocIdToPublicKey.get(revokedDocId);
255
+ if (revokedPublicKey) {
256
+ console.log(`[BaseMindooTenantDirectory] Cache: revoking key for doc ${revokedDocId}`);
257
+ this.trustedKeysCache.set(revokedPublicKey, false);
258
+ }
259
+ else {
260
+ console.warn(`[BaseMindooTenantDirectory] Cache: revocation for unknown grant doc ${revokedDocId}`);
261
+ }
262
+ }
263
+ }
264
+ // Process group documents
265
+ if (data.form === "group" &&
266
+ data.type === "group" &&
267
+ data.groupName &&
268
+ typeof data.groupName === "string") {
269
+ const normalizedGroupName = this.normalizeGroupName(data.groupName);
270
+ if (!groupDocsByName.has(normalizedGroupName)) {
271
+ groupDocsByName.set(normalizedGroupName, []);
272
+ }
273
+ groupDocsByName.get(normalizedGroupName).push(doc);
274
+ }
275
+ // Process tenant settings (no signature verification needed - Automerge handles merging)
276
+ if (data.form === "tenantsettings") {
277
+ // Just overwrite - last one seen will be the latest
278
+ this.tenantSettingsCache = doc;
279
+ }
280
+ // Process DB settings (no signature verification needed - Automerge handles merging)
281
+ if (data.form === "dbsettings" && data.dbid && typeof data.dbid === "string") {
282
+ const dbId = data.dbid;
283
+ // Just overwrite - last one seen will be the latest
284
+ this.dbSettingsCache.set(dbId, doc);
285
+ }
286
+ // Update cursor after each document
287
+ this.unifiedCacheLastCursor = cursor;
288
+ }
289
+ // Merge group documents with the same name
290
+ for (const [normalizedGroupName, docs] of groupDocsByName.entries()) {
291
+ // Collect all member hashes from all documents with this group name
292
+ const allMembersHashes = new Set();
293
+ let latestDoc = null;
294
+ let isDeleted = false;
295
+ for (const doc of docs) {
296
+ const data = doc.getData();
297
+ // Track the latest document (by iteration order, which is by lastModified)
298
+ latestDoc = doc;
299
+ // Check if document is deleted (via isDeleted() or _deleted flag in data)
300
+ if (doc.isDeleted() || data._deleted === true) {
301
+ isDeleted = true;
302
+ }
303
+ // Use members_hashes for the cache (privacy-preserving lookups)
304
+ if (data.members_hashes && Array.isArray(data.members_hashes)) {
305
+ for (const memberHash of data.members_hashes) {
306
+ if (typeof memberHash === "string") {
307
+ allMembersHashes.add(memberHash);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ // If latest version is deleted, remove from cache
313
+ if (isDeleted) {
314
+ this.groupsCache.delete(normalizedGroupName);
315
+ continue;
316
+ }
317
+ // Store merged group data in cache (without mutating the MindooDoc)
318
+ if (latestDoc) {
319
+ this.groupsCache.set(normalizedGroupName, {
320
+ docId: latestDoc.getId(),
321
+ members_hashes: Array.from(allMembersHashes),
322
+ });
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Normalize group name to lowercase for case-insensitive comparison.
328
+ */
329
+ normalizeGroupName(name) {
330
+ return name.toLowerCase();
331
+ }
332
+ /**
333
+ * Generate username variants with wildcards for hierarchical matching.
334
+ * For example: "CN=john/OU=team1/O=example.com" becomes an array containing:
335
+ * - The original username
336
+ * - Wildcard variants like "*\/OU=team1/O=example.com", "*\/O=example.com", and "*"
337
+ */
338
+ generateUsernameVariants(username) {
339
+ const variants = [username]; // Start with original username
340
+ // Parse the username format: CN=<name>/OU=<ou>/O=<org> (may have multiple OU components)
341
+ // Split by '/' to get components
342
+ const parts = username.split('/');
343
+ // Generate variants by replacing prefixes with a single wildcard
344
+ // For "CN=john/OU=team1/O=example.com":
345
+ // - */OU=team1/O=example.com (skip CN)
346
+ // - */O=example.com (skip CN and OU)
347
+ for (let i = 1; i < parts.length; i++) {
348
+ const suffixParts = parts.slice(i);
349
+ variants.push('*/' + suffixParts.join('/'));
350
+ }
351
+ // Always add the final wildcard
352
+ variants.push('*');
353
+ return variants;
354
+ }
355
+ /**
356
+ * Recursively resolve all groups that contain the given username (directly or via nested groups).
357
+ * This resolves upwards: if user is in group A, and group A is in group B, then user is also in group B.
358
+ * Stops on cycles with a console warning.
359
+ */
360
+ async resolveGroupsForUser(username) {
361
+ const resultGroups = new Set();
362
+ // Sync changes and update cache
363
+ const directoryDB = await this.getDirectoryDB();
364
+ await directoryDB.syncStoreChanges();
365
+ await this.updateUnifiedCache();
366
+ // Generate username variants for matching and compute their hashes
367
+ const usernameVariants = this.generateUsernameVariants(username);
368
+ // Hash all variants for comparison with members_hashes
369
+ const variantHashes = new Set(await Promise.all(usernameVariants.map(v => this.hashUsername(v))));
370
+ // First pass: find all groups that directly contain the user or username variants (by hash)
371
+ const directGroups = new Set();
372
+ for (const [groupName, groupData] of this.groupsCache.entries()) {
373
+ for (const memberHash of groupData.members_hashes) {
374
+ if (variantHashes.has(memberHash)) {
375
+ directGroups.add(groupName);
376
+ resultGroups.add(groupName);
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ // Second pass: find parent groups (groups that contain the user's groups)
382
+ // Groups can be nested - a group name can be a member of another group
383
+ // Keep iterating until no new groups are found
384
+ let groupsToCheck = new Set(directGroups);
385
+ const visitedGroups = new Set(); // Track visited groups for cycle detection
386
+ while (groupsToCheck.size > 0) {
387
+ const nextGroups = new Set();
388
+ for (const childGroup of groupsToCheck) {
389
+ // Skip if already checked (cycle detection)
390
+ if (visitedGroups.has(childGroup)) {
391
+ console.warn(`[BaseMindooTenantDirectory] Cycle detected in group resolution: ${childGroup} already visited, stopping recursion`);
392
+ continue;
393
+ }
394
+ visitedGroups.add(childGroup);
395
+ // Hash the child group name for comparison
396
+ const childGroupHash = await this.hashUsername(childGroup);
397
+ // Find all groups that contain this child group as a member (by hash)
398
+ for (const [parentGroupName, parentGroupData] of this.groupsCache.entries()) {
399
+ if (resultGroups.has(parentGroupName)) {
400
+ continue; // Already found this group
401
+ }
402
+ if (parentGroupData.members_hashes.includes(childGroupHash)) {
403
+ resultGroups.add(parentGroupName);
404
+ nextGroups.add(parentGroupName);
405
+ }
406
+ }
407
+ }
408
+ groupsToCheck = nextGroups;
409
+ }
410
+ return Array.from(resultGroups);
411
+ }
412
+ /**
413
+ * Get a user's public keys from the directory.
414
+ * Used for authentication (signature verification) and encryption (transport encryption).
415
+ *
416
+ * @param username The username to look up (format: "CN=<username>/O=<tenantId>")
417
+ * @return The user's public keys, or null if user not found or has been revoked
418
+ */
419
+ async getUserPublicKeys(username) {
420
+ console.log(`[BaseMindooTenantDirectory] Getting public keys for username: ${username}`);
421
+ // Find all active (non-revoked) grant access documents for this user
422
+ const activeDocs = await this.findGrantAccessDocuments(username);
423
+ if (activeDocs.length === 0) {
424
+ console.log(`[BaseMindooTenantDirectory] No active grant access documents found for username: ${username}`);
425
+ return null;
426
+ }
427
+ // Use the last active grant access document (most recent registration)
428
+ // In most cases there should only be one active grant access document per user
429
+ const doc = activeDocs[activeDocs.length - 1];
430
+ const data = doc.getData();
431
+ const signingPublicKey = data.userSigningPublicKey;
432
+ const encryptionPublicKey = data.userEncryptionPublicKey;
433
+ if (typeof signingPublicKey !== "string" || typeof encryptionPublicKey !== "string") {
434
+ console.error(`[BaseMindooTenantDirectory] Invalid key data in grant access document for username: ${username}`);
435
+ return null;
436
+ }
437
+ console.log(`[BaseMindooTenantDirectory] Found public keys for username: ${username}`);
438
+ return {
439
+ signingPublicKey,
440
+ encryptionPublicKey,
441
+ };
442
+ }
443
+ /**
444
+ * Check if a user has been revoked.
445
+ * A user is considered revoked if they have no active (non-revoked) grant access documents.
446
+ *
447
+ * @param username The username to check (format: "CN=<username>/O=<tenantId>")
448
+ * @return True if the user has been revoked, false if they have active access
449
+ */
450
+ async isUserRevoked(username) {
451
+ console.log(`[BaseMindooTenantDirectory] Checking if user is revoked: ${username}`);
452
+ // Find all active grant access documents for this user
453
+ const activeDocs = await this.findGrantAccessDocuments(username);
454
+ // User is revoked if they have no active grant access documents
455
+ const isRevoked = activeDocs.length === 0;
456
+ console.log(`[BaseMindooTenantDirectory] User ${username} revoked: ${isRevoked}`);
457
+ return isRevoked;
458
+ }
459
+ async requestDocHistoryPurge(dbId, docId, reason, administrationPrivateKey, administrationPrivateKeyPassword) {
460
+ console.log(`[BaseMindooTenantDirectory] Requesting purge for document: ${docId} in ${dbId}`);
461
+ const baseTenant = this.tenant;
462
+ const directoryDB = await this.getDirectoryDB();
463
+ const adminSigningKeyPair = {
464
+ publicKey: baseTenant.getAdministrationPublicKey(),
465
+ privateKey: administrationPrivateKey,
466
+ };
467
+ // Create document with admin signing key so the initial entry is trusted
468
+ const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
469
+ // Set the document data fields - changeDocWithSigningKey handles signing at entry level
470
+ await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
471
+ const data = doc.getData();
472
+ data.form = "useroperation";
473
+ data.type = "requestdochistorypurge";
474
+ data.dbId = dbId;
475
+ data.docId = docId;
476
+ if (reason) {
477
+ data.reason = reason;
478
+ }
479
+ data.requestedAt = Date.now();
480
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
481
+ console.log(`[BaseMindooTenantDirectory] Created purge request for ${docId} in ${dbId}`);
482
+ }
483
+ async getRequestedDocHistoryPurges() {
484
+ const directoryDB = await this.getDirectoryDB();
485
+ await directoryDB.syncStoreChanges();
486
+ const purgeRequests = [];
487
+ // No signature verification needed - DB already enforces admin-only
488
+ for await (const { doc } of directoryDB.iterateChangesSince(null, 100)) {
489
+ const data = doc.getData();
490
+ if (data.form === "useroperation" && data.type === "requestdochistorypurge") {
491
+ const dbId = data.dbId;
492
+ const docId = data.docId;
493
+ const reason = data.reason;
494
+ const requestedAt = data.requestedAt;
495
+ if (typeof dbId === "string" && typeof docId === "string") {
496
+ purgeRequests.push({
497
+ dbId: dbId,
498
+ docId: docId,
499
+ reason: typeof reason === "string" ? reason : undefined,
500
+ requestedAt: typeof requestedAt === "number" ? requestedAt : Date.now(),
501
+ purgeRequestDocId: doc.getId(),
502
+ });
503
+ }
504
+ }
505
+ }
506
+ return purgeRequests;
507
+ }
508
+ async getTenantSettings() {
509
+ const directoryDB = await this.getDirectoryDB();
510
+ await directoryDB.syncStoreChanges();
511
+ // Update unified cache if directory has new changes
512
+ await this.updateUnifiedCache();
513
+ return this.tenantSettingsCache;
514
+ }
515
+ async getDBSettings(dbId) {
516
+ const directoryDB = await this.getDirectoryDB();
517
+ await directoryDB.syncStoreChanges();
518
+ // Update unified cache if directory has new changes
519
+ await this.updateUnifiedCache();
520
+ return this.dbSettingsCache.get(dbId) || null;
521
+ }
522
+ async changeTenantSettings(changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
523
+ const baseTenant = this.tenant;
524
+ const directoryDB = await this.getDirectoryDB();
525
+ const adminSigningKeyPair = {
526
+ publicKey: baseTenant.getAdministrationPublicKey(),
527
+ privateKey: administrationPrivateKey,
528
+ };
529
+ // Get cached settings doc or create new one
530
+ let settingsDoc = await this.getTenantSettings();
531
+ if (!settingsDoc) {
532
+ // Create new document
533
+ settingsDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
534
+ }
535
+ // Apply changes and ensure form field is correct
536
+ await directoryDB.changeDocWithSigningKey(settingsDoc, async (d) => {
537
+ // Call user's change function
538
+ await changeFunc(d);
539
+ // Ensure form field is always correct (overwrite if user changed it)
540
+ d.getData().form = "tenantsettings";
541
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
542
+ // Invalidate cache
543
+ this.tenantSettingsCache = null;
544
+ this.unifiedCacheLastCursor = null;
545
+ }
546
+ async changeDBSettings(dbId, changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
547
+ const baseTenant = this.tenant;
548
+ const directoryDB = await this.getDirectoryDB();
549
+ const adminSigningKeyPair = {
550
+ publicKey: baseTenant.getAdministrationPublicKey(),
551
+ privateKey: administrationPrivateKey,
552
+ };
553
+ // Get cached settings doc for this dbId or create new one
554
+ let settingsDoc = await this.getDBSettings(dbId);
555
+ if (!settingsDoc) {
556
+ // Create new document
557
+ settingsDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
558
+ }
559
+ // Apply changes and ensure form and dbid fields are correct
560
+ await directoryDB.changeDocWithSigningKey(settingsDoc, async (d) => {
561
+ // Call user's change function
562
+ await changeFunc(d);
563
+ // Ensure form and dbid fields are always correct (overwrite if user changed them)
564
+ const data = d.getData();
565
+ data.form = "dbsettings";
566
+ data.dbid = dbId;
567
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
568
+ // Invalidate cache for this dbId
569
+ this.dbSettingsCache.delete(dbId);
570
+ this.unifiedCacheLastCursor = null;
571
+ }
572
+ async getGroups() {
573
+ const directoryDB = await this.getDirectoryDB();
574
+ await directoryDB.syncStoreChanges();
575
+ // Update unified cache to ensure we have latest data
576
+ await this.updateUnifiedCache();
577
+ // Return array of group names (keys from groupsCache)
578
+ return Array.from(this.groupsCache.keys());
579
+ }
580
+ async getGroupMembers(groupName) {
581
+ const normalizedGroupName = this.normalizeGroupName(groupName);
582
+ const directoryDB = await this.getDirectoryDB();
583
+ await directoryDB.syncStoreChanges();
584
+ // Update unified cache to ensure we have latest data
585
+ await this.updateUnifiedCache();
586
+ // Look up group in cache
587
+ const groupData = this.groupsCache.get(normalizedGroupName);
588
+ if (!groupData) {
589
+ return [];
590
+ }
591
+ // Return member hashes (actual usernames are encrypted and only admin can decrypt)
592
+ // This allows checking membership via hash comparison
593
+ return [...groupData.members_hashes];
594
+ }
595
+ async addUsersToGroup(groupName, username, administrationPrivateKey, administrationPrivateKeyPassword) {
596
+ const normalizedGroupName = this.normalizeGroupName(groupName);
597
+ const baseTenant = this.tenant;
598
+ const directoryDB = await this.getDirectoryDB();
599
+ const adminSigningKeyPair = {
600
+ publicKey: baseTenant.getAdministrationPublicKey(),
601
+ privateKey: administrationPrivateKey,
602
+ };
603
+ // Compute hashes and encrypted values for all new usernames
604
+ const newMembersHashes = await Promise.all(username.map(u => this.hashUsername(u)));
605
+ const newMembersEncrypted = await Promise.all(username.map(u => this.encryptForAdmin(u)));
606
+ // Get the actual document from the database (using cached docId if available)
607
+ const cachedGroup = this.groupsCache.get(normalizedGroupName);
608
+ let groupDoc;
609
+ if (cachedGroup) {
610
+ // Load the actual document from the database
611
+ groupDoc = await directoryDB.getDocument(cachedGroup.docId);
612
+ }
613
+ else {
614
+ // Create new document with PUBLIC_INFOS_KEY_ID for server access
615
+ groupDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
616
+ }
617
+ // Apply changes and ensure form, type, and groupName fields are correct
618
+ await directoryDB.changeDocWithSigningKey(groupDoc, async (d) => {
619
+ const data = d.getData();
620
+ // Get existing arrays (copy them to avoid mutating in place)
621
+ const existingHashes = Array.isArray(data.members_hashes)
622
+ ? data.members_hashes.filter((h) => typeof h === "string")
623
+ : [];
624
+ const existingEncrypted = Array.isArray(data.members_encrypted)
625
+ ? data.members_encrypted.filter((e) => typeof e === "string")
626
+ : [];
627
+ // Build new arrays with existing + new members (avoiding duplicates by hash)
628
+ const existingHashesSet = new Set(existingHashes);
629
+ const updatedHashes = [...existingHashes];
630
+ const updatedEncrypted = [...existingEncrypted];
631
+ for (let i = 0; i < newMembersHashes.length; i++) {
632
+ if (!existingHashesSet.has(newMembersHashes[i])) {
633
+ updatedHashes.push(newMembersHashes[i]);
634
+ updatedEncrypted.push(newMembersEncrypted[i]);
635
+ existingHashesSet.add(newMembersHashes[i]);
636
+ }
637
+ }
638
+ // Assign the complete arrays (this triggers the proxy's set handler)
639
+ data.members_hashes = updatedHashes;
640
+ data.members_encrypted = updatedEncrypted;
641
+ // Ensure form, type, and groupName fields are always correct
642
+ data.form = "group";
643
+ data.type = "group";
644
+ data.groupName = normalizedGroupName;
645
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
646
+ // Invalidate cache for this group
647
+ this.groupsCache.delete(normalizedGroupName);
648
+ this.unifiedCacheLastCursor = null;
649
+ }
650
+ async removeUsersFromGroup(groupName, username, administrationPrivateKey, administrationPrivateKeyPassword) {
651
+ const normalizedGroupName = this.normalizeGroupName(groupName);
652
+ const baseTenant = this.tenant;
653
+ const directoryDB = await this.getDirectoryDB();
654
+ // Sync and update cache to get latest group
655
+ await directoryDB.syncStoreChanges();
656
+ await this.updateUnifiedCache();
657
+ // Get group from cache
658
+ const cachedGroup = this.groupsCache.get(normalizedGroupName);
659
+ if (!cachedGroup) {
660
+ // Group doesn't exist, nothing to do
661
+ return;
662
+ }
663
+ // Load the actual document from the database
664
+ const groupDoc = await directoryDB.getDocument(cachedGroup.docId);
665
+ const adminSigningKeyPair = {
666
+ publicKey: baseTenant.getAdministrationPublicKey(),
667
+ privateKey: administrationPrivateKey,
668
+ };
669
+ // Compute hashes of usernames to remove
670
+ const hashesToRemove = new Set(await Promise.all(username.map(u => this.hashUsername(u))));
671
+ // Remove users from members arrays
672
+ await directoryDB.changeDocWithSigningKey(groupDoc, async (d) => {
673
+ const data = d.getData();
674
+ if (!data.members_hashes || !Array.isArray(data.members_hashes) ||
675
+ !data.members_encrypted || !Array.isArray(data.members_encrypted)) {
676
+ return;
677
+ }
678
+ // Filter out members whose hashes match the ones to remove
679
+ // Keep arrays in sync by filtering indices
680
+ const indicesToKeep = [];
681
+ const hashesArray = data.members_hashes;
682
+ for (let i = 0; i < hashesArray.length; i++) {
683
+ if (!hashesToRemove.has(hashesArray[i])) {
684
+ indicesToKeep.push(i);
685
+ }
686
+ }
687
+ const encryptedArray = data.members_encrypted;
688
+ data.members_hashes = indicesToKeep.map(i => hashesArray[i]);
689
+ data.members_encrypted = indicesToKeep.map(i => encryptedArray[i]);
690
+ // Ensure form, type, and groupName fields are always correct
691
+ data.form = "group";
692
+ data.type = "group";
693
+ data.groupName = normalizedGroupName;
694
+ }, adminSigningKeyPair, administrationPrivateKeyPassword);
695
+ // Invalidate cache for this group
696
+ this.groupsCache.delete(normalizedGroupName);
697
+ this.unifiedCacheLastCursor = null;
698
+ }
699
+ async deleteGroup(groupName, administrationPrivateKey, administrationPrivateKeyPassword) {
700
+ const normalizedGroupName = this.normalizeGroupName(groupName);
701
+ const baseTenant = this.tenant;
702
+ const directoryDB = await this.getDirectoryDB();
703
+ // Sync and update cache to get latest group
704
+ await directoryDB.syncStoreChanges();
705
+ await this.updateUnifiedCache();
706
+ // Get group from cache
707
+ const cachedGroup = this.groupsCache.get(normalizedGroupName);
708
+ if (!cachedGroup) {
709
+ // Group doesn't exist, nothing to do
710
+ return;
711
+ }
712
+ const adminSigningKeyPair = {
713
+ publicKey: baseTenant.getAdministrationPublicKey(),
714
+ privateKey: administrationPrivateKey,
715
+ };
716
+ // Delete the document using the proper API
717
+ await directoryDB.deleteDocumentWithSigningKey(cachedGroup.docId, adminSigningKeyPair, administrationPrivateKeyPassword);
718
+ // Remove from cache
719
+ this.groupsCache.delete(normalizedGroupName);
720
+ this.unifiedCacheLastCursor = null;
721
+ }
722
+ async getUserNamesList(username) {
723
+ // Generate username variants (wildcards)
724
+ const usernameVariants = this.generateUsernameVariants(username);
725
+ const result = new Set(usernameVariants);
726
+ // Find all groups containing the username (directly or via nested groups)
727
+ const groups = await this.resolveGroupsForUser(username);
728
+ // Add all groups to result
729
+ for (const group of groups) {
730
+ result.add(group);
731
+ }
732
+ return Array.from(result);
733
+ }
734
+ /**
735
+ * Compute SHA-256 hash of lowercase username for lookups.
736
+ * This enables searching for users without exposing the actual username.
737
+ *
738
+ * @param username The username to hash
739
+ * @return The hex-encoded SHA-256 hash of the lowercase username
740
+ */
741
+ async hashUsername(username) {
742
+ const encoder = new TextEncoder();
743
+ const data = encoder.encode(username.toLowerCase());
744
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
745
+ const hashBuffer = await subtle.digest('SHA-256', data);
746
+ return Array.from(new Uint8Array(hashBuffer))
747
+ .map(b => b.toString(16).padStart(2, '0'))
748
+ .join('');
749
+ }
750
+ /**
751
+ * Encrypt plaintext with admin's RSA public encryption key.
752
+ * Only the admin with the corresponding private key can decrypt.
753
+ *
754
+ * @param plaintext The text to encrypt
755
+ * @return The base64-encoded encrypted data
756
+ */
757
+ async encryptForAdmin(plaintext) {
758
+ const adminEncKey = this.tenant.getAdministrationEncryptionPublicKey();
759
+ const rsaEnc = new RSAEncryption_1.RSAEncryption(this.tenant.getCryptoAdapter());
760
+ const encoder = new TextEncoder();
761
+ const encrypted = await rsaEnc.encrypt(encoder.encode(plaintext), adminEncKey);
762
+ return this.uint8ArrayToBase64(encrypted);
763
+ }
764
+ /**
765
+ * Convert a Uint8Array to a base64 string.
766
+ */
767
+ uint8ArrayToBase64(bytes) {
768
+ let binary = "";
769
+ for (let i = 0; i < bytes.length; i++) {
770
+ binary += String.fromCharCode(bytes[i]);
771
+ }
772
+ return btoa(binary);
773
+ }
774
+ }
775
+ exports.BaseMindooTenantDirectory = BaseMindooTenantDirectory;
776
+ //# sourceMappingURL=BaseMindooTenantDirectory.js.map