mindoodb 0.0.51 → 0.0.54

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 (249) hide show
  1. package/README-server.md +136 -71
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +30 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +67 -20
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
  7. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/HttpTransport.js +6 -62
  9. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +82 -0
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +229 -25
  13. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  14. package/dist/browser/index.mjs +59 -0
  15. package/dist/core/BaseMindooDB.d.ts +148 -80
  16. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  17. package/dist/core/BaseMindooDB.js +890 -692
  18. package/dist/core/BaseMindooDB.js.map +1 -1
  19. package/dist/core/BaseMindooTenant.d.ts +59 -7
  20. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  21. package/dist/core/BaseMindooTenant.js +407 -86
  22. package/dist/core/BaseMindooTenant.js.map +1 -1
  23. package/dist/core/BaseMindooTenantDirectory.d.ts +61 -9
  24. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  25. package/dist/core/BaseMindooTenantDirectory.js +363 -35
  26. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  27. package/dist/core/BaseMindooTenantFactory.d.ts +17 -1
  28. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  29. package/dist/core/BaseMindooTenantFactory.js +286 -17
  30. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  31. package/dist/core/MindooDBServerAdmin.d.ts +44 -22
  32. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
  33. package/dist/core/MindooDBServerAdmin.js +45 -21
  34. package/dist/core/MindooDBServerAdmin.js.map +1 -1
  35. package/dist/core/accesscontrol/DirectoryStateNode.d.ts +68 -1
  36. package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
  37. package/dist/core/accesscontrol/DirectoryStateNode.js +157 -4
  38. package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
  39. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  40. package/dist/core/accesscontrol/directoryProjection.js +14 -5
  41. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  42. package/dist/core/accesscontrol/grantKeys.d.ts +17 -14
  43. package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
  44. package/dist/core/accesscontrol/grantKeys.js +51 -21
  45. package/dist/core/accesscontrol/grantKeys.js.map +1 -1
  46. package/dist/core/accesscontrol/types.d.ts +2 -1
  47. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  48. package/dist/core/accesscontrol/types.js +3 -2
  49. package/dist/core/accesscontrol/types.js.map +1 -1
  50. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
  51. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  52. package/dist/core/appendonlystores/network/AuthenticationService.js +70 -14
  53. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  54. package/dist/core/appendonlystores/network/entryWireCodec.d.ts +51 -0
  55. package/dist/core/appendonlystores/network/entryWireCodec.d.ts.map +1 -0
  56. package/dist/core/appendonlystores/network/entryWireCodec.js +93 -0
  57. package/dist/core/appendonlystores/network/entryWireCodec.js.map +1 -0
  58. package/dist/core/appendonlystores/syncStores.d.ts +147 -0
  59. package/dist/core/appendonlystores/syncStores.d.ts.map +1 -0
  60. package/dist/core/appendonlystores/syncStores.js +612 -0
  61. package/dist/core/appendonlystores/syncStores.js.map +1 -0
  62. package/dist/core/builtinDbInvariants.d.ts +83 -0
  63. package/dist/core/builtinDbInvariants.d.ts.map +1 -0
  64. package/dist/core/builtinDbInvariants.js +244 -0
  65. package/dist/core/builtinDbInvariants.js.map +1 -0
  66. package/dist/core/copy/feasibility.d.ts.map +1 -1
  67. package/dist/core/copy/feasibility.js +3 -0
  68. package/dist/core/copy/feasibility.js.map +1 -1
  69. package/dist/core/copy/flatten.d.ts.map +1 -1
  70. package/dist/core/copy/flatten.js +6 -1
  71. package/dist/core/copy/flatten.js.map +1 -1
  72. package/dist/core/copy/host.d.ts +5 -0
  73. package/dist/core/copy/host.d.ts.map +1 -1
  74. package/dist/core/copy/preflight.d.ts.map +1 -1
  75. package/dist/core/copy/preflight.js +7 -1
  76. package/dist/core/copy/preflight.js.map +1 -1
  77. package/dist/core/copy/types.d.ts +7 -0
  78. package/dist/core/copy/types.d.ts.map +1 -1
  79. package/dist/core/crypto/EntrySignature.d.ts +8 -2
  80. package/dist/core/crypto/EntrySignature.d.ts.map +1 -1
  81. package/dist/core/crypto/EntrySignature.js +14 -5
  82. package/dist/core/crypto/EntrySignature.js.map +1 -1
  83. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
  84. package/dist/core/crypto/MindooDocSigner.js +11 -2
  85. package/dist/core/crypto/MindooDocSigner.js.map +1 -1
  86. package/dist/core/crypto/RSAEncryption.d.ts +19 -0
  87. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
  88. package/dist/core/crypto/RSAEncryption.js +36 -3
  89. package/dist/core/crypto/RSAEncryption.js.map +1 -1
  90. package/dist/core/crypto/privateKeyEncryption.js +1 -1
  91. package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
  92. package/dist/core/crypto/rsaOaep3072.d.ts +12 -0
  93. package/dist/core/crypto/rsaOaep3072.d.ts.map +1 -0
  94. package/dist/core/crypto/rsaOaep3072.js +30 -0
  95. package/dist/core/crypto/rsaOaep3072.js.map +1 -0
  96. package/dist/core/index.d.ts +7 -3
  97. package/dist/core/index.d.ts.map +1 -1
  98. package/dist/core/index.js +33 -4
  99. package/dist/core/index.js.map +1 -1
  100. package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
  101. package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
  102. package/dist/core/join/deviceJoinBootstrap.js +37 -0
  103. package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
  104. package/dist/core/keys/KeyBag.d.ts +20 -2
  105. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  106. package/dist/core/keys/KeyBag.js +22 -10
  107. package/dist/core/keys/KeyBag.js.map +1 -1
  108. package/dist/core/logging/MindooLogger.d.ts.map +1 -1
  109. package/dist/core/logging/MindooLogger.js +65 -40
  110. package/dist/core/logging/MindooLogger.js.map +1 -1
  111. package/dist/core/tenantIdValidation.d.ts.map +1 -1
  112. package/dist/core/tenantIdValidation.js +1 -1
  113. package/dist/core/tenantIdValidation.js.map +1 -1
  114. package/dist/core/tenantSetup.d.ts +94 -9
  115. package/dist/core/tenantSetup.d.ts.map +1 -1
  116. package/dist/core/tenantSetup.js +208 -20
  117. package/dist/core/tenantSetup.js.map +1 -1
  118. package/dist/core/types.d.ts +313 -33
  119. package/dist/core/types.d.ts.map +1 -1
  120. package/dist/core/types.js +14 -1
  121. package/dist/core/types.js.map +1 -1
  122. package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
  123. package/dist/core/uri/joinRequestUri.js +12 -3
  124. package/dist/core/uri/joinRequestUri.js.map +1 -1
  125. package/dist/core/userid/canonicalUsername.d.ts +66 -0
  126. package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
  127. package/dist/core/userid/canonicalUsername.js +235 -0
  128. package/dist/core/userid/canonicalUsername.js.map +1 -0
  129. package/dist/core/userid/index.d.ts +9 -0
  130. package/dist/core/userid/index.d.ts.map +1 -1
  131. package/dist/core/userid/index.js +14 -0
  132. package/dist/core/userid/index.js.map +1 -1
  133. package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
  134. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
  135. package/dist/core/userkeys/UserKeyDocument.js +145 -0
  136. package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
  137. package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
  138. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
  139. package/dist/core/userkeys/UserKeyManager.js +1277 -0
  140. package/dist/core/userkeys/UserKeyManager.js.map +1 -0
  141. package/dist/core/userkeys/fingerprint.d.ts +17 -0
  142. package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
  143. package/dist/core/userkeys/fingerprint.js +59 -0
  144. package/dist/core/userkeys/fingerprint.js.map +1 -0
  145. package/dist/core/userkeys/index.d.ts +11 -0
  146. package/dist/core/userkeys/index.d.ts.map +1 -0
  147. package/dist/core/userkeys/index.js +49 -0
  148. package/dist/core/userkeys/index.js.map +1 -0
  149. package/dist/core/userkeys/recipients.d.ts +51 -0
  150. package/dist/core/userkeys/recipients.d.ts.map +1 -0
  151. package/dist/core/userkeys/recipients.js +292 -0
  152. package/dist/core/userkeys/recipients.js.map +1 -0
  153. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
  154. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
  155. package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
  156. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
  157. package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
  158. package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
  159. package/dist/core/userkeys/sealedCrypto.js +79 -0
  160. package/dist/core/userkeys/sealedCrypto.js.map +1 -0
  161. package/dist/core/userkeys/sealedTypes.d.ts +72 -0
  162. package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
  163. package/dist/core/userkeys/sealedTypes.js +14 -0
  164. package/dist/core/userkeys/sealedTypes.js.map +1 -0
  165. package/dist/core/userkeys/types.d.ts +70 -0
  166. package/dist/core/userkeys/types.d.ts.map +1 -0
  167. package/dist/core/userkeys/types.js +48 -0
  168. package/dist/core/userkeys/types.js.map +1 -0
  169. package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
  170. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
  171. package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
  172. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
  173. package/dist/node/server/CapabilityMatcher.d.ts +7 -2
  174. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
  175. package/dist/node/server/CapabilityMatcher.js +12 -3
  176. package/dist/node/server/CapabilityMatcher.js.map +1 -1
  177. package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
  178. package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
  179. package/dist/node/server/DeviceDiscoveryService.js +165 -0
  180. package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
  181. package/dist/node/server/MindooDBServer.d.ts +27 -1
  182. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  183. package/dist/node/server/MindooDBServer.js +265 -220
  184. package/dist/node/server/MindooDBServer.js.map +1 -1
  185. package/dist/node/server/SyncEventBus.d.ts +10 -0
  186. package/dist/node/server/SyncEventBus.d.ts.map +1 -1
  187. package/dist/node/server/SyncEventBus.js.map +1 -1
  188. package/dist/node/server/SystemAdminAuth.d.ts +7 -0
  189. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
  190. package/dist/node/server/SystemAdminAuth.js +4 -1
  191. package/dist/node/server/SystemAdminAuth.js.map +1 -1
  192. package/dist/node/server/TenantManager.d.ts +86 -5
  193. package/dist/node/server/TenantManager.d.ts.map +1 -1
  194. package/dist/node/server/TenantManager.js +235 -71
  195. package/dist/node/server/TenantManager.js.map +1 -1
  196. package/dist/node/server/add-to-network.d.ts.map +1 -1
  197. package/dist/node/server/add-to-network.js +33 -15
  198. package/dist/node/server/add-to-network.js.map +1 -1
  199. package/dist/node/server/canonicalSetupName.d.ts +22 -0
  200. package/dist/node/server/canonicalSetupName.d.ts.map +1 -0
  201. package/dist/node/server/canonicalSetupName.js +38 -0
  202. package/dist/node/server/canonicalSetupName.js.map +1 -0
  203. package/dist/node/server/config.d.ts.map +1 -1
  204. package/dist/node/server/config.js +33 -1
  205. package/dist/node/server/config.js.map +1 -1
  206. package/dist/node/server/peer/ClusterAuditLog.d.ts +64 -0
  207. package/dist/node/server/peer/ClusterAuditLog.d.ts.map +1 -0
  208. package/dist/node/server/peer/ClusterAuditLog.js +99 -0
  209. package/dist/node/server/peer/ClusterAuditLog.js.map +1 -0
  210. package/dist/node/server/peer/ClusterJobRunner.d.ts +43 -0
  211. package/dist/node/server/peer/ClusterJobRunner.d.ts.map +1 -0
  212. package/dist/node/server/peer/ClusterJobRunner.js +119 -0
  213. package/dist/node/server/peer/ClusterJobRunner.js.map +1 -0
  214. package/dist/node/server/peer/ClusterManager.d.ts +116 -0
  215. package/dist/node/server/peer/ClusterManager.d.ts.map +1 -0
  216. package/dist/node/server/peer/ClusterManager.js +274 -0
  217. package/dist/node/server/peer/ClusterManager.js.map +1 -0
  218. package/dist/node/server/peer/PeerAuthService.d.ts +69 -0
  219. package/dist/node/server/peer/PeerAuthService.d.ts.map +1 -0
  220. package/dist/node/server/peer/PeerAuthService.js +208 -0
  221. package/dist/node/server/peer/PeerAuthService.js.map +1 -0
  222. package/dist/node/server/peer/PeerReplicator.d.ts +232 -0
  223. package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -0
  224. package/dist/node/server/peer/PeerReplicator.js +879 -0
  225. package/dist/node/server/peer/PeerReplicator.js.map +1 -0
  226. package/dist/node/server/peer/routes.d.ts +66 -0
  227. package/dist/node/server/peer/routes.d.ts.map +1 -0
  228. package/dist/node/server/peer/routes.js +322 -0
  229. package/dist/node/server/peer/routes.js.map +1 -0
  230. package/dist/node/server/peer/types.d.ts +244 -0
  231. package/dist/node/server/peer/types.d.ts.map +1 -0
  232. package/dist/node/server/peer/types.js +189 -0
  233. package/dist/node/server/peer/types.js.map +1 -0
  234. package/dist/node/server/server.d.ts +1 -1
  235. package/dist/node/server/server.js +18 -37
  236. package/dist/node/server/server.js.map +1 -1
  237. package/dist/node/server/serverinit.d.ts +2 -1
  238. package/dist/node/server/serverinit.d.ts.map +1 -1
  239. package/dist/node/server/serverinit.js +34 -12
  240. package/dist/node/server/serverinit.js.map +1 -1
  241. package/dist/node/server/types.d.ts +42 -30
  242. package/dist/node/server/types.d.ts.map +1 -1
  243. package/dist/node/server/types.js +1 -7
  244. package/dist/node/server/types.js.map +1 -1
  245. package/dist/node/server/validation.d.ts +0 -6
  246. package/dist/node/server/validation.d.ts.map +1 -1
  247. package/dist/node/server/validation.js +1 -7
  248. package/dist/node/server/validation.js.map +1 -1
  249. package/package.json +5 -5
package/README-server.md CHANGED
@@ -261,7 +261,7 @@ Each rule in `config.json` is a key-value pair where:
261
261
  | `POST:/system/tenants/acme` | Can only create the specific tenant `acme` |
262
262
  | `PUT:/system/tenants/company-*` | Can update any tenant whose ID starts with `company-` |
263
263
  | `GET:/system/tenants` | Read-only: can list tenants but not modify anything |
264
- | `DELETE:/system/tenants/*` | Can delete any tenant |
264
+ | `DELETE:/system/tenants/*` | Can delete any tenant. Tenant admins can also delete **their own** tenant without this rule (see [Own-tenant delete](#own-tenant-delete)). |
265
265
 
266
266
  ### Multiple admins with different roles
267
267
 
@@ -304,6 +304,7 @@ On each `/system/*` request the server:
304
304
  3. Finds all capability rules whose method and path pattern match
305
305
  4. Unions all principal entries from matching rules
306
306
  5. **Allows** the request if any entry has **both** a matching `username` AND `publicsignkey`
307
+ 6. If that fails, **also allows** `DELETE /system/tenants/:tenantId` when the JWT signing key is that tenant's registered admin signing key (see [Own-tenant delete](#own-tenant-delete))
307
308
 
308
309
  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.
309
310
 
@@ -385,11 +386,21 @@ Prefix-restricted demo server (allow only tenant names starting with `demo_`):
385
386
 
386
387
  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.
387
388
 
389
+ ### Own-tenant delete
390
+
391
+ Independently of `config.json`, `DELETE /system/tenants/:tenantId` is also allowed when the caller's JWT signing key equals that tenant's registered `adminSigningPublicKey`. The tenant admin can remove **their own** tenant from the server. They cannot list tenants, change server config, touch sync-server sub-resources, or delete anyone else's tenant.
392
+
393
+ This is not a wildcard capability. `{ "username": "*", "publicsignkey": "*" }` remains valid only on `POST:/system/tenants/...`. A demo server that already allows open tenant creation therefore needs no extra `DELETE` rule — the creator's tenant-admin key is enough to unpublish that same tenant.
394
+
395
+ A system admin with `DELETE:/system/tenants/*` or `ALL:/system/*` can still delete any tenant.
396
+
388
397
  For key rotation, adding/removing admins, and the full authentication flow (challenge/response, JWT lifecycle), see [Server Security](docs/server-security.md).
389
398
 
390
399
  ## Walkthrough: Multi-Server Setup
391
400
 
392
- MindooDB servers can mirror encrypted data between each other. This section covers the full workflow: initializing servers, establishing trust, and configuring per-tenant sync.
401
+ MindooDB servers can mirror encrypted data between each other. This section covers the full workflow: initializing servers, establishing trust, seeding a tenant onto a second server, and running the mesh.
402
+
403
+ **The model in one paragraph.** Replication is configured per *peer*, not per tenant. Each trusted server with a `url` becomes one replication session, and over that session the two servers mirror **every tenant they both host** — all databases, documents and attachments. Which tenants those are is negotiated, not configured: the peers exchange a Bloom summary of their tenant ids and intersect it, so a tenant only one side hosts is never replicated and the other side never learns it exists. Servers relay encrypted entries and never see plaintext; a mirror is a full replica that cannot read what it stores.
393
404
 
394
405
  ### 1. Initialize servers
395
406
 
@@ -423,15 +434,16 @@ curl https://server1.example.com/.well-known/mindoodb-server-info
423
434
  {
424
435
  "name": "CN=server1",
425
436
  "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
426
- "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
437
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
438
+ "clusterRole": "peer"
427
439
  }
428
440
  ```
429
441
 
430
- This eliminates the need to manually copy public keys between servers.
442
+ This eliminates the need to manually copy public keys between servers. `clusterRole` is the role this server announces for itself (`peer`, `hub` or `spoke`), which `add-to-network` records on the other side.
431
443
 
432
444
  ### 3. Establish trust
433
445
 
434
- 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.
446
+ The `add-to-network` CLI automates mutual trust exchange. It fetches each server's public keys, url and cluster role from `/.well-known/mindoodb-server-info` and calls `POST /system/trusted-servers` on each side using your system admin identity. Because it records the peer's url, trust and replication are established in the same step — there is no separate "now configure sync" pass.
435
447
 
436
448
  ```bash
437
449
  printf '%s' 'admin-pass' > ./.admin-password && chmod 600 ./.admin-password
@@ -455,83 +467,112 @@ curl -X POST http://server1:1661/system/trusted-servers \
455
467
  -d '{
456
468
  "name": "CN=server2",
457
469
  "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
458
- "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
470
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
471
+ "url": "http://server2:3001",
472
+ "direction": "bidirectional",
473
+ "role": "peer",
474
+ "attachments": "eager"
459
475
  }'
460
476
  ```
461
477
 
462
- Trust changes take effect immediately -- no restart required.
478
+ Only `name` and the two public keys are required. The rest describe the replication session and all have defaults:
463
479
 
464
- ### 4. Publish tenant to servers
480
+ | Field | Values | Default | Meaning |
481
+ | --- | --- | --- | --- |
482
+ | `url` | base url | *(none)* | Where to reach the peer. **Without it the server is trusted but never dialled** — it can push to us, we never pull from it. |
483
+ | `direction` | `bidirectional`, `push`, `pull`, `disabled` | `bidirectional` | Which legs are allowed. `pull` makes a read-only mirror; `disabled` keeps the trust but stops replicating. |
484
+ | `role` | `peer`, `hub`, `spoke` | `peer` | Mesh shape. Two `spoke`s do not open a session with each other — they exchange data through their hub. |
485
+ | `attachments` | `eager`, `lazy`, `never` | `eager` | `eager` completes each database before the next. `lazy` replicates every document store first and attachments afterwards, so an interrupted or slow run has spent its bandwidth on the text. `never` mirrors metadata only. |
465
486
 
466
- Reuse the same system admin identity that is authorized in each server's `config.json`:
487
+ Trust and configuration changes take effect immediately -- no restart required.
467
488
 
468
- ```typescript
469
- await result.tenant.publishToServer("http://server1:1661", {
470
- systemAdminUser: systemAdminIdentity,
471
- systemAdminPassword: "sysadmin-pass",
472
- adminUsername: result.adminUser.username,
473
- });
489
+ ### 4. Seed the tenant onto both servers
474
490
 
475
- await result.tenant.publishToServer("http://server2:3001", {
476
- systemAdminUser: systemAdminIdentity,
477
- systemAdminPassword: "sysadmin-pass",
478
- adminUsername: result.adminUser.username,
479
- });
491
+ **A tenant is defined by its administration keypair, so "create the same tenant id on both servers" produces two different tenants that happen to share a name.** The second server would refuse every replicated entry as *not signed by a trusted user* — correctly, because for it the signer really is a stranger. Create the tenant once and publish it to each server that should host it:
492
+
493
+ ```typescript
494
+ for (const url of ["http://server1:1661", "http://server2:3001"]) {
495
+ await result.tenant.publishToServer(url, {
496
+ systemAdminUser: systemAdminIdentity,
497
+ systemAdminPassword: "sysadmin-pass",
498
+ adminUsername: result.adminUser.username,
499
+ });
500
+ }
480
501
  ```
481
502
 
482
- ### 5. Configure per-tenant sync
503
+ Publishing registers the tenant and its admin keys. The data itself arrives through replication; you do not have to push it to both servers by hand.
504
+
505
+ To add a mirror for a tenant that already exists, publish that same tenant object to the new server and let the mesh fill it.
483
506
 
484
- 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.
507
+ ### 5. Enable replication
485
508
 
486
- **Add a sync server for a tenant:**
509
+ Add `--auto-sync` via the `command` key in each server's `docker-compose.override.yml`, or set `"cluster": { "autoSync": true }` in `config.json`:
510
+
511
+ ```yaml
512
+ services:
513
+ mindoodb:
514
+ command: ["--auto-sync"]
515
+ ```
516
+
517
+ Then restart:
487
518
 
488
519
  ```bash
489
- curl -X POST http://server1:1661/system/tenants/acme/sync-servers \
490
- -H "Content-Type: application/json" \
491
- -H "Authorization: Bearer $SYSTEM_ADMIN_JWT" \
492
- -d '{
493
- "name": "CN=server2",
494
- "url": "http://server2:3001",
495
- "syncIntervalMs": 60000,
496
- "databases": ["directory", "main"]
497
- }'
520
+ docker compose up -d
498
521
  ```
499
522
 
500
- 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.
523
+ Each peer with a `url` gets one session. A session catches up on start, then follows the peer's change feed over SSE and pushes local writes as they happen, with a periodic full pass as a safety net. Nothing is per-tenant: adding a tenant to both servers is enough for it to start replicating.
501
524
 
502
- **List sync servers for a tenant:**
525
+ ### 6. Watch the mesh
526
+
527
+ `/system/cluster/*` is the administrative view, behind the same system-admin JWT and capability rules as the rest of `/system`:
503
528
 
504
529
  ```bash
505
- curl http://server1:1661/system/tenants/acme/sync-servers \
530
+ curl http://server1:1661/system/cluster/status \
506
531
  -H "Authorization: Bearer $SYSTEM_ADMIN_JWT"
507
532
  ```
508
533
 
509
- **Remove a sync server from a tenant:**
534
+ The status reports, per peer: session state, health with classified reasons, shared tenant count, lag, retry depth, rejected entries and the last error. Counts and classifications only — never a document id, so the console does not become a way around admin-blindness.
535
+
536
+ Two health reasons are worth recognizing:
537
+
538
+ - **`access-denied-backoff`** — the peer refused a batch (a purged document, a revoked key). The replicator holds its cursor and retries; it does not skip past the entries. This usually clears itself once the directory change that caused it has replicated.
539
+ - **`entries-awaiting-directory-trust`** — entries are waiting for a grant that has not arrived yet. Also self-clearing.
540
+
541
+ Mutating actions are jobs, so a long resync does not block the request:
510
542
 
511
543
  ```bash
512
- curl -X DELETE http://server1:1661/system/tenants/acme/sync-servers/CN%3Dserver2 \
544
+ curl -X POST http://server1:1661/system/cluster/actions/sync-now \
545
+ -H "Content-Type: application/json" \
546
+ -H "Authorization: Bearer $SYSTEM_ADMIN_JWT" \
547
+ -d '{"peer": "CN=server2", "tenantId": "acme"}'
548
+ # → { "jobId": "...", "job": { "state": "running", ... } }
549
+
550
+ curl http://server1:1661/system/cluster/jobs/$JOB_ID \
513
551
  -H "Authorization: Bearer $SYSTEM_ADMIN_JWT"
514
552
  ```
515
553
 
516
- Note: the server name in the URL must be percent-encoded (e.g., `CN%3Dserver2` for `CN=server2`).
554
+ Every action is recorded in an append-only audit log (`GET /system/cluster/audit`, paged) with who, what, where and when.
517
555
 
518
- ### 6. Enable auto-sync
556
+ Because each server is administered independently, there is no cluster-wide aggregate endpoint: a console shows the whole mesh by asking each server for its own view. That keeps one server's admin credentials from becoming credentials for the others.
519
557
 
520
- Add `--auto-sync` via the `command` key in each server's `docker-compose.override.yml`:
558
+ ### Client failover
521
559
 
522
- ```yaml
523
- services:
524
- mindoodb:
525
- command: ["--auto-sync"]
526
- ```
527
-
528
- Then restart:
560
+ A client can discover where else a tenant lives, without authenticating:
529
561
 
530
562
  ```bash
531
- docker compose up -d
563
+ curl "http://server1:1661/.well-known/mindoodb-cluster?tenantId=acme"
564
+ # → { "mirrors": [ { "name": "CN=server2", "url": "http://server2:3001", "role": "peer" } ] }
532
565
  ```
533
566
 
534
- 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.
567
+ Only mirrors of the tenant that was asked for are listed, so the answer says nothing about peers that exist for other tenants.
568
+
569
+ ### What the mesh guarantees
570
+
571
+ Three invariants make repeated replication converge rather than drift:
572
+
573
+ 1. **Directory first.** Within each tenant, `directory` and `userdirectory` replicate before application databases. Application entries are signed by keys the peer only learns from the directory, so any other order rejects entries that are in fact valid.
574
+ 2. **The cursor holds on rejection.** A client may skip an entry the server refuses and move on. Two servers that are meant to be identical may not: the cursor stays clamped until the entry is accepted, or the divergence would be permanent and silent.
575
+ 3. **Receipts are preserved.** A replicated entry keeps the witness receipt of the server that first accepted it, verified against `trusted-servers.json` on arrival. Re-stamping would make "trusted time" mean "the time it reached the last server in the chain", and would show every mirrored document as having appeared today.
535
576
 
536
577
  ## Data Directory Layout
537
578
 
@@ -611,11 +652,17 @@ Generated by `pnpm server:init`. Contains a `PrivateUserId` with encrypted priva
611
652
  {
612
653
  "name": "CN=server2",
613
654
  "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
614
- "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
655
+ "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
656
+ "url": "https://server2.example.com",
657
+ "direction": "bidirectional",
658
+ "role": "peer",
659
+ "attachments": "eager"
615
660
  }
616
661
  ]
617
662
  ```
618
663
 
664
+ This file is the whole replication topology. An entry with a `url` becomes one replication session covering every tenant both servers host; an entry without one is trusted (it may push to us) but never dialled. See [Server-to-Server Sync](#server-to-server-sync) for the field meanings.
665
+
619
666
  ### `<tenantId>/config.json`
620
667
 
621
668
  ```json
@@ -631,18 +678,12 @@ Generated by `pnpm server:init`. Contains a `PrivateUserId` with encrypted priva
631
678
  "signingPublicKey": "-----BEGIN PUBLIC KEY-----\n...",
632
679
  "encryptionPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
633
680
  }
634
- ],
635
- "remoteServers": [
636
- {
637
- "name": "CN=server2",
638
- "url": "https://server2.example.com",
639
- "syncIntervalMs": 60000,
640
- "databases": ["directory", "main"]
641
- }
642
681
  ]
643
682
  }
644
683
  ```
645
684
 
685
+ Replication is not configured here. It is a property of the peer (`trusted-servers.json`), and a tenant is mirrored simply by existing on both servers.
686
+
646
687
  The `users` array is a **public-key allowlist**: each entry's identity is its
647
688
  `signingPublicKey` (with the paired `encryptionPublicKey`). The cleartext
648
689
  `username` from previous versions is **no longer required or used for matching** — clients may
@@ -664,8 +705,6 @@ extra field) for human-readable notes. Entries are matched solely by
664
705
 
665
706
  > **Set `MINDOODB_TRUST_PROXY` if anything fronts the server.** Rate limits and the `/system/*` allowlist both key on `req.ip`. Without it, every user behind the proxy shares one address, so one busy client can exhaust the address-keyed budgets for everyone and the allowlist matches the proxy instead of the operator network.
666
707
 
667
- > **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).
668
-
669
708
  ## CLI Reference
670
709
 
671
710
  ### `pnpm server:dev` / `pnpm server:start` — Start the server
@@ -676,7 +715,7 @@ Launches the MindooDB server process. `server:dev` runs via `ts-node` for develo
676
715
  |--------|-------|-------------|---------|
677
716
  | `--data-dir` | `-d` | Data directory path | `./data` |
678
717
  | `--port` | `-p` | Server port | `1661` |
679
- | `--auto-sync` | `-s` | Enable automatic sync with remote servers | disabled |
718
+ | `--auto-sync` | `-s` | Start peer replication: one session per trusted server with a `url`, mirroring every tenant both sides host. Equivalent to `"cluster": { "autoSync": true }` in `config.json` | disabled |
680
719
  | `--static-dir` | `-w` | Serve static files at `/statics/` (e.g. bootstrap UI) | — |
681
720
  | `--tls-cert` | — | Path to TLS certificate file (PEM) | — |
682
721
  | `--tls-key` | — | Path to TLS private key file (PEM) | — |
@@ -686,16 +725,18 @@ Launches the MindooDB server process. `server:dev` runs via `ts-node` for develo
686
725
 
687
726
  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.
688
727
 
728
+ Both names — the server's and the system admin's — are stored in canonical form (`cn=…/o=…`). An abbreviated name is expanded, a canonical one is taken as it is, and a name without an organization is rejected: the system admin is matched against `config.json` as a string, so a client sending `cn=sysadmin/o=acme` would never match a stored `sysadmin/acme`, and an identity without an organization fails later in every path that resolves recipients.
729
+
689
730
  | Option | Alias | Description | Default |
690
731
  |--------|-------|-------------|---------|
691
- | `--name` | `-n` | Server name (e.g., "server1") | **required** |
732
+ | `--name` | `-n` | Server name. Canonical (`cn=server1/o=acme`) or abbreviated (`server1/acme`); a common name and an organization are both required, and the value is stored in canonical form | **required** |
692
733
  | `--data-dir` | `-d` | Data directory path | `./data` |
693
734
  | `--force` | `-f` | Overwrite existing identity | — |
694
735
  | `--help` | `-h` | Show help message | — |
695
736
 
696
737
  ### `pnpm server:add-to-network` — Add a server to the network
697
738
 
698
- 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.
739
+ Automates mutual trust exchange when adding a new server to an existing network. For each existing server it fetches public keys, url and cluster role from `/.well-known/mindoodb-server-info` and calls `POST /system/trusted-servers` in both directions, so the new server and every existing server both trust *and* can dial each other.
699
740
 
700
741
  | Option | Description | Default |
701
742
  |--------|-------------|---------|
@@ -766,24 +807,48 @@ The **`curl`** examples in this document use **`$SYSTEM_ADMIN_JWT`** as a placeh
766
807
  | `POST` | `/system/tenants/:tenantId` | JWT | Create a tenant |
767
808
  | `GET` | `/system/tenants` | JWT | List all registered tenants |
768
809
  | `PUT` | `/system/tenants/:tenantId` | JWT | Update tenant config |
769
- | `DELETE` | `/system/tenants/:tenantId` | JWT | Remove a tenant |
810
+ | `DELETE` | `/system/tenants/:tenantId` | JWT | Remove a tenant (capability **or** that tenant's admin signing key) |
770
811
 
771
812
  #### Trusted Server Management
772
813
 
773
814
  | Method | Endpoint | Auth | Description |
774
815
  |--------|----------|------|-------------|
775
816
  | `GET` | `/system/trusted-servers` | JWT | List trusted servers |
776
- | `POST` | `/system/trusted-servers` | JWT | Add a trusted server |
817
+ | `POST` | `/system/trusted-servers` | JWT | Add or update a trusted server (keys, `url`, `direction`, `role`, `attachments`) |
777
818
  | `DELETE` | `/system/trusted-servers/:serverName` | JWT | Remove a trusted server |
778
819
 
779
- #### Per-Tenant Sync Server Management
820
+ #### Cluster Administration
821
+
822
+ The console surface for peer replication. Reads and writes are separate capabilities, so an auditor role can be granted `GET:/system/cluster/*` without the ability to change anything.
780
823
 
781
824
  | Method | Endpoint | Auth | Description |
782
825
  |--------|----------|------|-------------|
783
- | `GET` | `/system/tenants/:tenantId/sync-servers` | JWT | List sync servers for a tenant |
784
- | `POST` | `/system/tenants/:tenantId/sync-servers` | JWT | Add or update a sync server |
785
- | `DELETE` | `/system/tenants/:tenantId/sync-servers/:serverName` | JWT | Remove a sync server |
786
- | `POST` | `/system/tenants/:tenantId/trigger-sync` | JWT | Trigger sync for a tenant |
826
+ | `GET` | `/system/cluster/topology` | JWT | Configured peers and their roles |
827
+ | `GET` | `/system/cluster/status` | JWT | Live per-peer state: health, lag, retry depth, last error |
828
+ | `GET` | `/system/cluster/tenants/:tenantId` | JWT | The same, narrowed to one tenant |
829
+ | `POST` | `/system/cluster/actions/:action` | JWT | Start a job: `sync-now`, `retry-rejected`, `refresh-intersection`, `pause`, `resume` |
830
+ | `GET` | `/system/cluster/jobs` | JWT | Recent jobs |
831
+ | `GET` | `/system/cluster/jobs/:jobId` | JWT | One job's state and result |
832
+ | `GET` | `/system/cluster/audit` | JWT | Append-only record of administrative actions (paged) |
833
+ | `PUT`/`POST` | `/system/cluster/peers/:name` | JWT | Create or reconfigure a peer |
834
+ | `DELETE` | `/system/cluster/peers/:name` | JWT | Remove a peer |
835
+
836
+ Status, job and audit responses carry counts and classified reasons only — never document ids or author keys.
837
+
838
+ #### Peer Protocol
839
+
840
+ Server-to-server only, authenticated with a peer token from `trusted-servers.json` (a system-admin JWT is refused, and vice versa). Listed for completeness; operators do not call these.
841
+
842
+ | Method | Endpoint | Description |
843
+ |--------|----------|-------------|
844
+ | `POST` | `/system/peer/challenge` | Request a challenge (only for a trusted signing key) |
845
+ | `POST` | `/system/peer/authenticate` | Exchange a signed challenge for a peer token |
846
+ | `POST` | `/system/peer/tenant-bloom` | Exchange Bloom summaries of tenant ids to find the intersection |
847
+ | `GET` | `/system/peer/databases` | Database ids held for a shared tenant |
848
+ | `GET` | `/system/peer/events` | SSE change feed for shared tenants |
849
+ | `POST` | `/system/peer/sync` | Ask the peer to run a sync pass towards us |
850
+
851
+ The data itself moves over the ordinary `/:tenantId/sync/*` routes: a peer speaks the client protocol with a peer token, rather than a second replication format existing alongside it.
787
852
 
788
853
  #### Server Config Management
789
854
 
@@ -1004,7 +1069,7 @@ docker run --rm -it \
1004
1069
  -v "$(pwd)/../mindoodb-data/.server_unlock:/run/secrets/server_unlock:ro" \
1005
1070
  -e MINDOODB_SERVER_PASSWORD_FILE=/run/secrets/server_unlock \
1006
1071
  --entrypoint node \
1007
- mindoodb-server dist/node/server/serverinit.js --data-dir /data --name server1
1072
+ mindoodb-server dist/node/server/serverinit.js --data-dir /data --name server1/acme
1008
1073
 
1009
1074
  # Start the server
1010
1075
  docker compose up -d
@@ -27,6 +27,8 @@ export declare class ClientNetworkContentAddressedStore implements ContentAddres
27
27
  private privateEncryptionKey;
28
28
  private username;
29
29
  private signingKey;
30
+ /** PEM of the device signing public key; sent with challenge for multi-device auth. */
31
+ private signingPublicKeyPem;
30
32
  private cryptoAdapter;
31
33
  private accessToken;
32
34
  private tokenExpiry;
@@ -36,6 +38,18 @@ export declare class ClientNetworkContentAddressedStore implements ContentAddres
36
38
  private capabilitiesPromise;
37
39
  private syncAuthOverride;
38
40
  private _syncAbortSignal?;
41
+ /**
42
+ * Supplies a ready-made bearer token instead of running the tenant
43
+ * challenge-response handshake.
44
+ *
45
+ * Used by server-to-server replication: a peer authenticates once at the
46
+ * cluster level (`/system/peer/authenticate`) against `trusted-servers.json`
47
+ * and holds no tenant grant, so the per-tenant handshake would always fail.
48
+ * Everything else — the entire sync protocol — is identical, which is the
49
+ * point: the mesh reuses the client protocol rather than inventing a second
50
+ * replication format.
51
+ */
52
+ private externalTokenProvider;
39
53
  /**
40
54
  * Create a new ClientNetworkContentAddressedStore.
41
55
  *
@@ -46,8 +60,11 @@ export declare class ClientNetworkContentAddressedStore implements ContentAddres
46
60
  * @param signingKey The user's private signing key (for signing challenges)
47
61
  * @param privateEncryptionKey The user's private RSA key (for decrypting received entries)
48
62
  * @param logger Optional logger instance
63
+ * @param signingPublicKeyPem Optional device signing public key (PEM). Sent with
64
+ * the auth challenge so the server can detect a second-device key that is
65
+ * not yet in its directory copy (instead of a vague "Invalid signature").
49
66
  */
50
- constructor(dbId: string, storeKind: StoreKind, transport: NetworkTransport, cryptoAdapter: CryptoAdapter, username: string, signingKey: CryptoKey, privateEncryptionKey: CryptoKey | string, logger?: Logger);
67
+ constructor(dbId: string, storeKind: StoreKind, transport: NetworkTransport, cryptoAdapter: CryptoAdapter, username: string, signingKey: CryptoKey, privateEncryptionKey: CryptoKey | string, logger?: Logger, signingPublicKeyPem?: string);
51
68
  getId(): string;
52
69
  getStoreKind(): StoreKind;
53
70
  getCacheIdentity(): string;
@@ -153,6 +170,11 @@ export declare class ClientNetworkContentAddressedStore implements ContentAddres
153
170
  /**
154
171
  * Ensure we have a valid access token, authenticating if necessary.
155
172
  */
173
+ /**
174
+ * Route every authenticated call through `provider` instead of the tenant
175
+ * handshake. See {@link externalTokenProvider}.
176
+ */
177
+ setExternalTokenProvider(provider: (() => Promise<string>) | null): void;
156
178
  private ensureAuthenticated;
157
179
  private performAuthentication;
158
180
  /**
@@ -186,15 +208,22 @@ export declare class ClientNetworkContentAddressedStore implements ContentAddres
186
208
  /**
187
209
  * Set or clear a temporary per-sync authentication override.
188
210
  * Intended for bootstrap scenarios (for example authenticating as admin).
211
+ *
212
+ * When overriding, pass `signingPublicKey` (PEM) for the same key material as
213
+ * `signingKey`. Auth challenges bind username + device key; leaving the
214
+ * store's default PEM in place while signing with a different key causes the
215
+ * server to reject the challenge as an unknown device.
189
216
  */
190
217
  setSyncAuthOverride(override: {
191
218
  username: string;
192
219
  signingKey: CryptoKey;
220
+ signingPublicKey?: string;
193
221
  privateEncryptionKey?: CryptoKey | string;
194
222
  } | null): void;
195
223
  clearSyncAuthOverride(): void;
196
224
  private getActiveAuthUsername;
197
225
  private getActiveSigningKey;
226
+ private getActiveSigningPublicKeyPem;
198
227
  private getActivePrivateEncryptionKey;
199
228
  }
200
229
  //# sourceMappingURL=ClientNetworkContentAddressedStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ClientNetworkContentAddressedStore.d.ts","sourceRoot":"","sources":["../../../src/appendonlystores/network/ClientNetworkContentAddressedStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ClientNetworkContentAddressedStore.d.ts","sourceRoot":"","sources":["../../../src/appendonlystores/network/ClientNetworkContentAddressedStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EACV,kBAAkB,EAClB,yBAAyB,EACzB,gCAAgC,EAChC,2BAA2B,EAC3B,0BAA0B,EAC1B,aAAa,EACb,SAAS,EACT,SAAS,EACV,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sDAAsD,CAAC;AAC7F,OAAO,KAAK,EAGV,gBAAgB,EAChB,uBAAuB,EACxB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,MAAM,EAAoC,MAAM,oBAAoB,CAAC;AAc9E;;;;;;;;;;;;;GAaG;AACH,qBAAa,kCAAmC,YAAW,qBAAqB;IAC9E,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAY;IAC9B,uFAAuF;IACvF,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,aAAa,CAAgB;IAGrC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,qBAAqB,CAAgC;IAC7D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,mBAAmB,CAAiD;IAC5E,OAAO,CAAC,gBAAgB,CAMR;IAChB,OAAO,CAAC,gBAAgB,CAAC,CAAc;IACvC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,qBAAqB,CAAwC;IAErE;;;;;;;;;;;;;OAaG;gBAED,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,gBAAgB,EAC3B,aAAa,EAAE,aAAa,EAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,EACrB,oBAAoB,EAAE,SAAS,GAAG,MAAM,EACxC,MAAM,CAAC,EAAE,MAAM,EACf,mBAAmB,CAAC,EAAE,MAAM;IAoB9B,KAAK,IAAI,MAAM;IAIf,YAAY,IAAI,SAAS;IAIzB,gBAAgB,IAAI,MAAM;IAK1B,kBAAkB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI;IAO9C,OAAO,CAAC,kBAAkB;IAQ1B;;;;;;;;;OASG;IACG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAgC/D;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAqDhD,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAQhE,mCAAmC,CACvC,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,kBAAkB,CAAC;IAW9B;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAclD;;;;;OAKG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAYvE;;OAEG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAW5F;;OAEG;IACG,WAAW,CACf,IAAI,EAAE,cAAc,EACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,EAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAgBhC,OAAO,CAAC,kCAAkC;IAQ1C;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWpC;;;OAGG;IACG,gBAAgB,CACpB,MAAM,EAAE,eAAe,GAAG,IAAI,EAC9B,KAAK,GAAE,MAAgC,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC;IA4D3B;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAWlC,iBAAiB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAcjD,eAAe,IAAI,OAAO,CAAC,uBAAuB,CAAC;IAiDzD;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;YAM7D,yBAAyB;IAkDvC,OAAO,CAAC,MAAM,CAAC,cAAc;IAgBvB,mBAAmB,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAwBrD,2BAA2B,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,0BAA0B,GACnC,OAAO,CAAC,2BAA2B,CAAC;IAWjC,gCAAgC,CACpC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,CAAC,EAAE,0BAA0B,GACnC,OAAO,CAAC,gCAAgC,CAAC;IAW5C;;;;OAIG;IACG,mBAAmB,CACvB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC;IAYpB;;;;;;;;OAQG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD;;OAEG;IACH;;;OAGG;IACH,wBAAwB,CAAC,QAAQ,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI;YAI1D,mBAAmB;YAwDnB,qBAAqB;IAkDnC;;OAEG;YACW,aAAa;YAIb,oBAAoB;IAalC;;;OAGG;YACW,8BAA8B;IAuD5C;;;OAGG;YACW,qBAAqB;IAyBnC,OAAO,CAAC,4BAA4B;IAKpC,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAQ9C,OAAO,CAAC,4BAA4B;IAgBpC,OAAO,CAAC,8BAA8B;IAYtC;;;OAGG;IACH,cAAc,IAAI,IAAI;YAYR,qBAAqB;IAKnC,OAAO,CAAC,mBAAmB;YAIb,qBAAqB;IAiBnC;;;;;;;;OAQG;IACH,mBAAmB,CACjB,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,SAAS,CAAC;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,oBAAoB,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;KAC3C,GAAG,IAAI,GACP,IAAI;IAeP,qBAAqB,IAAI,IAAI;IAS7B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,4BAA4B;IAQpC,OAAO,CAAC,6BAA6B;CAGtC"}
@@ -31,8 +31,11 @@ class ClientNetworkContentAddressedStore {
31
31
  * @param signingKey The user's private signing key (for signing challenges)
32
32
  * @param privateEncryptionKey The user's private RSA key (for decrypting received entries)
33
33
  * @param logger Optional logger instance
34
+ * @param signingPublicKeyPem Optional device signing public key (PEM). Sent with
35
+ * the auth challenge so the server can detect a second-device key that is
36
+ * not yet in its directory copy (instead of a vague "Invalid signature").
34
37
  */
35
- constructor(dbId, storeKind, transport, cryptoAdapter, username, signingKey, privateEncryptionKey, logger) {
38
+ constructor(dbId, storeKind, transport, cryptoAdapter, username, signingKey, privateEncryptionKey, logger, signingPublicKeyPem) {
36
39
  // Cached access token
37
40
  this.accessToken = null;
38
41
  this.tokenExpiry = 0;
@@ -40,6 +43,18 @@ class ClientNetworkContentAddressedStore {
40
43
  this.capabilitiesCache = null;
41
44
  this.capabilitiesPromise = null;
42
45
  this.syncAuthOverride = null;
46
+ /**
47
+ * Supplies a ready-made bearer token instead of running the tenant
48
+ * challenge-response handshake.
49
+ *
50
+ * Used by server-to-server replication: a peer authenticates once at the
51
+ * cluster level (`/system/peer/authenticate`) against `trusted-servers.json`
52
+ * and holds no tenant grant, so the per-tenant handshake would always fail.
53
+ * Everything else — the entire sync protocol — is identical, which is the
54
+ * point: the mesh reuses the client protocol rather than inventing a second
55
+ * replication format.
56
+ */
57
+ this.externalTokenProvider = null;
43
58
  this.dbId = dbId;
44
59
  this.storeKind = storeKind;
45
60
  this.transport = transport;
@@ -47,6 +62,10 @@ class ClientNetworkContentAddressedStore {
47
62
  this.username = username;
48
63
  this.signingKey = signingKey;
49
64
  this.privateEncryptionKey = privateEncryptionKey;
65
+ this.signingPublicKeyPem =
66
+ typeof signingPublicKeyPem === "string" && signingPublicKeyPem.trim().length > 0
67
+ ? signingPublicKeyPem.trim()
68
+ : undefined;
50
69
  this.logger =
51
70
  logger ||
52
71
  new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), `ClientNetworkStore:${dbId}:${storeKind}`, true);
@@ -524,7 +543,17 @@ class ClientNetworkContentAddressedStore {
524
543
  /**
525
544
  * Ensure we have a valid access token, authenticating if necessary.
526
545
  */
546
+ /**
547
+ * Route every authenticated call through `provider` instead of the tenant
548
+ * handshake. See {@link externalTokenProvider}.
549
+ */
550
+ setExternalTokenProvider(provider) {
551
+ this.externalTokenProvider = provider;
552
+ }
527
553
  async ensureAuthenticated() {
554
+ if (this.externalTokenProvider) {
555
+ return this.externalTokenProvider();
556
+ }
528
557
  const now = Date.now();
529
558
  const sharedState = this.getSharedAuthenticationState();
530
559
  const sharedKey = this.getSharedAuthenticationKey();
@@ -577,8 +606,12 @@ class ClientNetworkContentAddressedStore {
577
606
  const now = Date.now();
578
607
  const authUsername = this.getActiveAuthUsername();
579
608
  this.logger.debug(`Authenticating user: ${authUsername}`);
580
- // Request a challenge
581
- const challenge = await this.transport.requestChallenge(authUsername);
609
+ // Request a challenge. Prefer a non-empty username; always attach the
610
+ // signing public key that will actually sign (override PEM when set, else
611
+ // the store's default device PEM) so multi-device grants can be checked
612
+ // before the client signs.
613
+ const challengeSigningPublicKey = this.getActiveSigningPublicKeyPem();
614
+ const challenge = await this.transport.requestChallenge(authUsername.trim() ? authUsername : undefined, challengeSigningPublicKey ? { signingPublicKey: challengeSigningPublicKey } : undefined);
582
615
  this.logger.debug(`Received challenge: ${challenge}`);
583
616
  // Sign the challenge
584
617
  const signature = await this.signChallengeWithKey(challenge, this.getActiveSigningKey());
@@ -627,7 +660,15 @@ class ClientNetworkContentAddressedStore {
627
660
  return [];
628
661
  }
629
662
  const subtle = this.cryptoAdapter.getSubtle();
630
- const sessionKeyBytes = await this.rsaEncryption.unwrapKey(batch.wrappedSessionKey, this.getActivePrivateEncryptionKey());
663
+ let sessionKeyBytes;
664
+ try {
665
+ sessionKeyBytes = await this.rsaEncryption.unwrapKey(batch.wrappedSessionKey, this.getActivePrivateEncryptionKey());
666
+ }
667
+ catch (error) {
668
+ const name = error instanceof Error ? error.name : "Error";
669
+ const message = error instanceof Error ? error.message : String(error);
670
+ throw new Error(`Cannot unwrap the directory sync session key: it is wrapped to a different device's encryption key (${name}: ${message})`);
671
+ }
631
672
  const sessionKey = await subtle.importKey("raw", sessionKeyBytes.buffer.slice(sessionKeyBytes.byteOffset, sessionKeyBytes.byteOffset + sessionKeyBytes.byteLength), { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
632
673
  const results = [];
633
674
  for (const enc of batch.entries) {
@@ -654,22 +695,9 @@ class ClientNetworkContentAddressedStore {
654
695
  this.throwIfSyncAborted();
655
696
  // Decrypt the RSA layer to get the original symmetric-encrypted payload
656
697
  const encryptedData = await this.rsaEncryption.decrypt(enc.rsaEncryptedPayload, this.getActivePrivateEncryptionKey());
657
- // Create the full StoreEntry using metadata from the network entry
698
+ const { rsaEncryptedPayload: _payload, ...metadata } = enc;
658
699
  const entry = {
659
- entryType: enc.entryType,
660
- id: enc.id,
661
- contentHash: enc.contentHash,
662
- docId: enc.docId,
663
- dependencyIds: enc.dependencyIds,
664
- createdAt: enc.createdAt,
665
- createdByPublicKey: enc.createdByPublicKey,
666
- decryptionKeyId: enc.decryptionKeyId,
667
- snapshotHeadHashes: enc.snapshotHeadHashes,
668
- snapshotHeadEntryIds: enc.snapshotHeadEntryIds,
669
- signature: enc.signature,
670
- originalSize: enc.originalSize,
671
- encryptedSize: enc.encryptedSize,
672
- attachmentRefs: enc.attachmentRefs,
700
+ ...metadata,
673
701
  encryptedData,
674
702
  };
675
703
  results.push(entry);
@@ -752,10 +780,22 @@ class ClientNetworkContentAddressedStore {
752
780
  /**
753
781
  * Set or clear a temporary per-sync authentication override.
754
782
  * Intended for bootstrap scenarios (for example authenticating as admin).
783
+ *
784
+ * When overriding, pass `signingPublicKey` (PEM) for the same key material as
785
+ * `signingKey`. Auth challenges bind username + device key; leaving the
786
+ * store's default PEM in place while signing with a different key causes the
787
+ * server to reject the challenge as an unknown device.
755
788
  */
756
789
  setSyncAuthOverride(override) {
757
790
  const previousUsername = this.getActiveAuthUsername();
758
- this.syncAuthOverride = override;
791
+ this.syncAuthOverride = override
792
+ ? {
793
+ ...override,
794
+ ...(typeof override.signingPublicKey === "string" && override.signingPublicKey.trim()
795
+ ? { signingPublicKey: override.signingPublicKey.trim() }
796
+ : {}),
797
+ }
798
+ : null;
759
799
  // Changing auth identity requires token refresh.
760
800
  this.clearAuthCache();
761
801
  this.clearSharedAuthenticationState(previousUsername);
@@ -774,6 +814,13 @@ class ClientNetworkContentAddressedStore {
774
814
  getActiveSigningKey() {
775
815
  return this.syncAuthOverride?.signingKey ?? this.signingKey;
776
816
  }
817
+ getActiveSigningPublicKeyPem() {
818
+ const fromOverride = this.syncAuthOverride?.signingPublicKey;
819
+ if (typeof fromOverride === "string" && fromOverride.trim().length > 0) {
820
+ return fromOverride.trim();
821
+ }
822
+ return this.signingPublicKeyPem;
823
+ }
777
824
  getActivePrivateEncryptionKey() {
778
825
  return this.syncAuthOverride?.privateEncryptionKey ?? this.privateEncryptionKey;
779
826
  }