mindoodb 0.0.91 → 0.0.93
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 +34 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +12 -3
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/cloudflare/host.d.ts.map +1 -1
- package/dist/cloudflare/host.js +3 -0
- package/dist/cloudflare/host.js.map +1 -1
- package/dist/cloudflare/types.d.ts +2 -0
- package/dist/cloudflare/types.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.d.ts +59 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +383 -39
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +51 -2
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +131 -39
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/appendonlystores/MaterializationPlanner.d.ts.map +1 -1
- package/dist/core/appendonlystores/MaterializationPlanner.js +19 -6
- package/dist/core/appendonlystores/MaterializationPlanner.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +2 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +2 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/crypto/AuthChallenge.d.ts +27 -2
- package/dist/core/crypto/AuthChallenge.d.ts.map +1 -1
- package/dist/core/crypto/AuthChallenge.js +25 -4
- package/dist/core/crypto/AuthChallenge.js.map +1 -1
- package/dist/core/crypto/EntrySignature.d.ts +1 -1
- package/dist/core/crypto/EntrySignature.js +2 -2
- package/dist/core/crypto/EntrySignature.js.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +7 -2
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +4 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.js +110 -19
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +6 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/types.d.ts +56 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/userkeys/recipients.d.ts +12 -0
- package/dist/core/userkeys/recipients.d.ts.map +1 -1
- package/dist/core/userkeys/recipients.js +42 -9
- package/dist/core/userkeys/recipients.js.map +1 -1
- package/dist/core/userkeys/sealedCrypto.d.ts +17 -0
- package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -1
- package/dist/core/userkeys/sealedCrypto.js +31 -8
- package/dist/core/userkeys/sealedCrypto.js.map +1 -1
- package/dist/core/userkeys/sealedTypes.d.ts +52 -0
- package/dist/core/userkeys/sealedTypes.d.ts.map +1 -1
- package/dist/core/userkeys/sealedTypes.js +37 -1
- package/dist/core/userkeys/sealedTypes.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +4 -0
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/server/DeviceDiscoveryService.d.ts.map +1 -1
- package/dist/server/DeviceDiscoveryService.js +3 -1
- package/dist/server/DeviceDiscoveryService.js.map +1 -1
- package/dist/server/SystemAdminAuth.d.ts.map +1 -1
- package/dist/server/SystemAdminAuth.js +3 -1
- package/dist/server/SystemAdminAuth.js.map +1 -1
- package/dist/server/legacyChallengeSignatures.d.ts +11 -0
- package/dist/server/legacyChallengeSignatures.d.ts.map +1 -0
- package/dist/server/legacyChallengeSignatures.js +50 -0
- package/dist/server/legacyChallengeSignatures.js.map +1 -0
- package/dist/server/peer/PeerAuthService.d.ts.map +1 -1
- package/dist/server/peer/PeerAuthService.js +3 -1
- package/dist/server/peer/PeerAuthService.js.map +1 -1
- package/dist/server/types.d.ts +8 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +8 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +2 -1
|
@@ -568,6 +568,19 @@ class BaseMindooDB {
|
|
|
568
568
|
* Maintained only via {@link rememberL2Record} / {@link forgetL2Record}.
|
|
569
569
|
*/
|
|
570
570
|
this.l2RecordChangeSeqs = new Map();
|
|
571
|
+
/**
|
|
572
|
+
* Whether {@link l2RecordChangeSeqs} lists every L2 record this instance
|
|
573
|
+
* may use: true for a fresh cache and for checkpoints that say so
|
|
574
|
+
* (`l2Tracked`), false for older checkpoints. When true, a document
|
|
575
|
+
* without an entry has no usable L2 record and the read skips the L2
|
|
576
|
+
* lookup instead of asking the cache store (one store round trip per
|
|
577
|
+
* document on a cold start). Records another tab wrote meanwhile are
|
|
578
|
+
* missed until the next restore; the document is then materialized from
|
|
579
|
+
* the store, as on any L2 miss.
|
|
580
|
+
*/
|
|
581
|
+
this.l2RecordsTracked = true;
|
|
582
|
+
/** L2 records fetched in one batch for a prefetch window (consumed by tryLoadFromL2). */
|
|
583
|
+
this.l2Prefetched = new Map();
|
|
571
584
|
/**
|
|
572
585
|
* Tenant KeyBag fingerprint observed at the time the in-memory index
|
|
573
586
|
* was last reconciled with the underlying store.
|
|
@@ -615,6 +628,16 @@ class BaseMindooDB {
|
|
|
615
628
|
// ---------------------------------------------------------------------------
|
|
616
629
|
/** Serializes concurrent author-trust reconciles (per database). */
|
|
617
630
|
this.authorTrustReconcileChain = Promise.resolve();
|
|
631
|
+
/**
|
|
632
|
+
* Key series this replica gained or lost during the current pass. Documents of
|
|
633
|
+
* a group document are not re-checked one by one when its wraps change; the
|
|
634
|
+
* next {@link reconcileGroupKeyAccess} re-runs the key visibility reconcile.
|
|
635
|
+
*/
|
|
636
|
+
this.groupKeyAccessChanged = new Set();
|
|
637
|
+
/** Per document: whether its origin decrypted with the partial series held then. */
|
|
638
|
+
this.partialSeriesProbes = new Map();
|
|
639
|
+
/** Group key series last probed without success, by key id (ms), see ingestGroupKey. */
|
|
640
|
+
this.groupKeyMisses = new Map();
|
|
618
641
|
(0, databaseIdValidation_1.validateDatabaseId)(store.getId(), "dbId");
|
|
619
642
|
(0, databaseIdValidation_1.validateDatabaseId)(attachmentStore.getId(), "dbId");
|
|
620
643
|
if (store.getId() !== attachmentStore.getId()) {
|
|
@@ -1870,6 +1893,10 @@ class BaseMindooDB {
|
|
|
1870
1893
|
if (this.l2RecordChangeSeqs.size > 0) {
|
|
1871
1894
|
checkpoint.l2RecordChangeSeqs = Object.fromEntries(this.l2RecordChangeSeqs);
|
|
1872
1895
|
}
|
|
1896
|
+
// The map above is complete only when it was complete on restore.
|
|
1897
|
+
if (this.l2RecordsTracked) {
|
|
1898
|
+
checkpoint.l2Tracked = true;
|
|
1899
|
+
}
|
|
1873
1900
|
// Persist the KeyBag fingerprint observed at the last reconciliation
|
|
1874
1901
|
// so the next restore can decide whether to skip the visibility
|
|
1875
1902
|
// metadata scan. `null` is acceptable - older checkpoints simply
|
|
@@ -1932,6 +1959,7 @@ class BaseMindooDB {
|
|
|
1932
1959
|
? checkpoint.locallyIndexedEntryIds.filter((id) => typeof id === "string")
|
|
1933
1960
|
: []);
|
|
1934
1961
|
this.l2RecordChangeSeqs.clear();
|
|
1962
|
+
this.l2RecordsTracked = checkpoint.l2Tracked === true;
|
|
1935
1963
|
if (checkpoint.l2RecordChangeSeqs &&
|
|
1936
1964
|
typeof checkpoint.l2RecordChangeSeqs === "object") {
|
|
1937
1965
|
for (const [docId, changeSeq] of Object.entries(checkpoint.l2RecordChangeSeqs)) {
|
|
@@ -3143,7 +3171,7 @@ class BaseMindooDB {
|
|
|
3143
3171
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
3144
3172
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
3145
3173
|
await this.ingestSealed(visibility.decryptionKeyId, metadataByDoc.get(docId));
|
|
3146
|
-
const canRead = await this.
|
|
3174
|
+
const canRead = await this.canReadWithKey(visibility.decryptionKeyId, docId, metadataByDoc.get(docId));
|
|
3147
3175
|
if (canRead) {
|
|
3148
3176
|
// This scan is the authority on visibility, so it also heals a tally
|
|
3149
3177
|
// that a visible->visible short-circuit below would leave untouched.
|
|
@@ -3447,6 +3475,8 @@ class BaseMindooDB {
|
|
|
3447
3475
|
this.beginChangeNotificationHold();
|
|
3448
3476
|
try {
|
|
3449
3477
|
await this.syncStoreChangesInternal();
|
|
3478
|
+
// Joined or left a group document in this batch: its documents follow.
|
|
3479
|
+
await this.reconcileGroupKeyAccess();
|
|
3450
3480
|
}
|
|
3451
3481
|
finally {
|
|
3452
3482
|
this.endChangeNotificationHold();
|
|
@@ -3505,6 +3535,26 @@ class BaseMindooDB {
|
|
|
3505
3535
|
this.logger.debug(`Dropping cached sealed document ${docId} — newest wrap is not for this user`);
|
|
3506
3536
|
return;
|
|
3507
3537
|
}
|
|
3538
|
+
// Full rebuild: drop the cached state and materialize the document
|
|
3539
|
+
// again, through the access-control path when that is enforced.
|
|
3540
|
+
const reloadFully = async () => {
|
|
3541
|
+
this.docCache.delete(docId);
|
|
3542
|
+
updatedDoc = await this.loadDocumentInternal(docId);
|
|
3543
|
+
if (updatedDoc) {
|
|
3544
|
+
// New entries triggered this reload, so force the re-emit.
|
|
3545
|
+
this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted, updatedDoc.decryptionKeyId, "visible", updatedDoc.awaitingWitness ?? false, updatedDoc.witnessed ?? false, true);
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3548
|
+
if (await this.isAclEnforced()) {
|
|
3549
|
+
// The incremental path below applies new entries without the
|
|
3550
|
+
// access-control gate (docs/accesscontrol.md §10). With access
|
|
3551
|
+
// control active, a change by someone the rules do not allow must be
|
|
3552
|
+
// quarantined like on a fresh load, so rebuild the document through
|
|
3553
|
+
// maybeMaterializeWithAccessControl instead.
|
|
3554
|
+
this.logger.debug(`Document ${docId} found in cache, access control enforced: full reload`);
|
|
3555
|
+
await reloadFully();
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3508
3558
|
// Document is cached - try incremental update
|
|
3509
3559
|
this.logger.debug(`Document ${docId} found in cache, attempting incremental update`);
|
|
3510
3560
|
try {
|
|
@@ -3525,12 +3575,7 @@ class BaseMindooDB {
|
|
|
3525
3575
|
catch (error) {
|
|
3526
3576
|
// If incremental update fails, fall back to full reload
|
|
3527
3577
|
this.logger.warn(`Incremental update failed for document ${docId}, falling back to full reload:`, error);
|
|
3528
|
-
|
|
3529
|
-
updatedDoc = await this.loadDocumentInternal(docId);
|
|
3530
|
-
if (updatedDoc) {
|
|
3531
|
-
// New entries triggered this reload, so force the re-emit.
|
|
3532
|
-
this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted, updatedDoc.decryptionKeyId, "visible", updatedDoc.awaitingWitness ?? false, updatedDoc.witnessed ?? false, true);
|
|
3533
|
-
}
|
|
3578
|
+
await reloadFully();
|
|
3534
3579
|
}
|
|
3535
3580
|
}
|
|
3536
3581
|
else {
|
|
@@ -3568,7 +3613,7 @@ class BaseMindooDB {
|
|
|
3568
3613
|
sealedIngestEntries = fullDocMetadata;
|
|
3569
3614
|
}
|
|
3570
3615
|
await this.ingestSealed(representativeEntry.decryptionKeyId, sealedIngestEntries);
|
|
3571
|
-
const keyAvailable = await this.
|
|
3616
|
+
const keyAvailable = await this.canReadWithKey(representativeEntry.decryptionKeyId, docId, coldSync ? entryMetadataList : fullDocMetadata);
|
|
3572
3617
|
if (!keyAvailable) {
|
|
3573
3618
|
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
3574
3619
|
// This is where a device that is still waiting for its keys meets
|
|
@@ -4497,6 +4542,10 @@ class BaseMindooDB {
|
|
|
4497
4542
|
throw new Error("createDocument: recipients is not supported together with a convergent custom id; " +
|
|
4498
4543
|
"pass assumeUniqueId for random ids.");
|
|
4499
4544
|
}
|
|
4545
|
+
if (useDeterministicSeed && options.groupKeyDocId !== undefined) {
|
|
4546
|
+
throw new Error("createDocument: groupKeyDocId is not supported together with a convergent custom id; " +
|
|
4547
|
+
"pass assumeUniqueId for random ids.");
|
|
4548
|
+
}
|
|
4500
4549
|
if (useDeterministicSeed && (options.attachments?.length ?? 0) > 0) {
|
|
4501
4550
|
throw new Error("createDocument: attachments are not supported together with a convergent custom id; " +
|
|
4502
4551
|
"pass assumeUniqueId, or add them with changeDoc().");
|
|
@@ -4746,6 +4795,9 @@ class BaseMindooDB {
|
|
|
4746
4795
|
if (useDeterministicSeed && options.recipients !== undefined) {
|
|
4747
4796
|
throw new Error("createDocuments: recipients is not supported together with a convergent custom id; pass assumeUniqueId");
|
|
4748
4797
|
}
|
|
4798
|
+
if (useDeterministicSeed && options.groupKeyDocId !== undefined) {
|
|
4799
|
+
throw new Error("createDocuments: groupKeyDocId is not supported together with a convergent custom id; pass assumeUniqueId");
|
|
4800
|
+
}
|
|
4749
4801
|
// Sanitize caller-provided initial values: internal/reserved fields are
|
|
4750
4802
|
// managed by MindooDB and must not be seeded by callers. `undefined`
|
|
4751
4803
|
// values are treated as "not set" (Automerge cannot represent undefined).
|
|
@@ -5084,13 +5136,112 @@ class BaseMindooDB {
|
|
|
5084
5136
|
async ingestSealed(keyId, entries) {
|
|
5085
5137
|
if (!this.tenant.ingestSealedFromEntries)
|
|
5086
5138
|
return;
|
|
5139
|
+
const hadKey = (0, sealedTypes_1.isSealedKeyId)(keyId) ? await this.tenant.hasDecryptionKey(keyId) : false;
|
|
5140
|
+
const heldBefore = this.tenant.getSealedGenerations?.(keyId).length ?? 0;
|
|
5087
5141
|
this.sealedIngestDepth++;
|
|
5088
5142
|
try {
|
|
5089
5143
|
await this.tenant.ingestSealedFromEntries(keyId, entries);
|
|
5144
|
+
// A document encrypted with a group document's key series (`groupKeyDocId`)
|
|
5145
|
+
// carries no recipient block of its own: the wraps are on the group document.
|
|
5146
|
+
if ((0, sealedTypes_1.isSealedKeyId)(keyId) && !(0, recipients_1.newestRecipientBlock)(entries)) {
|
|
5147
|
+
await this.ingestGroupKey(keyId);
|
|
5148
|
+
}
|
|
5149
|
+
// A series that does not reach back to epoch 1 may be completed from the
|
|
5150
|
+
// key document's older recipient blocks; `entries` is often just a batch.
|
|
5151
|
+
if ((0, sealedTypes_1.isSealedKeyId)(keyId) && this.tenant.needsDeepSealedIngest?.(keyId)) {
|
|
5152
|
+
const keyDocId = (0, sealedTypes_1.sealedKeyDocId)(keyId);
|
|
5153
|
+
if (keyDocId) {
|
|
5154
|
+
await this.tenant.ingestSealedFromEntries(keyId, await this.scanAllMetadata(this.store, { docId: keyDocId }), { complete: true });
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5090
5157
|
}
|
|
5091
5158
|
finally {
|
|
5092
5159
|
this.sealedIngestDepth--;
|
|
5093
5160
|
}
|
|
5161
|
+
if ((0, sealedTypes_1.isSealedKeyId)(keyId) &&
|
|
5162
|
+
(hadKey !== (await this.tenant.hasDecryptionKey(keyId)) ||
|
|
5163
|
+
(hadKey &&
|
|
5164
|
+
heldBefore !== (this.tenant.getSealedGenerations?.(keyId).length ?? 0)))) {
|
|
5165
|
+
this.partialSeriesProbes.clear();
|
|
5166
|
+
// Joined or left the key series: documents of a group document that were
|
|
5167
|
+
// hidden (or readable) until now have to follow (see reconcileGroupKeyAccess).
|
|
5168
|
+
this.groupKeyAccessChanged.add(keyId);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Whether this replica can read a document of key `keyId`. Usually the key id
|
|
5173
|
+
* alone decides; a reader added to a group without its history
|
|
5174
|
+
* (`history: "none"`) knows the key id but holds only the newer generations,
|
|
5175
|
+
* so the document's origin entry is test-decrypted (once per document).
|
|
5176
|
+
*/
|
|
5177
|
+
async canReadWithKey(keyId, docId, metas) {
|
|
5178
|
+
if (!(await this.tenant.hasDecryptionKey(keyId)))
|
|
5179
|
+
return false;
|
|
5180
|
+
if (!(0, sealedTypes_1.isSealedKeyId)(keyId) || !this.tenant.isPartialSealedSeries?.(keyId))
|
|
5181
|
+
return true;
|
|
5182
|
+
const known = this.partialSeriesProbes.get(docId);
|
|
5183
|
+
if (known !== undefined)
|
|
5184
|
+
return known;
|
|
5185
|
+
const all = metas ?? (await this.scanAllMetadata(this.store, { docId }));
|
|
5186
|
+
// Loading starts from the newest snapshot when there is one (written under
|
|
5187
|
+
// the new key when readers were added without history), else from the create.
|
|
5188
|
+
const origin = all
|
|
5189
|
+
.filter((meta) => meta.entryType === "doc_snapshot")
|
|
5190
|
+
.sort((a, b) => b.createdAt - a.createdAt)[0] ??
|
|
5191
|
+
all.find((meta) => meta.entryType === "doc_create");
|
|
5192
|
+
if (!origin)
|
|
5193
|
+
return true;
|
|
5194
|
+
let ok = false;
|
|
5195
|
+
try {
|
|
5196
|
+
const [entry] = await this.store.getEntries([origin.id]);
|
|
5197
|
+
if (entry) {
|
|
5198
|
+
await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
|
|
5199
|
+
ok = true;
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
catch {
|
|
5203
|
+
ok = false;
|
|
5204
|
+
}
|
|
5205
|
+
this.partialSeriesProbes.set(docId, ok);
|
|
5206
|
+
return ok;
|
|
5207
|
+
}
|
|
5208
|
+
/**
|
|
5209
|
+
* Makes the key series `$sealed:<groupDocId>` available from the group
|
|
5210
|
+
* document's own entries when the session does not have it yet. A miss is
|
|
5211
|
+
* remembered briefly, so a few hundred documents of a group this replica is not
|
|
5212
|
+
* a member of cost one scan of the group document, not one each.
|
|
5213
|
+
*/
|
|
5214
|
+
async ingestGroupKey(keyId) {
|
|
5215
|
+
const groupDocId = (0, sealedTypes_1.sealedKeyDocId)(keyId);
|
|
5216
|
+
if (!groupDocId || (await this.tenant.hasDecryptionKey(keyId)))
|
|
5217
|
+
return;
|
|
5218
|
+
const lastMiss = this.groupKeyMisses.get(keyId);
|
|
5219
|
+
if (lastMiss !== undefined && Date.now() - lastMiss < BaseMindooDB.GROUP_KEY_MISS_TTL_MS)
|
|
5220
|
+
return;
|
|
5221
|
+
const groupEntries = await this.scanAllMetadata(this.store, { docId: groupDocId });
|
|
5222
|
+
if (groupEntries.length > 0) {
|
|
5223
|
+
await this.tenant.ingestSealedFromEntries(keyId, groupEntries);
|
|
5224
|
+
}
|
|
5225
|
+
if (await this.tenant.hasDecryptionKey(keyId)) {
|
|
5226
|
+
this.groupKeyMisses.delete(keyId);
|
|
5227
|
+
}
|
|
5228
|
+
else {
|
|
5229
|
+
this.groupKeyMisses.set(keyId, Date.now());
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
/**
|
|
5233
|
+
* After a pass that changed access to a key series (added to or removed from a
|
|
5234
|
+
* group document), reveal or hide the documents that use it. Runs the full key
|
|
5235
|
+
* visibility reconcile, which is rare enough: it only happens when this
|
|
5236
|
+
* replica's membership in a group changed.
|
|
5237
|
+
*/
|
|
5238
|
+
async reconcileGroupKeyAccess() {
|
|
5239
|
+
if (this.groupKeyAccessChanged.size === 0 || this.isTimeTravelMode())
|
|
5240
|
+
return;
|
|
5241
|
+
this.groupKeyAccessChanged.clear();
|
|
5242
|
+
this.groupKeyMisses.clear();
|
|
5243
|
+
await this.reconcileKeyVisibility();
|
|
5244
|
+
this.groupKeyAccessChanged.clear();
|
|
5094
5245
|
}
|
|
5095
5246
|
/**
|
|
5096
5247
|
* Unwrap a `$sealed:` DEK from entry recipient wraps into the tenant session
|
|
@@ -8509,7 +8660,9 @@ class BaseMindooDB {
|
|
|
8509
8660
|
isAccessible: () => true,
|
|
8510
8661
|
isAwaitingWitness: () => internalDoc.awaitingWitness ?? false,
|
|
8511
8662
|
isWitnessed: () => internalDoc.witnessed ?? false,
|
|
8512
|
-
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
8663
|
+
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
8664
|
+
(0, sealedTypes_1.groupKeyDocIdOf)(internalDoc.decryptionKeyId, internalDoc.id) === undefined,
|
|
8665
|
+
getGroupKeyDocId: () => (0, sealedTypes_1.groupKeyDocIdOf)(internalDoc.decryptionKeyId, internalDoc.id),
|
|
8513
8666
|
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
8514
8667
|
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
8515
8668
|
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
@@ -9670,6 +9823,7 @@ class BaseMindooDB {
|
|
|
9670
9823
|
attachmentIds,
|
|
9671
9824
|
bypassPrecheck,
|
|
9672
9825
|
recipients: options.recipients,
|
|
9826
|
+
forceSnapshot: options.forceSnapshot,
|
|
9673
9827
|
});
|
|
9674
9828
|
}
|
|
9675
9829
|
catch (error) {
|
|
@@ -9772,8 +9926,9 @@ class BaseMindooDB {
|
|
|
9772
9926
|
signingKeyPair,
|
|
9773
9927
|
signingKeyPassword,
|
|
9774
9928
|
createdByPublicKey: createdByPublicKey ?? "",
|
|
9929
|
+
force: options.forceSnapshot,
|
|
9775
9930
|
}
|
|
9776
|
-
: { createdByPublicKey: createdByPublicKey ?? "" });
|
|
9931
|
+
: { createdByPublicKey: createdByPublicKey ?? "", force: options.forceSnapshot });
|
|
9777
9932
|
}
|
|
9778
9933
|
async buildDocChangeStoreEntry(options) {
|
|
9779
9934
|
const { internalDoc, changeBytes, now, useCustomKey, signingKeyPair, signingKeyPassword, attachmentIds, attachmentRefs, recipients, } = options;
|
|
@@ -9840,7 +9995,7 @@ class BaseMindooDB {
|
|
|
9840
9995
|
// An absent counter (first write this session) forces one scan to learn
|
|
9841
9996
|
// the real backlog, after which the counter tracks it incrementally.
|
|
9842
9997
|
const knownWrites = this.writesSinceSnapshotCheck.get(docId);
|
|
9843
|
-
if (knownWrites !== undefined) {
|
|
9998
|
+
if (knownWrites !== undefined && !options.force) {
|
|
9844
9999
|
const writes = knownWrites + 1;
|
|
9845
10000
|
if (writes < this.snapshotMinChanges) {
|
|
9846
10001
|
this.writesSinceSnapshotCheck.set(docId, writes);
|
|
@@ -9856,10 +10011,11 @@ class BaseMindooDB {
|
|
|
9856
10011
|
const latestSnapshotAt = latestSnapshot?.createdAt ?? 0;
|
|
9857
10012
|
const changesSinceSnapshot = replayEntries.filter((em) => em.createdAt > latestSnapshotAt).length;
|
|
9858
10013
|
this.writesSinceSnapshotCheck.set(docId, changesSinceSnapshot);
|
|
9859
|
-
if (changesSinceSnapshot < this.snapshotMinChanges) {
|
|
10014
|
+
if (changesSinceSnapshot < this.snapshotMinChanges && !options.force) {
|
|
9860
10015
|
return;
|
|
9861
10016
|
}
|
|
9862
|
-
if (
|
|
10017
|
+
if (!options.force &&
|
|
10018
|
+
latestSnapshot &&
|
|
9863
10019
|
Date.now() - latestSnapshot.createdAt < this.snapshotCooldownMs) {
|
|
9864
10020
|
return;
|
|
9865
10021
|
}
|
|
@@ -10022,6 +10178,34 @@ class BaseMindooDB {
|
|
|
10022
10178
|
await flushPage();
|
|
10023
10179
|
return buffer;
|
|
10024
10180
|
}
|
|
10181
|
+
/**
|
|
10182
|
+
* Fetch the L2 records of a prefetch window in one batched read instead
|
|
10183
|
+
* of one cache-store round trip per document. Only documents that may
|
|
10184
|
+
* have a record are asked for (see {@link l2RecordsTracked}); the results
|
|
10185
|
+
* are consumed by {@link tryLoadFromL2}. Best effort: on failure the
|
|
10186
|
+
* loads fall back to their own reads.
|
|
10187
|
+
*/
|
|
10188
|
+
async prefetchL2Records(docIds) {
|
|
10189
|
+
const store = this.cacheManager?.getStore() ?? null;
|
|
10190
|
+
if (!store) {
|
|
10191
|
+
return [];
|
|
10192
|
+
}
|
|
10193
|
+
const wanted = docIds.filter((docId) => !this.l2Prefetched.has(docId) &&
|
|
10194
|
+
(!this.l2RecordsTracked || this.l2RecordChangeSeqs.has(docId)));
|
|
10195
|
+
if (wanted.length === 0) {
|
|
10196
|
+
return [];
|
|
10197
|
+
}
|
|
10198
|
+
const prefix = this.getCachePrefix();
|
|
10199
|
+
try {
|
|
10200
|
+
const records = await store.getMany("doc", wanted.map((docId) => `${prefix}/${docId}`));
|
|
10201
|
+
wanted.forEach((docId, i) => this.l2Prefetched.set(docId, records[i] ?? null));
|
|
10202
|
+
return wanted;
|
|
10203
|
+
}
|
|
10204
|
+
catch (err) {
|
|
10205
|
+
this.logger.warn(`Batched L2 read for a prefetch window failed:`, err);
|
|
10206
|
+
return [];
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10025
10209
|
/**
|
|
10026
10210
|
* Materialize a window of upcoming documents so iteration reads them from
|
|
10027
10211
|
* the cache instead of the store.
|
|
@@ -10037,7 +10221,7 @@ class BaseMindooDB {
|
|
|
10037
10221
|
*
|
|
10038
10222
|
* Returns the position prefetched through, which the caller threads back in.
|
|
10039
10223
|
*/
|
|
10040
|
-
async prefetchIterationWindow(indexSnapshot, startIndex, prefetchedThrough, idPrefix) {
|
|
10224
|
+
async prefetchIterationWindow(indexSnapshot, startIndex, prefetchedThrough, idPrefix, persist = true) {
|
|
10041
10225
|
const windowSize = this.iteratePrefetchWindowDocs;
|
|
10042
10226
|
if (windowSize <= 0) {
|
|
10043
10227
|
return { prefetchedThrough: startIndex, prefetchedDocuments: 0 };
|
|
@@ -10097,14 +10281,20 @@ class BaseMindooDB {
|
|
|
10097
10281
|
this.logger.warn(`Failed to batch-fetch entries for prefetch window:`, err);
|
|
10098
10282
|
this.materializationEntryBuffer = null;
|
|
10099
10283
|
}
|
|
10284
|
+
const prefetchedL2 = await this.prefetchL2Records(docIds);
|
|
10100
10285
|
try {
|
|
10101
|
-
await Promise.all(docIds.map((docId) => this.loadDocumentInternal(docId).catch((err) => {
|
|
10286
|
+
await Promise.all(docIds.map((docId) => this.loadDocumentInternal(docId, persist).catch((err) => {
|
|
10102
10287
|
this.logger.warn(`Failed to prefetch document ${docId}:`, err);
|
|
10103
10288
|
return null;
|
|
10104
10289
|
})));
|
|
10105
10290
|
}
|
|
10106
10291
|
finally {
|
|
10107
10292
|
this.materializationEntryBuffer = null;
|
|
10293
|
+
// Records a load did not consume (shared or failed load) must not be
|
|
10294
|
+
// used by a later read: they may be outdated by then.
|
|
10295
|
+
for (const docId of prefetchedL2) {
|
|
10296
|
+
this.l2Prefetched.delete(docId);
|
|
10297
|
+
}
|
|
10108
10298
|
}
|
|
10109
10299
|
return {
|
|
10110
10300
|
prefetchedThrough: scannedThrough,
|
|
@@ -10524,6 +10714,7 @@ class BaseMindooDB {
|
|
|
10524
10714
|
docId: "",
|
|
10525
10715
|
};
|
|
10526
10716
|
const idPrefix = this.normalizeIdPrefixFilter(options?.idPrefix);
|
|
10717
|
+
const persistLoaded = options?.persistLoadedDocuments !== false;
|
|
10527
10718
|
this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
|
|
10528
10719
|
// Use a stable snapshot of the index for this generator run so concurrent
|
|
10529
10720
|
// updates do not reorder/skip entries while iterating.
|
|
@@ -10536,7 +10727,7 @@ class BaseMindooDB {
|
|
|
10536
10727
|
// so concurrent iterations do not share a runway.
|
|
10537
10728
|
let prefetchedThrough = startIndex;
|
|
10538
10729
|
const prefetchFrom = async (from) => {
|
|
10539
|
-
const result = await this.prefetchIterationWindow(indexSnapshot, from, prefetchedThrough, idPrefix);
|
|
10730
|
+
const result = await this.prefetchIterationWindow(indexSnapshot, from, prefetchedThrough, idPrefix, persistLoaded);
|
|
10540
10731
|
prefetchedThrough = result.prefetchedThrough;
|
|
10541
10732
|
prefetchedDocuments += result.prefetchedDocuments;
|
|
10542
10733
|
};
|
|
@@ -10593,7 +10784,7 @@ class BaseMindooDB {
|
|
|
10593
10784
|
let internalDoc = this.getCachedDocument(entry.docId);
|
|
10594
10785
|
if (!internalDoc) {
|
|
10595
10786
|
this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
|
|
10596
|
-
internalDoc = await this.loadDocumentInternal(entry.docId);
|
|
10787
|
+
internalDoc = await this.loadDocumentInternal(entry.docId, persistLoaded);
|
|
10597
10788
|
if (internalDoc) {
|
|
10598
10789
|
loadedDocuments++;
|
|
10599
10790
|
}
|
|
@@ -10820,13 +11011,23 @@ class BaseMindooDB {
|
|
|
10820
11011
|
const prefix = this.getCachePrefix();
|
|
10821
11012
|
const key = `${prefix}/${docId}`;
|
|
10822
11013
|
let bytes;
|
|
10823
|
-
|
|
10824
|
-
bytes =
|
|
11014
|
+
if (this.l2Prefetched.has(docId)) {
|
|
11015
|
+
bytes = this.l2Prefetched.get(docId) ?? null;
|
|
11016
|
+
this.l2Prefetched.delete(docId);
|
|
10825
11017
|
}
|
|
10826
|
-
|
|
10827
|
-
|
|
11018
|
+
else if (this.l2RecordsTracked && !this.l2RecordChangeSeqs.has(docId)) {
|
|
11019
|
+
// Known to have no L2 record: skip the store round trip.
|
|
10828
11020
|
return null;
|
|
10829
11021
|
}
|
|
11022
|
+
else {
|
|
11023
|
+
try {
|
|
11024
|
+
bytes = await store.get("doc", key);
|
|
11025
|
+
}
|
|
11026
|
+
catch (e) {
|
|
11027
|
+
this.logger.warn(`L2 read failed for ${docId}; falling back to full materialization: ${e}`);
|
|
11028
|
+
return null;
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
10830
11031
|
if (!bytes)
|
|
10831
11032
|
return null;
|
|
10832
11033
|
let deserialized;
|
|
@@ -11347,6 +11548,12 @@ class BaseMindooDB {
|
|
|
11347
11548
|
options.decryptionKeyId !== undefined) {
|
|
11348
11549
|
throw new Error("createDocument: recipients and decryptionKeyId are mutually exclusive");
|
|
11349
11550
|
}
|
|
11551
|
+
if (options.groupKeyDocId !== undefined) {
|
|
11552
|
+
if (options.recipients !== undefined || options.decryptionKeyId !== undefined) {
|
|
11553
|
+
throw new Error("createDocument: groupKeyDocId is mutually exclusive with recipients and decryptionKeyId");
|
|
11554
|
+
}
|
|
11555
|
+
return await this.requireGroupKey(options.groupKeyDocId);
|
|
11556
|
+
}
|
|
11350
11557
|
if (options.recipients !== undefined) {
|
|
11351
11558
|
return (0, sealedTypes_1.sealedKeyId)(docId ?? options.id ?? "pending");
|
|
11352
11559
|
}
|
|
@@ -11366,6 +11573,27 @@ class BaseMindooDB {
|
|
|
11366
11573
|
}
|
|
11367
11574
|
return "default";
|
|
11368
11575
|
}
|
|
11576
|
+
/**
|
|
11577
|
+
* The key id of a group document's key series, after checking that the group
|
|
11578
|
+
* document is a sealed document of this database and that the caller is a member.
|
|
11579
|
+
*/
|
|
11580
|
+
async requireGroupKey(groupKeyDocId) {
|
|
11581
|
+
if (!types_1.CUSTOM_DOC_ID_REGEX.test(groupKeyDocId) && !/^[a-z0-9_]+$/.test(groupKeyDocId)) {
|
|
11582
|
+
throw new Error(`createDocument: invalid groupKeyDocId "${groupKeyDocId}"`);
|
|
11583
|
+
}
|
|
11584
|
+
const keyId = (0, sealedTypes_1.sealedKeyId)(groupKeyDocId);
|
|
11585
|
+
const groupEntries = await this.scanAllMetadata(this.store, { docId: groupKeyDocId });
|
|
11586
|
+
const groupCreate = groupEntries.find((entry) => entry.entryType === "doc_create");
|
|
11587
|
+
if (!groupCreate || groupCreate.decryptionKeyId !== keyId) {
|
|
11588
|
+
throw new Error(`createDocument: group document "${groupKeyDocId}" does not exist in this database or was not created with recipients`);
|
|
11589
|
+
}
|
|
11590
|
+
this.groupKeyMisses.delete(keyId);
|
|
11591
|
+
await this.ingestSealed(keyId, groupEntries);
|
|
11592
|
+
if (!(await this.tenant.hasDecryptionKey(keyId))) {
|
|
11593
|
+
throw new Error(`createDocument: you are not a member of group document "${groupKeyDocId}"`);
|
|
11594
|
+
}
|
|
11595
|
+
return keyId;
|
|
11596
|
+
}
|
|
11369
11597
|
async prepareSealedCreate(options, docId, keyId, now, signerPublicKey) {
|
|
11370
11598
|
if (options.recipients === undefined)
|
|
11371
11599
|
return undefined;
|
|
@@ -11510,6 +11738,9 @@ class BaseMindooDB {
|
|
|
11510
11738
|
async setRecipients(doc, recipients, options) {
|
|
11511
11739
|
return this.mutateRecipients(doc, "set", recipients, options);
|
|
11512
11740
|
}
|
|
11741
|
+
async revealHistory(doc, recipients, options) {
|
|
11742
|
+
return this.mutateRecipients(doc, "reveal", recipients, options);
|
|
11743
|
+
}
|
|
11513
11744
|
async canChangeRecipients(doc, next) {
|
|
11514
11745
|
const after = { ...doc.getData() };
|
|
11515
11746
|
after[sealedTypes_1.ENCRYPT_FOR_FIELD] = {
|
|
@@ -11523,6 +11754,10 @@ class BaseMindooDB {
|
|
|
11523
11754
|
if (!(0, sealedTypes_1.isSealedKeyId)(doc.getDecryptionKeyId())) {
|
|
11524
11755
|
throw new Error("Recipient mutators require a document created with `recipients`");
|
|
11525
11756
|
}
|
|
11757
|
+
const groupDocId = (0, sealedTypes_1.groupKeyDocIdOf)(doc.getDecryptionKeyId(), doc.getId());
|
|
11758
|
+
if (groupDocId !== undefined) {
|
|
11759
|
+
throw new Error(`Document ${doc.getId()} uses the key series of group document ${groupDocId}; change the recipients there`);
|
|
11760
|
+
}
|
|
11526
11761
|
const tenant = this.tenant;
|
|
11527
11762
|
const docId = doc.getId();
|
|
11528
11763
|
let internalDoc = this.getCachedDocument(docId) ?? (await this.loadDocumentInternal(docId));
|
|
@@ -11562,6 +11797,16 @@ class BaseMindooDB {
|
|
|
11562
11797
|
addedTargets.push(target);
|
|
11563
11798
|
}
|
|
11564
11799
|
}
|
|
11800
|
+
const revealedIds = [];
|
|
11801
|
+
if (mode === "reveal") {
|
|
11802
|
+
for (const [id, entry] of Object.entries(currentMap)) {
|
|
11803
|
+
if (entry.removedAt || !entry.historyFrom)
|
|
11804
|
+
continue;
|
|
11805
|
+
if (resolved.targets.some((target) => matchesTarget(id, entry, target))) {
|
|
11806
|
+
revealedIds.push(id);
|
|
11807
|
+
}
|
|
11808
|
+
}
|
|
11809
|
+
}
|
|
11565
11810
|
if (mode === "remove") {
|
|
11566
11811
|
for (const [id, entry] of Object.entries(currentMap)) {
|
|
11567
11812
|
if (entry.removedAt)
|
|
@@ -11582,19 +11827,69 @@ class BaseMindooDB {
|
|
|
11582
11827
|
removedStableIds.push(base);
|
|
11583
11828
|
}
|
|
11584
11829
|
}
|
|
11585
|
-
const
|
|
11586
|
-
|
|
11587
|
-
|
|
11830
|
+
const withoutHistory = (mode === "add" || mode === "set") &&
|
|
11831
|
+
addedTargets.length > 0 &&
|
|
11832
|
+
(options?.history ?? sealedTypes_1.DEFAULT_RECIPIENT_HISTORY) === "none";
|
|
11833
|
+
const rotate = removedStableIds.length > 0 || withoutHistory;
|
|
11834
|
+
const heldSeries = tenant.getSealedSeries
|
|
11835
|
+
? tenant.getSealedSeries(internalDoc.decryptionKeyId)
|
|
11836
|
+
: tenant
|
|
11837
|
+
.getSealedGenerations(internalDoc.decryptionKeyId)
|
|
11838
|
+
.map((key, index, all) => ({ epoch: all.length - index, key }));
|
|
11839
|
+
if (heldSeries.length === 0) {
|
|
11588
11840
|
throw new Error("Cannot change recipients: the current document key is not in this session");
|
|
11589
11841
|
}
|
|
11590
|
-
|
|
11591
|
-
|
|
11842
|
+
// The epoch comes from the store, not from the cached document.
|
|
11843
|
+
const newestBlock = (0, recipients_1.newestRecipientBlock)(await this.scanAllMetadata(this.store, { docId })) ??
|
|
11844
|
+
internalDoc.sealedRecipients;
|
|
11845
|
+
let epoch = Math.max(newestBlock?.epoch ?? 0, heldSeries[0].epoch);
|
|
11846
|
+
if (heldSeries[0].epoch < epoch) {
|
|
11847
|
+
throw new Error("Cannot change recipients: this replica does not hold the current document key yet; sync first");
|
|
11848
|
+
}
|
|
11849
|
+
// What this replica can seal: the contiguous run of generations that ends at
|
|
11850
|
+
// the current epoch. A member who joined without the earlier history holds a
|
|
11851
|
+
// shorter run; readers entitled to more keep taking the older generations
|
|
11852
|
+
// from the earlier recipient blocks (see ingestSealedFromEntries).
|
|
11853
|
+
let series = heldSeries;
|
|
11854
|
+
let run = [];
|
|
11855
|
+
for (const entry of heldSeries) {
|
|
11856
|
+
if (run.length > 0 && entry.epoch !== run[run.length - 1].epoch - 1)
|
|
11857
|
+
break;
|
|
11858
|
+
run.push(entry);
|
|
11859
|
+
}
|
|
11592
11860
|
if (rotate) {
|
|
11593
11861
|
const nextDek = tenant
|
|
11594
11862
|
.getCryptoAdapter()
|
|
11595
11863
|
.getRandomValues(new Uint8Array(32));
|
|
11596
|
-
generations = [nextDek, ...currentGens];
|
|
11597
11864
|
epoch += 1;
|
|
11865
|
+
run = [{ epoch, key: nextDek }, ...run];
|
|
11866
|
+
series = [{ epoch, key: nextDek }, ...heldSeries];
|
|
11867
|
+
}
|
|
11868
|
+
/** Oldest epoch this replica can hand to anyone. */
|
|
11869
|
+
const sealFrom = run[run.length - 1].epoch;
|
|
11870
|
+
const generations = run.map((entry) => entry.key);
|
|
11871
|
+
const addedHistoryFrom = withoutHistory
|
|
11872
|
+
? epoch
|
|
11873
|
+
: sealFrom > 1
|
|
11874
|
+
? sealFrom
|
|
11875
|
+
: undefined;
|
|
11876
|
+
// Revealing reaches as far back as the caller's own history.
|
|
11877
|
+
const revealTo = sealFrom > 1 ? sealFrom : undefined;
|
|
11878
|
+
for (const id of [...revealedIds]) {
|
|
11879
|
+
if ((currentMap[id]?.historyFrom ?? 1) <= sealFrom) {
|
|
11880
|
+
revealedIds.splice(revealedIds.indexOf(id), 1);
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
const historyFromByStable = new Map();
|
|
11884
|
+
for (const [id, entry] of Object.entries(currentMap)) {
|
|
11885
|
+
if (entry.removedAt || !entry.historyFrom)
|
|
11886
|
+
continue;
|
|
11887
|
+
const from = revealedIds.includes(id) ? revealTo : entry.historyFrom;
|
|
11888
|
+
if (!from)
|
|
11889
|
+
continue;
|
|
11890
|
+
historyFromByStable.set(id.split("#")[0], from);
|
|
11891
|
+
if (entry.keyFingerprint)
|
|
11892
|
+
historyFromByStable.set(entry.keyFingerprint, from);
|
|
11598
11893
|
}
|
|
11599
11894
|
const remainingByStable = new Map();
|
|
11600
11895
|
const existingSpecs = Object.entries(currentMap)
|
|
@@ -11613,8 +11908,14 @@ class BaseMindooDB {
|
|
|
11613
11908
|
for (const target of existingResolved.targets)
|
|
11614
11909
|
remainingByStable.set(target.stableId, target);
|
|
11615
11910
|
}
|
|
11911
|
+
for (const [stableId, target] of remainingByStable) {
|
|
11912
|
+
const from = historyFromByStable.get(stableId) ??
|
|
11913
|
+
historyFromByStable.get(target.keyFingerprint);
|
|
11914
|
+
if (from)
|
|
11915
|
+
remainingByStable.set(stableId, { ...target, historyFrom: from });
|
|
11916
|
+
}
|
|
11616
11917
|
for (const target of addedTargets)
|
|
11617
|
-
remainingByStable.set(target.stableId, target);
|
|
11918
|
+
remainingByStable.set(target.stableId, addedHistoryFrom ? { ...target, historyFrom: addedHistoryFrom } : target);
|
|
11618
11919
|
for (const id of removedStableIds) {
|
|
11619
11920
|
remainingByStable.delete(id);
|
|
11620
11921
|
for (const [stableId, target] of [...remainingByStable.entries()]) {
|
|
@@ -11635,7 +11936,7 @@ class BaseMindooDB {
|
|
|
11635
11936
|
targets: remaining,
|
|
11636
11937
|
epoch,
|
|
11637
11938
|
});
|
|
11638
|
-
tenant.rememberSealedGenerations(internalDoc.decryptionKeyId,
|
|
11939
|
+
tenant.rememberSealedGenerations(internalDoc.decryptionKeyId, series.map((entry) => entry.key), recipientsBlock, series.map((entry) => entry.epoch));
|
|
11639
11940
|
const headsBeforeChange = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
11640
11941
|
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, { message: changeMessage }, (draft) => {
|
|
11641
11942
|
if (addedTargets.length > 0) {
|
|
@@ -11647,10 +11948,22 @@ class BaseMindooDB {
|
|
|
11647
11948
|
addedBy: signer,
|
|
11648
11949
|
keyFingerprint: target.keyFingerprint,
|
|
11649
11950
|
...(target.label ? { label: target.label } : {}),
|
|
11951
|
+
...(addedHistoryFrom ? { historyFrom: addedHistoryFrom } : {}),
|
|
11650
11952
|
};
|
|
11651
11953
|
}
|
|
11652
11954
|
(0, recipients_1.applyEncryptForAdds)(draft, addMap, epoch);
|
|
11653
11955
|
}
|
|
11956
|
+
if (revealedIds.length > 0) {
|
|
11957
|
+
const map = draft[sealedTypes_1.ENCRYPT_FOR_FIELD];
|
|
11958
|
+
for (const id of revealedIds) {
|
|
11959
|
+
if (!map[id])
|
|
11960
|
+
continue;
|
|
11961
|
+
if (revealTo)
|
|
11962
|
+
map[id].historyFrom = revealTo;
|
|
11963
|
+
else
|
|
11964
|
+
delete map[id].historyFrom;
|
|
11965
|
+
}
|
|
11966
|
+
}
|
|
11654
11967
|
if (removedStableIds.length > 0) {
|
|
11655
11968
|
(0, recipients_1.applyEncryptForRemoves)(draft, removedStableIds, now, signer);
|
|
11656
11969
|
}
|
|
@@ -11671,6 +11984,9 @@ class BaseMindooDB {
|
|
|
11671
11984
|
successMessage: "recipients updated",
|
|
11672
11985
|
bypassPrecheck: options?.bypassAccessControlPrecheck,
|
|
11673
11986
|
recipients: recipientsBlock,
|
|
11987
|
+
// Readers added without history cannot decrypt the older entries of this
|
|
11988
|
+
// document itself: a snapshot under the new key gives them its current state.
|
|
11989
|
+
forceSnapshot: withoutHistory,
|
|
11674
11990
|
});
|
|
11675
11991
|
if (options?.change) {
|
|
11676
11992
|
await this.changeDoc(this.wrapDocument(internalDoc), options.change, {
|
|
@@ -12136,21 +12452,36 @@ class BaseMindooDB {
|
|
|
12136
12452
|
* document were indexed since it started, so no caller gets a state older
|
|
12137
12453
|
* than its call.
|
|
12138
12454
|
*/
|
|
12139
|
-
loadDocumentInternal(docId) {
|
|
12455
|
+
loadDocumentInternal(docId, persist = true) {
|
|
12140
12456
|
const generation = this.docEntryGeneration.get(docId) ?? 0;
|
|
12141
12457
|
const inFlight = this.documentLoadsInFlight.get(docId);
|
|
12458
|
+
let promise;
|
|
12142
12459
|
if (inFlight && inFlight.generation === generation) {
|
|
12143
|
-
|
|
12460
|
+
promise = inFlight.promise;
|
|
12461
|
+
}
|
|
12462
|
+
else {
|
|
12463
|
+
const started = this.loadDocumentUncoalesced(docId, persist).finally(() => {
|
|
12464
|
+
if (this.documentLoadsInFlight.get(docId)?.promise === started) {
|
|
12465
|
+
this.documentLoadsInFlight.delete(docId);
|
|
12466
|
+
}
|
|
12467
|
+
});
|
|
12468
|
+
this.documentLoadsInFlight.set(docId, { generation, promise: started });
|
|
12469
|
+
promise = started;
|
|
12144
12470
|
}
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12471
|
+
if (!persist) {
|
|
12472
|
+
return promise;
|
|
12473
|
+
}
|
|
12474
|
+
// A regular read of a document a scan materialized (shared load or L1
|
|
12475
|
+
// hit) wants it in the persistent cache as usual.
|
|
12476
|
+
return promise.then((loaded) => {
|
|
12477
|
+
if (loaded?.scanOnly && this.docCache.get(docId) === loaded) {
|
|
12478
|
+
delete loaded.scanOnly;
|
|
12479
|
+
this.markDocDirty(docId);
|
|
12148
12480
|
}
|
|
12481
|
+
return loaded;
|
|
12149
12482
|
});
|
|
12150
|
-
this.documentLoadsInFlight.set(docId, { generation, promise });
|
|
12151
|
-
return promise;
|
|
12152
12483
|
}
|
|
12153
|
-
async loadDocumentUncoalesced(docId) {
|
|
12484
|
+
async loadDocumentUncoalesced(docId, persist = true) {
|
|
12154
12485
|
const startedAt = Date.now();
|
|
12155
12486
|
const cacheCheckStartedAt = Date.now();
|
|
12156
12487
|
// Short-circuit on the visibility layer: a doc marked
|
|
@@ -12559,7 +12890,17 @@ class BaseMindooDB {
|
|
|
12559
12890
|
// result is already outdated and the next read materializes again)
|
|
12560
12891
|
if (this.isEntryGenerationCurrent(docId, entryGeneration)) {
|
|
12561
12892
|
await this.storeCachedDocument(internalDoc);
|
|
12562
|
-
|
|
12893
|
+
// Scan loads (index builds) keep the document in L1 only: marking it
|
|
12894
|
+
// dirty would write every document of a full pass to the persistent
|
|
12895
|
+
// L2 cache on eviction. While the background warmer is running, the
|
|
12896
|
+
// L2 cache is being filled anyway: persist then, so the warmer does
|
|
12897
|
+
// not materialize the same document a second time.
|
|
12898
|
+
if (persist || this.warmerPromise !== null) {
|
|
12899
|
+
this.markDocDirty(docId);
|
|
12900
|
+
}
|
|
12901
|
+
else {
|
|
12902
|
+
internalDoc.scanOnly = true;
|
|
12903
|
+
}
|
|
12563
12904
|
this.logger.debug(`===== Successfully loaded document ${docId} and cached it =====`);
|
|
12564
12905
|
}
|
|
12565
12906
|
else {
|
|
@@ -12702,7 +13043,9 @@ class BaseMindooDB {
|
|
|
12702
13043
|
isAccessible: () => internalDoc.accessible ?? true,
|
|
12703
13044
|
isAwaitingWitness: () => internalDoc.awaitingWitness ?? false,
|
|
12704
13045
|
isWitnessed: () => internalDoc.witnessed ?? false,
|
|
12705
|
-
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
13046
|
+
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
13047
|
+
(0, sealedTypes_1.groupKeyDocIdOf)(internalDoc.decryptionKeyId, internalDoc.id) === undefined,
|
|
13048
|
+
getGroupKeyDocId: () => (0, sealedTypes_1.groupKeyDocIdOf)(internalDoc.decryptionKeyId, internalDoc.id),
|
|
12706
13049
|
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
12707
13050
|
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
12708
13051
|
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
@@ -13764,6 +14107,7 @@ class BaseMindooDB {
|
|
|
13764
14107
|
}
|
|
13765
14108
|
}
|
|
13766
14109
|
exports.BaseMindooDB = BaseMindooDB;
|
|
14110
|
+
BaseMindooDB.GROUP_KEY_MISS_TTL_MS = 2000;
|
|
13767
14111
|
/** The "access control not enforced" allow decision returned by the
|
|
13768
14112
|
* non-throwing `canCreate`/`canChange`/`canDelete` prediction helpers. */
|
|
13769
14113
|
BaseMindooDB.ACL_NOT_ENFORCED_DECISION = {
|