mindoodb 0.0.3 → 0.0.5

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 (214) hide show
  1. package/README-server.md +1030 -0
  2. package/README.md +14 -26
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +27 -0
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +270 -121
  6. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  7. package/dist/appendonlystores/network/HttpTransport.d.ts +15 -0
  8. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  9. package/dist/appendonlystores/network/HttpTransport.js +235 -10
  10. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +5 -0
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  13. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +40 -4
  14. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  15. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +10 -3
  16. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
  17. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +138 -13
  18. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  19. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +21 -0
  20. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -0
  21. package/dist/browser/cache/IndexedDBLocalCacheStore.js +106 -0
  22. package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -0
  23. package/dist/browser/index.d.ts +3 -0
  24. package/dist/browser/index.d.ts.map +1 -1
  25. package/dist/browser/index.js +9 -1
  26. package/dist/browser/index.js.map +1 -1
  27. package/dist/core/BaseMindooDB.d.ts +108 -13
  28. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  29. package/dist/core/BaseMindooDB.js +1268 -292
  30. package/dist/core/BaseMindooDB.js.map +1 -1
  31. package/dist/core/BaseMindooTenant.d.ts +31 -1
  32. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  33. package/dist/core/BaseMindooTenant.js +212 -129
  34. package/dist/core/BaseMindooTenant.js.map +1 -1
  35. package/dist/core/BaseMindooTenantDirectory.d.ts +13 -10
  36. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  37. package/dist/core/BaseMindooTenantDirectory.js +134 -27
  38. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  39. package/dist/core/BaseMindooTenantFactory.d.ts +5 -1
  40. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  41. package/dist/core/BaseMindooTenantFactory.js +46 -69
  42. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  43. package/dist/core/MindooDBServerAdmin.d.ts +241 -0
  44. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -0
  45. package/dist/core/MindooDBServerAdmin.js +401 -0
  46. package/dist/core/MindooDBServerAdmin.js.map +1 -0
  47. package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts +22 -0
  48. package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts.map +1 -0
  49. package/dist/core/appendonlystores/AttachmentReadPlanner.js +124 -0
  50. package/dist/core/appendonlystores/AttachmentReadPlanner.js.map +1 -0
  51. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +10 -4
  52. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  53. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +44 -9
  54. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  55. package/dist/core/appendonlystores/MaterializationPlanner.d.ts +84 -0
  56. package/dist/core/appendonlystores/MaterializationPlanner.d.ts.map +1 -0
  57. package/dist/core/appendonlystores/MaterializationPlanner.js +326 -0
  58. package/dist/core/appendonlystores/MaterializationPlanner.js.map +1 -0
  59. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +33 -0
  60. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  61. package/dist/core/appendonlystores/network/types.d.ts +35 -0
  62. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  63. package/dist/core/appendonlystores/network/types.js +2 -0
  64. package/dist/core/appendonlystores/network/types.js.map +1 -1
  65. package/dist/core/appendonlystores/types.d.ts +174 -3
  66. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  67. package/dist/core/cache/CacheManager.d.ts +75 -0
  68. package/dist/core/cache/CacheManager.d.ts.map +1 -0
  69. package/dist/core/cache/CacheManager.js +101 -0
  70. package/dist/core/cache/CacheManager.js.map +1 -0
  71. package/dist/core/cache/EncryptedLocalCacheStore.d.ts +41 -0
  72. package/dist/core/cache/EncryptedLocalCacheStore.d.ts.map +1 -0
  73. package/dist/core/cache/EncryptedLocalCacheStore.js +120 -0
  74. package/dist/core/cache/EncryptedLocalCacheStore.js.map +1 -0
  75. package/dist/core/cache/LocalCacheStore.d.ts +50 -0
  76. package/dist/core/cache/LocalCacheStore.d.ts.map +1 -0
  77. package/dist/core/cache/LocalCacheStore.js +39 -0
  78. package/dist/core/cache/LocalCacheStore.js.map +1 -0
  79. package/dist/core/crypto/privateKeyEncryption.d.ts +5 -0
  80. package/dist/core/crypto/privateKeyEncryption.d.ts.map +1 -0
  81. package/dist/core/crypto/privateKeyEncryption.js +87 -0
  82. package/dist/core/crypto/privateKeyEncryption.js.map +1 -0
  83. package/dist/core/index.d.ts +13 -6
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/core/index.js +13 -1
  86. package/dist/core/index.js.map +1 -1
  87. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +4 -1
  88. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
  89. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +39 -11
  90. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  91. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +1 -0
  92. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -1
  93. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +3 -0
  94. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -1
  95. package/dist/core/indexing/virtualviews/VirtualView.d.ts +43 -0
  96. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
  97. package/dist/core/indexing/virtualviews/VirtualView.js +173 -0
  98. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
  99. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +7 -0
  100. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
  101. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +28 -0
  102. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
  103. package/dist/core/keys/KeyBag.d.ts +38 -14
  104. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  105. package/dist/core/keys/KeyBag.js +102 -76
  106. package/dist/core/keys/KeyBag.js.map +1 -1
  107. package/dist/core/keys/KeyContext.d.ts +3 -3
  108. package/dist/core/keys/KeyContext.d.ts.map +1 -1
  109. package/dist/core/keys/KeyContext.js +10 -4
  110. package/dist/core/keys/KeyContext.js.map +1 -1
  111. package/dist/core/types.d.ts +341 -22
  112. package/dist/core/types.d.ts.map +1 -1
  113. package/dist/core/types.js +7 -1
  114. package/dist/core/types.js.map +1 -1
  115. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  116. package/dist/core/utils/idGeneration.js +37 -3
  117. package/dist/core/utils/idGeneration.js.map +1 -1
  118. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +31 -7
  119. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  120. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +139 -17
  121. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  122. package/dist/node/attachments/writeAttachmentToFile.d.ts +15 -0
  123. package/dist/node/attachments/writeAttachmentToFile.d.ts.map +1 -0
  124. package/dist/node/attachments/writeAttachmentToFile.js +73 -0
  125. package/dist/node/attachments/writeAttachmentToFile.js.map +1 -0
  126. package/dist/node/cache/FileSystemLocalCacheStore.d.ts +17 -0
  127. package/dist/node/cache/FileSystemLocalCacheStore.d.ts.map +1 -0
  128. package/dist/node/cache/FileSystemLocalCacheStore.js +128 -0
  129. package/dist/node/cache/FileSystemLocalCacheStore.js.map +1 -0
  130. package/dist/node/cli/change-identity-password.d.ts +7 -0
  131. package/dist/node/cli/change-identity-password.d.ts.map +1 -0
  132. package/dist/node/cli/change-identity-password.js +67 -0
  133. package/dist/node/cli/change-identity-password.js.map +1 -0
  134. package/dist/node/cli/cli-utils.d.ts +16 -0
  135. package/dist/node/cli/cli-utils.d.ts.map +1 -0
  136. package/dist/node/cli/cli-utils.js +113 -0
  137. package/dist/node/cli/cli-utils.js.map +1 -0
  138. package/dist/node/cli/identity-export-public.d.ts +4 -0
  139. package/dist/node/cli/identity-export-public.d.ts.map +1 -0
  140. package/dist/node/cli/identity-export-public.js +55 -0
  141. package/dist/node/cli/identity-export-public.js.map +1 -0
  142. package/dist/node/cli/identity-info.d.ts +4 -0
  143. package/dist/node/cli/identity-info.d.ts.map +1 -0
  144. package/dist/node/cli/identity-info.js +57 -0
  145. package/dist/node/cli/identity-info.js.map +1 -0
  146. package/dist/node/index.d.ts +1 -0
  147. package/dist/node/index.d.ts.map +1 -1
  148. package/dist/node/index.js +4 -1
  149. package/dist/node/index.js.map +1 -1
  150. package/dist/node/server/CapabilityMatcher.d.ts +45 -0
  151. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -0
  152. package/dist/node/server/CapabilityMatcher.js +117 -0
  153. package/dist/node/server/CapabilityMatcher.js.map +1 -0
  154. package/dist/node/server/MindooDBServer.d.ts +94 -0
  155. package/dist/node/server/MindooDBServer.d.ts.map +1 -0
  156. package/dist/node/server/MindooDBServer.js +1256 -0
  157. package/dist/node/server/MindooDBServer.js.map +1 -0
  158. package/dist/node/server/ServerSync.d.ts +41 -0
  159. package/dist/node/server/ServerSync.d.ts.map +1 -0
  160. package/dist/node/server/ServerSync.js +187 -0
  161. package/dist/node/server/ServerSync.js.map +1 -0
  162. package/dist/node/server/StoreFactory.d.ts +45 -0
  163. package/dist/node/server/StoreFactory.d.ts.map +1 -0
  164. package/dist/node/server/StoreFactory.js +89 -0
  165. package/dist/node/server/StoreFactory.js.map +1 -0
  166. package/dist/node/server/SystemAdminAuth.d.ts +65 -0
  167. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -0
  168. package/dist/node/server/SystemAdminAuth.js +200 -0
  169. package/dist/node/server/SystemAdminAuth.js.map +1 -0
  170. package/dist/node/server/SystemIpAllowlist.d.ts +21 -0
  171. package/dist/node/server/SystemIpAllowlist.d.ts.map +1 -0
  172. package/dist/node/server/SystemIpAllowlist.js +210 -0
  173. package/dist/node/server/SystemIpAllowlist.js.map +1 -0
  174. package/dist/node/server/TenantManager.d.ts +100 -0
  175. package/dist/node/server/TenantManager.d.ts.map +1 -0
  176. package/dist/node/server/TenantManager.js +615 -0
  177. package/dist/node/server/TenantManager.js.map +1 -0
  178. package/dist/node/server/add-to-network.d.ts +20 -0
  179. package/dist/node/server/add-to-network.d.ts.map +1 -0
  180. package/dist/node/server/add-to-network.js +275 -0
  181. package/dist/node/server/add-to-network.js.map +1 -0
  182. package/dist/node/server/config.d.ts +51 -0
  183. package/dist/node/server/config.d.ts.map +1 -0
  184. package/dist/node/server/config.js +218 -0
  185. package/dist/node/server/config.js.map +1 -0
  186. package/dist/node/server/resolveServerPassword.d.ts +7 -0
  187. package/dist/node/server/resolveServerPassword.d.ts.map +1 -0
  188. package/dist/node/server/resolveServerPassword.js +25 -0
  189. package/dist/node/server/resolveServerPassword.js.map +1 -0
  190. package/dist/node/server/server.d.ts +22 -0
  191. package/dist/node/server/server.d.ts.map +1 -0
  192. package/dist/node/server/server.js +225 -0
  193. package/dist/node/server/server.js.map +1 -0
  194. package/dist/node/server/serverinit.d.ts +21 -0
  195. package/dist/node/server/serverinit.d.ts.map +1 -0
  196. package/dist/node/server/serverinit.js +317 -0
  197. package/dist/node/server/serverinit.js.map +1 -0
  198. package/dist/node/server/types.d.ts +167 -0
  199. package/dist/node/server/types.d.ts.map +1 -0
  200. package/dist/node/server/types.js +24 -0
  201. package/dist/node/server/types.js.map +1 -0
  202. package/dist/node/server/validation.d.ts +47 -0
  203. package/dist/node/server/validation.d.ts.map +1 -0
  204. package/dist/node/server/validation.js +114 -0
  205. package/dist/node/server/validation.js.map +1 -0
  206. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts +48 -0
  207. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts.map +1 -0
  208. package/dist/reactnative/cache/MMKVLocalCacheStore.js +113 -0
  209. package/dist/reactnative/cache/MMKVLocalCacheStore.js.map +1 -0
  210. package/dist/reactnative/index.d.ts +1 -0
  211. package/dist/reactnative/index.d.ts.map +1 -1
  212. package/dist/reactnative/index.js +4 -1
  213. package/dist/reactnative/index.js.map +1 -1
  214. package/package.json +15 -3
@@ -0,0 +1,1030 @@
1
+ # MindooDB reference server
2
+
3
+ Node.js/Express sources: `src/node/server/`. Docker: `src/node/server/Dockerfile`.
4
+
5
+ A Node.js/Express server implementing the MindooDB sync API with:
6
+
7
+ - **Client-to-server sync** — clients push/pull encrypted entries
8
+ - **Server-to-server mirroring** — servers relay ciphertext without decryption
9
+ - **Capabilities-based system admin security** — challenge/response auth with JWT and fine-grained route authorization
10
+ - **Multi-tenant** — each tenant is isolated with its own config, keybag, and stores
11
+
12
+ ## Prerequisites
13
+
14
+ - **Docker** — the recommended way to build and run the server
15
+ - Node.js 20 or later (only needed for local development without Docker)
16
+
17
+ ## Quick Start
18
+
19
+ The fastest way to get a server running is the interactive setup script. It prompts for the data directory first, detects whether this is a fresh setup or an existing deployment, and then asks only for the values needed for the chosen path (for example server name and password for fresh setup, or just bind settings for safe update). It then builds the Docker image and initialises the server identity when needed (including optional system admin creation):
20
+
21
+ ```bash
22
+ # 1. Clone and enter the repo
23
+ git clone https://github.com/klehmann/MindooDB.git && cd MindooDB
24
+
25
+ # 2. Run the interactive setup
26
+ bash serversetup.sh
27
+
28
+ # 3. Start the server
29
+ docker compose up -d
30
+
31
+ # 4. Verify
32
+ curl http://localhost:1661/health
33
+ ```
34
+
35
+ The setup script:
36
+ - builds the `mindoodb-server` Docker image
37
+ - creates the data directory (`../mindoodb-data/server`) and password file (`../mindoodb-data/.server_unlock`, mode 600)
38
+ - initialises the server identity and optionally creates a system admin keypair interactively
39
+ - writes a `docker-compose.override.yml` with your current host uid/gid, bind mounts, and one or more published port bindings based on your chosen bind address
40
+
41
+ For an existing deployment, use one of these update paths instead of reinitialising blindly:
42
+
43
+ ```bash
44
+ # Safe interactive update for an existing server
45
+ bash serversetup.sh --update
46
+
47
+ # Or rebuild the image and restart without touching setup files
48
+ docker compose up -d --build
49
+ ```
50
+
51
+ `bash serversetup.sh --update` preserves the existing `server.identity.json`, `server.keybag`, `config.json`, tenant data, `trusted-servers.json`, and `.server_unlock`, while still rebuilding the Docker image and regenerating `docker-compose.override.yml`. Before the override is rewritten, the script now saves the previous file as `docker-compose.override.<timestamp>.yml`.
52
+
53
+ After setup, manage the server with:
54
+
55
+ ```bash
56
+ docker compose up -d # start
57
+ docker compose down # stop
58
+ docker compose logs -f # follow logs
59
+ docker compose up -d --build # rebuild image and restart
60
+ ```
61
+
62
+ When rerunning `bash serversetup.sh` without `--update`, the script now detects an existing `server.identity.json` and offers three choices:
63
+
64
+ - safe update (preserve identity, keybag, config, tenant data, and password file)
65
+ - overwrite identity (destructive re-init)
66
+ - abort
67
+
68
+ Only choose overwrite if you intentionally want to replace the server identity and reinitialise the deployment.
69
+
70
+ For identity utilities on a Docker-deployed server, use the wrapper script from the repository root:
71
+
72
+ ```bash
73
+ ./mindoodb-cli.sh identity:info server.identity.json
74
+ ./mindoodb-cli.sh identity:change-password server.identity.json
75
+ ./mindoodb-cli.sh identity:export-public system-admin-cn-sysadmin-o-myorg.identity.json --output ./system-admin.public-identity.json
76
+ ```
77
+
78
+ Files inside the mounted server data directory can be referenced by filename alone. Running `./mindoodb-cli.sh` without arguments prints a command overview, and each `identity:*` command prints its own help when called without the required parameters.
79
+
80
+ ### What was created on disk?
81
+
82
+ After running `serversetup.sh` (with system admin creation), the data directory contains:
83
+
84
+ ```
85
+ ../mindoodb-data/server/
86
+ ├── server.identity.json # Server keypair (Ed25519 + RSA, password-encrypted)
87
+ ├── config.json # Admin access rules — who can call which /system/* endpoints
88
+ ├── trusted-servers.json # Public keys of remote servers trusted for sync (initially empty)
89
+ └── system-admin-cn-sysadmin-o-myorg.identity.json # System admin keypair (password-encrypted)
90
+ ```
91
+
92
+ The most important file for day-to-day operations is **`config.json`**. After a fresh setup with one system admin, it looks like this:
93
+
94
+ ```json
95
+ {
96
+ "capabilities": {
97
+ "ALL:/system/*": [
98
+ {
99
+ "username": "cn=sysadmin/o=myorg",
100
+ "publicsignkey": "-----BEGIN PUBLIC KEY-----\nMCow...base64...\n-----END PUBLIC KEY-----"
101
+ }
102
+ ]
103
+ }
104
+ }
105
+ ```
106
+
107
+ This single rule grants the admin `cn=sysadmin/o=myorg` full access to **all** `/system/*` endpoints -- tenant management, trusted server management, config updates, and everything else. The next section explains how to customize these rules.
108
+
109
+ ### What's next?
110
+
111
+ Once the server is healthy, a nice real-world workflow is:
112
+
113
+ 1. Create the tenant locally. This generates the tenant's own admin identity (`result.adminUser`).
114
+ 2. Share the tenant admin's **public** identity with the server owner.
115
+ 3. The server owner grants that admin access to `POST /system/tenants/...` via `MindooDBServerAdmin`, after which the tenant admin can publish the tenant directly.
116
+
117
+ ### 1. Create the tenant locally and export a public identity
118
+
119
+ ```typescript
120
+ import { writeFileSync } from "fs";
121
+ import { BaseMindooTenantFactory, InMemoryContentAddressedStoreFactory } from "mindoodb";
122
+
123
+ const factory = new BaseMindooTenantFactory(new InMemoryContentAddressedStoreFactory());
124
+
125
+ const result = await factory.createTenant({
126
+ tenantId: "acme",
127
+ adminName: "cn=admin/o=acme",
128
+ adminPassword: "admin-pass",
129
+ userName: "cn=alice/o=acme",
130
+ userPassword: "alice-pass",
131
+ });
132
+
133
+ const tenantAdminPublic = factory.toPublicUserId(result.adminUser);
134
+
135
+ writeFileSync(
136
+ "./acme-admin.public-identity.json",
137
+ JSON.stringify(tenantAdminPublic, null, 2),
138
+ "utf-8",
139
+ );
140
+ ```
141
+
142
+ The file `acme-admin.public-identity.json` contains only the tenant admin's public keys, so it can be shared with the server owner.
143
+
144
+ ### 2. Server owner grants tenant-creation access
145
+
146
+ The server owner loads the tenant admin's public identity and grants access for one tenant, a prefix, or all tenants:
147
+
148
+ ```typescript
149
+ import { readFileSync } from "fs";
150
+ import {
151
+ BaseMindooTenantFactory,
152
+ InMemoryContentAddressedStoreFactory,
153
+ MindooDBServerAdmin,
154
+ } from "mindoodb";
155
+ import { NodeCryptoAdapter } from "mindoodb/node";
156
+
157
+ const factory = new BaseMindooTenantFactory(new InMemoryContentAddressedStoreFactory());
158
+ const tenantAdminPublic = JSON.parse(
159
+ readFileSync("./acme-admin.public-identity.json", "utf-8"),
160
+ );
161
+ const systemAdminIdentity = JSON.parse(
162
+ readFileSync(
163
+ "../mindoodb-data/server/system-admin-cn-sysadmin-o-myorg.identity.json",
164
+ "utf-8",
165
+ ),
166
+ );
167
+
168
+ const serverAdmin = new MindooDBServerAdmin({
169
+ serverUrl: "http://localhost:1661",
170
+ systemAdminUser: systemAdminIdentity,
171
+ systemAdminPassword: "sysadmin-pass",
172
+ cryptoAdapter: new NodeCryptoAdapter(),
173
+ });
174
+
175
+ await serverAdmin.grantSystemAdminAccess(
176
+ {
177
+ username: tenantAdminPublic.username,
178
+ publicsignkey: tenantAdminPublic.userSigningPublicKey,
179
+ },
180
+ [
181
+ "POST:/system/tenants/acme", // exactly one tenant
182
+ // "POST:/system/tenants/customer-*", // or a naming prefix
183
+ // "POST:/system/tenants/*", // or all tenants
184
+ ],
185
+ );
186
+ ```
187
+
188
+ At that point the tenant admin is recognized by `/system/auth/challenge` and is allowed to create only the tenants covered by the granted rules.
189
+
190
+ ### 3. Tenant admin publishes the tenant
191
+
192
+ Now the tenant admin can authenticate with their own private identity and publish the tenant:
193
+
194
+ ```typescript
195
+ await result.tenant.publishToServer("http://localhost:1661", {
196
+ systemAdminUser: result.adminUser,
197
+ systemAdminPassword: "admin-pass",
198
+ adminUsername: result.adminUser.username,
199
+ registerUsers: [factory.toPublicUserId(result.appUser)],
200
+ });
201
+ ```
202
+
203
+ Here, `systemAdminUser` is the delegated tenant admin identity, while `adminUsername: result.adminUser.username` tells the server which tenant admin identity should be stored in the new tenant's `config.json`.
204
+
205
+ ```typescript
206
+ const remoteStore = await result.tenant.connectToServer(
207
+ "http://localhost:1661",
208
+ "main",
209
+ );
210
+
211
+ const db = await result.tenant.openDB("main");
212
+
213
+ await db.pushChangesTo(remoteStore);
214
+
215
+ await db.pullChangesFrom(remoteStore);
216
+ await db.syncStoreChanges();
217
+ ```
218
+
219
+ ## How `config.json` controls admin access
220
+
221
+ MindooDB uses a **capabilities-based model** for server administration. Instead of a single shared API key, each admin has their own Ed25519 keypair, and `config.json` declares exactly which endpoints each admin is allowed to call.
222
+
223
+ The server has two authentication tiers:
224
+
225
+ 1. **System admin** — `config.json` capabilities + Ed25519 challenge/response + short-lived JWT for `/system/*`. Optional **`MINDOODB_ADMIN_ALLOWED_IPS`** restricts which client IPs may use the `/system/*` HTTP surface.
226
+ 2. **User (per-tenant)** — Ed25519 challenge-response for sync endpoints (`/:tenantId/sync/*`). Users are authenticated against the tenant directory (admin-signed).
227
+
228
+ This section covers tier 1 -- system admin access. For the design rationale behind this model, see [Server Security](docs/server-security.md).
229
+
230
+ ### Rule format
231
+
232
+ Each rule in `config.json` is a key-value pair where:
233
+
234
+ - The **key** is `METHOD:PATHPATTERN` — an HTTP method (or `ALL`) paired with a URL path pattern
235
+ - The **value** is an array of **principals** (admin identities), each identified by `username` + `publicsignkey`
236
+
237
+ ```json
238
+ {
239
+ "capabilities": {
240
+ "METHOD:PATHPATTERN": [
241
+ { "username": "<admin-username>", "publicsignkey": "<ed25519-public-key-pem>" }
242
+ ]
243
+ }
244
+ }
245
+ ```
246
+
247
+ **METHOD** can be `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, or `ALL` (matches any HTTP method).
248
+
249
+ **PATHPATTERN** is a URL path that may end with `*` to match any remaining characters.
250
+
251
+ ### Wildcard patterns
252
+
253
+ | Rule | What it grants |
254
+ |------|---------------|
255
+ | `ALL:/system/*` | Full access to all system endpoints |
256
+ | `POST:/system/tenants/*` | Can create any tenant (but not delete, update, or list) |
257
+ | `POST:/system/tenants/acme` | Can only create the specific tenant `acme` |
258
+ | `PUT:/system/tenants/company-*` | Can update any tenant whose ID starts with `company-` |
259
+ | `GET:/system/tenants` | Read-only: can list tenants but not modify anything |
260
+ | `DELETE:/system/tenants/*` | Can delete any tenant |
261
+
262
+ ### Multiple admins with different roles
263
+
264
+ A realistic production config might look like this:
265
+
266
+ ```json
267
+ {
268
+ "capabilities": {
269
+ "ALL:/system/*": [
270
+ { "username": "cn=admin/o=myorg", "publicsignkey": "<key-admin>" }
271
+ ],
272
+ "POST:/system/tenants/*": [
273
+ { "username": "cn=provisioner/o=myorg", "publicsignkey": "<key-provisioner>" }
274
+ ],
275
+ "GET:/system/tenants": [
276
+ { "username": "cn=auditor/o=myorg", "publicsignkey": "<key-auditor>" }
277
+ ]
278
+ }
279
+ }
280
+ ```
281
+
282
+ - **`cn=admin`** is the super-admin with full access
283
+ - **`cn=provisioner`** can create tenants but cannot delete them, manage trusted servers, or change the config
284
+ - **`cn=auditor`** can list tenants (read-only) but has no write access
285
+
286
+ To delegate tenant management for a specific prefix to a team, add a scoped rule:
287
+
288
+ ```json
289
+ "PUT:/system/tenants/team-alpha-*": [
290
+ { "username": "cn=lead/o=alpha", "publicsignkey": "<key-lead>" }
291
+ ]
292
+ ```
293
+
294
+ ### How matching works at request time
295
+
296
+ On each `/system/*` request the server:
297
+
298
+ 1. Extracts the HTTP method and path from the request
299
+ 2. Validates the JWT and extracts `username` + `publicsignkey` from its payload
300
+ 3. Finds all capability rules whose method and path pattern match
301
+ 4. Unions all principal entries from matching rules
302
+ 5. **Allows** the request if any entry has **both** a matching `username` AND `publicsignkey`
303
+
304
+ A principal is identified by the combination of username + public key. Two admins may share a username but have different keys -- they are treated as distinct identities.
305
+
306
+ ### Updating config at runtime
307
+
308
+ You do not need to restart the server to change access rules. Use the system admin API:
309
+
310
+ - **`GET /system/config`** — read the current config
311
+ - **`PUT /system/config`** — replace the config (takes effect immediately)
312
+ - **`GET /system/config/backups`** — list previous config snapshots created by runtime updates
313
+ - **`GET /system/config/backups/:backupFile`** — read one previous validated config snapshot
314
+
315
+ Before overwriting, the server creates a timestamped backup (e.g., `config.2026-03-27T16-30-45.123Z.json`). Self-lockout protection rejects any change that would remove the calling admin's own `PUT /system/config` access.
316
+
317
+ In Node you can use `MindooDBServerAdmin` instead of raw HTTP:
318
+
319
+ ```typescript
320
+ const admin = new MindooDBServerAdmin({
321
+ serverUrl: "http://localhost:1661",
322
+ systemAdminUser: adminIdentity,
323
+ systemAdminPassword: "your-admin-password",
324
+ cryptoAdapter: new NodeCryptoAdapter(),
325
+ });
326
+
327
+ await admin.grantSystemAdminAccess(
328
+ {
329
+ username: "cn=newauditor/o=myorg",
330
+ publicsignkey: "<new-auditor-key>",
331
+ },
332
+ ["GET:/system/tenants"],
333
+ );
334
+
335
+ const access = await admin.findSystemAdminAccess({
336
+ username: "cn=newauditor/o=myorg",
337
+ publicsignkey: "<new-auditor-key>",
338
+ });
339
+
340
+ const backups = await admin.listConfigBackups();
341
+ const previousConfig = await admin.getConfigBackup(backups[0].file);
342
+ ```
343
+
344
+ #### Demo server `config.json` examples
345
+
346
+ For isolated demo environments, MindooDB Server also supports a special wildcard principal:
347
+
348
+ ```json
349
+ { "username": "*", "publicsignkey": "*" }
350
+ ```
351
+
352
+ This wildcard is intentionally narrow:
353
+
354
+ - It is only valid on `POST:/system/tenants/...` capability rules
355
+ - It allows any username + any signing key to authenticate for tenant creation
356
+ - It does **not** unlock `GET /system/tenants`, `PUT /system/config`, or any other `/system/*` route
357
+
358
+ Open demo server (allow creation of any tenant name):
359
+
360
+ ```json
361
+ {
362
+ "capabilities": {
363
+ "POST:/system/tenants/*": [
364
+ { "username": "*", "publicsignkey": "*" }
365
+ ]
366
+ }
367
+ }
368
+ ```
369
+
370
+ Prefix-restricted demo server (allow only tenant names starting with `demo_`):
371
+
372
+ ```json
373
+ {
374
+ "capabilities": {
375
+ "POST:/system/tenants/demo_*": [
376
+ { "username": "*", "publicsignkey": "*" }
377
+ ]
378
+ }
379
+ }
380
+ ```
381
+
382
+ These examples are useful for public demos or temporary onboarding servers, but they are intentionally less strict than normal production setups. For regular servers, prefer explicit principals with real public keys.
383
+
384
+ For key rotation, adding/removing admins, and the full authentication flow (challenge/response, JWT lifecycle), see [Server Security](docs/server-security.md).
385
+
386
+ ## Walkthrough: Multi-Server Setup
387
+
388
+ MindooDB servers can mirror encrypted data between each other. This section covers the full workflow: initializing servers, establishing trust, and configuring per-tenant sync.
389
+
390
+ ### 1. Initialize servers
391
+
392
+ Run `bash serversetup.sh` on each machine (or in separate data directories for local testing):
393
+
394
+ ```bash
395
+ # Server 1
396
+ bash serversetup.sh
397
+ # Choose: name=server1, data dir=../mindoodb-data-s1, bind 127.0.0.1
398
+
399
+ # Server 2
400
+ bash serversetup.sh
401
+ # Choose: name=server2, data dir=../mindoodb-data-s2, bind 127.0.0.1
402
+ ```
403
+
404
+ Both runs print the server's public keys. Start both servers:
405
+
406
+ ```bash
407
+ docker compose up -d
408
+ ```
409
+
410
+ ### 2. Server discovery
411
+
412
+ Every initialized server exposes its public identity at a well-known URL:
413
+
414
+ ```bash
415
+ curl https://server1.example.com/.well-known/mindoodb-server-info
416
+ ```
417
+
418
+ ```json
419
+ {
420
+ "name": "CN=server1",
421
+ "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
422
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
423
+ }
424
+ ```
425
+
426
+ This eliminates the need to manually copy public keys between servers.
427
+
428
+ ### 3. Establish trust
429
+
430
+ The `add-to-network` CLI automates mutual trust exchange. It fetches each server's public keys from `/.well-known/mindoodb-server-info` and calls `POST /system/trusted-servers` on each side using your system admin identity.
431
+
432
+ ```bash
433
+ printf '%s' 'admin-pass' > ./.admin-password && chmod 600 ./.admin-password
434
+ npm run server:add-to-network -- \
435
+ --new-server http://localhost:3001 \
436
+ --servers http://localhost:1661 \
437
+ --identity ../mindoodb-data-s1/system-admin-cn-sysadmin-o-myorg.identity.json \
438
+ --password-file ./.admin-password
439
+ ```
440
+
441
+ The CLI authenticates with the `--identity` file against every server it contacts (`--new-server` **and** each URL in `--servers`), so that identity's principal must be listed in each server's `config.json`. If a server pair is already trusted, the CLI skips it instead of failing.
442
+
443
+ **Secrets:** Prefer **`--password-file`** so secrets are not placed in the process environment. Use **`MINDOODB_SYSTEM_ADMIN_PASSWORD`** only when you must script without files. A leading space before the command or `HISTCONTROL=ignorespace` reduces shell-history leakage.
444
+
445
+ Alternatively, you can exchange trust manually via curl:
446
+
447
+ ```bash
448
+ curl -X POST http://server1:1661/system/trusted-servers \
449
+ -H "Content-Type: application/json" \
450
+ -H "Authorization: Bearer $SYSTEM_ADMIN_JWT" \
451
+ -d '{
452
+ "name": "CN=server2",
453
+ "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
454
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
455
+ }'
456
+ ```
457
+
458
+ Trust changes take effect immediately -- no restart required.
459
+
460
+ ### 4. Publish tenant to servers
461
+
462
+ Reuse the same system admin identity that is authorized in each server's `config.json`:
463
+
464
+ ```typescript
465
+ await result.tenant.publishToServer("http://server1:1661", {
466
+ systemAdminUser: systemAdminIdentity,
467
+ systemAdminPassword: "sysadmin-pass",
468
+ adminUsername: result.adminUser.username,
469
+ });
470
+
471
+ await result.tenant.publishToServer("http://server2:3001", {
472
+ systemAdminUser: systemAdminIdentity,
473
+ systemAdminPassword: "sysadmin-pass",
474
+ adminUsername: result.adminUser.username,
475
+ });
476
+ ```
477
+
478
+ ### 5. Configure per-tenant sync
479
+
480
+ After trust is established, configure which tenants each server syncs. This gives full control over sync topology -- not every server needs to sync every tenant.
481
+
482
+ **Add a sync server for a tenant:**
483
+
484
+ ```bash
485
+ curl -X POST http://server1:1661/system/tenants/acme/sync-servers \
486
+ -H "Content-Type: application/json" \
487
+ -H "Authorization: Bearer $SYSTEM_ADMIN_JWT" \
488
+ -d '{
489
+ "name": "CN=server2",
490
+ "url": "http://server2:3001",
491
+ "syncIntervalMs": 60000,
492
+ "databases": ["directory", "main"]
493
+ }'
494
+ ```
495
+
496
+ The `databases` field is required and controls which databases are synced with the remote server. The `name` field identifies the remote server (must match the trusted server name). If a server with the same name already exists for the tenant, it is updated.
497
+
498
+ **List sync servers for a tenant:**
499
+
500
+ ```bash
501
+ curl http://server1:1661/system/tenants/acme/sync-servers \
502
+ -H "Authorization: Bearer $SYSTEM_ADMIN_JWT"
503
+ ```
504
+
505
+ **Remove a sync server from a tenant:**
506
+
507
+ ```bash
508
+ curl -X DELETE http://server1:1661/system/tenants/acme/sync-servers/CN%3Dserver2 \
509
+ -H "Authorization: Bearer $SYSTEM_ADMIN_JWT"
510
+ ```
511
+
512
+ Note: the server name in the URL must be percent-encoded (e.g., `CN%3Dserver2` for `CN=server2`).
513
+
514
+ ### 6. Enable auto-sync
515
+
516
+ Add `--auto-sync` via the `command` key in each server's `docker-compose.override.yml`:
517
+
518
+ ```yaml
519
+ services:
520
+ mindoodb:
521
+ command: ["--auto-sync"]
522
+ ```
523
+
524
+ Then restart:
525
+
526
+ ```bash
527
+ docker compose up -d
528
+ ```
529
+
530
+ The servers will periodically sync all configured tenant databases, relaying encrypted entries without decrypting them. Sync config changes take effect on the next server restart or when auto-sync timers are restarted.
531
+
532
+ ## Data Directory Layout
533
+
534
+ ```
535
+ data/
536
+ ├── server.identity.json # Server keypair (Ed25519 + RSA, password-encrypted)
537
+ ├── config.json # Admin access rules (capabilities) — see "How config.json controls admin access"
538
+ ├── trusted-servers.json # Public keys of remote servers trusted for sync
539
+ ├── system-admin-*.identity.json # System admin keypair (password-encrypted)
540
+ ├── acme/ # Tenant "acme"
541
+ │ ├── config.json # Tenant config (admin keys, users, sync servers)
542
+ │ └── stores/ # Content-addressed entry stores
543
+ └── other-tenant/
544
+ ├── config.json
545
+ └── stores/
546
+ ```
547
+
548
+ ## Configuration File Formats
549
+
550
+ ### `config.json` (global — capabilities)
551
+
552
+ Controls which system admins can call which `/system/*` endpoints. See [How `config.json` controls admin access](#how-configjson-controls-admin-access) for a full explanation of the rule format and wildcard patterns.
553
+
554
+ ```json
555
+ {
556
+ "capabilities": {
557
+ "ALL:/system/*": [
558
+ {
559
+ "username": "cn=sysadmin/o=myorg",
560
+ "publicsignkey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
561
+ }
562
+ ],
563
+ "POST:/system/tenants/*": [
564
+ {
565
+ "username": "cn=provisioner/o=myorg",
566
+ "publicsignkey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
567
+ }
568
+ ]
569
+ }
570
+ }
571
+ ```
572
+
573
+ ### `server.identity.json` (global)
574
+
575
+ Generated by `npm run server:init`. Contains a `PrivateUserId` with encrypted private keys:
576
+
577
+ ```json
578
+ {
579
+ "username": "CN=server1",
580
+ "userSigningKeyPair": {
581
+ "publicKey": "-----BEGIN PUBLIC KEY-----\n...",
582
+ "privateKey": {
583
+ "ciphertext": "...",
584
+ "iv": "...",
585
+ "tag": "...",
586
+ "salt": "...",
587
+ "iterations": 100000
588
+ }
589
+ },
590
+ "userEncryptionKeyPair": {
591
+ "publicKey": "-----BEGIN PUBLIC KEY-----\n...",
592
+ "privateKey": {
593
+ "ciphertext": "...",
594
+ "iv": "...",
595
+ "tag": "...",
596
+ "salt": "...",
597
+ "iterations": 100000
598
+ }
599
+ }
600
+ }
601
+ ```
602
+
603
+ ### `trusted-servers.json` (global)
604
+
605
+ ```json
606
+ [
607
+ {
608
+ "name": "CN=server2",
609
+ "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
610
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
611
+ }
612
+ ]
613
+ ```
614
+
615
+ ### `<tenantId>/config.json`
616
+
617
+ ```json
618
+ {
619
+ "adminUsername": "cn=admin/o=acme",
620
+ "adminSigningPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
621
+ "adminEncryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
622
+ "publicInfosKey": "base64-encoded-aes-key",
623
+ "defaultStoreType": "file",
624
+ "users": [
625
+ {
626
+ "username": "alice",
627
+ "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
628
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
629
+ }
630
+ ],
631
+ "remoteServers": [
632
+ {
633
+ "name": "CN=server2",
634
+ "url": "https://server2.example.com",
635
+ "syncIntervalMs": 60000,
636
+ "databases": ["directory", "main"]
637
+ }
638
+ ]
639
+ }
640
+ ```
641
+
642
+ ## Environment Variables
643
+
644
+ | Variable | Required | Description |
645
+ |----------|----------|-------------|
646
+ | `MINDOODB_SERVER_PASSWORD` | If `server.identity.json` exists and `MINDOODB_SERVER_PASSWORD_FILE` is unset | Password to decrypt server identity and per-tenant keybags |
647
+ | `MINDOODB_SERVER_PASSWORD_FILE` | No | Path to a file whose contents are the server password (trimmed). If set, **used instead of** `MINDOODB_SERVER_PASSWORD`. Prefer for Docker: env only holds the path, not the secret. |
648
+ | `MINDOODB_SYSTEM_ADMIN_PASSWORD` | No | For `server:add-to-network`: password for `--identity` (if not using `--password-file` or interactive prompt). May be visible in `ps` on shared hosts. |
649
+ | `MINDOODB_CORS_ORIGIN` | No | Allowed CORS origin or comma-separated allowlist (e.g., `https://app.example.com` or `http://localhost:4174,https://otherserver.com`). If not set, CORS is disabled. |
650
+ | `MINDOODB_ADMIN_ALLOWED_IPS` | No | Optional comma-separated client IPs/CIDRs allowed to call **`/system/*`** (all system admin routes, including `/system/auth/*`). If unset or `*`, any source IP may reach `/system/*` (JWT + `config.json` capabilities still apply). Example: `127.0.0.1,::1,172.23.248.0/24,2001:db8::/32`. Behind a reverse proxy, configure Express `trust proxy` so `req.ip` is the real client. |
651
+
652
+ > **Note:** The old `MINDOODB_ADMIN_API_KEY` variable has been removed. System admin **authorization** is enforced by `config.json` capabilities and JWTs. **`MINDOODB_ADMIN_ALLOWED_IPS`** is an optional **network** layer for `/system/*` only. See [Server Security](docs/server-security.md).
653
+
654
+ ## CLI Reference
655
+
656
+ ### `npm run server:dev` / `npm run server:start` — Start the server
657
+
658
+ Launches the MindooDB server process. `server:dev` runs via `ts-node` for development; `server:start` runs the compiled JavaScript. The server loads its identity from `server.identity.json` and the capabilities-based authorization config from `config.json` in the data directory.
659
+
660
+ | Option | Alias | Description | Default |
661
+ |--------|-------|-------------|---------|
662
+ | `--data-dir` | `-d` | Data directory path | `./data` |
663
+ | `--port` | `-p` | Server port | `1661` |
664
+ | `--auto-sync` | `-s` | Enable automatic sync with remote servers | disabled |
665
+ | `--static-dir` | `-w` | Serve static files at `/statics/` (e.g. bootstrap UI) | — |
666
+ | `--tls-cert` | — | Path to TLS certificate file (PEM) | — |
667
+ | `--tls-key` | — | Path to TLS private key file (PEM) | — |
668
+ | `--help` | `-h` | Show help message | — |
669
+
670
+ ### `npm run server:init` — Initialize server identity
671
+
672
+ One-time setup that generates the server's Ed25519/RSA keypair (`server.identity.json`) and an empty `trusted-servers.json`. It also interactively offers to create a first system admin keypair and writes the initial `config.json` with that admin's public key in the `capabilities` section.
673
+
674
+ | Option | Alias | Description | Default |
675
+ |--------|-------|-------------|---------|
676
+ | `--name` | `-n` | Server name (e.g., "server1") | **required** |
677
+ | `--data-dir` | `-d` | Data directory path | `./data` |
678
+ | `--force` | `-f` | Overwrite existing identity | — |
679
+ | `--help` | `-h` | Show help message | — |
680
+
681
+ ### `npm run server:add-to-network` — Add a server to the network
682
+
683
+ Automates mutual trust exchange when adding a new server to an existing network. For each existing server it fetches public keys from `/.well-known/mindoodb-server-info` and calls `POST /system/trusted-servers` in both directions so that the new server and every existing server trust each other.
684
+
685
+ | Option | Description | Default |
686
+ |--------|-------------|---------|
687
+ | `--new-server` | URL of the server being added | **required** |
688
+ | `--servers` | Comma-separated URLs of existing servers | **required** |
689
+ | `--identity` | Path to system admin `*.identity.json` (`PrivateUserId`) | **required** |
690
+ | `--password-file` | File containing the system admin password (optional if env or TTY) | — |
691
+ | `--help` | Show help message | — |
692
+
693
+ Password resolution order: read `MINDOODB_SYSTEM_ADMIN_PASSWORD`, then `--password-file`, then a hidden prompt on an interactive TTY. The CLI authenticates with the `--identity` file against every server it contacts (`--new-server` **and** each URL in `--servers`), so that identity's principal (username + public signing key) must be listed in the `capabilities` section of each server's `config.json`.
694
+
695
+ ### `npm run identity:info` — Show public information from an identity file
696
+
697
+ | Option | Description | Default |
698
+ |--------|-------------|---------|
699
+ | `--identity` | Path to an `*.identity.json` file | **required** |
700
+ | `--help` | Show help message | — |
701
+
702
+ Prints the username, a stable SHA-256 username hash (`Public user ID (hex)`), both public keys, the fixed salt string names (`signing`, `encryption`), and whether encrypted private keys are present.
703
+
704
+ ### `npm run identity:change-password` — Re-encrypt an identity with a new password
705
+
706
+ | Option | Description | Default |
707
+ |--------|-------------|---------|
708
+ | `--identity` | Path to an `*.identity.json` file | **required** |
709
+ | `--help` | Show help message | — |
710
+
711
+ Prompts for the current password, the new password, and confirmation using hidden input. The file is updated atomically in place.
712
+
713
+ ### `npm run identity:export-public` — Export only the public portion of an identity
714
+
715
+ | Option | Description | Default |
716
+ |--------|-------------|---------|
717
+ | `--identity` | Path to an `*.identity.json` file | **required** |
718
+ | `--output` | Write JSON to a file instead of stdout | stdout |
719
+ | `--help` | Show help message | — |
720
+
721
+ This emits the corresponding `PublicUserId` JSON object:
722
+
723
+ ```json
724
+ {
725
+ "username": "cn=sysadmin/o=myorg",
726
+ "userSigningPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
727
+ "userEncryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
728
+ }
729
+ ```
730
+
731
+ ## API Reference
732
+
733
+ ### System Admin Endpoints (`/system/*`)
734
+
735
+ All system admin endpoints require a JWT obtained via challenge/response authentication.
736
+ See [Server Security](docs/server-security.md) for full details.
737
+
738
+ The **`curl`** examples in this document use **`$SYSTEM_ADMIN_JWT`** as a placeholder for that token. In Node you can use **`MindooDBServerAdmin`** (with your `CryptoAdapter`) instead of raw `curl`.
739
+
740
+ #### Authentication
741
+
742
+ | Method | Endpoint | Auth | Description |
743
+ |--------|----------|------|-------------|
744
+ | `POST` | `/system/auth/challenge` | None | Request a challenge (body: `{ username, publicsignkey }`) |
745
+ | `POST` | `/system/auth/authenticate` | None | Submit signed challenge |
746
+
747
+ #### Tenant Management
748
+
749
+ | Method | Endpoint | Auth | Description |
750
+ |--------|----------|------|-------------|
751
+ | `POST` | `/system/tenants/:tenantId` | JWT | Create a tenant |
752
+ | `GET` | `/system/tenants` | JWT | List all registered tenants |
753
+ | `PUT` | `/system/tenants/:tenantId` | JWT | Update tenant config |
754
+ | `DELETE` | `/system/tenants/:tenantId` | JWT | Remove a tenant |
755
+
756
+ #### Trusted Server Management
757
+
758
+ | Method | Endpoint | Auth | Description |
759
+ |--------|----------|------|-------------|
760
+ | `GET` | `/system/trusted-servers` | JWT | List trusted servers |
761
+ | `POST` | `/system/trusted-servers` | JWT | Add a trusted server |
762
+ | `DELETE` | `/system/trusted-servers/:serverName` | JWT | Remove a trusted server |
763
+
764
+ #### Per-Tenant Sync Server Management
765
+
766
+ | Method | Endpoint | Auth | Description |
767
+ |--------|----------|------|-------------|
768
+ | `GET` | `/system/tenants/:tenantId/sync-servers` | JWT | List sync servers for a tenant |
769
+ | `POST` | `/system/tenants/:tenantId/sync-servers` | JWT | Add or update a sync server |
770
+ | `DELETE` | `/system/tenants/:tenantId/sync-servers/:serverName` | JWT | Remove a sync server |
771
+ | `POST` | `/system/tenants/:tenantId/trigger-sync` | JWT | Trigger sync for a tenant |
772
+
773
+ #### Server Config Management
774
+
775
+ | Method | Endpoint | Auth | Description |
776
+ |--------|----------|------|-------------|
777
+ | `GET` | `/system/config` | JWT | Read the current server config |
778
+ | `PUT` | `/system/config` | JWT | Replace the server config (no restart needed) |
779
+
780
+ ### Authentication Endpoints
781
+
782
+ | Method | Endpoint | Description |
783
+ |--------|----------|-------------|
784
+ | `POST` | `/:tenantId/auth/challenge` | Request authentication challenge |
785
+ | `POST` | `/:tenantId/auth/authenticate` | Authenticate with signed challenge |
786
+
787
+ ### Sync Endpoints (requires JWT token)
788
+
789
+ | Method | Endpoint | Description |
790
+ |--------|----------|-------------|
791
+ | `POST` | `/:tenantId/sync/findNewEntries` | Find entries we don't have |
792
+ | `POST` | `/:tenantId/sync/findNewEntriesForDoc` | Find entries for specific document |
793
+ | `POST` | `/:tenantId/sync/getEntries` | Get specific entries |
794
+ | `POST` | `/:tenantId/sync/getEntryMetadata` | Get metadata for a single entry |
795
+ | `POST` | `/:tenantId/sync/putEntries` | Push entries to server |
796
+ | `POST` | `/:tenantId/sync/hasEntries` | Check which entry IDs exist |
797
+ | `GET` | `/:tenantId/sync/getAllIds` | Get all entry IDs |
798
+ | `POST` | `/:tenantId/sync/resolveDependencies` | Resolve dependency chain |
799
+ | `POST` | `/:tenantId/sync/planAttachmentReadByWalkingMetadata` | Build an attachment read plan on the server |
800
+ | `POST` | `/:tenantId/sync/findEntries` | Find entries by type/date |
801
+ | `POST` | `/:tenantId/sync/scanEntriesSince` | Cursor-based entry scanning |
802
+ | `POST` | `/:tenantId/sync/getIdBloomSummary` | Get Bloom filter summary |
803
+ | `GET` | `/:tenantId/sync/capabilities` | Get server capabilities |
804
+
805
+ ### Server Info & Health
806
+
807
+ | Method | Endpoint | Description |
808
+ |--------|----------|-------------|
809
+ | `GET` | `/health` | Server health status |
810
+ | `GET` | `/.well-known/mindoodb-server-info` | Server name and public keys (unauthenticated) |
811
+
812
+ ## Security
813
+
814
+ The server includes the following hardening measures:
815
+
816
+ - **Input validation** -- all identifiers (tenantId, dbId, serverName) are validated to prevent path traversal. Only lowercase alphanumeric characters and hyphens are allowed, max 64 characters. Tenant IDs that collide with server route prefixes (`admin`, `health`, `statics`) are rejected as reserved names.
817
+ - **Rate limiting** -- tiered per-IP rate limits: auth endpoints (20/min), admin endpoints (30/min), sync endpoints (200/min), global fallback (500/min). Returns `429 Too Many Requests` when exceeded.
818
+ - **Security headers** -- `helmet` middleware sets X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Strict-Transport-Security, and others.
819
+ - **CORS** -- disabled by default. Set `MINDOODB_CORS_ORIGIN` to allow one specific origin or a comma-separated allowlist of origins.
820
+ - **Constant-time key comparison** -- tenant creation API keys are compared using `crypto.timingSafeEqual()` where applicable.
821
+ - **Error sanitization** -- internal errors never leak file paths or stack traces to clients. Only known auth/validation errors return specific messages.
822
+ - **Request size limits** -- JSON body limited to 5MB. Array sizes capped (100k IDs, 10k entries for putEntries).
823
+ - **Connection timeouts** -- idle connections are closed after 30 seconds.
824
+ - **Optional `/system/*` IP allowlist** — set `MINDOODB_ADMIN_ALLOWED_IPS` to a comma-separated list of IPs or IPv4/IPv6 CIDRs (e.g., `127.0.0.1,::1,10.0.0.0/8,2001:db8::/32`) to restrict which client addresses may call **any** `/system/*` route (including auth). If unset or `*`, there is no IP restriction at this layer. IPv4-mapped IPv6 (`::ffff:127.0.0.1`) is normalized. Behind a reverse proxy, configure Express `trust proxy` so `req.ip` is accurate.
825
+
826
+ For production deployments, also consider:
827
+
828
+ - Enabling TLS (see below) or running behind a reverse proxy (nginx, Caddy) with TLS termination
829
+ - Setting `MINDOODB_ADMIN_ALLOWED_IPS` if `/system/*` should only be reachable from operator networks
830
+ - Using a process manager (PM2, systemd) for automatic restarts
831
+
832
+ ## Static File Serving
833
+
834
+ The server can serve static files from a local directory, which is useful for hosting a bootstrap UI for [distributed web applications](docs/distributed-webapps.md). When `--static-dir` is provided, two additional routes are available:
835
+
836
+ | Method | Endpoint | Description |
837
+ |--------|----------|-------------|
838
+ | `GET` | `/` | Redirects to `/statics/index.html` if the file exists (302), otherwise returns 404 |
839
+ | `GET` | `/statics/*` | Serves files from the configured static directory |
840
+
841
+ ### Usage
842
+
843
+ ```bash
844
+ MINDOODB_SERVER_PASSWORD_FILE=./.server-password npm run server:dev -- --static-dir ./webapp-bootstrap
845
+ ```
846
+
847
+ The static directory might contain a minimal bootstrap page that registers a service worker and triggers the initial MindooDB sync for a distributed web application:
848
+
849
+ ```
850
+ webapp-bootstrap/
851
+ ├── index.html # Minimal HTML shell, registers the service worker
852
+ ├── bootstrap.js # Sync logic: check pointer DB, sync UI DB
853
+ ├── sw.js # Service worker: serves UI assets from IndexedDB
854
+ └── style.css # Loading indicator styles
855
+ ```
856
+
857
+ Path traversal is prevented by an explicit guard that rejects requests containing `..`, in addition to the built-in protection provided by Express's static middleware. Dotfiles (`.env`, `.git`, etc.) are not served.
858
+
859
+ ## TLS / HTTPS
860
+
861
+ The server supports TLS directly via `--tls-cert` and `--tls-key` flags. No additional dependencies are required.
862
+
863
+ ### Starting with TLS
864
+
865
+ ```bash
866
+ MINDOODB_SERVER_PASSWORD_FILE=./.server-password npm run server:dev -- \
867
+ --tls-cert /etc/letsencrypt/live/sync.example.com/fullchain.pem \
868
+ --tls-key /etc/letsencrypt/live/sync.example.com/privkey.pem \
869
+ -p 443
870
+ ```
871
+
872
+ Both flags must be provided together. The certificate file should be the full chain (PEM format).
873
+
874
+ ### Free certificates with Let's Encrypt
875
+
876
+ #### Method A: Standalone (HTTP-01)
877
+
878
+ Certbot briefly binds port 80 to prove domain ownership. Best when port 80 is available.
879
+
880
+ ```bash
881
+ # Install certbot (Ubuntu/Debian)
882
+ sudo apt install certbot
883
+
884
+ # Obtain certificate
885
+ sudo certbot certonly --standalone -d sync.example.com
886
+ ```
887
+
888
+ Auto-renewal is handled by certbot's systemd timer, which runs automatically on most Linux distributions. After renewal, restart the server to pick up the new certificate.
889
+
890
+ #### Method B: DNS (DNS-01)
891
+
892
+ Prove ownership via a DNS TXT record. No port 80 required -- works behind firewalls and on non-standard ports.
893
+
894
+ **Automated (recommended for production)** -- use a DNS provider plugin so renewal is fully unattended:
895
+
896
+ ```bash
897
+ # Example with Cloudflare
898
+ sudo apt install certbot python3-certbot-dns-cloudflare
899
+
900
+ # Create credentials file
901
+ cat > /etc/letsencrypt/cloudflare.ini << EOF
902
+ dns_cloudflare_api_token = your-cloudflare-api-token
903
+ EOF
904
+ chmod 600 /etc/letsencrypt/cloudflare.ini
905
+
906
+ # Obtain certificate
907
+ sudo certbot certonly --dns-cloudflare \
908
+ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
909
+ -d sync.example.com
910
+ ```
911
+
912
+ With a DNS plugin, certbot creates and removes the TXT record via the provider's API automatically. The systemd timer handles renewal with zero manual intervention.
913
+
914
+ **Manual (testing only)** -- requires updating the DNS TXT record by hand every 90 days:
915
+
916
+ ```bash
917
+ sudo certbot certonly --manual --preferred-challenges dns -d sync.example.com
918
+ # Certbot will ask you to create: _acme-challenge.sync.example.com TXT "..."
919
+ ```
920
+
921
+ Available DNS plugins include Cloudflare, Route53, Google Cloud DNS, DigitalOcean, Linode, and OVH. See the [certbot documentation](https://eff-certbot.readthedocs.io/en/latest/using.html#dns-plugins) for the full list.
922
+
923
+ ## Docker
924
+
925
+ The server ships with a multi-stage `Dockerfile` under `src/node/server/` that produces a minimal Alpine-based image. The build context must be the **repository root** so the library and server compile together.
926
+
927
+ **Recommended:** use `bash serversetup.sh` for first-time setup, and `bash serversetup.sh --update` for safe interactive updates to an existing deployment. The rest of this section covers manual Docker commands for advanced use cases.
928
+
929
+ ### docker-compose.yml
930
+
931
+ The repository includes a `docker-compose.yml` that uses the default data paths (`../mindoodb-data`). `serversetup.sh` writes a `docker-compose.override.yml` that docker compose merges automatically. It pins the container to your current host uid/gid so the non-root container can read the password file and write to the mounted data directory. On SELinux hosts it also adds the required mount suffixes.
932
+
933
+ The generated override also contains the published port bindings. This supports all three common setups:
934
+ - bind all interfaces (`0.0.0.0`)
935
+ - bind a single specific IP (for example a VPN address)
936
+ - bind both `127.0.0.1` and one specific extra IP
937
+
938
+ If you only changed application code and do not need to adjust ports or bind addresses, `docker compose up -d --build` is usually enough.
939
+
940
+ ### Manual Docker commands (without serversetup.sh)
941
+
942
+ If you prefer not to use the setup script, here are the individual steps:
943
+
944
+ ```bash
945
+ # Build the image
946
+ docker build -f src/node/server/Dockerfile -t mindoodb-server .
947
+
948
+ # Create data directory and password file
949
+ mkdir -p ../mindoodb-data/server
950
+ printf '%s' 'your-secret' > ../mindoodb-data/.server_unlock
951
+ chmod 600 ../mindoodb-data/.server_unlock
952
+
953
+ # Initialize server identity (interactive — prompts for system admin creation)
954
+ # Run as your current host uid/gid so the container can read/write the bind mounts.
955
+ docker run --rm -it \
956
+ --user "$(id -u):$(id -g)" \
957
+ -v "$(pwd)/../mindoodb-data/server:/data" \
958
+ -v "$(pwd)/../mindoodb-data/.server_unlock:/run/secrets/server_unlock:ro" \
959
+ -e MINDOODB_SERVER_PASSWORD_FILE=/run/secrets/server_unlock \
960
+ --entrypoint node \
961
+ mindoodb-server dist/node/server/serverinit.js --data-dir /data --name server1
962
+
963
+ # Start the server
964
+ docker compose up -d
965
+
966
+ # Verify
967
+ curl http://localhost:1661/health
968
+ ```
969
+
970
+ On SELinux hosts, append `:Z` to the `/data` bind mount and `,Z` to the read-only password-file mount.
971
+
972
+ ### Bind to a specific IP
973
+
974
+ To restrict the server to a specific network interface (e.g. a VPN), rerun `bash serversetup.sh --update` and provide the bind address when prompted. Update mode preserves the existing server identity, keybag, config, tenant data, and password file while regenerating `docker-compose.override.yml`.
975
+
976
+ If you also want local checks from the same host, answer `y` when asked whether to also bind `127.0.0.1`. The generated `docker-compose.override.yml` will then contain both mappings.
977
+
978
+ Avoid the overwrite path unless you intentionally want to replace `server.identity.json`. Replacing the server identity breaks the relationship to server-owned encrypted state such as `server.keybag`, so it should be treated as a destructive reinitialization step, not a normal upgrade.
979
+
980
+ ### Additional flags
981
+
982
+ Environment variables and CLI flags can be added to `docker-compose.override.yml`:
983
+
984
+ ```yaml
985
+ services:
986
+ mindoodb:
987
+ environment:
988
+ MINDOODB_CORS_ORIGIN: "http://localhost:4174,https://otherserver.com"
989
+ MINDOODB_ADMIN_ALLOWED_IPS: "127.0.0.1,10.0.0.0/8"
990
+ command: ["--port", "8443", "--auto-sync"]
991
+ ports:
992
+ - "0.0.0.0:8443:8443"
993
+ ```
994
+
995
+ If `serversetup.sh` needs to regenerate the override later, it first creates a timestamped backup such as `docker-compose.override.20260407-103000.yml` and tells you where to find it so custom settings can be merged forward if needed.
996
+
997
+ ### Password handling
998
+
999
+ Prefer **`MINDOODB_SERVER_PASSWORD_FILE`** over **`MINDOODB_SERVER_PASSWORD`** so the plaintext secret is not stored in the process environment block (visible via `docker inspect`, `/proc/<pid>/environ`). The setup script and `docker-compose.yml` use file-based passwords by default. See [Server Security](docs/server-security.md) for details on Docker secrets.
1000
+
1001
+ ## Development
1002
+
1003
+ From the **repository root** (where `README-server.md` lives):
1004
+
1005
+ ### Build
1006
+
1007
+ ```bash
1008
+ npm run build
1009
+ ```
1010
+
1011
+ ### Run in development mode
1012
+
1013
+ ```bash
1014
+ MINDOODB_SERVER_PASSWORD_FILE=./.server-password npm run server:dev -- -d ./data -p 1661
1015
+ ```
1016
+
1017
+ ### Run built version
1018
+
1019
+ ```bash
1020
+ MINDOODB_SERVER_PASSWORD_FILE=./.server-password npm run server:start -- -d ./data -p 1661
1021
+ ```
1022
+
1023
+ ## Testing
1024
+
1025
+ The integration tests are located in the main MindooDB package at `src/__tests__/ExampleServer.test.ts`. Run them with:
1026
+
1027
+ ```bash
1028
+ # From the root mindoodb directory
1029
+ npm test -- ExampleServer
1030
+ ```