mindoodb 0.0.86 → 0.0.88
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 +5 -2
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +2 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +151 -12
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +340 -24
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +14 -0
- package/dist/core/BaseMindooDB.d.ts +67 -9
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +261 -12
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +29 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +111 -6
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +93 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +292 -0
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +2 -1
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +2 -1
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.js +19 -0
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
- package/dist/core/accesscontrol/evaluate.d.ts +14 -0
- package/dist/core/accesscontrol/evaluate.d.ts.map +1 -1
- package/dist/core/accesscontrol/evaluate.js +42 -1
- package/dist/core/accesscontrol/evaluate.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts +88 -4
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +114 -1
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +6 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +8 -7
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js +12 -0
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts +6 -0
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js +20 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts +7 -0
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +18 -0
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
- package/dist/core/appendonlystores/syncStores.js +25 -5
- package/dist/core/appendonlystores/syncStores.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +7 -1
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.js +1 -0
- package/dist/core/appendonlystores/types.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +49 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +86 -1
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/crypto/AuthChallenge.d.ts +68 -0
- package/dist/core/crypto/AuthChallenge.d.ts.map +1 -0
- package/dist/core/crypto/AuthChallenge.js +118 -0
- package/dist/core/crypto/AuthChallenge.js.map +1 -0
- package/dist/core/crypto/DomainStatement.d.ts +5 -3
- package/dist/core/crypto/DomainStatement.d.ts.map +1 -1
- package/dist/core/crypto/DomainStatement.js +5 -3
- package/dist/core/crypto/DomainStatement.js.map +1 -1
- package/dist/core/crypto/encryptedFields.d.ts +11 -0
- package/dist/core/crypto/encryptedFields.d.ts.map +1 -1
- package/dist/core/crypto/encryptedFields.js +18 -0
- package/dist/core/crypto/encryptedFields.js.map +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +17 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/query/executeQuery.d.ts.map +1 -1
- package/dist/core/query/executeQuery.js +5 -1
- package/dist/core/query/executeQuery.js.map +1 -1
- package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
- package/dist/core/query/executeQueryInclude.js +2 -0
- package/dist/core/query/executeQueryInclude.js.map +1 -1
- package/dist/core/query/queryView.d.ts.map +1 -1
- package/dist/core/query/queryView.js +5 -1
- package/dist/core/query/queryView.js.map +1 -1
- package/dist/core/types.d.ts +143 -8
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/userkeys/index.d.ts +1 -0
- package/dist/core/userkeys/index.d.ts.map +1 -1
- package/dist/core/userkeys/index.js +5 -1
- package/dist/core/userkeys/index.js.map +1 -1
- package/dist/core/userkeys/sealedValues.d.ts +54 -0
- package/dist/core/userkeys/sealedValues.d.ts.map +1 -0
- package/dist/core/userkeys/sealedValues.js +67 -0
- package/dist/core/userkeys/sealedValues.js.map +1 -0
- package/dist/core/utils/dynamicImportInterop.d.ts +10 -0
- package/dist/core/utils/dynamicImportInterop.d.ts.map +1 -0
- package/dist/core/utils/dynamicImportInterop.js +22 -0
- package/dist/core/utils/dynamicImportInterop.js.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +7 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/server/DeviceDiscoveryService.d.ts.map +1 -1
- package/dist/server/DeviceDiscoveryService.js +3 -4
- package/dist/server/DeviceDiscoveryService.js.map +1 -1
- package/dist/server/SystemAdminAuth.d.ts.map +1 -1
- package/dist/server/SystemAdminAuth.js +3 -4
- package/dist/server/SystemAdminAuth.js.map +1 -1
- package/dist/server/TenantManager.d.ts +36 -0
- package/dist/server/TenantManager.d.ts.map +1 -1
- package/dist/server/TenantManager.js +234 -4
- package/dist/server/TenantManager.js.map +1 -1
- package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
- package/dist/server/http/MindooDBServerCore.js +7 -0
- package/dist/server/http/MindooDBServerCore.js.map +1 -1
- package/dist/server/peer/PeerAuthService.d.ts.map +1 -1
- package/dist/server/peer/PeerAuthService.js +3 -4
- package/dist/server/peer/PeerAuthService.js.map +1 -1
- package/dist/server/peer/PeerReplicator.d.ts.map +1 -1
- package/dist/server/peer/PeerReplicator.js +2 -1
- package/dist/server/peer/PeerReplicator.js.map +1 -1
- package/package.json +1 -1
|
@@ -482,6 +482,16 @@ class BaseMindooDB {
|
|
|
482
482
|
this.syncStoreChangesInFlight = null;
|
|
483
483
|
/** Shared follow-up run for callers that arrive mid-sync. */
|
|
484
484
|
this.syncStoreChangesQueued = null;
|
|
485
|
+
/**
|
|
486
|
+
* > 0 while this database hands a sealed document's wrap list to the tenant
|
|
487
|
+
* ({@link ingestSealed}). Unwrapping may resolve the own User-Key, which
|
|
488
|
+
* reads `directory` (grants, username hashes) and so calls
|
|
489
|
+
* {@link syncStoreChanges} on it. When this database *is* `directory` (a
|
|
490
|
+
* sealed admin document there, e.g. a mail gateway configuration sealed to
|
|
491
|
+
* the gateway), that call would wait for the very run that is waiting for
|
|
492
|
+
* it. Such nested calls return at once and read the index as it is.
|
|
493
|
+
*/
|
|
494
|
+
this.sealedIngestDepth = 0;
|
|
485
495
|
this.changeListeners = new Set();
|
|
486
496
|
this.pendingChangeNotifications = new Map();
|
|
487
497
|
this.changeNotifyTimer = null;
|
|
@@ -784,16 +794,97 @@ class BaseMindooDB {
|
|
|
784
794
|
* — a peer sync — and therefore have to apply the admin/owner rules
|
|
785
795
|
* themselves.
|
|
786
796
|
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
797
|
+
* This is the server's write rule ({@link evaluateBuiltinWrite}), not the
|
|
798
|
+
* lenient load-path filter. The load path may fail open because whatever it
|
|
799
|
+
* reads already passed a server; an entry arriving from a peer has passed
|
|
800
|
+
* nothing, so an owner hash that cannot be resolved is a refusal here, as it
|
|
801
|
+
* is on the server. Everything this admits also passes the load path, so an
|
|
802
|
+
* admitted entry still materializes.
|
|
803
|
+
*
|
|
804
|
+
* `batch` is the rest of the delivery: a new document arrives as its
|
|
805
|
+
* `doc_create` plus later changes in one batch, and the create is what names
|
|
806
|
+
* the owner. Pass entries with their payload (`encryptedData`) where you have
|
|
807
|
+
* it; a `doc_create` of a userkey document is decrypted to read
|
|
808
|
+
* `username_hash`.
|
|
791
809
|
*
|
|
792
810
|
* Returns false for databases without a builtin invariant, so callers can
|
|
793
811
|
* ask unconditionally.
|
|
794
812
|
*/
|
|
795
|
-
async violatesBuiltinWriteInvariant(entry) {
|
|
796
|
-
return this.
|
|
813
|
+
async violatesBuiltinWriteInvariant(entry, batch = []) {
|
|
814
|
+
return (await this.builtinIngestDenial(entry, batch)) !== null;
|
|
815
|
+
}
|
|
816
|
+
/** Reason the builtin invariant refuses `entry` from a peer, or null. */
|
|
817
|
+
async builtinIngestDenial(entry, batch) {
|
|
818
|
+
const dbId = this.effectiveBuiltinDbId();
|
|
819
|
+
if (!(0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(dbId)) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
const entryType = entry.entryType ?? "doc_change";
|
|
823
|
+
const op = (0, builtinDbInvariants_1.entryTypeToBuiltinOp)(entryType);
|
|
824
|
+
if (!op) {
|
|
825
|
+
// Not a document lifecycle entry (attachment chunks and the like). The
|
|
826
|
+
// admin-only rule still holds for `directory`; `userdirectory` has no
|
|
827
|
+
// rule for them, exactly as on the server.
|
|
828
|
+
if (dbId === types_1.DIRECTORY_DB_ID && !this.isAdminKey(entry.createdByPublicKey)) {
|
|
829
|
+
return "Admin-only database: only the admin key can modify data";
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
let documentUsernameHash = null;
|
|
834
|
+
let signerUsernameHash = null;
|
|
835
|
+
let creatorUsernameHash = null;
|
|
836
|
+
if (dbId === types_1.USER_DIRECTORY_DB_ID) {
|
|
837
|
+
// Fresh, not the load-path cache: that one remembers a miss for a key
|
|
838
|
+
// that has since been granted.
|
|
839
|
+
signerUsernameHash = await this.resolveSignerUsernameHash(entry.createdByPublicKey);
|
|
840
|
+
const createInBatch = entryType === "doc_create"
|
|
841
|
+
? entry
|
|
842
|
+
: batch.find((candidate) => candidate.entryType === "doc_create" && candidate.docId === entry.docId);
|
|
843
|
+
if ((0, builtinDbInvariants_1.isPersonalUserdirectoryDocId)(entry.docId)) {
|
|
844
|
+
if (op === "doc_create") {
|
|
845
|
+
creatorUsernameHash = signerUsernameHash;
|
|
846
|
+
}
|
|
847
|
+
else if (createInBatch) {
|
|
848
|
+
creatorUsernameHash = await this.resolveSignerUsernameHash(createInBatch.createdByPublicKey);
|
|
849
|
+
}
|
|
850
|
+
else if (entry.docId) {
|
|
851
|
+
creatorUsernameHash = await this.ensurePersonalDocCreatorHash(entry.docId);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
else if (createInBatch?.encryptedData) {
|
|
855
|
+
documentUsernameHash = await this.usernameHashFromCreateEntry(createInBatch);
|
|
856
|
+
}
|
|
857
|
+
else if (entry.docId && op !== "doc_create") {
|
|
858
|
+
documentUsernameHash = await this.ensureUserdirectoryDocHash(entry.docId);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const decision = (0, builtinDbInvariants_1.evaluateBuiltinWrite)({
|
|
862
|
+
dbId,
|
|
863
|
+
op,
|
|
864
|
+
signerKey: entry.createdByPublicKey,
|
|
865
|
+
adminPublicKey: this.getAdminPublicKey(),
|
|
866
|
+
documentUsernameHash,
|
|
867
|
+
signerUsernameHash,
|
|
868
|
+
docId: entry.docId,
|
|
869
|
+
creatorUsernameHash,
|
|
870
|
+
});
|
|
871
|
+
return decision.allowed ? null : decision.reason;
|
|
872
|
+
}
|
|
873
|
+
isAdminKey(publicKey) {
|
|
874
|
+
return (0, DirectoryStateNode_1.signingKeysEqual)(publicKey, this.getAdminPublicKey());
|
|
875
|
+
}
|
|
876
|
+
/** `username_hash` out of a userkey document's `doc_create` payload. */
|
|
877
|
+
async usernameHashFromCreateEntry(entry) {
|
|
878
|
+
if (!entry.encryptedData || !entry.decryptionKeyId) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const decrypted = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
|
|
883
|
+
return (0, builtinDbInvariants_1.usernameHashFromCreateChangeBytes)(decrypted);
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
797
888
|
}
|
|
798
889
|
async ensureSignerUsernameHash(signerKey) {
|
|
799
890
|
if (this.signerUsernameHashByKey.has(signerKey)) {
|
|
@@ -2828,6 +2919,93 @@ class BaseMindooDB {
|
|
|
2828
2919
|
}
|
|
2829
2920
|
return outcome;
|
|
2830
2921
|
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Request the hard delete of deleted documents in a personal database
|
|
2924
|
+
* (`mmail_…`, `mmailout_…`; docs/accesscontrol.md §6.11, "Purge"). Writes a
|
|
2925
|
+
* purge request document (`purgereq_…`, `$publicinfos`) that names the
|
|
2926
|
+
* documents; push it like any change. The server accepts it only from the
|
|
2927
|
+
* owner's devices, a service with a matching `accessPrefixes` grant or the
|
|
2928
|
+
* tenant admin, and only for documents that are deleted; it then removes
|
|
2929
|
+
* their history and refuses it from then on. Every replica (also this one)
|
|
2930
|
+
* removes the documents when it next pulls the request
|
|
2931
|
+
* ({@link applyPurgeRequests}).
|
|
2932
|
+
*
|
|
2933
|
+
* @returns The id of the purge request document.
|
|
2934
|
+
*/
|
|
2935
|
+
async requestPurgeOfDeletedDocuments(docIds) {
|
|
2936
|
+
this.assertWritable("requestPurgeOfDeletedDocuments");
|
|
2937
|
+
const dbId = this.store.getId();
|
|
2938
|
+
if (!(0, builtinDbInvariants_1.personalDatabaseOwner)(dbId)) {
|
|
2939
|
+
throw new Error(`Purge requests are only for personal databases, not ${dbId}`);
|
|
2940
|
+
}
|
|
2941
|
+
const ids = Array.from(new Set(docIds.filter((id) => typeof id === "string" && id.length > 0)));
|
|
2942
|
+
if (ids.length === 0)
|
|
2943
|
+
throw new Error("No document ids to purge");
|
|
2944
|
+
const deleted = new Set(await this.getDeletedDocumentIds());
|
|
2945
|
+
for (const id of ids) {
|
|
2946
|
+
if ((0, builtinDbInvariants_1.isPurgeRequestDocId)(id))
|
|
2947
|
+
throw new Error(`A purge request cannot be purged: ${id}`);
|
|
2948
|
+
if (!deleted.has(id))
|
|
2949
|
+
throw new Error(`Document ${id} is not deleted; delete it before purging`);
|
|
2950
|
+
}
|
|
2951
|
+
const request = await this.createDocument({
|
|
2952
|
+
idPrefix: builtinDbInvariants_1.PURGE_REQUEST_ID_PREFIX.slice(0, -1),
|
|
2953
|
+
decryptionKeyId: types_1.PUBLIC_INFOS_KEY_ID,
|
|
2954
|
+
initialValues: { form: builtinDbInvariants_1.PURGE_REQUEST_FORM, docIds: ids, requestedAt: Date.now() },
|
|
2955
|
+
});
|
|
2956
|
+
return request.getId();
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* The document ids the purge requests of this personal database name
|
|
2960
|
+
* (§6.11). Empty for any other database. With `onlyAccepted`, only requests
|
|
2961
|
+
* the server has accepted (their `doc_create` carries a witness receipt):
|
|
2962
|
+
* a request that is still local, or that the server refused, purges
|
|
2963
|
+
* nothing, so a refused request never loses data that stays on the server.
|
|
2964
|
+
*/
|
|
2965
|
+
async getPurgeRequestedDocIds(options) {
|
|
2966
|
+
const result = new Set();
|
|
2967
|
+
if (!(0, builtinDbInvariants_1.personalDatabaseOwner)(this.store.getId()))
|
|
2968
|
+
return result;
|
|
2969
|
+
for (const requestId of await this.getAllDocumentIds({ idPrefix: builtinDbInvariants_1.PURGE_REQUEST_ID_PREFIX.slice(0, -1) })) {
|
|
2970
|
+
if (options?.onlyAccepted) {
|
|
2971
|
+
const metas = await this.store.findNewEntriesForDoc([], requestId);
|
|
2972
|
+
const create = metas.find((m) => m.entryType === "doc_create");
|
|
2973
|
+
if (!create?.receivedAt)
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
try {
|
|
2977
|
+
const data = (await this.getDocument(requestId)).getData();
|
|
2978
|
+
if (data.form !== builtinDbInvariants_1.PURGE_REQUEST_FORM || !Array.isArray(data.docIds))
|
|
2979
|
+
continue;
|
|
2980
|
+
for (const id of data.docIds) {
|
|
2981
|
+
const docId = String(id);
|
|
2982
|
+
if (docId && !(0, builtinDbInvariants_1.isPurgeRequestDocId)(docId))
|
|
2983
|
+
result.add(docId);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
catch {
|
|
2987
|
+
// Not readable here (no $publicinfos yet): picked up on a later run.
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
return result;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Execute the purge requests of this personal database locally (§6.11):
|
|
2994
|
+
* remove every named document that still has entries here. Idempotent; runs
|
|
2995
|
+
* after every docs pull of a personal database.
|
|
2996
|
+
*
|
|
2997
|
+
* @returns How many documents were removed in this run.
|
|
2998
|
+
*/
|
|
2999
|
+
async applyPurgeRequests() {
|
|
3000
|
+
this.assertWritable("applyPurgeRequests");
|
|
3001
|
+
let removed = 0;
|
|
3002
|
+
for (const docId of await this.getPurgeRequestedDocIds({ onlyAccepted: true })) {
|
|
3003
|
+
const outcome = await this.purgeDocumentHistory(docId);
|
|
3004
|
+
if (outcome.docEntriesPurged > 0 || outcome.attachmentChunksPurged > 0)
|
|
3005
|
+
removed++;
|
|
3006
|
+
}
|
|
3007
|
+
return removed;
|
|
3008
|
+
}
|
|
2831
3009
|
/**
|
|
2832
3010
|
* Drop everything this instance derived from a document whose store entries
|
|
2833
3011
|
* were purged: the L1/L2 caches, summary values and full-text tokens (via
|
|
@@ -2907,7 +3085,7 @@ class BaseMindooDB {
|
|
|
2907
3085
|
}
|
|
2908
3086
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
2909
3087
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
2910
|
-
await this.
|
|
3088
|
+
await this.ingestSealed(visibility.decryptionKeyId, metadataByDoc.get(docId));
|
|
2911
3089
|
const canRead = await this.tenant.hasDecryptionKey(visibility.decryptionKeyId);
|
|
2912
3090
|
if (canRead) {
|
|
2913
3091
|
// This scan is the authority on visibility, so it also heals a tally
|
|
@@ -3187,6 +3365,10 @@ class BaseMindooDB {
|
|
|
3187
3365
|
* {@link syncStoreChangesInFlight}).
|
|
3188
3366
|
*/
|
|
3189
3367
|
async syncStoreChanges() {
|
|
3368
|
+
if (this.sealedIngestDepth > 0) {
|
|
3369
|
+
// Called back from our own sealed ingest (see sealedIngestDepth).
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3190
3372
|
if (this.syncStoreChangesInFlight) {
|
|
3191
3373
|
if (!this.syncStoreChangesQueued) {
|
|
3192
3374
|
// The running scan may have started before the entries this caller
|
|
@@ -3328,7 +3510,7 @@ class BaseMindooDB {
|
|
|
3328
3510
|
});
|
|
3329
3511
|
sealedIngestEntries = fullDocMetadata;
|
|
3330
3512
|
}
|
|
3331
|
-
await this.
|
|
3513
|
+
await this.ingestSealed(representativeEntry.decryptionKeyId, sealedIngestEntries);
|
|
3332
3514
|
const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
|
|
3333
3515
|
if (!keyAvailable) {
|
|
3334
3516
|
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
@@ -4077,6 +4259,9 @@ class BaseMindooDB {
|
|
|
4077
4259
|
}
|
|
4078
4260
|
this.validateIdPrefixOption("createDocuments", options);
|
|
4079
4261
|
this.validateAssumeUniqueIdOption("createDocuments", options);
|
|
4262
|
+
if ((options.attachments?.length ?? 0) > 0) {
|
|
4263
|
+
throw new Error("createDocuments: attachments are only supported by createDocument()");
|
|
4264
|
+
}
|
|
4080
4265
|
(0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
4081
4266
|
}
|
|
4082
4267
|
const results = new Array(inputs.length).fill(null);
|
|
@@ -4255,6 +4440,10 @@ class BaseMindooDB {
|
|
|
4255
4440
|
throw new Error("createDocument: recipients is not supported together with a convergent custom id; " +
|
|
4256
4441
|
"pass assumeUniqueId for random ids.");
|
|
4257
4442
|
}
|
|
4443
|
+
if (useDeterministicSeed && (options.attachments?.length ?? 0) > 0) {
|
|
4444
|
+
throw new Error("createDocument: attachments are not supported together with a convergent custom id; " +
|
|
4445
|
+
"pass assumeUniqueId, or add them with changeDoc().");
|
|
4446
|
+
}
|
|
4258
4447
|
}
|
|
4259
4448
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4260
4449
|
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
@@ -4313,11 +4502,17 @@ class BaseMindooDB {
|
|
|
4313
4502
|
else {
|
|
4314
4503
|
this.logger.debug(`Creating initial Automerge change for document ${docId}`);
|
|
4315
4504
|
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
4505
|
+
// Attachments of the first entry: chunks are written now, under the
|
|
4506
|
+
// document's key, and referenced from the doc_create change below.
|
|
4507
|
+
const createAttachments = [];
|
|
4508
|
+
for (const attachment of options.attachments ?? []) {
|
|
4509
|
+
createAttachments.push(await this.addAttachmentInternal(docId, attachment.data, attachment.fileName, attachment.mimeType, keyId, now));
|
|
4510
|
+
}
|
|
4316
4511
|
try {
|
|
4317
4512
|
newDoc = automerge_adapter_1.Automerge.change(initialDoc, changeMessage ? { message: changeMessage } : {}, (doc) => {
|
|
4318
4513
|
// Store metadata in the document payload
|
|
4319
4514
|
// We need to modify the document to ensure a change is created
|
|
4320
|
-
doc._attachments =
|
|
4515
|
+
doc._attachments = createAttachments.map((ref) => ({ ...ref }));
|
|
4321
4516
|
if (sealedCreate) {
|
|
4322
4517
|
(0, recipients_1.applyEncryptForCreate)(doc, sealedCreate.encryptFor);
|
|
4323
4518
|
}
|
|
@@ -4824,6 +5019,22 @@ class BaseMindooDB {
|
|
|
4824
5019
|
});
|
|
4825
5020
|
return this.wrapDocument(internalDoc);
|
|
4826
5021
|
}
|
|
5022
|
+
/**
|
|
5023
|
+
* Hands a sealed document's newest wrap list to the tenant, with nested
|
|
5024
|
+
* {@link syncStoreChanges} calls on this database short-circuited meanwhile
|
|
5025
|
+
* (see {@link sealedIngestDepth}).
|
|
5026
|
+
*/
|
|
5027
|
+
async ingestSealed(keyId, entries) {
|
|
5028
|
+
if (!this.tenant.ingestSealedFromEntries)
|
|
5029
|
+
return;
|
|
5030
|
+
this.sealedIngestDepth++;
|
|
5031
|
+
try {
|
|
5032
|
+
await this.tenant.ingestSealedFromEntries(keyId, entries);
|
|
5033
|
+
}
|
|
5034
|
+
finally {
|
|
5035
|
+
this.sealedIngestDepth--;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
4827
5038
|
/**
|
|
4828
5039
|
* Unwrap a `$sealed:` DEK from entry recipient wraps into the tenant session
|
|
4829
5040
|
* cache. `loadDocumentInternal` already does this; history, timestamp, and
|
|
@@ -4836,7 +5047,7 @@ class BaseMindooDB {
|
|
|
4836
5047
|
return;
|
|
4837
5048
|
const keyId = entries.find((entry) => entry.entryType === "doc_create")
|
|
4838
5049
|
?.decryptionKeyId ?? entries[0].decryptionKeyId;
|
|
4839
|
-
await this.
|
|
5050
|
+
await this.ingestSealed(keyId, entries);
|
|
4840
5051
|
}
|
|
4841
5052
|
/**
|
|
4842
5053
|
* Re-ingest the newest sealed wrap list. When this replica can no longer
|
|
@@ -5221,6 +5432,23 @@ class BaseMindooDB {
|
|
|
5221
5432
|
// of fold order (under the trusted-time rule un-witnessed entries can sort
|
|
5222
5433
|
// before the create, so we must not rely on seeing the create first).
|
|
5223
5434
|
const createMeta = replayMeta.find((m) => m.entryType === "doc_create");
|
|
5435
|
+
// A document this replica holds no key for (e.g. an admin document in
|
|
5436
|
+
// `directory` encrypted with `default` or sealed to someone else, read by a
|
|
5437
|
+
// `$publicinfos`-only member or the sync server) is not part of its feed,
|
|
5438
|
+
// just as it is not part of its index; decrypting it would throw and stop
|
|
5439
|
+
// the whole feed (and with it the directory state and access checks).
|
|
5440
|
+
const feedKeyId = createMeta?.decryptionKeyId ?? replayMeta[0]?.decryptionKeyId;
|
|
5441
|
+
if (feedKeyId && !(await this.tenant.hasDecryptionKey(feedKeyId))) {
|
|
5442
|
+
// Sealed: the wrap list may not be ingested yet. Not for admin-only
|
|
5443
|
+
// databases: ingesting there can read `directory`, whose state this
|
|
5444
|
+
// feed may be building (the directory sync already ingested them).
|
|
5445
|
+
if ((0, sealedTypes_1.isSealedKeyId)(feedKeyId) && !this._isAdminOnlyDb) {
|
|
5446
|
+
await this.ingestSealed(feedKeyId, replayMeta);
|
|
5447
|
+
}
|
|
5448
|
+
if (!(await this.tenant.hasDecryptionKey(feedKeyId))) {
|
|
5449
|
+
return;
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5224
5452
|
// Seed the running doc to the merge of the unchanged prefix.
|
|
5225
5453
|
const prefix = ttSorted.slice(0, firstChangedIdx).map((r) => r.meta);
|
|
5226
5454
|
const seed = await this.seedRevisionFold(docId, prefix, docAllMeta);
|
|
@@ -11968,7 +12196,7 @@ class BaseMindooDB {
|
|
|
11968
12196
|
.map((em) => `${em.entryType}@${em.createdAt}`)
|
|
11969
12197
|
.join(", ");
|
|
11970
12198
|
this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
|
|
11971
|
-
await this.
|
|
12199
|
+
await this.ingestSealed(allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
11972
12200
|
?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId, allEntryMetadata);
|
|
11973
12201
|
const loadKeyId = allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
11974
12202
|
?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId;
|
|
@@ -13275,9 +13503,19 @@ class BaseMindooDB {
|
|
|
13275
13503
|
// erased history back (docs/accesscontrol.md §13). Never applied to the
|
|
13276
13504
|
// directory, which carries the purge requests themselves.
|
|
13277
13505
|
const dbId = this.store.getId();
|
|
13506
|
+
// Personal databases carry their own purge requests (§6.11) on top.
|
|
13507
|
+
const isPersonal = (0, builtinDbInvariants_1.personalDatabaseOwner)(dbId) !== null;
|
|
13278
13508
|
const localStore = dbId === "directory"
|
|
13279
13509
|
? this.getStoreForKind(storeKind)
|
|
13280
|
-
: (0, purgedDocumentGuard_1.guardStoreAgainstPurgedDocuments)(this.getStoreForKind(storeKind), () =>
|
|
13510
|
+
: (0, purgedDocumentGuard_1.guardStoreAgainstPurgedDocuments)(this.getStoreForKind(storeKind), async () => {
|
|
13511
|
+
const fromDirectory = await this.tenant.getPurgedDocumentIds(dbId);
|
|
13512
|
+
if (!isPersonal)
|
|
13513
|
+
return fromDirectory;
|
|
13514
|
+
const own = await this.getPurgeRequestedDocIds();
|
|
13515
|
+
for (const id of fromDirectory)
|
|
13516
|
+
own.add(id);
|
|
13517
|
+
return own;
|
|
13518
|
+
}, this.logger);
|
|
13281
13519
|
if (localStore.getId() !== remoteStore.getId() ||
|
|
13282
13520
|
localStore.getStoreKind() !== remoteStore.getStoreKind()) {
|
|
13283
13521
|
throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
|
|
@@ -13385,6 +13623,17 @@ class BaseMindooDB {
|
|
|
13385
13623
|
await this.tenant.reconcileUserKeysSafe?.();
|
|
13386
13624
|
await this.tenant.reconcileKeyDistributionsForCurrentUserSafe();
|
|
13387
13625
|
}
|
|
13626
|
+
// Personal databases: execute the purge requests the server accepted (§6.11).
|
|
13627
|
+
if ((0, builtinDbInvariants_1.personalDatabaseOwner)(this.store.getId())) {
|
|
13628
|
+
try {
|
|
13629
|
+
const removed = await this.applyPurgeRequests();
|
|
13630
|
+
if (removed > 0)
|
|
13631
|
+
this.logger.info(`Purged ${removed} document(s) by purge request`);
|
|
13632
|
+
}
|
|
13633
|
+
catch (error) {
|
|
13634
|
+
this.logger.warn(`Applying purge requests failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
13635
|
+
}
|
|
13636
|
+
}
|
|
13388
13637
|
}
|
|
13389
13638
|
/**
|
|
13390
13639
|
* Push changes to a remote content-addressed store or another MindooDB instance.
|