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.
- package/README-server.md +136 -71
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +30 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +67 -20
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +6 -62
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +82 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +229 -25
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +59 -0
- package/dist/core/BaseMindooDB.d.ts +148 -80
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +890 -692
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +59 -7
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +407 -86
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +61 -9
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +363 -35
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +17 -1
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +286 -17
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts +44 -22
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +45 -21
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts +68 -1
- package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
- package/dist/core/accesscontrol/DirectoryStateNode.js +157 -4
- package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.js +14 -5
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
- package/dist/core/accesscontrol/grantKeys.d.ts +17 -14
- package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
- package/dist/core/accesscontrol/grantKeys.js +51 -21
- package/dist/core/accesscontrol/grantKeys.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts +2 -1
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +3 -2
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +70 -14
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/entryWireCodec.d.ts +51 -0
- package/dist/core/appendonlystores/network/entryWireCodec.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/entryWireCodec.js +93 -0
- package/dist/core/appendonlystores/network/entryWireCodec.js.map +1 -0
- package/dist/core/appendonlystores/syncStores.d.ts +147 -0
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -0
- package/dist/core/appendonlystores/syncStores.js +612 -0
- package/dist/core/appendonlystores/syncStores.js.map +1 -0
- package/dist/core/builtinDbInvariants.d.ts +83 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -0
- package/dist/core/builtinDbInvariants.js +244 -0
- package/dist/core/builtinDbInvariants.js.map +1 -0
- package/dist/core/copy/feasibility.d.ts.map +1 -1
- package/dist/core/copy/feasibility.js +3 -0
- package/dist/core/copy/feasibility.js.map +1 -1
- package/dist/core/copy/flatten.d.ts.map +1 -1
- package/dist/core/copy/flatten.js +6 -1
- package/dist/core/copy/flatten.js.map +1 -1
- package/dist/core/copy/host.d.ts +5 -0
- package/dist/core/copy/host.d.ts.map +1 -1
- package/dist/core/copy/preflight.d.ts.map +1 -1
- package/dist/core/copy/preflight.js +7 -1
- package/dist/core/copy/preflight.js.map +1 -1
- package/dist/core/copy/types.d.ts +7 -0
- package/dist/core/copy/types.d.ts.map +1 -1
- package/dist/core/crypto/EntrySignature.d.ts +8 -2
- package/dist/core/crypto/EntrySignature.d.ts.map +1 -1
- package/dist/core/crypto/EntrySignature.js +14 -5
- package/dist/core/crypto/EntrySignature.js.map +1 -1
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
- package/dist/core/crypto/MindooDocSigner.js +11 -2
- package/dist/core/crypto/MindooDocSigner.js.map +1 -1
- package/dist/core/crypto/RSAEncryption.d.ts +19 -0
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
- package/dist/core/crypto/RSAEncryption.js +36 -3
- package/dist/core/crypto/RSAEncryption.js.map +1 -1
- package/dist/core/crypto/privateKeyEncryption.js +1 -1
- package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
- package/dist/core/crypto/rsaOaep3072.d.ts +12 -0
- package/dist/core/crypto/rsaOaep3072.d.ts.map +1 -0
- package/dist/core/crypto/rsaOaep3072.js +30 -0
- package/dist/core/crypto/rsaOaep3072.js.map +1 -0
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +33 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
- package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
- package/dist/core/join/deviceJoinBootstrap.js +37 -0
- package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
- package/dist/core/keys/KeyBag.d.ts +20 -2
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +22 -10
- package/dist/core/keys/KeyBag.js.map +1 -1
- package/dist/core/logging/MindooLogger.d.ts.map +1 -1
- package/dist/core/logging/MindooLogger.js +65 -40
- package/dist/core/logging/MindooLogger.js.map +1 -1
- package/dist/core/tenantIdValidation.d.ts.map +1 -1
- package/dist/core/tenantIdValidation.js +1 -1
- package/dist/core/tenantIdValidation.js.map +1 -1
- package/dist/core/tenantSetup.d.ts +94 -9
- package/dist/core/tenantSetup.d.ts.map +1 -1
- package/dist/core/tenantSetup.js +208 -20
- package/dist/core/tenantSetup.js.map +1 -1
- package/dist/core/types.d.ts +313 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/joinRequestUri.d.ts.map +1 -1
- package/dist/core/uri/joinRequestUri.js +12 -3
- package/dist/core/uri/joinRequestUri.js.map +1 -1
- package/dist/core/userid/canonicalUsername.d.ts +66 -0
- package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
- package/dist/core/userid/canonicalUsername.js +235 -0
- package/dist/core/userid/canonicalUsername.js.map +1 -0
- package/dist/core/userid/index.d.ts +9 -0
- package/dist/core/userid/index.d.ts.map +1 -1
- package/dist/core/userid/index.js +14 -0
- package/dist/core/userid/index.js.map +1 -1
- package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
- package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/UserKeyDocument.js +145 -0
- package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
- package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
- package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
- package/dist/core/userkeys/UserKeyManager.js +1277 -0
- package/dist/core/userkeys/UserKeyManager.js.map +1 -0
- package/dist/core/userkeys/fingerprint.d.ts +17 -0
- package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
- package/dist/core/userkeys/fingerprint.js +59 -0
- package/dist/core/userkeys/fingerprint.js.map +1 -0
- package/dist/core/userkeys/index.d.ts +11 -0
- package/dist/core/userkeys/index.d.ts.map +1 -0
- package/dist/core/userkeys/index.js +49 -0
- package/dist/core/userkeys/index.js.map +1 -0
- package/dist/core/userkeys/recipients.d.ts +51 -0
- package/dist/core/userkeys/recipients.d.ts.map +1 -0
- package/dist/core/userkeys/recipients.js +292 -0
- package/dist/core/userkeys/recipients.js.map +1 -0
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
- package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
- package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
- package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
- package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
- package/dist/core/userkeys/sealedCrypto.js +79 -0
- package/dist/core/userkeys/sealedCrypto.js.map +1 -0
- package/dist/core/userkeys/sealedTypes.d.ts +72 -0
- package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
- package/dist/core/userkeys/sealedTypes.js +14 -0
- package/dist/core/userkeys/sealedTypes.js.map +1 -0
- package/dist/core/userkeys/types.d.ts +70 -0
- package/dist/core/userkeys/types.d.ts.map +1 -0
- package/dist/core/userkeys/types.js +48 -0
- package/dist/core/userkeys/types.js.map +1 -0
- package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
- package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
- package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
- package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
- package/dist/node/server/CapabilityMatcher.d.ts +7 -2
- package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.js +12 -3
- package/dist/node/server/CapabilityMatcher.js.map +1 -1
- package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
- package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
- package/dist/node/server/DeviceDiscoveryService.js +165 -0
- package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
- package/dist/node/server/MindooDBServer.d.ts +27 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +265 -220
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/SyncEventBus.d.ts +10 -0
- package/dist/node/server/SyncEventBus.d.ts.map +1 -1
- package/dist/node/server/SyncEventBus.js.map +1 -1
- package/dist/node/server/SystemAdminAuth.d.ts +7 -0
- package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
- package/dist/node/server/SystemAdminAuth.js +4 -1
- package/dist/node/server/SystemAdminAuth.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +86 -5
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +235 -71
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/add-to-network.d.ts.map +1 -1
- package/dist/node/server/add-to-network.js +33 -15
- package/dist/node/server/add-to-network.js.map +1 -1
- package/dist/node/server/canonicalSetupName.d.ts +22 -0
- package/dist/node/server/canonicalSetupName.d.ts.map +1 -0
- package/dist/node/server/canonicalSetupName.js +38 -0
- package/dist/node/server/canonicalSetupName.js.map +1 -0
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +33 -1
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/peer/ClusterAuditLog.d.ts +64 -0
- package/dist/node/server/peer/ClusterAuditLog.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterAuditLog.js +99 -0
- package/dist/node/server/peer/ClusterAuditLog.js.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts +43 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.js +119 -0
- package/dist/node/server/peer/ClusterJobRunner.js.map +1 -0
- package/dist/node/server/peer/ClusterManager.d.ts +116 -0
- package/dist/node/server/peer/ClusterManager.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterManager.js +274 -0
- package/dist/node/server/peer/ClusterManager.js.map +1 -0
- package/dist/node/server/peer/PeerAuthService.d.ts +69 -0
- package/dist/node/server/peer/PeerAuthService.d.ts.map +1 -0
- package/dist/node/server/peer/PeerAuthService.js +208 -0
- package/dist/node/server/peer/PeerAuthService.js.map +1 -0
- package/dist/node/server/peer/PeerReplicator.d.ts +232 -0
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -0
- package/dist/node/server/peer/PeerReplicator.js +879 -0
- package/dist/node/server/peer/PeerReplicator.js.map +1 -0
- package/dist/node/server/peer/routes.d.ts +66 -0
- package/dist/node/server/peer/routes.d.ts.map +1 -0
- package/dist/node/server/peer/routes.js +322 -0
- package/dist/node/server/peer/routes.js.map +1 -0
- package/dist/node/server/peer/types.d.ts +244 -0
- package/dist/node/server/peer/types.d.ts.map +1 -0
- package/dist/node/server/peer/types.js +189 -0
- package/dist/node/server/peer/types.js.map +1 -0
- package/dist/node/server/server.d.ts +1 -1
- package/dist/node/server/server.js +18 -37
- package/dist/node/server/server.js.map +1 -1
- package/dist/node/server/serverinit.d.ts +2 -1
- package/dist/node/server/serverinit.d.ts.map +1 -1
- package/dist/node/server/serverinit.js +34 -12
- package/dist/node/server/serverinit.js.map +1 -1
- package/dist/node/server/types.d.ts +42 -30
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js +1 -7
- package/dist/node/server/types.js.map +1 -1
- package/dist/node/server/validation.d.ts +0 -6
- package/dist/node/server/validation.d.ts.map +1 -1
- package/dist/node/server/validation.js +1 -7
- package/dist/node/server/validation.js.map +1 -1
- 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,
|
|
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
|
-
|
|
478
|
+
Only `name` and the two public keys are required. The rest describe the replication session and all have defaults:
|
|
463
479
|
|
|
464
|
-
|
|
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
|
-
|
|
487
|
+
Trust and configuration changes take effect immediately -- no restart required.
|
|
467
488
|
|
|
468
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
507
|
+
### 5. Enable replication
|
|
485
508
|
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
530
|
+
curl http://server1:1661/system/cluster/status \
|
|
506
531
|
-H "Authorization: Bearer $SYSTEM_ADMIN_JWT"
|
|
507
532
|
```
|
|
508
533
|
|
|
509
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
+
### Client failover
|
|
521
559
|
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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` |
|
|
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 (
|
|
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
|
|
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
|
-
####
|
|
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/
|
|
784
|
-
| `
|
|
785
|
-
| `
|
|
786
|
-
| `POST` | `/system/
|
|
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":"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,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,
|
|
1
|
+
{"version":3,"file":"ClientNetworkContentAddressedStore.d.ts","sourceRoot":"","sources":["../../../src/appendonlystores/network/ClientNetworkContentAddressedStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
+
const { rsaEncryptedPayload: _payload, ...metadata } = enc;
|
|
658
699
|
const entry = {
|
|
659
|
-
|
|
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
|
}
|