mindoodb 0.0.50 → 0.0.53
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 +11 -2
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +13 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +45 -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 +24 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +75 -24
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +72 -0
- package/dist/core/BaseMindooDB.d.ts +89 -14
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +692 -201
- 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 +427 -90
- 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 +292 -16
- package/dist/core/BaseMindooTenantFactory.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 +13 -7
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +16 -10
- 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/builtinDbInvariants.d.ts +53 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -0
- package/dist/core/builtinDbInvariants.js +147 -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 +7 -2
- 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 +8 -1
- 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/databaseIdValidation.d.ts +23 -0
- package/dist/core/databaseIdValidation.d.ts.map +1 -1
- package/dist/core/databaseIdValidation.js +59 -1
- package/dist/core/databaseIdValidation.js.map +1 -1
- package/dist/core/index.d.ts +9 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +40 -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 +109 -0
- package/dist/core/tenantSetup.d.ts.map +1 -0
- package/dist/core/tenantSetup.js +250 -0
- package/dist/core/tenantSetup.js.map +1 -0
- package/dist/core/types.d.ts +333 -43
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +27 -9
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/joinRequestUri.d.ts +37 -0
- package/dist/core/uri/joinRequestUri.d.ts.map +1 -0
- package/dist/core/uri/joinRequestUri.js +168 -0
- package/dist/core/uri/joinRequestUri.js.map +1 -0
- 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/core/utils/idGeneration.d.ts +39 -20
- package/dist/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +82 -62
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +4 -1
- package/dist/core/utils/index.js.map +1 -1
- package/dist/node/server/CapabilityMatcher.d.ts +5 -0
- package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.js +10 -1
- 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 +13 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +99 -61
- package/dist/node/server/MindooDBServer.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 +29 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +146 -7
- package/dist/node/server/TenantManager.js.map +1 -1
- package/package.json +5 -5
|
@@ -24,8 +24,12 @@ const copyDocuments_1 = require("./copy/copyDocuments");
|
|
|
24
24
|
const materializationGuard_1 = require("./accesscontrol/materializationGuard");
|
|
25
25
|
const Ed25519WitnessProvider_1 = require("./accesscontrol/timestamp/Ed25519WitnessProvider");
|
|
26
26
|
const AccessDeniedError_1 = require("./accesscontrol/AccessDeniedError");
|
|
27
|
+
const DirectoryStateNode_1 = require("./accesscontrol/DirectoryStateNode");
|
|
27
28
|
const logging_1 = require("./logging");
|
|
28
29
|
const databaseIdValidation_1 = require("./databaseIdValidation");
|
|
30
|
+
const builtinDbInvariants_1 = require("./builtinDbInvariants");
|
|
31
|
+
const recipients_1 = require("./userkeys/recipients");
|
|
32
|
+
const sealedTypes_1 = require("./userkeys/sealedTypes");
|
|
29
33
|
const DocumentSummaryStore_1 = require("./indexing/summary/DocumentSummaryStore");
|
|
30
34
|
const types_4 = require("./indexing/summary/types");
|
|
31
35
|
const DocumentFullTextIndex_1 = require("./indexing/fulltext/DocumentFullTextIndex");
|
|
@@ -219,12 +223,13 @@ class BaseMindooDB {
|
|
|
219
223
|
*/
|
|
220
224
|
this.aclActiveCache = null;
|
|
221
225
|
/**
|
|
222
|
-
* Per-document cache of the original creator's signing public key
|
|
223
|
-
* `doc_create` entry
|
|
224
|
-
*
|
|
225
|
-
* every change/delete. Populated on create and lazily on first
|
|
226
|
+
* Per-document cache of the original creator's signing public key and the
|
|
227
|
+
* trusted time of the `doc_create` entry. Used by the client write prechecks
|
|
228
|
+
* to resolve `$author` (`isAuthor`) at grant level without re-scanning
|
|
229
|
+
* metadata on every change/delete. Populated on create and lazily on first
|
|
230
|
+
* lookup.
|
|
226
231
|
*/
|
|
227
|
-
this.
|
|
232
|
+
this.creatorInfoCache = new Map();
|
|
228
233
|
// Internal changefeed index: sorted by (changeSeq, docId) for deterministic iteration.
|
|
229
234
|
// lastModified remains available for UX metadata but is not the primary cursor key.
|
|
230
235
|
this.index = [];
|
|
@@ -453,6 +458,10 @@ class BaseMindooDB {
|
|
|
453
458
|
* the warmer settled can still display its outcome.
|
|
454
459
|
*/
|
|
455
460
|
this.warmerProgress = null;
|
|
461
|
+
/** `username_hash` per userdirectory document, filled from creates. */
|
|
462
|
+
this.userdirectoryHashByDocId = new Map();
|
|
463
|
+
/** Grant `username_hash` for a signing public key at "now". */
|
|
464
|
+
this.signerUsernameHashByKey = new Map();
|
|
456
465
|
// ---------------------------------------------------------------------------
|
|
457
466
|
// Author-trust reconciliation: heal documents whose entries were skipped
|
|
458
467
|
// because their author was not yet known to the tenant directory.
|
|
@@ -528,6 +537,125 @@ class BaseMindooDB {
|
|
|
528
537
|
getAdminPublicKey() {
|
|
529
538
|
return this.tenant.getAdministrationPublicKey();
|
|
530
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Database id used by the builtin-write registry. Admin-only databases that
|
|
542
|
+
* are not `userdirectory` are treated like `directory` (admin-only writes).
|
|
543
|
+
*/
|
|
544
|
+
effectiveBuiltinDbId() {
|
|
545
|
+
const dbId = this.store.getId();
|
|
546
|
+
if (this._isAdminOnlyDb && dbId !== types_1.USER_DIRECTORY_DB_ID) {
|
|
547
|
+
return types_1.DIRECTORY_DB_ID;
|
|
548
|
+
}
|
|
549
|
+
return dbId;
|
|
550
|
+
}
|
|
551
|
+
usernameHashFromRecord(payload) {
|
|
552
|
+
if (!payload || typeof payload !== "object")
|
|
553
|
+
return null;
|
|
554
|
+
const value = payload.username_hash;
|
|
555
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
556
|
+
}
|
|
557
|
+
usernameHashFromInternalDoc(internalDoc) {
|
|
558
|
+
return this.usernameHashFromRecord(this.convertAutomergeToJS(internalDoc.doc));
|
|
559
|
+
}
|
|
560
|
+
async resolveSignerUsernameHash(signerKey) {
|
|
561
|
+
try {
|
|
562
|
+
const directory = await this.tenant.openDirectory();
|
|
563
|
+
if (typeof directory.resolveUsernameHashForSigningKey !== "function") {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
return directory.resolveUsernameHashForSigningKey(signerKey, Number.MAX_SAFE_INTEGER);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async resolveSignerPublicKey(signingKeyPair) {
|
|
573
|
+
if (signingKeyPair)
|
|
574
|
+
return signingKeyPair.publicKey;
|
|
575
|
+
return (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Hard-wired write gate for `directory` / `userdirectory` (and other
|
|
579
|
+
* admin-only databases). Independent of the ACL master switch.
|
|
580
|
+
*/
|
|
581
|
+
async assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash) {
|
|
582
|
+
const dbId = this.effectiveBuiltinDbId();
|
|
583
|
+
if (!(0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(dbId) && !this._isAdminOnlyDb) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
let signerUsernameHash = null;
|
|
587
|
+
if (dbId === types_1.USER_DIRECTORY_DB_ID) {
|
|
588
|
+
signerUsernameHash = await this.resolveSignerUsernameHash(signerPublicKey);
|
|
589
|
+
this.signerUsernameHashByKey.set(signerPublicKey, signerUsernameHash);
|
|
590
|
+
}
|
|
591
|
+
const decision = (0, builtinDbInvariants_1.evaluateBuiltinWrite)({
|
|
592
|
+
dbId,
|
|
593
|
+
op,
|
|
594
|
+
signerKey: signerPublicKey,
|
|
595
|
+
adminPublicKey: this.getAdminPublicKey(),
|
|
596
|
+
documentUsernameHash,
|
|
597
|
+
signerUsernameHash,
|
|
598
|
+
});
|
|
599
|
+
if (!decision.allowed) {
|
|
600
|
+
throw new Error(decision.reason);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Load-path counterpart: drop entries that the builtin invariant forbids.
|
|
605
|
+
* Username-hash matching for userdirectory creates is enforced on write and
|
|
606
|
+
* on the server. Admin-signed changes load only when the signer is also the
|
|
607
|
+
* person named by the document (the tenant founder wrapping their own key).
|
|
608
|
+
*/
|
|
609
|
+
async shouldSkipLoadedEntryForBuiltin(entry) {
|
|
610
|
+
const dbId = this.effectiveBuiltinDbId();
|
|
611
|
+
const entryType = entry.entryType ?? "doc_change";
|
|
612
|
+
let documentUsernameHash = null;
|
|
613
|
+
let signerUsernameHash = null;
|
|
614
|
+
if (dbId === types_1.USER_DIRECTORY_DB_ID &&
|
|
615
|
+
(entryType === "doc_change" || entryType === "doc_snapshot") &&
|
|
616
|
+
entry.docId) {
|
|
617
|
+
documentUsernameHash = await this.ensureUserdirectoryDocHash(entry.docId);
|
|
618
|
+
signerUsernameHash = await this.ensureSignerUsernameHash(entry.createdByPublicKey);
|
|
619
|
+
}
|
|
620
|
+
return (0, builtinDbInvariants_1.shouldSkipLoadedEntry)({
|
|
621
|
+
dbId,
|
|
622
|
+
entryType,
|
|
623
|
+
signerKey: entry.createdByPublicKey,
|
|
624
|
+
adminPublicKey: this.getAdminPublicKey(),
|
|
625
|
+
documentUsernameHash,
|
|
626
|
+
signerUsernameHash,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async ensureSignerUsernameHash(signerKey) {
|
|
630
|
+
if (this.signerUsernameHashByKey.has(signerKey)) {
|
|
631
|
+
return this.signerUsernameHashByKey.get(signerKey) ?? null;
|
|
632
|
+
}
|
|
633
|
+
const hash = await this.resolveSignerUsernameHash(signerKey);
|
|
634
|
+
this.signerUsernameHashByKey.set(signerKey, hash);
|
|
635
|
+
return hash;
|
|
636
|
+
}
|
|
637
|
+
async ensureUserdirectoryDocHash(docId) {
|
|
638
|
+
const cached = this.userdirectoryHashByDocId.get(docId);
|
|
639
|
+
if (cached)
|
|
640
|
+
return cached;
|
|
641
|
+
try {
|
|
642
|
+
const metas = await this.scanAllMetadata(this.store, { docId });
|
|
643
|
+
const create = metas.find((meta) => meta.entryType === "doc_create");
|
|
644
|
+
if (!create)
|
|
645
|
+
return null;
|
|
646
|
+
const [entry] = await this.store.getEntries([create.id]);
|
|
647
|
+
if (!entry)
|
|
648
|
+
return null;
|
|
649
|
+
const decrypted = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
|
|
650
|
+
const hash = (0, builtinDbInvariants_1.usernameHashFromCreateChangeBytes)(decrypted);
|
|
651
|
+
if (hash)
|
|
652
|
+
this.userdirectoryHashByDocId.set(docId, hash);
|
|
653
|
+
return hash;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
531
659
|
isAdminOnlyDb() {
|
|
532
660
|
return this._isAdminOnlyDb;
|
|
533
661
|
}
|
|
@@ -715,7 +843,7 @@ class BaseMindooDB {
|
|
|
715
843
|
// indexing (DirectoryTimeTravelIndex) and its change events interleave
|
|
716
844
|
// with access-control evaluation; admin-only databases hold no user
|
|
717
845
|
// data worth summarizing. Explicit getSummaryStore() calls still work.
|
|
718
|
-
if (this._isAdminOnlyDb || this.store.getId() ===
|
|
846
|
+
if (this._isAdminOnlyDb || this.store.getId() === types_1.DIRECTORY_DB_ID || this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
719
847
|
return;
|
|
720
848
|
}
|
|
721
849
|
const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
|
|
@@ -950,7 +1078,7 @@ class BaseMindooDB {
|
|
|
950
1078
|
return;
|
|
951
1079
|
}
|
|
952
1080
|
// Same system-database exclusion as the summary auto-activation.
|
|
953
|
-
if (this._isAdminOnlyDb || this.store.getId() ===
|
|
1081
|
+
if (this._isAdminOnlyDb || this.store.getId() === types_1.DIRECTORY_DB_ID || this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
954
1082
|
return;
|
|
955
1083
|
}
|
|
956
1084
|
const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
|
|
@@ -1557,27 +1685,34 @@ class BaseMindooDB {
|
|
|
1557
1685
|
/**
|
|
1558
1686
|
* Get or import a CryptoKey for signature verification, with caching.
|
|
1559
1687
|
* This avoids re-importing the same public key multiple times.
|
|
1688
|
+
*
|
|
1689
|
+
* Trust is evaluated at `trustedTime` when provided (grant membership in the
|
|
1690
|
+
* directory node covering that time, or the tenant admin key). Callers that
|
|
1691
|
+
* omit it keep the historical HEAD check via `validatePublicSigningKey`.
|
|
1560
1692
|
*/
|
|
1561
|
-
async getOrImportPublicKey(publicKey) {
|
|
1562
|
-
// Check cache first
|
|
1563
|
-
if (this.publicKeyCache.has(publicKey)) {
|
|
1564
|
-
return this.publicKeyCache.get(publicKey);
|
|
1565
|
-
}
|
|
1566
|
-
// Validate the public key is trusted
|
|
1693
|
+
async getOrImportPublicKey(publicKey, trustedTime) {
|
|
1567
1694
|
const directory = await this.tenant.openDirectory();
|
|
1568
|
-
|
|
1695
|
+
let isTrusted = false;
|
|
1696
|
+
if (trustedTime !== undefined && typeof directory.getDirectoryStateAt === "function") {
|
|
1697
|
+
const node = await directory.getDirectoryStateAt(trustedTime);
|
|
1698
|
+
isTrusted =
|
|
1699
|
+
!!(0, DirectoryStateNode_1.grantForSigningKey)(node, publicKey) || publicKey === this.getAdminPublicKey();
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
isTrusted = await directory.validatePublicSigningKey(publicKey);
|
|
1703
|
+
}
|
|
1569
1704
|
if (!isTrusted) {
|
|
1570
1705
|
this.logger.warn(`Public key not trusted: ${publicKey}`);
|
|
1571
1706
|
return null;
|
|
1572
1707
|
}
|
|
1573
|
-
|
|
1708
|
+
const cached = this.publicKeyCache.get(publicKey);
|
|
1709
|
+
if (cached)
|
|
1710
|
+
return cached;
|
|
1574
1711
|
const subtle = this.tenant.getCryptoAdapter().getSubtle();
|
|
1575
1712
|
const publicKeyBuffer = this.tenant.pemToArrayBuffer(publicKey);
|
|
1576
1713
|
const cryptoKey = await subtle.importKey("spki", publicKeyBuffer, {
|
|
1577
1714
|
name: "Ed25519",
|
|
1578
|
-
}, false,
|
|
1579
|
-
["verify"]);
|
|
1580
|
-
// Cache the imported key
|
|
1715
|
+
}, false, ["verify"]);
|
|
1581
1716
|
this.publicKeyCache.set(publicKey, cryptoKey);
|
|
1582
1717
|
return cryptoKey;
|
|
1583
1718
|
}
|
|
@@ -2055,7 +2190,7 @@ class BaseMindooDB {
|
|
|
2055
2190
|
if (!this.summaryAutoUpdateEnabled || this.summarySetupProbed || this.summaryStore) {
|
|
2056
2191
|
return;
|
|
2057
2192
|
}
|
|
2058
|
-
if (this._isAdminOnlyDb || this.store.getId() ===
|
|
2193
|
+
if (this._isAdminOnlyDb || this.store.getId() === types_1.DIRECTORY_DB_ID || this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2059
2194
|
return;
|
|
2060
2195
|
}
|
|
2061
2196
|
this.summarySetupProbed = true;
|
|
@@ -2089,7 +2224,7 @@ class BaseMindooDB {
|
|
|
2089
2224
|
if (!this.fulltextAutoUpdateEnabled || this.fulltextSetupProbed || this.fulltextIndex) {
|
|
2090
2225
|
return;
|
|
2091
2226
|
}
|
|
2092
|
-
if (this._isAdminOnlyDb || this.store.getId() ===
|
|
2227
|
+
if (this._isAdminOnlyDb || this.store.getId() === types_1.DIRECTORY_DB_ID || this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2093
2228
|
return;
|
|
2094
2229
|
}
|
|
2095
2230
|
if (this.isTimeTravelMode()) {
|
|
@@ -2254,10 +2389,7 @@ class BaseMindooDB {
|
|
|
2254
2389
|
}
|
|
2255
2390
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
2256
2391
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
2257
|
-
|
|
2258
|
-
// decryption key is in the bag are visible; the rest are hidden. Which
|
|
2259
|
-
// keys a user holds is governed by the key distribution model
|
|
2260
|
-
// (docs/accesscontrol.md §13), not by a server/client read gate.
|
|
2392
|
+
await this.tenant.ingestSealedFromEntries?.(visibility.decryptionKeyId, metadataByDoc.get(docId));
|
|
2261
2393
|
const canRead = await this.tenant.hasDecryptionKey(visibility.decryptionKeyId);
|
|
2262
2394
|
if (canRead) {
|
|
2263
2395
|
// Reveal-on-add only triggers when the previous state was
|
|
@@ -2407,7 +2539,7 @@ class BaseMindooDB {
|
|
|
2407
2539
|
* directory was synced in a session that never opened this DB).
|
|
2408
2540
|
*/
|
|
2409
2541
|
async reconcileAuthorTrustAtOpen() {
|
|
2410
|
-
if (this.isTimeTravelMode() || this._isAdminOnlyDb || this.store.getId() ===
|
|
2542
|
+
if (this.isTimeTravelMode() || this._isAdminOnlyDb || this.store.getId() === types_1.DIRECTORY_DB_ID || this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2411
2543
|
return;
|
|
2412
2544
|
}
|
|
2413
2545
|
if (this.legacyCheckpointNeedsAuthorTrustScan) {
|
|
@@ -2601,6 +2733,10 @@ class BaseMindooDB {
|
|
|
2601
2733
|
const cachedDoc = this.getCachedDocument(docId);
|
|
2602
2734
|
let updatedDoc = null;
|
|
2603
2735
|
if (cachedDoc) {
|
|
2736
|
+
if (await this.dropSealedAccessIfUnwrapFailed(cachedDoc)) {
|
|
2737
|
+
this.logger.debug(`Dropping cached sealed document ${docId} — newest wrap is not for this user`);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2604
2740
|
// Document is cached - try incremental update
|
|
2605
2741
|
this.logger.debug(`Document ${docId} found in cache, attempting incremental update`);
|
|
2606
2742
|
try {
|
|
@@ -2646,9 +2782,18 @@ class BaseMindooDB {
|
|
|
2646
2782
|
// Before indexing, verify the user has the decryption key so that
|
|
2647
2783
|
// documents the user cannot access do not appear in getAllDocumentIds.
|
|
2648
2784
|
const representativeEntry = docLifecycleEntries[0];
|
|
2785
|
+
await this.tenant.ingestSealedFromEntries?.(representativeEntry.decryptionKeyId, docLifecycleEntries);
|
|
2649
2786
|
const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
|
|
2650
2787
|
if (!keyAvailable) {
|
|
2651
2788
|
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
2789
|
+
const existingIndex = this.getDocIndexPosition(docId);
|
|
2790
|
+
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
2791
|
+
if (existing?.accessState === "visible") {
|
|
2792
|
+
await this.purgeMaterializedDocument(docId);
|
|
2793
|
+
this.updateIndex(docId, existing.lastModified, true, representativeEntry.decryptionKeyId, "inaccessible");
|
|
2794
|
+
this.cacheMetaDirty = true;
|
|
2795
|
+
this.cacheManager?.markDirty();
|
|
2796
|
+
}
|
|
2652
2797
|
}
|
|
2653
2798
|
else {
|
|
2654
2799
|
// Cold sync: entryMetadataList is already the full doc history from
|
|
@@ -3861,8 +4006,8 @@ class BaseMindooDB {
|
|
|
3861
4006
|
return stripped.length > 0 ? stripped : undefined;
|
|
3862
4007
|
}
|
|
3863
4008
|
/**
|
|
3864
|
-
* Generate a fresh document id (`<prefix>_<
|
|
3865
|
-
* bare
|
|
4009
|
+
* Generate a fresh document id (`<prefix>_<24-char-objectid>`, or the
|
|
4010
|
+
* bare ObjectId without a prefix) that does not collide with any document
|
|
3866
4011
|
* already present in the local index. A collision is practically impossible
|
|
3867
4012
|
* (128-bit UUID7), so this is only a cheap O(1) safety net reusing the
|
|
3868
4013
|
* existing `indexLookup` map — no extra bookkeeping.
|
|
@@ -3879,10 +4024,10 @@ class BaseMindooDB {
|
|
|
3879
4024
|
* Internal method to create a new document.
|
|
3880
4025
|
*
|
|
3881
4026
|
* Handles the creation flavors expressible through `CreateOptions`:
|
|
3882
|
-
* - generated ID (time-sortable
|
|
4027
|
+
* - generated ID (time-sortable MongoDB-style ObjectId), current user signs,
|
|
3883
4028
|
* default tenant key
|
|
3884
4029
|
* - generated ID with custom signing key (e.g. directory admin operations)
|
|
3885
|
-
* - generated ID with a caller-provided `idPrefix` (`<prefix>_<
|
|
4030
|
+
* - generated ID with a caller-provided `idPrefix` (`<prefix>_<objectId>`)
|
|
3886
4031
|
* - caller-provided ID, current user signs (idempotent on existing IDs)
|
|
3887
4032
|
* - caller-provided ID with custom signing key
|
|
3888
4033
|
*
|
|
@@ -3899,7 +4044,6 @@ class BaseMindooDB {
|
|
|
3899
4044
|
throw new Error("createDocument: signingKeyPair and signingKeyPassword must be provided together");
|
|
3900
4045
|
}
|
|
3901
4046
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
3902
|
-
const keyId = await this.resolveCreateKeyId(options);
|
|
3903
4047
|
const useCustomDocId = options.id !== undefined;
|
|
3904
4048
|
this.validateIdPrefixOption("createDocument", options);
|
|
3905
4049
|
this.validateAssumeUniqueIdOption("createDocument", options);
|
|
@@ -3931,17 +4075,14 @@ class BaseMindooDB {
|
|
|
3931
4075
|
throw new Error("createDocument: initialValues is not supported together with a custom id; " +
|
|
3932
4076
|
"create the document, then apply values with changeDoc().");
|
|
3933
4077
|
}
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
3938
|
-
const signerPublicKey = useCustomSigningKey
|
|
3939
|
-
? signingKeyPair.publicKey
|
|
3940
|
-
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
3941
|
-
if (signerPublicKey !== adminPublicKey) {
|
|
3942
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
4078
|
+
if (useDeterministicSeed && options.recipients !== undefined) {
|
|
4079
|
+
throw new Error("createDocument: recipients is not supported together with a convergent custom id; " +
|
|
4080
|
+
"pass assumeUniqueId for random ids.");
|
|
3943
4081
|
}
|
|
3944
4082
|
}
|
|
4083
|
+
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4084
|
+
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
4085
|
+
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash);
|
|
3945
4086
|
const docId = useCustomDocId ? options.id : this.generateUnusedDocId(options.idPrefix);
|
|
3946
4087
|
// Idempotent create: when a caller-provided id already exists locally,
|
|
3947
4088
|
// return the existing document instead of producing a duplicate doc_create.
|
|
@@ -3961,6 +4102,7 @@ class BaseMindooDB {
|
|
|
3961
4102
|
return this.wrapDocument(existing);
|
|
3962
4103
|
}
|
|
3963
4104
|
}
|
|
4105
|
+
const keyId = await this.resolveCreateKeyId(options, docId);
|
|
3964
4106
|
this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}${useCustomDocId ? ' with caller-provided id' : ''}`);
|
|
3965
4107
|
// Build the initial Automerge document and its first change bytes.
|
|
3966
4108
|
//
|
|
@@ -3970,6 +4112,7 @@ class BaseMindooDB {
|
|
|
3970
4112
|
// replicas using the same id produce the same Automerge hash and
|
|
3971
4113
|
// `doc_create` entry id, allowing later changes to merge.
|
|
3972
4114
|
const now = (0, timeSource_1.semanticNow)();
|
|
4115
|
+
const sealedCreate = await this.prepareSealedCreate(options, docId, keyId, now, signerPublicKey);
|
|
3973
4116
|
let newDoc;
|
|
3974
4117
|
let changeBytes;
|
|
3975
4118
|
if (useDeterministicSeed) {
|
|
@@ -3995,6 +4138,9 @@ class BaseMindooDB {
|
|
|
3995
4138
|
// Store metadata in the document payload
|
|
3996
4139
|
// We need to modify the document to ensure a change is created
|
|
3997
4140
|
doc._attachments = [];
|
|
4141
|
+
if (sealedCreate) {
|
|
4142
|
+
(0, recipients_1.applyEncryptForCreate)(doc, sealedCreate.encryptFor);
|
|
4143
|
+
}
|
|
3998
4144
|
// Seed caller-provided initial values within the same doc_create
|
|
3999
4145
|
// change so a doc_create Tier 2 rule can evaluate them in the
|
|
4000
4146
|
// "after" state (docs/accesscontrol.md §6.3, §9).
|
|
@@ -4074,9 +4220,12 @@ class BaseMindooDB {
|
|
|
4074
4220
|
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
4075
4221
|
}
|
|
4076
4222
|
this.logger.debug(`Signed payload, signature length: ${signature.length} bytes`);
|
|
4077
|
-
// Remember the creator
|
|
4078
|
-
//
|
|
4079
|
-
|
|
4223
|
+
// Remember the creator so $author prechecks on subsequent change/delete
|
|
4224
|
+
// operations don't need a metadata scan. `now` is the local createdAt;
|
|
4225
|
+
// a later witness may stamp receivedAt, which resolveCreatorInfo picks up
|
|
4226
|
+
// on a cache miss. For the local writer, createdAt is sufficient: the
|
|
4227
|
+
// creating device is still granted.
|
|
4228
|
+
this.creatorInfoCache.set(docId, { signingKey: createdByPublicKey, trustedTime: now });
|
|
4080
4229
|
// Snapshot of attachments referenced by the freshly created doc (normally
|
|
4081
4230
|
// empty; the public create API does not add attachments inline).
|
|
4082
4231
|
const createAttachmentRefs = this.collectAttachmentRefs(newDoc);
|
|
@@ -4094,6 +4243,7 @@ class BaseMindooDB {
|
|
|
4094
4243
|
originalSize: changeBytes.length,
|
|
4095
4244
|
encryptedSize: encryptedPayload.length,
|
|
4096
4245
|
attachmentRefs: createAttachmentRefs.length > 0 ? createAttachmentRefs : undefined,
|
|
4246
|
+
recipients: sealedCreate?.recipients,
|
|
4097
4247
|
entryVersion: types_1.CURRENT_STORE_ENTRY_VERSION,
|
|
4098
4248
|
};
|
|
4099
4249
|
// Bind the metadata with the author signature (audit finding #5).
|
|
@@ -4123,6 +4273,7 @@ class BaseMindooDB {
|
|
|
4123
4273
|
isDeleted: false,
|
|
4124
4274
|
awaitingWitness,
|
|
4125
4275
|
witnessed,
|
|
4276
|
+
sealedRecipients: sealedCreate?.recipients,
|
|
4126
4277
|
};
|
|
4127
4278
|
// Update cache and index
|
|
4128
4279
|
await this.storeCachedDocument(internalDoc);
|
|
@@ -4149,7 +4300,6 @@ class BaseMindooDB {
|
|
|
4149
4300
|
throw new Error("createDocuments: signingKeyPair and signingKeyPassword must be provided together");
|
|
4150
4301
|
}
|
|
4151
4302
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
4152
|
-
const keyId = await this.resolveCreateKeyId(options);
|
|
4153
4303
|
const useCustomDocId = options.id !== undefined;
|
|
4154
4304
|
this.validateIdPrefixOption("createDocuments", options);
|
|
4155
4305
|
this.validateAssumeUniqueIdOption("createDocuments", options);
|
|
@@ -4157,24 +4307,22 @@ class BaseMindooDB {
|
|
|
4157
4307
|
// out of the deterministic seed, so initialValues fold into doc_create
|
|
4158
4308
|
// and the document costs a single store entry.
|
|
4159
4309
|
const useDeterministicSeed = useCustomDocId && !options.assumeUniqueId;
|
|
4310
|
+
if (useDeterministicSeed && options.recipients !== undefined) {
|
|
4311
|
+
throw new Error("createDocuments: recipients is not supported together with a convergent custom id; pass assumeUniqueId");
|
|
4312
|
+
}
|
|
4160
4313
|
// Sanitize caller-provided initial values: internal/reserved fields are
|
|
4161
4314
|
// managed by MindooDB and must not be seeded by callers. `undefined`
|
|
4162
4315
|
// values are treated as "not set" (Automerge cannot represent undefined).
|
|
4163
4316
|
const initialValueEntries = options.initialValues
|
|
4164
4317
|
? Object.entries(options.initialValues).filter(([k, v]) => !k.startsWith("_") && v !== undefined)
|
|
4165
4318
|
: [];
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
const signerPublicKey = useCustomSigningKey
|
|
4170
|
-
? signingKeyPair.publicKey
|
|
4171
|
-
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
4172
|
-
if (signerPublicKey !== adminPublicKey) {
|
|
4173
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4319
|
+
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4320
|
+
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
4321
|
+
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash);
|
|
4176
4322
|
const docId = useCustomDocId ? options.id : this.generateUnusedDocId(options.idPrefix);
|
|
4177
4323
|
const now = (0, timeSource_1.semanticNow)();
|
|
4324
|
+
const keyId = await this.resolveCreateKeyId(options, docId);
|
|
4325
|
+
const sealedCreate = await this.prepareSealedCreate(options, docId, keyId, now, signerPublicKey);
|
|
4178
4326
|
// Build the initial Automerge document and its first change bytes,
|
|
4179
4327
|
// following the same seeding rules as the single-create path.
|
|
4180
4328
|
let seedDoc;
|
|
@@ -4189,6 +4337,9 @@ class BaseMindooDB {
|
|
|
4189
4337
|
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
4190
4338
|
seedDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
|
|
4191
4339
|
doc._attachments = [];
|
|
4340
|
+
if (sealedCreate) {
|
|
4341
|
+
(0, recipients_1.applyEncryptForCreate)(doc, sealedCreate.encryptFor);
|
|
4342
|
+
}
|
|
4192
4343
|
// Seed caller-provided initial values within the same doc_create change
|
|
4193
4344
|
// so a doc_create Tier 2 rule can evaluate them in the "after" state.
|
|
4194
4345
|
for (const [key, value] of initialValueEntries) {
|
|
@@ -4254,9 +4405,12 @@ class BaseMindooDB {
|
|
|
4254
4405
|
signature = await this.tenant.signPayload(encryptedPayload);
|
|
4255
4406
|
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
4256
4407
|
}
|
|
4257
|
-
// Remember the creator
|
|
4258
|
-
//
|
|
4259
|
-
|
|
4408
|
+
// Remember the creator so $author prechecks on subsequent change/delete
|
|
4409
|
+
// operations don't need a metadata scan. `now` is the local createdAt;
|
|
4410
|
+
// a later witness may stamp receivedAt, which resolveCreatorInfo picks up
|
|
4411
|
+
// on a cache miss. For the local writer, createdAt is sufficient: the
|
|
4412
|
+
// creating device is still granted.
|
|
4413
|
+
this.creatorInfoCache.set(docId, { signingKey: createdByPublicKey, trustedTime: now });
|
|
4260
4414
|
const createAttachmentRefs = this.collectAttachmentRefs(finalDoc);
|
|
4261
4415
|
const entryMetadata = {
|
|
4262
4416
|
entryType: "doc_create",
|
|
@@ -4271,6 +4425,7 @@ class BaseMindooDB {
|
|
|
4271
4425
|
originalSize: createChangeBytes.length,
|
|
4272
4426
|
encryptedSize: encryptedPayload.length,
|
|
4273
4427
|
attachmentRefs: createAttachmentRefs.length > 0 ? createAttachmentRefs : undefined,
|
|
4428
|
+
recipients: sealedCreate?.recipients,
|
|
4274
4429
|
entryVersion: types_1.CURRENT_STORE_ENTRY_VERSION,
|
|
4275
4430
|
};
|
|
4276
4431
|
// Bind the metadata with the author signature (audit finding #5).
|
|
@@ -4317,6 +4472,7 @@ class BaseMindooDB {
|
|
|
4317
4472
|
isDeleted: false,
|
|
4318
4473
|
awaitingWitness,
|
|
4319
4474
|
witnessed,
|
|
4475
|
+
sealedRecipients: sealedCreate?.recipients,
|
|
4320
4476
|
};
|
|
4321
4477
|
return { internalDoc, entries };
|
|
4322
4478
|
}
|
|
@@ -4335,6 +4491,9 @@ class BaseMindooDB {
|
|
|
4335
4491
|
this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
|
|
4336
4492
|
// Get all entry metadata for this document
|
|
4337
4493
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
4494
|
+
if (allEntryMetadata.length > 0) {
|
|
4495
|
+
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
4496
|
+
}
|
|
4338
4497
|
// Filter to document replay metadata plus snapshots up to the timestamp.
|
|
4339
4498
|
// Attachment chunks are not part of the Automerge replay DAG and must not
|
|
4340
4499
|
// participate in historical materialization.
|
|
@@ -4380,7 +4539,7 @@ class BaseMindooDB {
|
|
|
4380
4539
|
if (snapshotEntries.length > 0) {
|
|
4381
4540
|
const snapshotData = snapshotEntries[0];
|
|
4382
4541
|
let isValid = false;
|
|
4383
|
-
if (
|
|
4542
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(snapshotData)) {
|
|
4384
4543
|
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
4385
4544
|
}
|
|
4386
4545
|
else {
|
|
@@ -4427,7 +4586,7 @@ class BaseMindooDB {
|
|
|
4427
4586
|
this.logger.warn(`Entry ${entryMeta.id} not found in store, skipping`);
|
|
4428
4587
|
continue;
|
|
4429
4588
|
}
|
|
4430
|
-
if (
|
|
4589
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entryData)) {
|
|
4431
4590
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
4432
4591
|
continue;
|
|
4433
4592
|
}
|
|
@@ -4470,10 +4629,62 @@ class BaseMindooDB {
|
|
|
4470
4629
|
});
|
|
4471
4630
|
return this.wrapDocument(internalDoc);
|
|
4472
4631
|
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Unwrap a `$sealed:` DEK from entry recipient wraps into the tenant session
|
|
4634
|
+
* cache. `loadDocumentInternal` already does this; history / timestamp reads
|
|
4635
|
+
* must as well — otherwise Haven's revision list throws SymmetricKeyNotFoundError
|
|
4636
|
+
* on a cold tenant even when this device is in the wrap list.
|
|
4637
|
+
*/
|
|
4638
|
+
async ingestSealedKeyFromEntries(entries) {
|
|
4639
|
+
if (entries.length === 0)
|
|
4640
|
+
return;
|
|
4641
|
+
const keyId = entries.find((entry) => entry.entryType === "doc_create")?.decryptionKeyId
|
|
4642
|
+
?? entries[0].decryptionKeyId;
|
|
4643
|
+
await this.tenant.ingestSealedFromEntries?.(keyId, entries);
|
|
4644
|
+
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Re-ingest the newest sealed wrap list. When this replica can no longer
|
|
4647
|
+
* unwrap it (recipient removal), drop the leftover session DEK and any
|
|
4648
|
+
* materialized plaintext so later honest-client writes cannot reuse it.
|
|
4649
|
+
*
|
|
4650
|
+
* @returns true when access was revoked
|
|
4651
|
+
*/
|
|
4652
|
+
async dropSealedAccessIfUnwrapFailed(internalDoc) {
|
|
4653
|
+
if (!(0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)) {
|
|
4654
|
+
return false;
|
|
4655
|
+
}
|
|
4656
|
+
const metas = await this.scanAllMetadata(this.store, { docId: internalDoc.id });
|
|
4657
|
+
await this.ingestSealedKeyFromEntries(metas);
|
|
4658
|
+
if (await this.tenant.hasDecryptionKey(internalDoc.decryptionKeyId)) {
|
|
4659
|
+
return false;
|
|
4660
|
+
}
|
|
4661
|
+
await this.purgeMaterializedDocument(internalDoc.id);
|
|
4662
|
+
const existingIndex = this.getDocIndexPosition(internalDoc.id);
|
|
4663
|
+
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
4664
|
+
if (existing?.accessState === "visible") {
|
|
4665
|
+
this.updateIndex(internalDoc.id, existing.lastModified, true, internalDoc.decryptionKeyId, "inaccessible");
|
|
4666
|
+
this.cacheMetaDirty = true;
|
|
4667
|
+
this.cacheManager?.markDirty();
|
|
4668
|
+
}
|
|
4669
|
+
return true;
|
|
4670
|
+
}
|
|
4671
|
+
/**
|
|
4672
|
+
* Patch paths (`applyTextPatch`, rich-text, Automerge) used to persist with
|
|
4673
|
+
* a leftover session DEK after the newest wrap excluded this user.
|
|
4674
|
+
* `changeDoc` already refused those writes; this is the shared chokepoint.
|
|
4675
|
+
*/
|
|
4676
|
+
async assertCurrentSealedAccess(internalDoc) {
|
|
4677
|
+
if (await this.dropSealedAccessIfUnwrapFailed(internalDoc)) {
|
|
4678
|
+
throw new errors_1.DocumentNotFoundError(internalDoc.id);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4473
4681
|
async *iterateDocumentHistory(docId) {
|
|
4474
4682
|
this.logger.debug(`Iterating document history for ${docId}`);
|
|
4475
4683
|
// Get all entry metadata for this document
|
|
4476
4684
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
4685
|
+
if (allEntryMetadata.length > 0) {
|
|
4686
|
+
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
4687
|
+
}
|
|
4477
4688
|
// Filter to document replay entries (exclude snapshots) and order them
|
|
4478
4689
|
// causally: a plain chronological sort can place an effect before its
|
|
4479
4690
|
// cause when several entries share the same millisecond (or when replica
|
|
@@ -4486,6 +4697,7 @@ class BaseMindooDB {
|
|
|
4486
4697
|
}
|
|
4487
4698
|
// Load all entries
|
|
4488
4699
|
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
4700
|
+
await this.ingestSealedKeyFromEntries(entries.length > 0 ? entries : relevantEntries);
|
|
4489
4701
|
// Build a map for quick lookup
|
|
4490
4702
|
const entryMap = new Map(entries.map(e => [e.id, e]));
|
|
4491
4703
|
// Apply changes in order
|
|
@@ -4500,7 +4712,7 @@ class BaseMindooDB {
|
|
|
4500
4712
|
continue;
|
|
4501
4713
|
}
|
|
4502
4714
|
// Admin-only mode: only accept entries signed by the admin key
|
|
4503
|
-
if (
|
|
4715
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entryData)) {
|
|
4504
4716
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
4505
4717
|
continue;
|
|
4506
4718
|
}
|
|
@@ -4660,14 +4872,14 @@ class BaseMindooDB {
|
|
|
4660
4872
|
// leading witnessed prefix.
|
|
4661
4873
|
let stableCursor = cursor;
|
|
4662
4874
|
let sawProvisional = false;
|
|
4663
|
-
const consider = (em) => {
|
|
4875
|
+
const consider = async (em) => {
|
|
4664
4876
|
if (!this.isDocumentReplayEntry(em)) {
|
|
4665
4877
|
return;
|
|
4666
4878
|
}
|
|
4667
4879
|
// Admin-only mode: only accept entries signed by the admin key (mirrors
|
|
4668
4880
|
// materialization / iterateDocumentHistory). Non-admin entries are
|
|
4669
4881
|
// permanently ignored and do not gate the stable cursor.
|
|
4670
|
-
if (
|
|
4882
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(em)) {
|
|
4671
4883
|
return;
|
|
4672
4884
|
}
|
|
4673
4885
|
newEntries.push(em);
|
|
@@ -4699,7 +4911,7 @@ class BaseMindooDB {
|
|
|
4699
4911
|
while (true) {
|
|
4700
4912
|
const page = await this.store.scanEntriesSince(scanCursor, 1000, scanFilters);
|
|
4701
4913
|
for (const em of page.entries) {
|
|
4702
|
-
consider(em);
|
|
4914
|
+
await consider(em);
|
|
4703
4915
|
}
|
|
4704
4916
|
scanCursor = page.nextCursor;
|
|
4705
4917
|
if (!page.hasMore) {
|
|
@@ -4720,7 +4932,7 @@ class BaseMindooDB {
|
|
|
4720
4932
|
if (cursor && !this.isAfterReceiptCursor(em, cursor)) {
|
|
4721
4933
|
continue;
|
|
4722
4934
|
}
|
|
4723
|
-
consider(em);
|
|
4935
|
+
await consider(em);
|
|
4724
4936
|
}
|
|
4725
4937
|
}
|
|
4726
4938
|
return { newEntries, resumeCursor: stableCursor };
|
|
@@ -4749,10 +4961,12 @@ class BaseMindooDB {
|
|
|
4749
4961
|
// All metadata for this doc (metadata only, cheap). Includes snapshots, used
|
|
4750
4962
|
// for cache-miss seed reconstruction.
|
|
4751
4963
|
const docAllMeta = await this.scanAllMetadata(this.store, { docId });
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4964
|
+
const replayCandidates = docAllMeta.filter((em) => this.isDocumentReplayEntry(em));
|
|
4965
|
+
const replayMeta = [];
|
|
4966
|
+
for (const em of replayCandidates) {
|
|
4967
|
+
if (!(await this.shouldSkipLoadedEntryForBuiltin(em))) {
|
|
4968
|
+
replayMeta.push(em);
|
|
4969
|
+
}
|
|
4756
4970
|
}
|
|
4757
4971
|
if (replayMeta.length === 0) {
|
|
4758
4972
|
return;
|
|
@@ -4951,7 +5165,7 @@ class BaseMindooDB {
|
|
|
4951
5165
|
return null;
|
|
4952
5166
|
}
|
|
4953
5167
|
const snapshotData = entries[0];
|
|
4954
|
-
if (
|
|
5168
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(snapshotData)) {
|
|
4955
5169
|
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
4956
5170
|
return null;
|
|
4957
5171
|
}
|
|
@@ -5120,7 +5334,7 @@ class BaseMindooDB {
|
|
|
5120
5334
|
result.set(metadata.id, null);
|
|
5121
5335
|
continue;
|
|
5122
5336
|
}
|
|
5123
|
-
if (
|
|
5337
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entry)) {
|
|
5124
5338
|
result.set(metadata.id, null);
|
|
5125
5339
|
continue;
|
|
5126
5340
|
}
|
|
@@ -5286,7 +5500,7 @@ class BaseMindooDB {
|
|
|
5286
5500
|
const entry = entries[0];
|
|
5287
5501
|
if (entry) {
|
|
5288
5502
|
let isValid = false;
|
|
5289
|
-
if (
|
|
5503
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entry)) {
|
|
5290
5504
|
this.logger.warn(`Admin-only DB: skipping DAG details for ${entry.id} not signed by admin key`);
|
|
5291
5505
|
}
|
|
5292
5506
|
else {
|
|
@@ -5983,7 +6197,7 @@ class BaseMindooDB {
|
|
|
5983
6197
|
this.logger.warn(`Conflict analysis: entry ${metadata.id} not found in store, skipping`);
|
|
5984
6198
|
continue;
|
|
5985
6199
|
}
|
|
5986
|
-
if (
|
|
6200
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entry)) {
|
|
5987
6201
|
this.logger.warn(`Admin-only DB: skipping conflict analysis entry ${entry.id} not signed by admin key`);
|
|
5988
6202
|
continue;
|
|
5989
6203
|
}
|
|
@@ -6425,7 +6639,7 @@ class BaseMindooDB {
|
|
|
6425
6639
|
createdAt: createdAtByDocId.get(entry.docId) ?? entry.lastModified,
|
|
6426
6640
|
isDeleted: entry.isDeleted,
|
|
6427
6641
|
}));
|
|
6428
|
-
// Plain comparison, NOT localeCompare: ids are
|
|
6642
|
+
// Plain comparison, NOT localeCompare: ids are ObjectIds in ASCII order and
|
|
6429
6643
|
// locale collation would reorder digits against letters, so the tie-break
|
|
6430
6644
|
// for same-millisecond documents would no longer follow their ids.
|
|
6431
6645
|
results.sort((left, right) => left.createdAt !== right.createdAt
|
|
@@ -6533,7 +6747,7 @@ class BaseMindooDB {
|
|
|
6533
6747
|
if (docIds.length === 0) {
|
|
6534
6748
|
return;
|
|
6535
6749
|
}
|
|
6536
|
-
await this.assertLifecycleMutationAllowed(options.signingKeyPair, options.signingKeyPassword);
|
|
6750
|
+
await this.assertLifecycleMutationAllowed("doc_delete", options.signingKeyPair, options.signingKeyPassword);
|
|
6537
6751
|
this.logger.info(`Bulk-deleting ${docIds.length} documents`);
|
|
6538
6752
|
// Unlike creation, a delete must first materialize the CURRENT document
|
|
6539
6753
|
// (the doc_delete entry is an Automerge change on its heads), which costs
|
|
@@ -6580,21 +6794,14 @@ class BaseMindooDB {
|
|
|
6580
6794
|
async undeleteDocument(docId, options = {}) {
|
|
6581
6795
|
return this.undeleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
6582
6796
|
}
|
|
6583
|
-
async assertLifecycleMutationAllowed(signingKeyPair, signingKeyPassword) {
|
|
6797
|
+
async assertLifecycleMutationAllowed(op, signingKeyPair, signingKeyPassword, documentUsernameHash) {
|
|
6584
6798
|
this.assertWritable("document lifecycle mutation");
|
|
6585
6799
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6586
6800
|
if ((signingKeyPair !== undefined) !== (signingKeyPassword !== undefined)) {
|
|
6587
6801
|
throw new Error("Lifecycle mutation requires both signingKeyPair and signingKeyPassword");
|
|
6588
6802
|
}
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
const signerPublicKey = useCustomSigningKey
|
|
6592
|
-
? signingKeyPair.publicKey
|
|
6593
|
-
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
6594
|
-
if (signerPublicKey !== adminPublicKey) {
|
|
6595
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
6596
|
-
}
|
|
6597
|
-
}
|
|
6803
|
+
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
6804
|
+
await this.assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash);
|
|
6598
6805
|
}
|
|
6599
6806
|
/**
|
|
6600
6807
|
* Write a delete or undelete lifecycle entry for an existing document.
|
|
@@ -6700,12 +6907,11 @@ class BaseMindooDB {
|
|
|
6700
6907
|
// default to the "before" state; both states are supplied so either `when`
|
|
6701
6908
|
// resolves. Throws AccessDeniedError if denied.
|
|
6702
6909
|
{
|
|
6703
|
-
const creatorKey = await this.resolveCreatorSigningKey(docId);
|
|
6704
6910
|
const beforeState = internalDoc.doc;
|
|
6705
6911
|
await this.assertWriteAllowed({
|
|
6706
6912
|
op: entryType,
|
|
6707
6913
|
signerKey: createdByPublicKey,
|
|
6708
|
-
isAuthor:
|
|
6914
|
+
isAuthor: await this.resolveIsAuthor(docId, createdByPublicKey, Number.MAX_SAFE_INTEGER),
|
|
6709
6915
|
bypass: bypassPrecheck,
|
|
6710
6916
|
getBeforeDoc: () => this.convertAutomergeToJS(beforeState),
|
|
6711
6917
|
getAfterDoc: () => this.convertAutomergeToJS(newDoc),
|
|
@@ -6769,7 +6975,7 @@ class BaseMindooDB {
|
|
|
6769
6975
|
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6770
6976
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6771
6977
|
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
6772
|
-
await this.assertLifecycleMutationAllowed(signingKeyPair, signingKeyPassword);
|
|
6978
|
+
await this.assertLifecycleMutationAllowed("doc_delete", signingKeyPair, signingKeyPassword);
|
|
6773
6979
|
// Get current document
|
|
6774
6980
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
6775
6981
|
if (!internalDoc || internalDoc.isDeleted) {
|
|
@@ -6781,11 +6987,11 @@ class BaseMindooDB {
|
|
|
6781
6987
|
async undeleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6782
6988
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6783
6989
|
this.logger.debug(`Undeleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
6784
|
-
await this.assertLifecycleMutationAllowed(signingKeyPair, signingKeyPassword);
|
|
6785
6990
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
6786
6991
|
if (!internalDoc) {
|
|
6787
6992
|
throw new errors_1.DocumentNotFoundError(docId);
|
|
6788
6993
|
}
|
|
6994
|
+
await this.assertLifecycleMutationAllowed("doc_undelete", signingKeyPair, signingKeyPassword, this.usernameHashFromInternalDoc(internalDoc));
|
|
6789
6995
|
if (!internalDoc.isDeleted) {
|
|
6790
6996
|
this.logger.debug(`Document ${docId} is already alive; undelete is a no-op`);
|
|
6791
6997
|
return;
|
|
@@ -6803,13 +7009,6 @@ class BaseMindooDB {
|
|
|
6803
7009
|
this.assertWritable("applyTextPatch");
|
|
6804
7010
|
const docId = doc.getId();
|
|
6805
7011
|
this.logger.debug(`===== applyTextPatch called for document ${docId} =====`);
|
|
6806
|
-
if (this._isAdminOnlyDb) {
|
|
6807
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
6808
|
-
const currentUser = await this.tenant.getCurrentUserId();
|
|
6809
|
-
if (currentUser.userSigningPublicKey !== adminPublicKey) {
|
|
6810
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
6811
|
-
}
|
|
6812
|
-
}
|
|
6813
7012
|
let internalDoc = this.getCachedDocument(docId);
|
|
6814
7013
|
if (!internalDoc) {
|
|
6815
7014
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
@@ -6867,13 +7066,6 @@ class BaseMindooDB {
|
|
|
6867
7066
|
this.assertWritable("applyRichTextPatch");
|
|
6868
7067
|
const docId = doc.getId();
|
|
6869
7068
|
this.logger.debug(`===== applyRichTextPatch called for document ${docId} =====`);
|
|
6870
|
-
if (this._isAdminOnlyDb) {
|
|
6871
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
6872
|
-
const currentUser = await this.tenant.getCurrentUserId();
|
|
6873
|
-
if (currentUser.userSigningPublicKey !== adminPublicKey) {
|
|
6874
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
6875
|
-
}
|
|
6876
|
-
}
|
|
6877
7069
|
let internalDoc = this.getCachedDocument(docId);
|
|
6878
7070
|
if (!internalDoc) {
|
|
6879
7071
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
@@ -6942,13 +7134,6 @@ class BaseMindooDB {
|
|
|
6942
7134
|
this.assertWritable("applyRichTextStepsPatch");
|
|
6943
7135
|
const docId = doc.getId();
|
|
6944
7136
|
this.logger.debug(`===== applyRichTextStepsPatch called for document ${docId} =====`);
|
|
6945
|
-
if (this._isAdminOnlyDb) {
|
|
6946
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
6947
|
-
const currentUser = await this.tenant.getCurrentUserId();
|
|
6948
|
-
if (currentUser.userSigningPublicKey !== adminPublicKey) {
|
|
6949
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
6950
|
-
}
|
|
6951
|
-
}
|
|
6952
7137
|
let internalDoc = this.getCachedDocument(docId);
|
|
6953
7138
|
if (!internalDoc) {
|
|
6954
7139
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
@@ -7061,13 +7246,6 @@ class BaseMindooDB {
|
|
|
7061
7246
|
this.assertWritable("applyAutomergeChanges");
|
|
7062
7247
|
const docId = doc.getId();
|
|
7063
7248
|
this.logger.debug(`===== applyAutomergeChanges called for document ${docId} =====`);
|
|
7064
|
-
if (this._isAdminOnlyDb) {
|
|
7065
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
7066
|
-
const currentUser = await this.tenant.getCurrentUserId();
|
|
7067
|
-
if (currentUser.userSigningPublicKey !== adminPublicKey) {
|
|
7068
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
7069
|
-
}
|
|
7070
|
-
}
|
|
7071
7249
|
if (!Array.isArray(patch.changes) || patch.changes.length === 0) {
|
|
7072
7250
|
throw new Error("Automerge changes patch must include at least one change byte sequence");
|
|
7073
7251
|
}
|
|
@@ -7171,13 +7349,6 @@ class BaseMindooDB {
|
|
|
7171
7349
|
this.assertWritable("applyJsonPatch");
|
|
7172
7350
|
const docId = doc.getId();
|
|
7173
7351
|
this.logger.debug(`===== applyJsonPatch called for document ${docId} =====`);
|
|
7174
|
-
if (this._isAdminOnlyDb) {
|
|
7175
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
7176
|
-
const currentUser = await this.tenant.getCurrentUserId();
|
|
7177
|
-
if (currentUser.userSigningPublicKey !== adminPublicKey) {
|
|
7178
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
7179
|
-
}
|
|
7180
|
-
}
|
|
7181
7352
|
let internalDoc = this.getCachedDocument(docId);
|
|
7182
7353
|
if (!internalDoc) {
|
|
7183
7354
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
@@ -7235,16 +7406,6 @@ class BaseMindooDB {
|
|
|
7235
7406
|
}
|
|
7236
7407
|
const useCustomKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
7237
7408
|
this.logger.debug(`===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
|
|
7238
|
-
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
7239
|
-
if (this._isAdminOnlyDb) {
|
|
7240
|
-
const adminPublicKey = this.getAdminPublicKey();
|
|
7241
|
-
const signerPublicKey = useCustomKey
|
|
7242
|
-
? signingKeyPair.publicKey
|
|
7243
|
-
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
7244
|
-
if (signerPublicKey !== adminPublicKey) {
|
|
7245
|
-
throw new Error("Admin-only database: only the admin key can modify data");
|
|
7246
|
-
}
|
|
7247
|
-
}
|
|
7248
7409
|
// Get internal document from cache or load it
|
|
7249
7410
|
let internalDoc = this.getCachedDocument(docId);
|
|
7250
7411
|
if (!internalDoc) {
|
|
@@ -7262,6 +7423,7 @@ class BaseMindooDB {
|
|
|
7262
7423
|
if (internalDoc.isDeleted) {
|
|
7263
7424
|
throw new errors_1.DocumentDeletedError(docId);
|
|
7264
7425
|
}
|
|
7426
|
+
await this.assertCurrentSealedAccess(internalDoc);
|
|
7265
7427
|
// Apply the change function
|
|
7266
7428
|
const now = (0, timeSource_1.semanticNow)();
|
|
7267
7429
|
this.logger.debug(`Applying change function to document ${docId}`);
|
|
@@ -7304,6 +7466,11 @@ class BaseMindooDB {
|
|
|
7304
7466
|
isAccessible: () => true,
|
|
7305
7467
|
isAwaitingWitness: () => internalDoc.awaitingWitness ?? false,
|
|
7306
7468
|
isWitnessed: () => internalDoc.witnessed ?? false,
|
|
7469
|
+
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId),
|
|
7470
|
+
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
7471
|
+
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
7472
|
+
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
7473
|
+
&& (0, recipients_1.isPayloadEncryptedFor)(internalDoc.doc, internalDoc.sealedRecipients, Array.isArray(users) ? users : [users]),
|
|
7307
7474
|
getHeads: () => automerge_adapter_1.Automerge.getHeads(internalDoc.doc),
|
|
7308
7475
|
getData: () => {
|
|
7309
7476
|
// Return a proxy that collects property assignments and deletions
|
|
@@ -7584,6 +7751,7 @@ class BaseMindooDB {
|
|
|
7584
7751
|
successMessage: useCustomKey ? "changed with custom signing key" : "changed",
|
|
7585
7752
|
attachmentIds: pendingAttachmentAdditions.map((ref) => ref.attachmentId),
|
|
7586
7753
|
bypassPrecheck,
|
|
7754
|
+
recipients: internalDoc.sealedRecipients,
|
|
7587
7755
|
});
|
|
7588
7756
|
}
|
|
7589
7757
|
catch (error) {
|
|
@@ -8087,7 +8255,7 @@ class BaseMindooDB {
|
|
|
8087
8255
|
}
|
|
8088
8256
|
}
|
|
8089
8257
|
async persistDocumentChange(options) {
|
|
8090
|
-
const { internalDoc, newDoc, now, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck } = options;
|
|
8258
|
+
const { internalDoc, newDoc, now, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients } = options;
|
|
8091
8259
|
const docId = internalDoc.id;
|
|
8092
8260
|
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
8093
8261
|
const changesSincePreviousHeads = headsBeforeChange
|
|
@@ -8111,17 +8279,21 @@ class BaseMindooDB {
|
|
|
8111
8279
|
successMessage,
|
|
8112
8280
|
attachmentIds,
|
|
8113
8281
|
bypassPrecheck,
|
|
8282
|
+
recipients: options.recipients,
|
|
8114
8283
|
});
|
|
8115
8284
|
}
|
|
8116
8285
|
async persistAutomergeDocumentChanges(options) {
|
|
8117
|
-
const { internalDoc, newDoc, now, changeBytesList, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, } = options;
|
|
8286
|
+
const { internalDoc, newDoc, now, changeBytesList, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients, } = options;
|
|
8118
8287
|
const docId = internalDoc.id;
|
|
8288
|
+
await this.assertCurrentSealedAccess(internalDoc);
|
|
8119
8289
|
const resolvedChangeBytesList = changeBytesList ?? (headsBeforeChange
|
|
8120
8290
|
? automerge_adapter_1.Automerge.getChangesSince(newDoc, headsBeforeChange)
|
|
8121
8291
|
: []);
|
|
8122
8292
|
if (resolvedChangeBytesList.length === 0) {
|
|
8123
8293
|
throw new Error("No Automerge changes to persist");
|
|
8124
8294
|
}
|
|
8295
|
+
const signerPublicKey = await this.resolveSignerPublicKey(useCustomKey ? signingKeyPair : undefined);
|
|
8296
|
+
await this.assertBuiltinWriteAllowed("doc_change", signerPublicKey, this.usernameHashFromInternalDoc(internalDoc));
|
|
8125
8297
|
// Signed snapshot of the document's attachments after this change, computed
|
|
8126
8298
|
// once from the resulting document state so EVERY change path (changeDoc,
|
|
8127
8299
|
// text patches, merges, ...) carries it consistently on each produced entry.
|
|
@@ -8140,6 +8312,7 @@ class BaseMindooDB {
|
|
|
8140
8312
|
// The full attachment snapshot describes the document's resulting state,
|
|
8141
8313
|
// so it goes on every produced entry (unlike the add-only attachmentIds).
|
|
8142
8314
|
attachmentRefs,
|
|
8315
|
+
recipients: index === 0 ? recipients : undefined,
|
|
8143
8316
|
});
|
|
8144
8317
|
entries.push(entry);
|
|
8145
8318
|
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
@@ -8153,12 +8326,11 @@ class BaseMindooDB {
|
|
|
8153
8326
|
const signerKey = useCustomKey
|
|
8154
8327
|
? signingKeyPair.publicKey
|
|
8155
8328
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
8156
|
-
const creatorKey = await this.resolveCreatorSigningKey(docId);
|
|
8157
8329
|
const beforeState = internalDoc.doc;
|
|
8158
8330
|
await this.assertWriteAllowed({
|
|
8159
8331
|
op: "doc_change",
|
|
8160
8332
|
signerKey,
|
|
8161
|
-
isAuthor:
|
|
8333
|
+
isAuthor: await this.resolveIsAuthor(docId, signerKey, Number.MAX_SAFE_INTEGER),
|
|
8162
8334
|
bypass: bypassPrecheck,
|
|
8163
8335
|
getBeforeDoc: () => this.convertAutomergeToJS(beforeState),
|
|
8164
8336
|
getAfterDoc: () => this.convertAutomergeToJS(newDoc),
|
|
@@ -8181,6 +8353,9 @@ class BaseMindooDB {
|
|
|
8181
8353
|
// changefeed must re-emit the doc even when `lastModified` collides with
|
|
8182
8354
|
// the previous change in the same millisecond.
|
|
8183
8355
|
this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted, internalDoc.decryptionKeyId, "visible", internalDoc.awaitingWitness, internalDoc.witnessed, true);
|
|
8356
|
+
if (recipients) {
|
|
8357
|
+
internalDoc.sealedRecipients = recipients;
|
|
8358
|
+
}
|
|
8184
8359
|
this.markDocDirty(docId);
|
|
8185
8360
|
this.logger.info(`Document ${docId} ${successMessage} successfully`);
|
|
8186
8361
|
const createdByPublicKey = useCustomKey
|
|
@@ -8191,7 +8366,7 @@ class BaseMindooDB {
|
|
|
8191
8366
|
: { createdByPublicKey: createdByPublicKey ?? "" });
|
|
8192
8367
|
}
|
|
8193
8368
|
async buildDocChangeStoreEntry(options) {
|
|
8194
|
-
const { internalDoc, changeBytes, now, useCustomKey, signingKeyPair, signingKeyPassword, attachmentIds, attachmentRefs } = options;
|
|
8369
|
+
const { internalDoc, changeBytes, now, useCustomKey, signingKeyPair, signingKeyPassword, attachmentIds, attachmentRefs, recipients } = options;
|
|
8195
8370
|
const docId = internalDoc.id;
|
|
8196
8371
|
this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
|
|
8197
8372
|
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
@@ -8230,6 +8405,7 @@ class BaseMindooDB {
|
|
|
8230
8405
|
encryptedSize: encryptedPayload.length,
|
|
8231
8406
|
attachmentIds: attachmentIds && attachmentIds.length > 0 ? attachmentIds : undefined,
|
|
8232
8407
|
attachmentRefs: attachmentRefs && attachmentRefs.length > 0 ? attachmentRefs : undefined,
|
|
8408
|
+
recipients,
|
|
8233
8409
|
entryVersion: types_1.CURRENT_STORE_ENTRY_VERSION,
|
|
8234
8410
|
};
|
|
8235
8411
|
// Bind the metadata with the author signature (audit finding #5).
|
|
@@ -8741,7 +8917,7 @@ class BaseMindooDB {
|
|
|
8741
8917
|
if (snapshotEntries.length > 0) {
|
|
8742
8918
|
const snapshotData = snapshotEntries[0];
|
|
8743
8919
|
let isValid = false;
|
|
8744
|
-
if (
|
|
8920
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(snapshotData)) {
|
|
8745
8921
|
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
8746
8922
|
}
|
|
8747
8923
|
else {
|
|
@@ -8786,7 +8962,7 @@ class BaseMindooDB {
|
|
|
8786
8962
|
this.logger.warn(`Entry ${entryMeta.id} not found in store, skipping`);
|
|
8787
8963
|
continue;
|
|
8788
8964
|
}
|
|
8789
|
-
if (
|
|
8965
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entryData)) {
|
|
8790
8966
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
8791
8967
|
continue;
|
|
8792
8968
|
}
|
|
@@ -9035,13 +9211,14 @@ class BaseMindooDB {
|
|
|
9035
9211
|
// Load entries from store
|
|
9036
9212
|
const entries = await this.store.getEntries(orderedEntryIds);
|
|
9037
9213
|
// Filter entries for admin-only mode first
|
|
9038
|
-
const validEntries =
|
|
9039
|
-
|
|
9214
|
+
const validEntries = [];
|
|
9215
|
+
for (const entryData of entries) {
|
|
9216
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entryData)) {
|
|
9040
9217
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
9041
|
-
|
|
9218
|
+
continue;
|
|
9042
9219
|
}
|
|
9043
|
-
|
|
9044
|
-
}
|
|
9220
|
+
validEntries.push(entryData);
|
|
9221
|
+
}
|
|
9045
9222
|
if (validEntries.length === 0) {
|
|
9046
9223
|
this.logger.debug(`No valid entries to process for cached document ${docId}`);
|
|
9047
9224
|
return null;
|
|
@@ -9592,7 +9769,13 @@ class BaseMindooDB {
|
|
|
9592
9769
|
* server. Directory ACL writes always pass an explicit key, so this never
|
|
9593
9770
|
* re-enters the directory for them.
|
|
9594
9771
|
*/
|
|
9595
|
-
async resolveCreateKeyId(options) {
|
|
9772
|
+
async resolveCreateKeyId(options, docId) {
|
|
9773
|
+
if (options.recipients !== undefined && options.decryptionKeyId !== undefined) {
|
|
9774
|
+
throw new Error("createDocument: recipients and decryptionKeyId are mutually exclusive");
|
|
9775
|
+
}
|
|
9776
|
+
if (options.recipients !== undefined) {
|
|
9777
|
+
return (0, sealedTypes_1.sealedKeyId)(docId ?? options.id ?? "pending");
|
|
9778
|
+
}
|
|
9596
9779
|
if (options.decryptionKeyId !== undefined)
|
|
9597
9780
|
return options.decryptionKeyId;
|
|
9598
9781
|
try {
|
|
@@ -9609,6 +9792,34 @@ class BaseMindooDB {
|
|
|
9609
9792
|
}
|
|
9610
9793
|
return "default";
|
|
9611
9794
|
}
|
|
9795
|
+
async prepareSealedCreate(options, docId, keyId, now, signerPublicKey) {
|
|
9796
|
+
if (options.recipients === undefined)
|
|
9797
|
+
return undefined;
|
|
9798
|
+
const tenant = this.tenant;
|
|
9799
|
+
const resolved = await (0, recipients_1.resolveRecipientSpecs)({
|
|
9800
|
+
tenant,
|
|
9801
|
+
specs: options.recipients,
|
|
9802
|
+
options: options.recipientOptions,
|
|
9803
|
+
addedBy: signerPublicKey,
|
|
9804
|
+
now,
|
|
9805
|
+
});
|
|
9806
|
+
const dek = tenant.getCryptoAdapter().getRandomValues(new Uint8Array(32));
|
|
9807
|
+
console.log("[sealed] prepareSealedCreate", {
|
|
9808
|
+
docId,
|
|
9809
|
+
keyId,
|
|
9810
|
+
includeSelf: options.recipientOptions?.includeSelf !== false,
|
|
9811
|
+
userTargets: resolved.targets.map((target) => ({ kind: target.kind, id: target.id })),
|
|
9812
|
+
skipped: resolved.skipped,
|
|
9813
|
+
});
|
|
9814
|
+
const recipients = await (0, recipients_1.sealToTargets)({
|
|
9815
|
+
tenant,
|
|
9816
|
+
generations: [dek],
|
|
9817
|
+
targets: resolved.targets,
|
|
9818
|
+
epoch: 1,
|
|
9819
|
+
});
|
|
9820
|
+
tenant.rememberSealedGenerations(keyId, [dek]);
|
|
9821
|
+
return { encryptFor: resolved.encryptFor, recipients };
|
|
9822
|
+
}
|
|
9612
9823
|
/**
|
|
9613
9824
|
* Evaluate the full client-side (Tier 1 + Tier 2) write ruleset for a
|
|
9614
9825
|
* candidate write, returning the {@link AccessDecision}. Returns `null` only
|
|
@@ -9701,17 +9912,203 @@ class BaseMindooDB {
|
|
|
9701
9912
|
const signerKey = signingKeyPair
|
|
9702
9913
|
? signingKeyPair.publicKey
|
|
9703
9914
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
9704
|
-
const creatorKey = await this.resolveCreatorSigningKey(docId);
|
|
9705
9915
|
const before = doc.getData();
|
|
9706
9916
|
const decision = await this.evaluateClientWriteAccess({
|
|
9707
9917
|
op: "doc_change",
|
|
9708
9918
|
signerKey,
|
|
9709
|
-
isAuthor:
|
|
9919
|
+
isAuthor: await this.resolveIsAuthor(docId, signerKey, Number.MAX_SAFE_INTEGER),
|
|
9710
9920
|
getBeforeDoc: () => before,
|
|
9711
9921
|
getAfterDoc: () => candidateAfter,
|
|
9712
9922
|
});
|
|
9713
9923
|
return decision ?? BaseMindooDB.ACL_NOT_ENFORCED_DECISION;
|
|
9714
9924
|
}
|
|
9925
|
+
async addRecipients(doc, recipients, options) {
|
|
9926
|
+
return this.mutateRecipients(doc, "add", recipients, options);
|
|
9927
|
+
}
|
|
9928
|
+
async removeRecipients(doc, recipients, options) {
|
|
9929
|
+
return this.mutateRecipients(doc, "remove", recipients, options);
|
|
9930
|
+
}
|
|
9931
|
+
async setRecipients(doc, recipients, options) {
|
|
9932
|
+
return this.mutateRecipients(doc, "set", recipients, options);
|
|
9933
|
+
}
|
|
9934
|
+
async canChangeRecipients(doc, next) {
|
|
9935
|
+
const after = { ...doc.getData() };
|
|
9936
|
+
after[sealedTypes_1.ENCRYPT_FOR_FIELD] = { ...(0, recipients_1.readEncryptFor)(after) };
|
|
9937
|
+
return this.canChange(doc, after);
|
|
9938
|
+
}
|
|
9939
|
+
async mutateRecipients(doc, mode, specs, options) {
|
|
9940
|
+
this.assertWritable("changeDoc");
|
|
9941
|
+
if (!(0, sealedTypes_1.isSealedKeyId)(doc.getDecryptionKeyId())) {
|
|
9942
|
+
throw new Error("Recipient mutators require a document created with `recipients`");
|
|
9943
|
+
}
|
|
9944
|
+
const tenant = this.tenant;
|
|
9945
|
+
const docId = doc.getId();
|
|
9946
|
+
let internalDoc = this.getCachedDocument(docId) ?? (await this.loadDocumentInternal(docId));
|
|
9947
|
+
if (!internalDoc || internalDoc.isDeleted) {
|
|
9948
|
+
throw new errors_1.DocumentNotFoundError(docId);
|
|
9949
|
+
}
|
|
9950
|
+
const now = (0, timeSource_1.semanticNow)();
|
|
9951
|
+
const signer = options?.signingKeyPair?.publicKey
|
|
9952
|
+
?? (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
9953
|
+
const resolved = await (0, recipients_1.resolveRecipientSpecs)({
|
|
9954
|
+
tenant,
|
|
9955
|
+
specs,
|
|
9956
|
+
options: {
|
|
9957
|
+
includeSelf: mode === "set" ? options?.includeSelf !== false : false,
|
|
9958
|
+
strict: options?.strict,
|
|
9959
|
+
},
|
|
9960
|
+
addedBy: signer,
|
|
9961
|
+
now,
|
|
9962
|
+
});
|
|
9963
|
+
const currentMap = (0, recipients_1.readEncryptFor)(internalDoc.doc);
|
|
9964
|
+
const activeIds = new Set(Object.entries(currentMap).filter(([, e]) => !e.removedAt).map(([id]) => id.split("#")[0]));
|
|
9965
|
+
const addedTargets = [];
|
|
9966
|
+
const removedStableIds = [];
|
|
9967
|
+
const matchesTarget = (id, entry, target) => {
|
|
9968
|
+
const base = id.split("#")[0];
|
|
9969
|
+
return (target.stableId === base
|
|
9970
|
+
|| target.id === base
|
|
9971
|
+
|| (!!entry.label && target.id === entry.label)
|
|
9972
|
+
|| (!!entry.keyFingerprint && target.keyFingerprint === entry.keyFingerprint));
|
|
9973
|
+
};
|
|
9974
|
+
if (mode === "add" || mode === "set") {
|
|
9975
|
+
for (const target of resolved.targets) {
|
|
9976
|
+
if (!activeIds.has(target.stableId) && !activeIds.has(target.id))
|
|
9977
|
+
addedTargets.push(target);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
if (mode === "remove") {
|
|
9981
|
+
for (const [id, entry] of Object.entries(currentMap)) {
|
|
9982
|
+
if (entry.removedAt)
|
|
9983
|
+
continue;
|
|
9984
|
+
if (resolved.targets.some((target) => matchesTarget(id, entry, target))) {
|
|
9985
|
+
removedStableIds.push(id.split("#")[0]);
|
|
9986
|
+
}
|
|
9987
|
+
}
|
|
9988
|
+
}
|
|
9989
|
+
else if (mode === "set") {
|
|
9990
|
+
const nextIds = new Set(resolved.targets.flatMap((t) => [t.stableId, t.id]));
|
|
9991
|
+
for (const [id, entry] of Object.entries(currentMap)) {
|
|
9992
|
+
if (entry.removedAt)
|
|
9993
|
+
continue;
|
|
9994
|
+
const base = id.split("#")[0];
|
|
9995
|
+
const keep = resolved.targets.some((target) => matchesTarget(id, entry, target));
|
|
9996
|
+
if (!keep && !nextIds.has(base))
|
|
9997
|
+
removedStableIds.push(base);
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
const rotate = removedStableIds.length > 0;
|
|
10001
|
+
const currentGens = tenant.getSealedGenerations(internalDoc.decryptionKeyId);
|
|
10002
|
+
if (currentGens.length === 0) {
|
|
10003
|
+
throw new Error("Cannot change recipients: the current document key is not in this session");
|
|
10004
|
+
}
|
|
10005
|
+
let generations = currentGens;
|
|
10006
|
+
let epoch = internalDoc.sealedRecipients?.epoch ?? currentGens.length;
|
|
10007
|
+
if (rotate) {
|
|
10008
|
+
const nextDek = tenant.getCryptoAdapter().getRandomValues(new Uint8Array(32));
|
|
10009
|
+
generations = [nextDek, ...currentGens];
|
|
10010
|
+
epoch += 1;
|
|
10011
|
+
}
|
|
10012
|
+
const remainingByStable = new Map();
|
|
10013
|
+
const existingSpecs = Object.entries(currentMap)
|
|
10014
|
+
.filter(([, e]) => !e.removedAt)
|
|
10015
|
+
.map(([id, e]) => e.kind === "device"
|
|
10016
|
+
? { device: e.keyFingerprint ?? id, label: e.label }
|
|
10017
|
+
: (e.label ?? id));
|
|
10018
|
+
if (existingSpecs.length > 0) {
|
|
10019
|
+
const existingResolved = await (0, recipients_1.resolveRecipientSpecs)({
|
|
10020
|
+
tenant,
|
|
10021
|
+
specs: existingSpecs,
|
|
10022
|
+
options: { includeSelf: false, strict: false },
|
|
10023
|
+
addedBy: signer,
|
|
10024
|
+
now,
|
|
10025
|
+
});
|
|
10026
|
+
for (const target of existingResolved.targets)
|
|
10027
|
+
remainingByStable.set(target.stableId, target);
|
|
10028
|
+
}
|
|
10029
|
+
for (const target of addedTargets)
|
|
10030
|
+
remainingByStable.set(target.stableId, target);
|
|
10031
|
+
for (const id of removedStableIds) {
|
|
10032
|
+
remainingByStable.delete(id);
|
|
10033
|
+
for (const [stableId, target] of [...remainingByStable.entries()]) {
|
|
10034
|
+
if (target.id === id || target.stableId === id || target.keyFingerprint === id) {
|
|
10035
|
+
remainingByStable.delete(stableId);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
10039
|
+
const remaining = [...remainingByStable.values()];
|
|
10040
|
+
if (remaining.length === 0) {
|
|
10041
|
+
throw new Error("Refusing to leave a sealed document with no recipients");
|
|
10042
|
+
}
|
|
10043
|
+
const recipientsBlock = await (0, recipients_1.sealToTargets)({
|
|
10044
|
+
tenant,
|
|
10045
|
+
generations,
|
|
10046
|
+
targets: remaining,
|
|
10047
|
+
epoch,
|
|
10048
|
+
});
|
|
10049
|
+
tenant.rememberSealedGenerations(internalDoc.decryptionKeyId, generations);
|
|
10050
|
+
const headsBeforeChange = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
10051
|
+
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (draft) => {
|
|
10052
|
+
if (addedTargets.length > 0) {
|
|
10053
|
+
const addMap = {};
|
|
10054
|
+
for (const target of addedTargets) {
|
|
10055
|
+
addMap[target.stableId] = {
|
|
10056
|
+
kind: target.kind,
|
|
10057
|
+
addedAt: now,
|
|
10058
|
+
addedBy: signer,
|
|
10059
|
+
keyFingerprint: target.keyFingerprint,
|
|
10060
|
+
...(target.label ? { label: target.label } : {}),
|
|
10061
|
+
};
|
|
10062
|
+
}
|
|
10063
|
+
(0, recipients_1.applyEncryptForAdds)(draft, addMap, epoch);
|
|
10064
|
+
}
|
|
10065
|
+
if (removedStableIds.length > 0) {
|
|
10066
|
+
(0, recipients_1.applyEncryptForRemoves)(draft, removedStableIds, now, signer);
|
|
10067
|
+
}
|
|
10068
|
+
if (options?.change) {
|
|
10069
|
+
// Content edits from the caller happen in a follow-up changeDoc; the
|
|
10070
|
+
// recipient field itself is the carrier entry.
|
|
10071
|
+
}
|
|
10072
|
+
draft._lastModified = now;
|
|
10073
|
+
});
|
|
10074
|
+
await this.persistDocumentChange({
|
|
10075
|
+
internalDoc,
|
|
10076
|
+
newDoc,
|
|
10077
|
+
now,
|
|
10078
|
+
headsBeforeChange,
|
|
10079
|
+
useCustomKey: !!(options?.signingKeyPair && options.signingKeyPassword),
|
|
10080
|
+
signingKeyPair: options?.signingKeyPair,
|
|
10081
|
+
signingKeyPassword: options?.signingKeyPassword,
|
|
10082
|
+
successMessage: "recipients updated",
|
|
10083
|
+
bypassPrecheck: options?.bypassAccessControlPrecheck,
|
|
10084
|
+
recipients: recipientsBlock,
|
|
10085
|
+
});
|
|
10086
|
+
if (options?.change) {
|
|
10087
|
+
await this.changeDoc(this.wrapDocument(internalDoc), options.change, {
|
|
10088
|
+
signingKeyPair: options.signingKeyPair,
|
|
10089
|
+
signingKeyPassword: options.signingKeyPassword,
|
|
10090
|
+
bypassAccessControlPrecheck: options.bypassAccessControlPrecheck,
|
|
10091
|
+
});
|
|
10092
|
+
}
|
|
10093
|
+
const wrapped = this.wrapDocument(internalDoc);
|
|
10094
|
+
return {
|
|
10095
|
+
epoch,
|
|
10096
|
+
rotated: rotate,
|
|
10097
|
+
added: wrapped.getRecipients().filter((r) => addedTargets.some((t) => t.keyFingerprint === r.keyFingerprint)),
|
|
10098
|
+
removed: Object.entries((0, recipients_1.readEncryptFor)(internalDoc.doc))
|
|
10099
|
+
.filter(([id, e]) => e.removedAt && removedStableIds.includes(id.split("#")[0]))
|
|
10100
|
+
.map(([id, e]) => ({
|
|
10101
|
+
kind: e.kind === "device" ? "device" : "user",
|
|
10102
|
+
id: e.kind === "device" ? (e.keyFingerprint ?? id) : id,
|
|
10103
|
+
keyFingerprint: e.keyFingerprint ?? "",
|
|
10104
|
+
label: e.label,
|
|
10105
|
+
addedInEpoch: epoch,
|
|
10106
|
+
sealed: false,
|
|
10107
|
+
})),
|
|
10108
|
+
skipped: resolved.skipped,
|
|
10109
|
+
entryId: "",
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
9715
10112
|
/**
|
|
9716
10113
|
* Predict whether deleting `doc` via {@link deleteDocument} would be allowed
|
|
9717
10114
|
* by the active write policy, without writing anything.
|
|
@@ -9721,12 +10118,11 @@ class BaseMindooDB {
|
|
|
9721
10118
|
const signerKey = signingKeyPair
|
|
9722
10119
|
? signingKeyPair.publicKey
|
|
9723
10120
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
9724
|
-
const creatorKey = await this.resolveCreatorSigningKey(docId);
|
|
9725
10121
|
const current = doc.getData();
|
|
9726
10122
|
const decision = await this.evaluateClientWriteAccess({
|
|
9727
10123
|
op: "doc_delete",
|
|
9728
10124
|
signerKey,
|
|
9729
|
-
isAuthor:
|
|
10125
|
+
isAuthor: await this.resolveIsAuthor(docId, signerKey, Number.MAX_SAFE_INTEGER),
|
|
9730
10126
|
getBeforeDoc: () => current,
|
|
9731
10127
|
getAfterDoc: () => current,
|
|
9732
10128
|
});
|
|
@@ -9741,40 +10137,71 @@ class BaseMindooDB {
|
|
|
9741
10137
|
const signerKey = signingKeyPair
|
|
9742
10138
|
? signingKeyPair.publicKey
|
|
9743
10139
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
9744
|
-
const creatorKey = await this.resolveCreatorSigningKey(docId);
|
|
9745
10140
|
const current = doc.getData();
|
|
9746
10141
|
const decision = await this.evaluateClientWriteAccess({
|
|
9747
10142
|
op: "doc_undelete",
|
|
9748
10143
|
signerKey,
|
|
9749
|
-
isAuthor:
|
|
10144
|
+
isAuthor: await this.resolveIsAuthor(docId, signerKey, Number.MAX_SAFE_INTEGER),
|
|
9750
10145
|
getBeforeDoc: () => current,
|
|
9751
10146
|
getAfterDoc: () => current,
|
|
9752
10147
|
});
|
|
9753
10148
|
return decision ?? BaseMindooDB.ACL_NOT_ENFORCED_DECISION;
|
|
9754
10149
|
}
|
|
9755
10150
|
/**
|
|
9756
|
-
* Resolve the signing public key of the document's original creator
|
|
9757
|
-
* `doc_create` entry
|
|
9758
|
-
*
|
|
9759
|
-
*
|
|
9760
|
-
* (in which case `isAuthor` is treated as false, matching materialization).
|
|
10151
|
+
* Resolve the signing public key of the document's original creator and the
|
|
10152
|
+
* trusted time of the `doc_create` entry, used to set `$author` (`isAuthor`)
|
|
10153
|
+
* at grant level. Cached per document; returns `null` if the create entry
|
|
10154
|
+
* isn't present yet (in which case `isAuthor` is treated as false).
|
|
9761
10155
|
*/
|
|
9762
|
-
async
|
|
9763
|
-
const cached = this.
|
|
10156
|
+
async resolveCreatorInfo(docId) {
|
|
10157
|
+
const cached = this.creatorInfoCache.get(docId);
|
|
9764
10158
|
if (cached)
|
|
9765
10159
|
return cached;
|
|
9766
10160
|
try {
|
|
9767
10161
|
const metas = await this.scanAllMetadata(this.store, { docId });
|
|
9768
|
-
const
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
10162
|
+
const createMeta = metas.find((m) => m.entryType === "doc_create");
|
|
10163
|
+
const createKey = createMeta?.createdByPublicKey ?? null;
|
|
10164
|
+
if (!createMeta || !createKey)
|
|
10165
|
+
return null;
|
|
10166
|
+
const trustedTime = createMeta.receivedAt ?? createMeta.createdAt;
|
|
10167
|
+
const info = { signingKey: createKey, trustedTime };
|
|
10168
|
+
this.creatorInfoCache.set(docId, info);
|
|
10169
|
+
return info;
|
|
9772
10170
|
}
|
|
9773
10171
|
catch (error) {
|
|
9774
10172
|
this.logger.debug(`[ACL] creator-key resolution failed for ${docId}: ${error}`);
|
|
9775
10173
|
return null;
|
|
9776
10174
|
}
|
|
9777
10175
|
}
|
|
10176
|
+
/**
|
|
10177
|
+
* Whether `signerKey` is the document author at grant level: the creator is
|
|
10178
|
+
* resolved at the `doc_create` trusted time, the signer at `signerTrustedTime`.
|
|
10179
|
+
*/
|
|
10180
|
+
async resolveIsAuthor(docId, signerKey, signerTrustedTime) {
|
|
10181
|
+
const creator = await this.resolveCreatorInfo(docId);
|
|
10182
|
+
if (!creator)
|
|
10183
|
+
return false;
|
|
10184
|
+
try {
|
|
10185
|
+
const directory = await this.tenant.openDirectory();
|
|
10186
|
+
if (typeof directory.isSamePerson === "function") {
|
|
10187
|
+
return directory.isSamePerson({
|
|
10188
|
+
creatorSigningKey: creator.signingKey,
|
|
10189
|
+
signerSigningKey: signerKey,
|
|
10190
|
+
creatorTrustedTime: creator.trustedTime,
|
|
10191
|
+
signerTrustedTime,
|
|
10192
|
+
});
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
catch (error) {
|
|
10196
|
+
this.logger.debug(`[ACL] grant-level $author resolution failed for ${docId}: ${error}`);
|
|
10197
|
+
}
|
|
10198
|
+
return creator.signingKey === signerKey;
|
|
10199
|
+
}
|
|
10200
|
+
/** @deprecated kept as a thin wrapper for any remaining device-key callers. */
|
|
10201
|
+
async resolveCreatorSigningKey(docId) {
|
|
10202
|
+
const info = await this.resolveCreatorInfo(docId);
|
|
10203
|
+
return info?.signingKey ?? null;
|
|
10204
|
+
}
|
|
9778
10205
|
/** Append a record to the quarantine/audit log (and log it). */
|
|
9779
10206
|
recordQuarantine(rec) {
|
|
9780
10207
|
this.quarantineLog.push(rec);
|
|
@@ -9835,7 +10262,12 @@ class BaseMindooDB {
|
|
|
9835
10262
|
: (a.receivedAt ?? a.createdAt) - (b.receivedAt ?? b.createdAt));
|
|
9836
10263
|
if (dataEntries.length === 0)
|
|
9837
10264
|
return { doc: null, cacheable };
|
|
9838
|
-
const
|
|
10265
|
+
const createMeta = dataEntries.find((m) => m.entryType === "doc_create");
|
|
10266
|
+
const creatorKey = createMeta?.createdByPublicKey ?? null;
|
|
10267
|
+
const creatorTrustedTime = createMeta
|
|
10268
|
+
? (createMeta.receivedAt ?? createMeta.createdAt)
|
|
10269
|
+
: 0;
|
|
10270
|
+
const isSamePersonFn = directory.isSamePerson?.bind(directory);
|
|
9839
10271
|
const fetched = await this.store.getEntries(dataEntries.map((m) => m.id));
|
|
9840
10272
|
const entryById = new Map(fetched.map((e) => [e.id, e]));
|
|
9841
10273
|
let currentDoc = null;
|
|
@@ -9869,8 +10301,13 @@ class BaseMindooDB {
|
|
|
9869
10301
|
quarantine(meta, "cascade_dependent", "causally depends on a quarantined entry");
|
|
9870
10302
|
continue;
|
|
9871
10303
|
}
|
|
9872
|
-
// Verify the author signature over the encrypted payload.
|
|
9873
|
-
|
|
10304
|
+
// Verify the author signature over the encrypted payload. Trust the
|
|
10305
|
+
// signing key at the entry's trusted time (not HEAD) so a later revoke
|
|
10306
|
+
// does not rewrite history.
|
|
10307
|
+
const trustedTimeForTrust = (0, storeEntryTime_1.isProvisional)(meta)
|
|
10308
|
+
? Number.MAX_SAFE_INTEGER
|
|
10309
|
+
: (meta.receivedAt ?? meta.createdAt);
|
|
10310
|
+
const cryptoKey = await this.getOrImportPublicKey(entry.createdByPublicKey, trustedTimeForTrust);
|
|
9874
10311
|
if (!cryptoKey) {
|
|
9875
10312
|
// Author key not (yet) trusted by the directory. Remember it so a
|
|
9876
10313
|
// later grantaccess sync can re-materialize the doc (reconcileAuthorTrust).
|
|
@@ -9931,20 +10368,29 @@ class BaseMindooDB {
|
|
|
9931
10368
|
? this.convertAutomergeToJS(currentDoc)
|
|
9932
10369
|
: null;
|
|
9933
10370
|
const afterJS = this.convertAutomergeToJS(afterDoc);
|
|
9934
|
-
const isAuthor = creatorKey !== null && entry.createdByPublicKey === creatorKey;
|
|
9935
|
-
// Trusted-time rule (docs/accesscontrol.md §8, version-aware): a witnessed
|
|
9936
|
-
// entry is judged against the directory as it provably was at its
|
|
9937
|
-
// `receivedAt`. A versioned un-witnessed entry (provisional) has no
|
|
9938
|
-
// provable position, so it is judged against the current directory HEAD —
|
|
9939
|
-
// `MAX_SAFE_INTEGER` selects the head node deterministically regardless of
|
|
9940
|
-
// where the directory's own provisional (un-witnessed) entries currently
|
|
9941
|
-
// float, which a wall-clock `now` would not (those entries can sit at a
|
|
9942
|
-
// later `now`). A legacy un-witnessed entry predates witnessing and is
|
|
9943
|
-
// stable at its `createdAt`, so it is judged against the historical
|
|
9944
|
-
// directory state in effect then. See {@link isProvisional}.
|
|
9945
10371
|
const trustedTime = (0, storeEntryTime_1.isProvisional)(meta)
|
|
9946
10372
|
? Number.MAX_SAFE_INTEGER
|
|
9947
10373
|
: (meta.receivedAt ?? meta.createdAt);
|
|
10374
|
+
let isAuthor = false;
|
|
10375
|
+
if (creatorKey !== null) {
|
|
10376
|
+
if (isSamePersonFn) {
|
|
10377
|
+
try {
|
|
10378
|
+
isAuthor = await isSamePersonFn({
|
|
10379
|
+
creatorSigningKey: creatorKey,
|
|
10380
|
+
signerSigningKey: entry.createdByPublicKey,
|
|
10381
|
+
creatorTrustedTime,
|
|
10382
|
+
signerTrustedTime: trustedTime,
|
|
10383
|
+
});
|
|
10384
|
+
}
|
|
10385
|
+
catch (error) {
|
|
10386
|
+
this.logger.debug(`[ACL] grant-level $author resolution failed during materialization: ${error}`);
|
|
10387
|
+
isAuthor = entry.createdByPublicKey === creatorKey;
|
|
10388
|
+
}
|
|
10389
|
+
}
|
|
10390
|
+
else {
|
|
10391
|
+
isAuthor = entry.createdByPublicKey === creatorKey;
|
|
10392
|
+
}
|
|
10393
|
+
}
|
|
9948
10394
|
let decision;
|
|
9949
10395
|
try {
|
|
9950
10396
|
decision = await evaluateClientAccess({
|
|
@@ -10107,6 +10553,22 @@ class BaseMindooDB {
|
|
|
10107
10553
|
// Log all entry types
|
|
10108
10554
|
const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
|
|
10109
10555
|
this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
|
|
10556
|
+
await this.tenant.ingestSealedFromEntries?.(allEntryMetadata.find((em) => em.entryType === "doc_create")?.decryptionKeyId
|
|
10557
|
+
?? allEntryMetadata[0].decryptionKeyId, allEntryMetadata);
|
|
10558
|
+
const loadKeyId = allEntryMetadata.find((em) => em.entryType === "doc_create")?.decryptionKeyId
|
|
10559
|
+
?? allEntryMetadata[0].decryptionKeyId;
|
|
10560
|
+
if ((0, sealedTypes_1.isSealedKeyId)(loadKeyId) && !(await this.tenant.hasDecryptionKey(loadKeyId))) {
|
|
10561
|
+
this.logger.debug(`Document ${docId} is sealed and not unwrapable; treating as missing`);
|
|
10562
|
+
const existingIndex = this.getDocIndexPosition(docId);
|
|
10563
|
+
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
10564
|
+
if (existing?.accessState === "visible") {
|
|
10565
|
+
await this.purgeMaterializedDocument(docId);
|
|
10566
|
+
this.updateIndex(docId, existing.lastModified, true, loadKeyId, "inaccessible");
|
|
10567
|
+
this.cacheMetaDirty = true;
|
|
10568
|
+
this.cacheManager?.markDirty();
|
|
10569
|
+
}
|
|
10570
|
+
return null;
|
|
10571
|
+
}
|
|
10110
10572
|
// Access-control materialization (docs/accesscontrol.md §10): when the
|
|
10111
10573
|
// tenant has access control active, materialize through the Tier 2
|
|
10112
10574
|
// quarantine path (deterministic per-entry evaluation + cascade) instead of
|
|
@@ -10184,7 +10646,7 @@ class BaseMindooDB {
|
|
|
10184
10646
|
const snapshotData = snapshotEntries[0];
|
|
10185
10647
|
// Admin-only mode: only accept snapshots signed by admin
|
|
10186
10648
|
let isValid = false;
|
|
10187
|
-
if (
|
|
10649
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(snapshotData)) {
|
|
10188
10650
|
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
10189
10651
|
}
|
|
10190
10652
|
else {
|
|
@@ -10253,13 +10715,14 @@ class BaseMindooDB {
|
|
|
10253
10715
|
if (this.isDebugEnabled())
|
|
10254
10716
|
this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
10255
10717
|
// Filter entries for admin-only mode first
|
|
10256
|
-
const validEntries =
|
|
10257
|
-
|
|
10718
|
+
const validEntries = [];
|
|
10719
|
+
for (const entryData of entries) {
|
|
10720
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(entryData)) {
|
|
10258
10721
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
10259
|
-
|
|
10722
|
+
continue;
|
|
10260
10723
|
}
|
|
10261
|
-
|
|
10262
|
-
}
|
|
10724
|
+
validEntries.push(entryData);
|
|
10725
|
+
}
|
|
10263
10726
|
if (validEntries.length === 0) {
|
|
10264
10727
|
this.logger.debug(`No valid entries to process for document ${docId}`);
|
|
10265
10728
|
}
|
|
@@ -10385,6 +10848,7 @@ class BaseMindooDB {
|
|
|
10385
10848
|
isDeleted,
|
|
10386
10849
|
awaitingWitness: witnessState.awaitingWitness,
|
|
10387
10850
|
witnessed: witnessState.witnessed,
|
|
10851
|
+
sealedRecipients: (0, recipients_1.newestRecipientBlock)(allEntryMetadata),
|
|
10388
10852
|
};
|
|
10389
10853
|
// Update cache
|
|
10390
10854
|
await this.storeCachedDocument(internalDoc);
|
|
@@ -10486,6 +10950,11 @@ class BaseMindooDB {
|
|
|
10486
10950
|
isAccessible: () => internalDoc.accessible ?? true,
|
|
10487
10951
|
isAwaitingWitness: () => internalDoc.awaitingWitness ?? false,
|
|
10488
10952
|
isWitnessed: () => internalDoc.witnessed ?? false,
|
|
10953
|
+
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId),
|
|
10954
|
+
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
10955
|
+
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
10956
|
+
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
10957
|
+
&& (0, recipients_1.isPayloadEncryptedFor)(internalDoc.doc, internalDoc.sealedRecipients, Array.isArray(users) ? users : [users]),
|
|
10489
10958
|
getHeads: () => automerge_adapter_1.Automerge.getHeads(internalDoc.doc),
|
|
10490
10959
|
getData: () => {
|
|
10491
10960
|
// Convert Automerge document to plain JS object, converting Text
|
|
@@ -10618,7 +11087,7 @@ class BaseMindooDB {
|
|
|
10618
11087
|
});
|
|
10619
11088
|
}
|
|
10620
11089
|
async decryptAttachmentChunk(chunk) {
|
|
10621
|
-
if (
|
|
11090
|
+
if (await this.shouldSkipLoadedEntryForBuiltin(chunk)) {
|
|
10622
11091
|
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
10623
11092
|
}
|
|
10624
11093
|
const isValid = await this.tenant.verifyEntrySignature(chunk, chunk.encryptedData);
|
|
@@ -11090,10 +11559,13 @@ class BaseMindooDB {
|
|
|
11090
11559
|
// entries can be decrypted with the same override identity.
|
|
11091
11560
|
const encryptionKeyBuffer = await this.tenant.decryptPrivateKey(override.user.userEncryptionKeyPair.privateKey, override.password, "encryption");
|
|
11092
11561
|
const encryptionKey = await subtle.importKey("pkcs8", encryptionKeyBuffer, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
|
|
11093
|
-
// Activate temporary override on the remote store.
|
|
11562
|
+
// Activate temporary override on the remote store. Include the public PEM
|
|
11563
|
+
// so the auth challenge names the same device key that will sign — otherwise
|
|
11564
|
+
// the server rejects bootstrap pushes as an unknown device key.
|
|
11094
11565
|
overrideCapableStore.setSyncAuthOverride({
|
|
11095
11566
|
username: override.user.username,
|
|
11096
11567
|
signingKey,
|
|
11568
|
+
signingPublicKey: override.user.userSigningKeyPair.publicKey,
|
|
11097
11569
|
privateEncryptionKey: encryptionKey,
|
|
11098
11570
|
});
|
|
11099
11571
|
// Return cleanup callback so caller can always restore normal auth in finally.
|
|
@@ -11132,6 +11604,18 @@ class BaseMindooDB {
|
|
|
11132
11604
|
encryptAttachmentPayload: (payload, keyId) => this.tenant.encryptAttachmentPayload(payload, keyId),
|
|
11133
11605
|
syncStoreChanges: () => this.syncStoreChanges(),
|
|
11134
11606
|
evaluateWriteAccess: (op, signerKey, isAuthor) => this.evaluateClientWriteAccess({ op, signerKey, isAuthor }),
|
|
11607
|
+
isSamePerson: async (creatorSigningKey, signerSigningKey, creatorTrustedTime, signerTrustedTime) => {
|
|
11608
|
+
const directory = await this.tenant.openDirectory();
|
|
11609
|
+
if (typeof directory.isSamePerson === "function") {
|
|
11610
|
+
return directory.isSamePerson({
|
|
11611
|
+
creatorSigningKey,
|
|
11612
|
+
signerSigningKey,
|
|
11613
|
+
creatorTrustedTime,
|
|
11614
|
+
signerTrustedTime,
|
|
11615
|
+
});
|
|
11616
|
+
}
|
|
11617
|
+
return creatorSigningKey === signerSigningKey;
|
|
11618
|
+
},
|
|
11135
11619
|
hasWriteContentRules: async (op) => {
|
|
11136
11620
|
const directory = await this.tenant.openDirectory();
|
|
11137
11621
|
return (await directory.hasWriteContentRules?.(op, this.store.getId())) ?? false;
|
|
@@ -11306,6 +11790,9 @@ class BaseMindooDB {
|
|
|
11306
11790
|
}
|
|
11307
11791
|
if (syncResult.transferred === 0) {
|
|
11308
11792
|
this.logger.debug(`No new entries to pull`);
|
|
11793
|
+
if (storeKind === types_2.StoreKind.docs) {
|
|
11794
|
+
await this.reconcileAfterDocsPull();
|
|
11795
|
+
}
|
|
11309
11796
|
return { transferredEntries: 0, transferredBytes, scannedEntries: syncResult.scanned, cancelled: false };
|
|
11310
11797
|
}
|
|
11311
11798
|
options?.onProgress?.({
|
|
@@ -11328,23 +11815,7 @@ class BaseMindooDB {
|
|
|
11328
11815
|
}
|
|
11329
11816
|
if (storeKind === types_2.StoreKind.docs) {
|
|
11330
11817
|
await this.syncStoreChanges();
|
|
11331
|
-
|
|
11332
|
-
// After the directory pulls and processes new entries, reconcile the
|
|
11333
|
-
// local KeyBag against the directory head: bulk-remove revoked keys
|
|
11334
|
-
// (forgetting now-inaccessible docs) and import keys newly pushed to
|
|
11335
|
-
// this user (revealing their docs on the next content-DB pull). Runs in
|
|
11336
|
-
// the SDK so standalone apps get it too, not just Haven. Best-effort and
|
|
11337
|
-
// single-flight; never blocks or fails the sync.
|
|
11338
|
-
if (this.store.getId() === "directory") {
|
|
11339
|
-
await this.tenant.reconcileKeyDistributionsForCurrentUserSafe();
|
|
11340
|
-
// Author-trust reconcile: refresh the directory trust caches NOW
|
|
11341
|
-
// (instead of waiting up to DIRECTORY_SYNC_INTERVAL_MS for the next
|
|
11342
|
-
// cache-miss validation) so grantaccess documents that just arrived
|
|
11343
|
-
// immediately unblock entries other open databases skipped while
|
|
11344
|
-
// the author was still unknown. Awaited so a subsequent content-DB
|
|
11345
|
-
// sync in the same flow observes the healed state.
|
|
11346
|
-
await this.tenant.reconcileAuthorTrustAfterDirectorySyncSafe();
|
|
11347
|
-
}
|
|
11818
|
+
await this.reconcileAfterDocsPull();
|
|
11348
11819
|
}
|
|
11349
11820
|
if (options?.signal?.aborted) {
|
|
11350
11821
|
this.logger.info(`Pull cancelled after local processing for ${storeKind} entries`);
|
|
@@ -11374,6 +11845,26 @@ class BaseMindooDB {
|
|
|
11374
11845
|
restoreAuthOverride();
|
|
11375
11846
|
}
|
|
11376
11847
|
}
|
|
11848
|
+
/**
|
|
11849
|
+
* After a directory / userdirectory docs pull — including a no-op pull
|
|
11850
|
+
* (`transferred === 0`). Device 2 typically already has
|
|
11851
|
+
* `acl_keydistribution_default` from bootstrap, then later imports the
|
|
11852
|
+
* User-Key from `userdirectory`. A subsequent directory pull transfers
|
|
11853
|
+
* nothing and used to skip reconcile, so `default` never landed in the bag.
|
|
11854
|
+
* Re-run both passes whenever either database is fetched.
|
|
11855
|
+
*/
|
|
11856
|
+
async reconcileAfterDocsPull() {
|
|
11857
|
+
if (this.store.getId() === "directory") {
|
|
11858
|
+
await this.tenant.reconcileKeyDistributionsForCurrentUserSafe();
|
|
11859
|
+
await this.tenant.reconcileAuthorTrustAfterDirectorySyncSafe();
|
|
11860
|
+
await this.tenant.reconcileUserKeysSafe?.();
|
|
11861
|
+
}
|
|
11862
|
+
if (this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
11863
|
+
this.tenant.noteUserDirectoryFetched?.();
|
|
11864
|
+
await this.tenant.reconcileUserKeysSafe?.();
|
|
11865
|
+
await this.tenant.reconcileKeyDistributionsForCurrentUserSafe();
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11377
11868
|
/**
|
|
11378
11869
|
* Push changes to a remote content-addressed store or another MindooDB instance.
|
|
11379
11870
|
*
|