mindoodb 0.0.64 → 0.0.65
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/dist/browser/index.mjs +11 -0
- package/dist/core/BaseMindooDB.d.ts +19 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +607 -323
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/accesscontrol/tier1Evaluator.d.ts +59 -0
- package/dist/core/accesscontrol/tier1Evaluator.d.ts.map +1 -0
- package/dist/core/accesscontrol/tier1Evaluator.js +77 -0
- package/dist/core/accesscontrol/tier1Evaluator.js.map +1 -0
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts +58 -5
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js +77 -8
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts +32 -10
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.js +26 -10
- package/dist/core/appendonlystores/network/IrohStreamIO.js.map +1 -1
- package/dist/core/appendonlystores/network/irohLocator.d.ts +16 -0
- package/dist/core/appendonlystores/network/irohLocator.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/irohLocator.js +36 -0
- package/dist/core/appendonlystores/network/irohLocator.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +12 -6
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +7 -1
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -11
- package/dist/core/index.js.map +1 -1
- package/dist/core/peerdevices/PeerDeviceDocument.d.ts +144 -0
- package/dist/core/peerdevices/PeerDeviceDocument.d.ts.map +1 -0
- package/dist/core/peerdevices/PeerDeviceDocument.js +209 -0
- package/dist/core/peerdevices/PeerDeviceDocument.js.map +1 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/userkeys/fingerprint.d.ts +12 -4
- package/dist/core/userkeys/fingerprint.d.ts.map +1 -1
- package/dist/core/userkeys/fingerprint.js +14 -5
- package/dist/core/userkeys/fingerprint.js.map +1 -1
- package/dist/iroh/index.d.ts +3 -2
- package/dist/iroh/index.d.ts.map +1 -1
- package/dist/iroh/index.js +8 -1
- package/dist/iroh/index.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +5 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +35 -56
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/nativeIrohStreamIO.d.ts.map +1 -1
- package/dist/node/server/nativeIrohStreamIO.js +17 -4
- package/dist/node/server/nativeIrohStreamIO.js.map +1 -1
- package/package.json +1 -1
|
@@ -101,7 +101,9 @@ const PREFETCH_MAX_RATE_LIMIT_DELAY_MS = 10000;
|
|
|
101
101
|
*/
|
|
102
102
|
function networkErrorType(error) {
|
|
103
103
|
const candidate = error;
|
|
104
|
-
if (!candidate ||
|
|
104
|
+
if (!candidate ||
|
|
105
|
+
candidate.name !== "NetworkError" ||
|
|
106
|
+
typeof candidate.type !== "string") {
|
|
105
107
|
return null;
|
|
106
108
|
}
|
|
107
109
|
return candidate.type;
|
|
@@ -117,7 +119,9 @@ function rateLimitRetryDelayMs(error, attempt) {
|
|
|
117
119
|
return null;
|
|
118
120
|
}
|
|
119
121
|
const retryAfterMs = error.retryAfterMs;
|
|
120
|
-
const requested = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs)
|
|
122
|
+
const requested = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs)
|
|
123
|
+
? retryAfterMs
|
|
124
|
+
: 0;
|
|
121
125
|
return Math.min(Math.max(requested, PREFETCH_RATE_LIMIT_DELAYS_MS[attempt]), PREFETCH_MAX_RATE_LIMIT_DELAY_MS);
|
|
122
126
|
}
|
|
123
127
|
/**
|
|
@@ -176,10 +180,9 @@ const DOC_CACHE_HEADER_VERSION = 2;
|
|
|
176
180
|
*/
|
|
177
181
|
/** @internal Exported for tests only; do not import from app code. */
|
|
178
182
|
exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES = new Uint8Array([
|
|
179
|
-
133, 111, 74, 131, 181, 94, 251, 69, 1, 47, 0, 8, 0, 0, 0, 0,
|
|
180
|
-
0, 0, 0,
|
|
181
|
-
|
|
182
|
-
116, 115, 1, 127, 2, 127, 0, 127, 0,
|
|
183
|
+
133, 111, 74, 131, 181, 94, 251, 69, 1, 47, 0, 8, 0, 0, 0, 0, 0, 0, 0, 192, 1,
|
|
184
|
+
1, 0, 0, 0, 5, 21, 14, 52, 1, 66, 2, 86, 2, 112, 2, 127, 12, 95, 97, 116, 116,
|
|
185
|
+
97, 99, 104, 109, 101, 110, 116, 115, 1, 127, 2, 127, 0, 127, 0,
|
|
183
186
|
]);
|
|
184
187
|
/**
|
|
185
188
|
* Returns the hard-coded initial Automerge change bytes used to seed every
|
|
@@ -513,7 +516,8 @@ class BaseMindooDB {
|
|
|
513
516
|
this.tenant = tenant;
|
|
514
517
|
this.store = store;
|
|
515
518
|
this.attachmentStore = attachmentStore;
|
|
516
|
-
this.chunkSizeBytes =
|
|
519
|
+
this.chunkSizeBytes =
|
|
520
|
+
attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
|
|
517
521
|
this._isAdminOnlyDb = adminOnlyDb;
|
|
518
522
|
this.timeTravelDate = timeTravelDate;
|
|
519
523
|
this.maxCachedDocs = Math.max(1, Math.floor(documentCacheConfig?.maxEntries ?? DEFAULT_MAX_CACHED_DOCS));
|
|
@@ -529,7 +533,8 @@ class BaseMindooDB {
|
|
|
529
533
|
: 0;
|
|
530
534
|
this.cacheRestoreLimit = Math.max(1, Math.floor(documentCacheConfig?.restoreLimit ?? this.maxCachedDocs));
|
|
531
535
|
this.cacheRestoreBatchSize = Math.max(1, Math.floor(documentCacheConfig?.restoreBatchSize ?? DEFAULT_RESTORE_BATCH_SIZE));
|
|
532
|
-
this.reconcileRestoredIndexOnInit =
|
|
536
|
+
this.reconcileRestoredIndexOnInit =
|
|
537
|
+
documentCacheConfig?.reconcileRestoredIndexOnInit ?? false;
|
|
533
538
|
this.restoreToL2 = documentCacheConfig?.restoreToL2 ?? false;
|
|
534
539
|
this.warmerBatchSize = Math.max(1, Math.floor(documentCacheConfig?.warmer?.batchSize ?? DEFAULT_WARMER_BATCH_SIZE));
|
|
535
540
|
this.warmerScheduler =
|
|
@@ -538,8 +543,7 @@ class BaseMindooDB {
|
|
|
538
543
|
this.snapshotCooldownMs = Math.max(0, Math.floor(snapshotConfig?.cooldownMs ?? DEFAULT_SNAPSHOT_COOLDOWN_MS));
|
|
539
544
|
// Create logger if not provided (for backward compatibility)
|
|
540
545
|
this.logger =
|
|
541
|
-
logger ||
|
|
542
|
-
new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), "BaseMindooDB", true);
|
|
546
|
+
logger || new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), "BaseMindooDB", true);
|
|
543
547
|
this.performanceCallback = performanceCallback;
|
|
544
548
|
// Summary auto-follow: piggybacks on the coalesced change events (one
|
|
545
549
|
// per sync batch / event-loop turn). Costs a docId check per event for
|
|
@@ -617,7 +621,8 @@ class BaseMindooDB {
|
|
|
617
621
|
let signerUsernameHash = null;
|
|
618
622
|
let creatorUsernameHash = null;
|
|
619
623
|
if (dbId === types_1.USER_DIRECTORY_DB_ID) {
|
|
620
|
-
signerUsernameHash =
|
|
624
|
+
signerUsernameHash =
|
|
625
|
+
await this.resolveSignerUsernameHash(signerPublicKey);
|
|
621
626
|
this.signerUsernameHashByKey.set(signerPublicKey, signerUsernameHash);
|
|
622
627
|
if ((0, builtinDbInvariants_1.isPersonalUserdirectoryDocId)(docId)) {
|
|
623
628
|
creatorUsernameHash =
|
|
@@ -652,7 +657,8 @@ class BaseMindooDB {
|
|
|
652
657
|
let documentUsernameHash = null;
|
|
653
658
|
let signerUsernameHash = null;
|
|
654
659
|
let creatorUsernameHash = null;
|
|
655
|
-
if (dbId === types_1.USER_DIRECTORY_DB_ID &&
|
|
660
|
+
if (dbId === types_1.USER_DIRECTORY_DB_ID &&
|
|
661
|
+
(0, builtinDbInvariants_1.isPersonalUserdirectoryDocId)(entry.docId)) {
|
|
656
662
|
if (entryType !== "doc_create") {
|
|
657
663
|
creatorUsernameHash = await this.ensurePersonalDocCreatorHash(entry.docId);
|
|
658
664
|
signerUsernameHash = await this.ensureSignerUsernameHash(entry.createdByPublicKey);
|
|
@@ -675,6 +681,23 @@ class BaseMindooDB {
|
|
|
675
681
|
creatorUsernameHash,
|
|
676
682
|
});
|
|
677
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Would the builtin `directory` / `userdirectory` invariant forbid this
|
|
686
|
+
* entry? For callers that accept entries from somewhere other than a server
|
|
687
|
+
* — a peer sync — and therefore have to apply the admin/owner rules
|
|
688
|
+
* themselves.
|
|
689
|
+
*
|
|
690
|
+
* Deliberately the same check the load path uses, so an entry admitted here
|
|
691
|
+
* is one that will also materialize. The alternative, accepting on ingest
|
|
692
|
+
* and dropping on load, leaves the entry sitting in the store and pushes it
|
|
693
|
+
* on to the server at the next sync, where it is refused and fails the run.
|
|
694
|
+
*
|
|
695
|
+
* Returns false for databases without a builtin invariant, so callers can
|
|
696
|
+
* ask unconditionally.
|
|
697
|
+
*/
|
|
698
|
+
async violatesBuiltinWriteInvariant(entry) {
|
|
699
|
+
return this.shouldSkipLoadedEntryForBuiltin(entry);
|
|
700
|
+
}
|
|
678
701
|
async ensureSignerUsernameHash(signerKey) {
|
|
679
702
|
if (this.signerUsernameHashByKey.has(signerKey)) {
|
|
680
703
|
return this.signerUsernameHashByKey.get(signerKey) ?? null;
|
|
@@ -751,10 +774,12 @@ class BaseMindooDB {
|
|
|
751
774
|
}
|
|
752
775
|
}
|
|
753
776
|
metadataVisibleAtTimeTravelDate(metadata) {
|
|
754
|
-
return this.timeTravelDate == null || metadata.createdAt < this.timeTravelDate;
|
|
777
|
+
return (this.timeTravelDate == null || metadata.createdAt < this.timeTravelDate);
|
|
755
778
|
}
|
|
756
779
|
applyTimeTravelFilter(metadata) {
|
|
757
|
-
return this.timeTravelDate == null
|
|
780
|
+
return this.timeTravelDate == null
|
|
781
|
+
? metadata
|
|
782
|
+
: metadata.filter((entry) => this.metadataVisibleAtTimeTravelDate(entry));
|
|
758
783
|
}
|
|
759
784
|
mergeTimeTravelScanFilters(filters) {
|
|
760
785
|
if (this.timeTravelDate == null) {
|
|
@@ -781,7 +806,9 @@ class BaseMindooDB {
|
|
|
781
806
|
computeCachePrefix() {
|
|
782
807
|
const cacheIdentity = this.store.getCacheIdentity?.() ?? this.store.getId();
|
|
783
808
|
const base = `${this.tenant.getId()}/${cacheIdentity}`;
|
|
784
|
-
return this.timeTravelDate == null
|
|
809
|
+
return this.timeTravelDate == null
|
|
810
|
+
? base
|
|
811
|
+
: `${base}/tt/${this.timeTravelDate}`;
|
|
785
812
|
}
|
|
786
813
|
/**
|
|
787
814
|
* Attach a CacheManager so this DB participates in periodic cache flushing.
|
|
@@ -856,7 +883,8 @@ class BaseMindooDB {
|
|
|
856
883
|
return (0, types_4.sanitizeSummaryConfig)(doc.getData()[types_4.SUMMARY_SETUP_FIELD]) ?? null;
|
|
857
884
|
}
|
|
858
885
|
catch (error) {
|
|
859
|
-
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
886
|
+
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
887
|
+
error instanceof errors_1.DocumentDeletedError) {
|
|
860
888
|
return null;
|
|
861
889
|
}
|
|
862
890
|
throw error;
|
|
@@ -920,7 +948,9 @@ class BaseMindooDB {
|
|
|
920
948
|
// indexing (DirectoryTimeTravelIndex) and its change events interleave
|
|
921
949
|
// with access-control evaluation; admin-only databases hold no user
|
|
922
950
|
// data worth summarizing. Explicit getSummaryStore() calls still work.
|
|
923
|
-
if (this._isAdminOnlyDb ||
|
|
951
|
+
if (this._isAdminOnlyDb ||
|
|
952
|
+
this.store.getId() === types_1.DIRECTORY_DB_ID ||
|
|
953
|
+
this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
924
954
|
return;
|
|
925
955
|
}
|
|
926
956
|
const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
|
|
@@ -934,7 +964,9 @@ class BaseMindooDB {
|
|
|
934
964
|
async activateSummaryFromSetupDoc() {
|
|
935
965
|
try {
|
|
936
966
|
const config = await this.getSummarySetup();
|
|
937
|
-
if (config === null ||
|
|
967
|
+
if (config === null ||
|
|
968
|
+
this.summaryStore ||
|
|
969
|
+
!this.summaryAutoUpdateEnabled) {
|
|
938
970
|
return;
|
|
939
971
|
}
|
|
940
972
|
// No explicit config: the store adopts the setup-document config on
|
|
@@ -962,7 +994,8 @@ class BaseMindooDB {
|
|
|
962
994
|
do {
|
|
963
995
|
this.summaryAutoUpdatePending = false;
|
|
964
996
|
await this.summaryStore?.update();
|
|
965
|
-
} while (this.summaryAutoUpdatePending &&
|
|
997
|
+
} while (this.summaryAutoUpdatePending &&
|
|
998
|
+
this.summaryAutoUpdateEnabled);
|
|
966
999
|
}
|
|
967
1000
|
catch (error) {
|
|
968
1001
|
this.logger.warn(`Summary auto-update failed: ${error}`);
|
|
@@ -1011,10 +1044,11 @@ class BaseMindooDB {
|
|
|
1011
1044
|
async getFulltextSetup() {
|
|
1012
1045
|
try {
|
|
1013
1046
|
const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
|
|
1014
|
-
return (0, types_5.sanitizeFulltextConfig)(doc.getData()[types_5.FULLTEXT_SETUP_FIELD]) ?? null;
|
|
1047
|
+
return ((0, types_5.sanitizeFulltextConfig)(doc.getData()[types_5.FULLTEXT_SETUP_FIELD]) ?? null);
|
|
1015
1048
|
}
|
|
1016
1049
|
catch (error) {
|
|
1017
|
-
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
1050
|
+
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
1051
|
+
error instanceof errors_1.DocumentDeletedError) {
|
|
1018
1052
|
return null;
|
|
1019
1053
|
}
|
|
1020
1054
|
throw error;
|
|
@@ -1034,7 +1068,7 @@ class BaseMindooDB {
|
|
|
1034
1068
|
* resumable backfill — locally and, after sync, on every other replica.
|
|
1035
1069
|
*/
|
|
1036
1070
|
async setFulltextSetup(config) {
|
|
1037
|
-
const sanitized = config === null ? null : (0, types_5.sanitizeFulltextConfig)(config) ?? {};
|
|
1071
|
+
const sanitized = config === null ? null : ((0, types_5.sanitizeFulltextConfig)(config) ?? {});
|
|
1038
1072
|
const current = await this.getFulltextSetup();
|
|
1039
1073
|
if (JSON.stringify(current) === JSON.stringify(sanitized)) {
|
|
1040
1074
|
return;
|
|
@@ -1061,10 +1095,11 @@ class BaseMindooDB {
|
|
|
1061
1095
|
async getExtractionSetup() {
|
|
1062
1096
|
try {
|
|
1063
1097
|
const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
|
|
1064
|
-
return (0, types_6.sanitizeExtractionConfig)(doc.getData()[types_6.EXTRACTION_SETUP_FIELD]) ?? null;
|
|
1098
|
+
return ((0, types_6.sanitizeExtractionConfig)(doc.getData()[types_6.EXTRACTION_SETUP_FIELD]) ?? null);
|
|
1065
1099
|
}
|
|
1066
1100
|
catch (error) {
|
|
1067
|
-
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
1101
|
+
if (error instanceof errors_1.DocumentNotFoundError ||
|
|
1102
|
+
error instanceof errors_1.DocumentDeletedError) {
|
|
1068
1103
|
return null;
|
|
1069
1104
|
}
|
|
1070
1105
|
throw error;
|
|
@@ -1080,7 +1115,7 @@ class BaseMindooDB {
|
|
|
1080
1115
|
* without producing changefeed/sync churn.
|
|
1081
1116
|
*/
|
|
1082
1117
|
async setExtractionSetup(config) {
|
|
1083
|
-
const sanitized = config === null ? null : (0, types_6.sanitizeExtractionConfig)(config) ?? {};
|
|
1118
|
+
const sanitized = config === null ? null : ((0, types_6.sanitizeExtractionConfig)(config) ?? {});
|
|
1084
1119
|
const current = await this.getExtractionSetup();
|
|
1085
1120
|
if (JSON.stringify(current) === JSON.stringify(sanitized)) {
|
|
1086
1121
|
return;
|
|
@@ -1155,7 +1190,9 @@ class BaseMindooDB {
|
|
|
1155
1190
|
return;
|
|
1156
1191
|
}
|
|
1157
1192
|
// Same system-database exclusion as the summary auto-activation.
|
|
1158
|
-
if (this._isAdminOnlyDb ||
|
|
1193
|
+
if (this._isAdminOnlyDb ||
|
|
1194
|
+
this.store.getId() === types_1.DIRECTORY_DB_ID ||
|
|
1195
|
+
this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
1159
1196
|
return;
|
|
1160
1197
|
}
|
|
1161
1198
|
const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
|
|
@@ -1198,7 +1235,8 @@ class BaseMindooDB {
|
|
|
1198
1235
|
do {
|
|
1199
1236
|
this.fulltextAutoUpdatePending = false;
|
|
1200
1237
|
await this.fulltextIndex?.update();
|
|
1201
|
-
} while (this.fulltextAutoUpdatePending &&
|
|
1238
|
+
} while (this.fulltextAutoUpdatePending &&
|
|
1239
|
+
this.fulltextAutoUpdateEnabled);
|
|
1202
1240
|
}
|
|
1203
1241
|
catch (error) {
|
|
1204
1242
|
this.logger.warn(`Full-text auto-update failed: ${error}`);
|
|
@@ -1232,7 +1270,11 @@ class BaseMindooDB {
|
|
|
1232
1270
|
if (this.changeListeners.size === 0) {
|
|
1233
1271
|
return;
|
|
1234
1272
|
}
|
|
1235
|
-
this.pendingChangeNotifications.set(docId, {
|
|
1273
|
+
this.pendingChangeNotifications.set(docId, {
|
|
1274
|
+
docId,
|
|
1275
|
+
isDeleted,
|
|
1276
|
+
lastModified,
|
|
1277
|
+
});
|
|
1236
1278
|
this.scheduleChangeNotification();
|
|
1237
1279
|
}
|
|
1238
1280
|
scheduleChangeNotification() {
|
|
@@ -1475,9 +1517,7 @@ class BaseMindooDB {
|
|
|
1475
1517
|
*/
|
|
1476
1518
|
async flushDirtyDocToCache(store, prefix, docId, internal) {
|
|
1477
1519
|
const indexEntryIdx = this.getDocIndexPosition(docId);
|
|
1478
|
-
const changeSeq = indexEntryIdx === undefined
|
|
1479
|
-
? 0
|
|
1480
|
-
: this.index[indexEntryIdx].changeSeq;
|
|
1520
|
+
const changeSeq = indexEntryIdx === undefined ? 0 : this.index[indexEntryIdx].changeSeq;
|
|
1481
1521
|
const automergeHeads = automerge_adapter_1.Automerge.getHeads(internal.doc);
|
|
1482
1522
|
// Skip the Automerge.save + L2 write when the exact same state was
|
|
1483
1523
|
// already flushed (docs marked dirty by a load rather than a change).
|
|
@@ -1581,12 +1621,15 @@ class BaseMindooDB {
|
|
|
1581
1621
|
this.processedEntryIds = checkpoint.processedEntryIds;
|
|
1582
1622
|
}
|
|
1583
1623
|
this.lastReconciledKeyBagFingerprint =
|
|
1584
|
-
typeof checkpoint.keyBagFingerprint === "string"
|
|
1624
|
+
typeof checkpoint.keyBagFingerprint === "string"
|
|
1625
|
+
? checkpoint.keyBagFingerprint
|
|
1626
|
+
: null;
|
|
1585
1627
|
// Pending untrusted-author map. A checkpoint written before this field
|
|
1586
1628
|
// existed may hide entries that were silently skipped because their
|
|
1587
1629
|
// author was unknown at the time - flag it for the one-time heal scan.
|
|
1588
1630
|
this.pendingUntrustedAuthors.clear();
|
|
1589
|
-
if (checkpoint.pendingUntrustedAuthors &&
|
|
1631
|
+
if (checkpoint.pendingUntrustedAuthors &&
|
|
1632
|
+
typeof checkpoint.pendingUntrustedAuthors === "object") {
|
|
1590
1633
|
this.legacyCheckpointNeedsAuthorTrustScan = false;
|
|
1591
1634
|
for (const [publicKey, docIds] of Object.entries(checkpoint.pendingUntrustedAuthors)) {
|
|
1592
1635
|
if (!Array.isArray(docIds))
|
|
@@ -1617,7 +1660,8 @@ class BaseMindooDB {
|
|
|
1617
1660
|
this.legacyCheckpointNeedsVisibilityTally = true;
|
|
1618
1661
|
}
|
|
1619
1662
|
this.syncScanCursors.clear();
|
|
1620
|
-
if (checkpoint.syncScanCursors &&
|
|
1663
|
+
if (checkpoint.syncScanCursors &&
|
|
1664
|
+
typeof checkpoint.syncScanCursors === "object") {
|
|
1621
1665
|
for (const [key, value] of Object.entries(checkpoint.syncScanCursors)) {
|
|
1622
1666
|
if (typeof value?.sourceEpoch === "string" &&
|
|
1623
1667
|
typeof value?.targetEpoch === "string" &&
|
|
@@ -1626,7 +1670,10 @@ class BaseMindooDB {
|
|
|
1626
1670
|
this.syncScanCursors.set(key, {
|
|
1627
1671
|
sourceEpoch: value.sourceEpoch,
|
|
1628
1672
|
targetEpoch: value.targetEpoch,
|
|
1629
|
-
cursor: {
|
|
1673
|
+
cursor: {
|
|
1674
|
+
receiptOrder: value.cursor.receiptOrder,
|
|
1675
|
+
id: value.cursor.id,
|
|
1676
|
+
},
|
|
1630
1677
|
});
|
|
1631
1678
|
}
|
|
1632
1679
|
}
|
|
@@ -1655,7 +1702,8 @@ class BaseMindooDB {
|
|
|
1655
1702
|
this.indexLookup.set(this.index[i].docId, i);
|
|
1656
1703
|
}
|
|
1657
1704
|
if (this.index.length > 0 && this.nextChangeSeq <= this.index.length) {
|
|
1658
|
-
this.nextChangeSeq =
|
|
1705
|
+
this.nextChangeSeq =
|
|
1706
|
+
Math.max(...this.index.map((e) => e.changeSeq)) + 1;
|
|
1659
1707
|
}
|
|
1660
1708
|
// Restore automergeHashToEntryId
|
|
1661
1709
|
if (checkpoint.automergeHashToEntryId) {
|
|
@@ -1771,13 +1819,17 @@ class BaseMindooDB {
|
|
|
1771
1819
|
lastModified: header.lastModified,
|
|
1772
1820
|
decryptionKeyId: header.decryptionKeyId,
|
|
1773
1821
|
isDeleted: header.isDeleted,
|
|
1774
|
-
awaitingWitness: typeof header.awaitingWitness === "boolean"
|
|
1822
|
+
awaitingWitness: typeof header.awaitingWitness === "boolean"
|
|
1823
|
+
? header.awaitingWitness
|
|
1824
|
+
: false,
|
|
1775
1825
|
witnessed: typeof header.witnessed === "boolean" ? header.witnessed : false,
|
|
1776
1826
|
};
|
|
1777
1827
|
return {
|
|
1778
1828
|
internal,
|
|
1779
1829
|
persistedChangeSeq: typeof header.changeSeq === "number" ? header.changeSeq : 0,
|
|
1780
|
-
persistedHeads: Array.isArray(header.automergeHeads)
|
|
1830
|
+
persistedHeads: Array.isArray(header.automergeHeads)
|
|
1831
|
+
? header.automergeHeads
|
|
1832
|
+
: [],
|
|
1781
1833
|
};
|
|
1782
1834
|
}
|
|
1783
1835
|
/**
|
|
@@ -1791,10 +1843,12 @@ class BaseMindooDB {
|
|
|
1791
1843
|
async getOrImportPublicKey(publicKey, trustedTime) {
|
|
1792
1844
|
const directory = await this.tenant.openDirectory();
|
|
1793
1845
|
let isTrusted = false;
|
|
1794
|
-
if (trustedTime !== undefined &&
|
|
1846
|
+
if (trustedTime !== undefined &&
|
|
1847
|
+
typeof directory.getDirectoryStateAt === "function") {
|
|
1795
1848
|
const node = await directory.getDirectoryStateAt(trustedTime);
|
|
1796
1849
|
isTrusted =
|
|
1797
|
-
!!(0, DirectoryStateNode_1.grantForSigningKey)(node, publicKey) ||
|
|
1850
|
+
!!(0, DirectoryStateNode_1.grantForSigningKey)(node, publicKey) ||
|
|
1851
|
+
publicKey === this.getAdminPublicKey();
|
|
1798
1852
|
}
|
|
1799
1853
|
else {
|
|
1800
1854
|
isTrusted = await directory.validatePublicSigningKey(publicKey);
|
|
@@ -1868,7 +1922,11 @@ class BaseMindooDB {
|
|
|
1868
1922
|
lastChunkId: a.lastChunkId,
|
|
1869
1923
|
size: a.size,
|
|
1870
1924
|
}))
|
|
1871
|
-
.sort((x, y) => x.attachmentId < y.attachmentId
|
|
1925
|
+
.sort((x, y) => x.attachmentId < y.attachmentId
|
|
1926
|
+
? -1
|
|
1927
|
+
: x.attachmentId > y.attachmentId
|
|
1928
|
+
? 1
|
|
1929
|
+
: 0);
|
|
1872
1930
|
}
|
|
1873
1931
|
/**
|
|
1874
1932
|
* Verify a store entry's author signature using a pre-imported CryptoKey,
|
|
@@ -2002,13 +2060,13 @@ class BaseMindooDB {
|
|
|
2002
2060
|
// bypass this via forceChangeSeqBump: two changes within the same
|
|
2003
2061
|
// millisecond would otherwise be indistinguishable here and the second
|
|
2004
2062
|
// one would silently vanish from the changefeed.
|
|
2005
|
-
if (!forceChangeSeqBump
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2063
|
+
if (!forceChangeSeqBump &&
|
|
2064
|
+
existingEntry.lastModified === lastModified &&
|
|
2065
|
+
existingEntry.isDeleted === isDeleted &&
|
|
2066
|
+
existingEntry.decryptionKeyId === decryptionKeyId &&
|
|
2067
|
+
existingEntry.accessState === accessState &&
|
|
2068
|
+
existingEntry.awaitingWitness === awaitingWitness &&
|
|
2069
|
+
existingEntry.witnessed === witnessed) {
|
|
2012
2070
|
return; // No change needed
|
|
2013
2071
|
}
|
|
2014
2072
|
// Remove from current position; positions after it are repaired lazily
|
|
@@ -2046,9 +2104,10 @@ class BaseMindooDB {
|
|
|
2046
2104
|
? Math.min(existingIndex, insertIndex)
|
|
2047
2105
|
: insertIndex;
|
|
2048
2106
|
if (shiftedFrom < this.index.length - 1) {
|
|
2049
|
-
this.indexLookupStaleFrom =
|
|
2050
|
-
|
|
2051
|
-
|
|
2107
|
+
this.indexLookupStaleFrom =
|
|
2108
|
+
this.indexLookupStaleFrom === null
|
|
2109
|
+
? shiftedFrom
|
|
2110
|
+
: Math.min(this.indexLookupStaleFrom, shiftedFrom);
|
|
2052
2111
|
}
|
|
2053
2112
|
// Reactive support: record the transition for coalesced change events.
|
|
2054
2113
|
// Reached only when the index actually changed (identical replays
|
|
@@ -2246,7 +2305,8 @@ class BaseMindooDB {
|
|
|
2246
2305
|
// not expose a fingerprint so we never silently miss a key
|
|
2247
2306
|
// transition.
|
|
2248
2307
|
const currentFingerprint = await this.computeCurrentKeyBagFingerprint();
|
|
2249
|
-
const fingerprintMatches = currentFingerprint !== null &&
|
|
2308
|
+
const fingerprintMatches = currentFingerprint !== null &&
|
|
2309
|
+
currentFingerprint === this.lastReconciledKeyBagFingerprint;
|
|
2250
2310
|
if (!fingerprintMatches || this.legacyCheckpointNeedsVisibilityTally) {
|
|
2251
2311
|
this.legacyCheckpointNeedsVisibilityTally = false;
|
|
2252
2312
|
await this.reconcileKeyVisibility();
|
|
@@ -2297,10 +2357,14 @@ class BaseMindooDB {
|
|
|
2297
2357
|
if (!this.isTimeTravelMode()) {
|
|
2298
2358
|
return;
|
|
2299
2359
|
}
|
|
2300
|
-
if (!this.summaryAutoUpdateEnabled ||
|
|
2360
|
+
if (!this.summaryAutoUpdateEnabled ||
|
|
2361
|
+
this.summarySetupProbed ||
|
|
2362
|
+
this.summaryStore) {
|
|
2301
2363
|
return;
|
|
2302
2364
|
}
|
|
2303
|
-
if (this._isAdminOnlyDb ||
|
|
2365
|
+
if (this._isAdminOnlyDb ||
|
|
2366
|
+
this.store.getId() === types_1.DIRECTORY_DB_ID ||
|
|
2367
|
+
this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2304
2368
|
return;
|
|
2305
2369
|
}
|
|
2306
2370
|
this.summarySetupProbed = true;
|
|
@@ -2331,10 +2395,14 @@ class BaseMindooDB {
|
|
|
2331
2395
|
* {@link handleChangesForFulltextAutoUpdate}).
|
|
2332
2396
|
*/
|
|
2333
2397
|
probeFulltextSetupAtOpen() {
|
|
2334
|
-
if (!this.fulltextAutoUpdateEnabled ||
|
|
2398
|
+
if (!this.fulltextAutoUpdateEnabled ||
|
|
2399
|
+
this.fulltextSetupProbed ||
|
|
2400
|
+
this.fulltextIndex) {
|
|
2335
2401
|
return;
|
|
2336
2402
|
}
|
|
2337
|
-
if (this._isAdminOnlyDb ||
|
|
2403
|
+
if (this._isAdminOnlyDb ||
|
|
2404
|
+
this.store.getId() === types_1.DIRECTORY_DB_ID ||
|
|
2405
|
+
this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2338
2406
|
return;
|
|
2339
2407
|
}
|
|
2340
2408
|
if (this.isTimeTravelMode()) {
|
|
@@ -2452,15 +2520,17 @@ class BaseMindooDB {
|
|
|
2452
2520
|
* meaning there is nothing to reconcile.
|
|
2453
2521
|
*/
|
|
2454
2522
|
deriveDocumentVisibilityMetadata(metadata) {
|
|
2455
|
-
const lifecycleEntries = metadata.filter((entry) => entry.entryType === "doc_create"
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2523
|
+
const lifecycleEntries = metadata.filter((entry) => entry.entryType === "doc_create" ||
|
|
2524
|
+
entry.entryType === "doc_change" ||
|
|
2525
|
+
entry.entryType === "doc_delete" ||
|
|
2526
|
+
entry.entryType === "doc_undelete" ||
|
|
2527
|
+
entry.entryType === "doc_snapshot");
|
|
2460
2528
|
if (lifecycleEntries.length === 0) {
|
|
2461
2529
|
return null;
|
|
2462
2530
|
}
|
|
2463
|
-
lifecycleEntries.sort((left, right) => left.createdAt !== right.createdAt
|
|
2531
|
+
lifecycleEntries.sort((left, right) => left.createdAt !== right.createdAt
|
|
2532
|
+
? left.createdAt - right.createdAt
|
|
2533
|
+
: left.id.localeCompare(right.id));
|
|
2464
2534
|
const replayEntries = metadata.filter((entry) => this.isDocumentReplayEntry(entry));
|
|
2465
2535
|
const entriesForLastModified = replayEntries.length > 0 ? replayEntries : lifecycleEntries;
|
|
2466
2536
|
return {
|
|
@@ -2544,11 +2614,11 @@ class BaseMindooDB {
|
|
|
2544
2614
|
const lifecycleMetadata = await this.scanAllMetadata(this.store);
|
|
2545
2615
|
const metadataByDoc = new Map();
|
|
2546
2616
|
for (const entry of lifecycleMetadata) {
|
|
2547
|
-
if (entry.entryType !== "doc_create"
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2617
|
+
if (entry.entryType !== "doc_create" &&
|
|
2618
|
+
entry.entryType !== "doc_change" &&
|
|
2619
|
+
entry.entryType !== "doc_delete" &&
|
|
2620
|
+
entry.entryType !== "doc_undelete" &&
|
|
2621
|
+
entry.entryType !== "doc_snapshot") {
|
|
2552
2622
|
continue;
|
|
2553
2623
|
}
|
|
2554
2624
|
const entries = metadataByDoc.get(entry.docId) ?? [];
|
|
@@ -2687,11 +2757,11 @@ class BaseMindooDB {
|
|
|
2687
2757
|
// next load rebuilds from the store with the now-trusted entries.
|
|
2688
2758
|
await this.purgeMaterializedDocument(docId);
|
|
2689
2759
|
const metadata = await this.scanAllMetadata(this.store, { docId });
|
|
2690
|
-
const lifecycleEntries = metadata.filter((entry) => entry.entryType === "doc_create"
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2760
|
+
const lifecycleEntries = metadata.filter((entry) => entry.entryType === "doc_create" ||
|
|
2761
|
+
entry.entryType === "doc_change" ||
|
|
2762
|
+
entry.entryType === "doc_delete" ||
|
|
2763
|
+
entry.entryType === "doc_undelete" ||
|
|
2764
|
+
entry.entryType === "doc_snapshot");
|
|
2695
2765
|
const visibility = this.deriveDocumentVisibilityMetadata(metadata);
|
|
2696
2766
|
if (!visibility) {
|
|
2697
2767
|
continue;
|
|
@@ -2722,7 +2792,10 @@ class BaseMindooDB {
|
|
|
2722
2792
|
* directory was synced in a session that never opened this DB).
|
|
2723
2793
|
*/
|
|
2724
2794
|
async reconcileAuthorTrustAtOpen() {
|
|
2725
|
-
if (this.isTimeTravelMode() ||
|
|
2795
|
+
if (this.isTimeTravelMode() ||
|
|
2796
|
+
this._isAdminOnlyDb ||
|
|
2797
|
+
this.store.getId() === types_1.DIRECTORY_DB_ID ||
|
|
2798
|
+
this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
|
|
2726
2799
|
return;
|
|
2727
2800
|
}
|
|
2728
2801
|
if (this.legacyCheckpointNeedsAuthorTrustScan) {
|
|
@@ -2775,7 +2848,7 @@ class BaseMindooDB {
|
|
|
2775
2848
|
const headEntryIds = this.findActiveReplayHeadEntryIds(replayEntries);
|
|
2776
2849
|
const hasUnappliedHead = headEntryIds.some((entryId) => {
|
|
2777
2850
|
const parsed = (0, idGeneration_1.parseDocEntryId)(entryId);
|
|
2778
|
-
return parsed !== null && !
|
|
2851
|
+
return parsed !== null && !applied?.has(parsed.automergeHash);
|
|
2779
2852
|
});
|
|
2780
2853
|
if (!hasUnappliedHead) {
|
|
2781
2854
|
continue;
|
|
@@ -2796,11 +2869,11 @@ class BaseMindooDB {
|
|
|
2796
2869
|
async reconcileRestoredIndexWithStore() {
|
|
2797
2870
|
const lifecycleMetadata = await this.scanAllMetadata(this.store);
|
|
2798
2871
|
const lifecycleDocIds = Array.from(new Set(lifecycleMetadata
|
|
2799
|
-
.filter((entry) => entry.entryType === "doc_create"
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2872
|
+
.filter((entry) => entry.entryType === "doc_create" ||
|
|
2873
|
+
entry.entryType === "doc_change" ||
|
|
2874
|
+
entry.entryType === "doc_delete" ||
|
|
2875
|
+
entry.entryType === "doc_undelete" ||
|
|
2876
|
+
entry.entryType === "doc_snapshot")
|
|
2804
2877
|
.map((entry) => entry.docId))).sort((left, right) => left.localeCompare(right));
|
|
2805
2878
|
const missingDocIds = lifecycleDocIds.filter((docId) => !this.indexLookup.has(docId));
|
|
2806
2879
|
if (missingDocIds.length === 0) {
|
|
@@ -2974,7 +3047,9 @@ class BaseMindooDB {
|
|
|
2974
3047
|
// the index, so record it for `getInaccessibleDocumentCount`.
|
|
2975
3048
|
this.noteInaccessibleDocument(docId);
|
|
2976
3049
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
2977
|
-
const existing = existingIndex === undefined
|
|
3050
|
+
const existing = existingIndex === undefined
|
|
3051
|
+
? undefined
|
|
3052
|
+
: this.index[existingIndex];
|
|
2978
3053
|
if (existing?.accessState === "visible") {
|
|
2979
3054
|
await this.purgeMaterializedDocument(docId);
|
|
2980
3055
|
this.updateIndex(docId, existing.lastModified, true, representativeEntry.decryptionKeyId, "inaccessible");
|
|
@@ -2992,7 +3067,9 @@ class BaseMindooDB {
|
|
|
2992
3067
|
const mutationEntries = allDocMetadata.filter((e) => this.isDocumentReplayEntry(e));
|
|
2993
3068
|
// Snapshots compact replay history but should not change the user-visible
|
|
2994
3069
|
// modification time when the original create/change/delete/undelete entries still exist.
|
|
2995
|
-
const entriesForLastModified = mutationEntries.length > 0
|
|
3070
|
+
const entriesForLastModified = mutationEntries.length > 0
|
|
3071
|
+
? mutationEntries
|
|
3072
|
+
: docLifecycleEntries;
|
|
2996
3073
|
const lastModified = Math.max(...entriesForLastModified.map((e) => e.createdAt));
|
|
2997
3074
|
const isDeleted = this.computeIsDeletedFromMetadata(allDocMetadata);
|
|
2998
3075
|
const { awaitingWitness, witnessed } = (0, storeEntryTime_1.metadataWitnessState)(allDocMetadata);
|
|
@@ -3036,11 +3113,11 @@ class BaseMindooDB {
|
|
|
3036
3113
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
3037
3114
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
3038
3115
|
if (existing?.accessState === "visible") {
|
|
3039
|
-
const lifecycleEntriesInBatch = entryMetadataList.filter((entry) => entry.entryType === "doc_create"
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3116
|
+
const lifecycleEntriesInBatch = entryMetadataList.filter((entry) => entry.entryType === "doc_create" ||
|
|
3117
|
+
entry.entryType === "doc_change" ||
|
|
3118
|
+
entry.entryType === "doc_delete" ||
|
|
3119
|
+
entry.entryType === "doc_undelete" ||
|
|
3120
|
+
entry.entryType === "doc_snapshot");
|
|
3044
3121
|
// Prefer a lifecycle entry from the failing batch for the
|
|
3045
3122
|
// key id (they all share the same `decryptionKeyId` for a
|
|
3046
3123
|
// doc), then fall back to any entry, then to the existing
|
|
@@ -3058,7 +3135,7 @@ class BaseMindooDB {
|
|
|
3058
3135
|
this.cacheManager?.markDirty();
|
|
3059
3136
|
}
|
|
3060
3137
|
if (!this.supportsCursorScan(this.store)) {
|
|
3061
|
-
this.processedEntryIds.push(...entryMetadataList.map(em => em.id));
|
|
3138
|
+
this.processedEntryIds.push(...entryMetadataList.map((em) => em.id));
|
|
3062
3139
|
}
|
|
3063
3140
|
return;
|
|
3064
3141
|
}
|
|
@@ -3078,8 +3155,7 @@ class BaseMindooDB {
|
|
|
3078
3155
|
});
|
|
3079
3156
|
}
|
|
3080
3157
|
};
|
|
3081
|
-
const documentEntries = Array.from(entriesByDoc.entries())
|
|
3082
|
-
.sort((a, b) => {
|
|
3158
|
+
const documentEntries = Array.from(entriesByDoc.entries()).sort((a, b) => {
|
|
3083
3159
|
// Prefer the store's receiptOrder (unique, reflects the actual write
|
|
3084
3160
|
// order) so a cold rebuild assigns changeSeqs in the same order the
|
|
3085
3161
|
// live session did — `createdAt` has millisecond resolution and ties
|
|
@@ -3106,7 +3182,7 @@ class BaseMindooDB {
|
|
|
3106
3182
|
// cursor-capable stores track progress via processedEntryCursor, so we
|
|
3107
3183
|
// avoid growing an unbounded in-memory id list.
|
|
3108
3184
|
if (!this.supportsCursorScan(this.store)) {
|
|
3109
|
-
this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
|
|
3185
|
+
this.processedEntryIds.push(...newEntryMetadata.map((em) => em.id));
|
|
3110
3186
|
}
|
|
3111
3187
|
this.processedEntryCursor = nextCursor;
|
|
3112
3188
|
this.cacheMetaDirty = true;
|
|
@@ -3498,10 +3574,18 @@ class BaseMindooDB {
|
|
|
3498
3574
|
this.logger.info(`Dense sync: ${missingIds.length} entries to transfer ` +
|
|
3499
3575
|
`(${allNeededArray.length - missingIds.length} already present)`);
|
|
3500
3576
|
if (missingIds.length === 0) {
|
|
3501
|
-
return {
|
|
3577
|
+
return {
|
|
3578
|
+
transferred: 0,
|
|
3579
|
+
scanned: allNeededArray.length,
|
|
3580
|
+
cancelled: false,
|
|
3581
|
+
};
|
|
3502
3582
|
}
|
|
3503
3583
|
if (options?.signal?.aborted) {
|
|
3504
|
-
return {
|
|
3584
|
+
return {
|
|
3585
|
+
transferred: 0,
|
|
3586
|
+
scanned: allNeededArray.length,
|
|
3587
|
+
cancelled: true,
|
|
3588
|
+
};
|
|
3505
3589
|
}
|
|
3506
3590
|
// ── Phase 5: transfer missing entries in pages ───────────────────
|
|
3507
3591
|
const pageSize = options?.pageSize ?? 500;
|
|
@@ -3510,7 +3594,13 @@ class BaseMindooDB {
|
|
|
3510
3594
|
const rejected = [];
|
|
3511
3595
|
for (let offset = 0; offset < missingIds.length; offset += pageSize) {
|
|
3512
3596
|
if (options?.signal?.aborted) {
|
|
3513
|
-
return {
|
|
3597
|
+
return {
|
|
3598
|
+
transferred,
|
|
3599
|
+
transferredBytes,
|
|
3600
|
+
scanned: allNeededArray.length,
|
|
3601
|
+
cancelled: true,
|
|
3602
|
+
rejected,
|
|
3603
|
+
};
|
|
3514
3604
|
}
|
|
3515
3605
|
const batch = missingIds.slice(offset, offset + pageSize);
|
|
3516
3606
|
const entries = await sourceStore.getEntries(batch);
|
|
@@ -3526,7 +3616,9 @@ class BaseMindooDB {
|
|
|
3526
3616
|
if (receipts.length > 0 && sourceStore.applyWitnessReceipts) {
|
|
3527
3617
|
await sourceStore.applyWitnessReceipts(receipts);
|
|
3528
3618
|
}
|
|
3529
|
-
const rejectedIds = batchRejected.length > 0
|
|
3619
|
+
const rejectedIds = batchRejected.length > 0
|
|
3620
|
+
? new Set(batchRejected.map((entry) => entry.id))
|
|
3621
|
+
: null;
|
|
3530
3622
|
if (rejectedIds) {
|
|
3531
3623
|
rejected.push(...batchRejected);
|
|
3532
3624
|
this.logger.warn(`Dense sync: target rejected ${batchRejected.length} of ${entries.length} entries in batch at offset ${offset}`);
|
|
@@ -3549,7 +3641,13 @@ class BaseMindooDB {
|
|
|
3549
3641
|
}
|
|
3550
3642
|
this.logger.info(`Dense sync complete: transferred ${transferred} entries` +
|
|
3551
3643
|
(rejected.length > 0 ? `, ${rejected.length} rejected by target` : ""));
|
|
3552
|
-
return {
|
|
3644
|
+
return {
|
|
3645
|
+
transferred,
|
|
3646
|
+
transferredBytes,
|
|
3647
|
+
scanned: allNeededArray.length,
|
|
3648
|
+
cancelled: false,
|
|
3649
|
+
rejected,
|
|
3650
|
+
};
|
|
3553
3651
|
}
|
|
3554
3652
|
getStore() {
|
|
3555
3653
|
return this.store;
|
|
@@ -3578,20 +3676,23 @@ class BaseMindooDB {
|
|
|
3578
3676
|
continue;
|
|
3579
3677
|
}
|
|
3580
3678
|
const liveDoc = await this.getDocument(ledger.docId).catch(() => null);
|
|
3581
|
-
if (liveDoc
|
|
3679
|
+
if (liveDoc
|
|
3680
|
+
?.getAttachments()
|
|
3681
|
+
.some((attachment) => attachment.attachmentId === attachmentId)) {
|
|
3582
3682
|
await this.clearPendingAttachmentUploadLedger(attachmentId);
|
|
3583
3683
|
result.keptCommittedUploads++;
|
|
3584
3684
|
continue;
|
|
3585
3685
|
}
|
|
3586
|
-
const docEntries = await this.scanAllMetadata(this.store, {
|
|
3686
|
+
const docEntries = await this.scanAllMetadata(this.store, {
|
|
3687
|
+
docId: ledger.docId,
|
|
3688
|
+
});
|
|
3587
3689
|
const mentionedByHistory = docEntries.some((entry) => entry.attachmentIds?.includes(attachmentId));
|
|
3588
3690
|
if (mentionedByHistory) {
|
|
3589
3691
|
await this.clearPendingAttachmentUploadLedger(attachmentId);
|
|
3590
3692
|
result.keptCommittedUploads++;
|
|
3591
3693
|
continue;
|
|
3592
3694
|
}
|
|
3593
|
-
const deletedChunks = await this.getEffectiveAttachmentStore()
|
|
3594
|
-
.deleteEntriesForAttachment?.(ledger.docId, attachmentId) ?? 0;
|
|
3695
|
+
const deletedChunks = (await this.getEffectiveAttachmentStore().deleteEntriesForAttachment?.(ledger.docId, attachmentId)) ?? 0;
|
|
3595
3696
|
await this.clearPendingAttachmentUploadLedger(attachmentId);
|
|
3596
3697
|
result.reclaimedUploads++;
|
|
3597
3698
|
result.reclaimedChunks += deletedChunks;
|
|
@@ -3617,7 +3718,11 @@ class BaseMindooDB {
|
|
|
3617
3718
|
* @deprecated Use `createDocument({ signingKeyPair, signingKeyPassword, decryptionKeyId })` instead.
|
|
3618
3719
|
*/
|
|
3619
3720
|
async createDocumentWithSigningKey(signingKeyPair, signingKeyPassword, decryptionKeyId) {
|
|
3620
|
-
return this.createDocumentInternal({
|
|
3721
|
+
return this.createDocumentInternal({
|
|
3722
|
+
signingKeyPair,
|
|
3723
|
+
signingKeyPassword,
|
|
3724
|
+
decryptionKeyId,
|
|
3725
|
+
});
|
|
3621
3726
|
}
|
|
3622
3727
|
/**
|
|
3623
3728
|
* Bulk-create documents in a single append-only store write.
|
|
@@ -3655,7 +3760,8 @@ class BaseMindooDB {
|
|
|
3655
3760
|
// in the batch must reject the whole call without side effects.
|
|
3656
3761
|
for (const input of inputs) {
|
|
3657
3762
|
const options = input ?? {};
|
|
3658
|
-
if ((options.signingKeyPair !== undefined) !==
|
|
3763
|
+
if ((options.signingKeyPair !== undefined) !==
|
|
3764
|
+
(options.signingKeyPassword !== undefined)) {
|
|
3659
3765
|
throw new Error("createDocuments: signingKeyPair and signingKeyPassword must be provided together");
|
|
3660
3766
|
}
|
|
3661
3767
|
if (options.id !== undefined && !types_1.CUSTOM_DOC_ID_REGEX.test(options.id)) {
|
|
@@ -3839,7 +3945,9 @@ class BaseMindooDB {
|
|
|
3839
3945
|
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
3840
3946
|
// The id is settled before the write gate because it decides which rule
|
|
3841
3947
|
// applies in `userdirectory`: a personal document is recognized by its id.
|
|
3842
|
-
const docId = useCustomDocId
|
|
3948
|
+
const docId = useCustomDocId
|
|
3949
|
+
? options.id
|
|
3950
|
+
: this.generateUnusedDocId(options.idPrefix);
|
|
3843
3951
|
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash, docId);
|
|
3844
3952
|
// Idempotent create: when a caller-provided id already exists locally,
|
|
3845
3953
|
// return the existing document instead of producing a duplicate doc_create.
|
|
@@ -3860,7 +3968,7 @@ class BaseMindooDB {
|
|
|
3860
3968
|
}
|
|
3861
3969
|
}
|
|
3862
3970
|
const keyId = await this.resolveCreateKeyId(options, docId);
|
|
3863
|
-
this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ?
|
|
3971
|
+
this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? " using custom signing key" : ""}${useCustomDocId ? " with caller-provided id" : ""}`);
|
|
3864
3972
|
// Build the initial Automerge document and its first change bytes.
|
|
3865
3973
|
//
|
|
3866
3974
|
// For UUID7 documents (and unique-by-assertion custom ids) we use the
|
|
@@ -3982,7 +4090,10 @@ class BaseMindooDB {
|
|
|
3982
4090
|
// a later witness may stamp receivedAt, which resolveCreatorInfo picks up
|
|
3983
4091
|
// on a cache miss. For the local writer, createdAt is sufficient: the
|
|
3984
4092
|
// creating device is still granted.
|
|
3985
|
-
this.creatorInfoCache.set(docId, {
|
|
4093
|
+
this.creatorInfoCache.set(docId, {
|
|
4094
|
+
signingKey: createdByPublicKey,
|
|
4095
|
+
trustedTime: now,
|
|
4096
|
+
});
|
|
3986
4097
|
// Snapshot of attachments referenced by the freshly created doc (normally
|
|
3987
4098
|
// empty; the public create API does not add attachments inline).
|
|
3988
4099
|
const createAttachmentRefs = this.collectAttachmentRefs(newDoc);
|
|
@@ -4075,7 +4186,9 @@ class BaseMindooDB {
|
|
|
4075
4186
|
: [];
|
|
4076
4187
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4077
4188
|
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
4078
|
-
const docId = useCustomDocId
|
|
4189
|
+
const docId = useCustomDocId
|
|
4190
|
+
? options.id
|
|
4191
|
+
: this.generateUnusedDocId(options.idPrefix);
|
|
4079
4192
|
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash, docId);
|
|
4080
4193
|
const now = (0, timeSource_1.semanticNow)();
|
|
4081
4194
|
const keyId = await this.resolveCreateKeyId(options, docId);
|
|
@@ -4167,7 +4280,10 @@ class BaseMindooDB {
|
|
|
4167
4280
|
// a later witness may stamp receivedAt, which resolveCreatorInfo picks up
|
|
4168
4281
|
// on a cache miss. For the local writer, createdAt is sufficient: the
|
|
4169
4282
|
// creating device is still granted.
|
|
4170
|
-
this.creatorInfoCache.set(docId, {
|
|
4283
|
+
this.creatorInfoCache.set(docId, {
|
|
4284
|
+
signingKey: createdByPublicKey,
|
|
4285
|
+
trustedTime: now,
|
|
4286
|
+
});
|
|
4171
4287
|
const createAttachmentRefs = this.collectAttachmentRefs(finalDoc);
|
|
4172
4288
|
const entryMetadata = {
|
|
4173
4289
|
entryType: "doc_create",
|
|
@@ -4187,10 +4303,12 @@ class BaseMindooDB {
|
|
|
4187
4303
|
};
|
|
4188
4304
|
// Bind the metadata with the author signature (audit finding #5).
|
|
4189
4305
|
entryMetadata.metadataSignature = await this.computeEntryMetadataSignature(entryMetadata, useCustomSigningKey ? { signingKeyPair, signingKeyPassword } : undefined);
|
|
4190
|
-
const entries = [
|
|
4306
|
+
const entries = [
|
|
4307
|
+
{
|
|
4191
4308
|
...entryMetadata,
|
|
4192
4309
|
encryptedData: encryptedPayload,
|
|
4193
|
-
}
|
|
4310
|
+
},
|
|
4311
|
+
];
|
|
4194
4312
|
// Register the mapping BEFORE building the follow-up change entry so its
|
|
4195
4313
|
// dependency resolution finds the doc_create parent.
|
|
4196
4314
|
this.registerAutomergeHashMapping(docId, createHash, entryId);
|
|
@@ -4263,12 +4381,12 @@ class BaseMindooDB {
|
|
|
4263
4381
|
// consistent: the state at `timestamp` is the deterministic merge of all
|
|
4264
4382
|
// complete causal chains up to that instant.
|
|
4265
4383
|
const relevantEntries = (0, DocumentDagAnalysis_1.pruneToGroundedEntries)(allEntryMetadata
|
|
4266
|
-
.filter((em) => em.createdAt <= timestamp
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4384
|
+
.filter((em) => em.createdAt <= timestamp &&
|
|
4385
|
+
(em.entryType === "doc_create" ||
|
|
4386
|
+
em.entryType === "doc_change" ||
|
|
4387
|
+
em.entryType === "doc_delete" ||
|
|
4388
|
+
em.entryType === "doc_undelete" ||
|
|
4389
|
+
em.entryType === "doc_snapshot"))
|
|
4272
4390
|
.sort((a, b) => a.createdAt - b.createdAt));
|
|
4273
4391
|
if (relevantEntries.length === 0) {
|
|
4274
4392
|
this.performanceCallback?.onHistoryOperation?.({
|
|
@@ -4284,7 +4402,7 @@ class BaseMindooDB {
|
|
|
4284
4402
|
const materializationPlan = (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, relevantEntries);
|
|
4285
4403
|
let startFromSnapshot = materializationPlan.snapshotEntryId !== null;
|
|
4286
4404
|
const snapshotMeta = materializationPlan.snapshotEntryId
|
|
4287
|
-
?
|
|
4405
|
+
? metadataById.get(materializationPlan.snapshotEntryId) || null
|
|
4288
4406
|
: null;
|
|
4289
4407
|
if (startFromSnapshot && !snapshotMeta) {
|
|
4290
4408
|
this.logger.warn(`Planner referenced snapshot ${materializationPlan.snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
|
|
@@ -4363,9 +4481,9 @@ class BaseMindooDB {
|
|
|
4363
4481
|
const firstReplayEntry = replayEntries.length > 0 ? replayEntries[0] : null;
|
|
4364
4482
|
const lastReplayEntry = replayEntries.length > 0 ? replayEntries[replayEntries.length - 1] : null;
|
|
4365
4483
|
const createdAt = firstReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? timestamp;
|
|
4366
|
-
const decryptionKeyId = firstReplayEntry?.decryptionKeyId
|
|
4367
|
-
|
|
4368
|
-
|
|
4484
|
+
const decryptionKeyId = firstReplayEntry?.decryptionKeyId ??
|
|
4485
|
+
snapshotMeta?.decryptionKeyId ??
|
|
4486
|
+
"default";
|
|
4369
4487
|
const isDeleted = this.computeIsDeletedFromMetadata(relevantEntries);
|
|
4370
4488
|
const lastModified = lastReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? timestamp;
|
|
4371
4489
|
const internalDoc = {
|
|
@@ -4396,8 +4514,8 @@ class BaseMindooDB {
|
|
|
4396
4514
|
async ingestSealedKeyFromEntries(entries) {
|
|
4397
4515
|
if (entries.length === 0)
|
|
4398
4516
|
return;
|
|
4399
|
-
const keyId = entries.find((entry) => entry.entryType === "doc_create")
|
|
4400
|
-
?? entries[0].decryptionKeyId;
|
|
4517
|
+
const keyId = entries.find((entry) => entry.entryType === "doc_create")
|
|
4518
|
+
?.decryptionKeyId ?? entries[0].decryptionKeyId;
|
|
4401
4519
|
await this.tenant.ingestSealedFromEntries?.(keyId, entries);
|
|
4402
4520
|
}
|
|
4403
4521
|
/**
|
|
@@ -4411,7 +4529,9 @@ class BaseMindooDB {
|
|
|
4411
4529
|
if (!(0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)) {
|
|
4412
4530
|
return false;
|
|
4413
4531
|
}
|
|
4414
|
-
const metas = await this.scanAllMetadata(this.store, {
|
|
4532
|
+
const metas = await this.scanAllMetadata(this.store, {
|
|
4533
|
+
docId: internalDoc.id,
|
|
4534
|
+
});
|
|
4415
4535
|
await this.ingestSealedKeyFromEntries(metas);
|
|
4416
4536
|
if (await this.tenant.hasDecryptionKey(internalDoc.decryptionKeyId)) {
|
|
4417
4537
|
return false;
|
|
@@ -4454,10 +4574,10 @@ class BaseMindooDB {
|
|
|
4454
4574
|
return; // Document has no history
|
|
4455
4575
|
}
|
|
4456
4576
|
// Load all entries
|
|
4457
|
-
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
4577
|
+
const entries = await this.store.getEntries(relevantEntries.map((em) => em.id));
|
|
4458
4578
|
await this.ingestSealedKeyFromEntries(entries.length > 0 ? entries : relevantEntries);
|
|
4459
4579
|
// Build a map for quick lookup
|
|
4460
|
-
const entryMap = new Map(entries.map(e => [e.id, e]));
|
|
4580
|
+
const entryMap = new Map(entries.map((e) => [e.id, e]));
|
|
4461
4581
|
// Apply changes in order
|
|
4462
4582
|
let currentDoc = null;
|
|
4463
4583
|
let createdAt = null;
|
|
@@ -4494,7 +4614,11 @@ class BaseMindooDB {
|
|
|
4494
4614
|
decryptionKeyId = entryMetadata.decryptionKeyId;
|
|
4495
4615
|
}
|
|
4496
4616
|
// Check document heads before applying change (for non-first entries)
|
|
4497
|
-
const headsBefore = isFirstEntry
|
|
4617
|
+
const headsBefore = isFirstEntry
|
|
4618
|
+
? null
|
|
4619
|
+
: currentDoc
|
|
4620
|
+
? automerge_adapter_1.Automerge.getHeads(currentDoc)
|
|
4621
|
+
: null;
|
|
4498
4622
|
// Apply change using loadIncremental
|
|
4499
4623
|
if (currentDoc === null) {
|
|
4500
4624
|
throw new Error("currentDoc should not be null at this point");
|
|
@@ -4505,12 +4629,15 @@ class BaseMindooDB {
|
|
|
4505
4629
|
// For lifecycle terminal entries, always yield
|
|
4506
4630
|
// For other entries, only yield if heads changed
|
|
4507
4631
|
let shouldYield = false;
|
|
4508
|
-
if (isFirstEntry ||
|
|
4632
|
+
if (isFirstEntry ||
|
|
4633
|
+
entryMetadata.entryType === "doc_delete" ||
|
|
4634
|
+
entryMetadata.entryType === "doc_undelete") {
|
|
4509
4635
|
shouldYield = true;
|
|
4510
4636
|
}
|
|
4511
4637
|
else {
|
|
4512
4638
|
const headsAfter = automerge_adapter_1.Automerge.getHeads(currentDoc);
|
|
4513
|
-
const headsChanged = headsBefore !== null &&
|
|
4639
|
+
const headsChanged = headsBefore !== null &&
|
|
4640
|
+
JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
|
|
4514
4641
|
shouldYield = headsChanged;
|
|
4515
4642
|
}
|
|
4516
4643
|
// Register automerge hash mapping
|
|
@@ -4739,7 +4866,7 @@ class BaseMindooDB {
|
|
|
4739
4866
|
tt: (0, storeEntryTime_1.entryTrustedTime)(meta, now),
|
|
4740
4867
|
witnessed: meta.receivedAt !== undefined,
|
|
4741
4868
|
}))
|
|
4742
|
-
.sort((a, b) =>
|
|
4869
|
+
.sort((a, b) => a.tt !== b.tt ? a.tt - b.tt : a.meta.id.localeCompare(b.meta.id));
|
|
4743
4870
|
// First trusted-time position touched by a newly-discovered entry. From here
|
|
4744
4871
|
// the doc's merged state (and every later revision) may have changed, so we
|
|
4745
4872
|
// re-fold and re-emit the suffix; earlier revisions are unchanged.
|
|
@@ -4870,7 +4997,8 @@ class BaseMindooDB {
|
|
|
4870
4997
|
};
|
|
4871
4998
|
}
|
|
4872
4999
|
// Cache miss: reconstruct from the best in-prefix snapshot + uncovered tail.
|
|
4873
|
-
const prefixSnapshots = docAllMeta.filter((m) => m.entryType === "doc_snapshot" &&
|
|
5000
|
+
const prefixSnapshots = docAllMeta.filter((m) => m.entryType === "doc_snapshot" &&
|
|
5001
|
+
this.snapshotRootsWithin(m, prefixIds));
|
|
4874
5002
|
const planInput = [...prefix, ...prefixSnapshots];
|
|
4875
5003
|
const plan = (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, planInput);
|
|
4876
5004
|
const metadataById = new Map(planInput.map((m) => [m.id, m]));
|
|
@@ -4908,7 +5036,8 @@ class BaseMindooDB {
|
|
|
4908
5036
|
* use as a seed without pulling in later changes).
|
|
4909
5037
|
*/
|
|
4910
5038
|
snapshotRootsWithin(snapshotMeta, idSet) {
|
|
4911
|
-
const roots = snapshotMeta.snapshotHeadEntryIds &&
|
|
5039
|
+
const roots = snapshotMeta.snapshotHeadEntryIds &&
|
|
5040
|
+
snapshotMeta.snapshotHeadEntryIds.length > 0
|
|
4912
5041
|
? snapshotMeta.snapshotHeadEntryIds
|
|
4913
5042
|
: snapshotMeta.dependencyIds;
|
|
4914
5043
|
if (!roots || roots.length === 0) {
|
|
@@ -5059,8 +5188,7 @@ class BaseMindooDB {
|
|
|
5059
5188
|
if (allEntryMetadata.length > 0) {
|
|
5060
5189
|
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
5061
5190
|
}
|
|
5062
|
-
const relevantEntries = allEntryMetadata
|
|
5063
|
-
.filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
|
|
5191
|
+
const relevantEntries = allEntryMetadata.filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
|
|
5064
5192
|
const result = (0, DocumentDagAnalysis_1.computeDocumentDagAnalysis)(docId, relevantEntries, resolvedTimestamp);
|
|
5065
5193
|
const actorIdByEntryId = await this.decodeAutomergeActorIds(relevantEntries);
|
|
5066
5194
|
result.entries = result.entries.map((entry) => ({
|
|
@@ -5106,7 +5234,9 @@ class BaseMindooDB {
|
|
|
5106
5234
|
}
|
|
5107
5235
|
const decryptedPayload = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
|
|
5108
5236
|
const decodedAutomergeChange = automerge_adapter_1.Automerge.decodeChange(decryptedPayload);
|
|
5109
|
-
result.set(metadata.id, typeof decodedAutomergeChange.actor === "string"
|
|
5237
|
+
result.set(metadata.id, typeof decodedAutomergeChange.actor === "string"
|
|
5238
|
+
? decodedAutomergeChange.actor
|
|
5239
|
+
: null);
|
|
5110
5240
|
}
|
|
5111
5241
|
return result;
|
|
5112
5242
|
}
|
|
@@ -5199,8 +5329,7 @@ class BaseMindooDB {
|
|
|
5199
5329
|
const startedAt = Date.now();
|
|
5200
5330
|
const resolvedTimestamp = this.resolveDagTimestamp(timestamp);
|
|
5201
5331
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
5202
|
-
const relevantEntries = allEntryMetadata
|
|
5203
|
-
.filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
|
|
5332
|
+
const relevantEntries = allEntryMetadata.filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
|
|
5204
5333
|
const plan = (0, DocumentDagAnalysis_1.computeBranchMaterializationPlan)(docId, relevantEntries, headEntryId);
|
|
5205
5334
|
if (!plan) {
|
|
5206
5335
|
this.performanceCallback?.onHistoryOperation?.({
|
|
@@ -5256,7 +5385,9 @@ class BaseMindooDB {
|
|
|
5256
5385
|
}
|
|
5257
5386
|
let decodedChange = null;
|
|
5258
5387
|
if (metadata.entryType !== "doc_snapshot") {
|
|
5259
|
-
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
5388
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
5389
|
+
docId,
|
|
5390
|
+
});
|
|
5260
5391
|
if (allEntryMetadata.length > 0) {
|
|
5261
5392
|
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
5262
5393
|
}
|
|
@@ -5330,7 +5461,9 @@ class BaseMindooDB {
|
|
|
5330
5461
|
const docStartedAt = Date.now();
|
|
5331
5462
|
yield { type: "docStart", docId };
|
|
5332
5463
|
try {
|
|
5333
|
-
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
5464
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
5465
|
+
docId,
|
|
5466
|
+
});
|
|
5334
5467
|
yield {
|
|
5335
5468
|
type: "progress",
|
|
5336
5469
|
scannedDocs,
|
|
@@ -5731,9 +5864,10 @@ class BaseMindooDB {
|
|
|
5731
5864
|
if (typeof entry.receiptOrder !== "number") {
|
|
5732
5865
|
continue;
|
|
5733
5866
|
}
|
|
5734
|
-
maxReceiptOrder =
|
|
5735
|
-
|
|
5736
|
-
|
|
5867
|
+
maxReceiptOrder =
|
|
5868
|
+
maxReceiptOrder === undefined
|
|
5869
|
+
? entry.receiptOrder
|
|
5870
|
+
: Math.max(maxReceiptOrder, entry.receiptOrder);
|
|
5737
5871
|
}
|
|
5738
5872
|
return maxReceiptOrder;
|
|
5739
5873
|
}
|
|
@@ -5799,7 +5933,9 @@ class BaseMindooDB {
|
|
|
5799
5933
|
conflicts.push({
|
|
5800
5934
|
docId,
|
|
5801
5935
|
location: {
|
|
5802
|
-
kind: change.entry.dependencyIds.length > 1
|
|
5936
|
+
kind: change.entry.dependencyIds.length > 1
|
|
5937
|
+
? "merge-deps"
|
|
5938
|
+
: "active-heads",
|
|
5803
5939
|
entryId: change.entry.id,
|
|
5804
5940
|
createdAt: change.entry.createdAt,
|
|
5805
5941
|
receiptOrder: change.entry.receiptOrder,
|
|
@@ -5831,14 +5967,17 @@ class BaseMindooDB {
|
|
|
5831
5967
|
}
|
|
5832
5968
|
const pathString = (0, DocumentConflictAnalysis_1.formatDocumentConflictPath)(path);
|
|
5833
5969
|
const action = this.getConflictPatchAction(patch);
|
|
5834
|
-
const isConflictPut = action === "put" &&
|
|
5970
|
+
const isConflictPut = action === "put" &&
|
|
5971
|
+
(this.isConflictPatch(patch) ||
|
|
5972
|
+
this.hasDocumentConflictAtPath(afterDoc, path));
|
|
5835
5973
|
if (isConflictPut) {
|
|
5836
5974
|
const conflictPath = this.buildDocumentConflictPath(afterDoc, path, options.detail === "values");
|
|
5837
5975
|
detectedPaths.set(pathString, conflictPath);
|
|
5838
5976
|
activeConflictPaths.set(pathString, conflictPath);
|
|
5839
5977
|
continue;
|
|
5840
5978
|
}
|
|
5841
|
-
if ((action === "put" || action === "del") &&
|
|
5979
|
+
if ((action === "put" || action === "del") &&
|
|
5980
|
+
activeConflictPaths.has(pathString)) {
|
|
5842
5981
|
if (this.shouldEmitConflictObservation(change.entry, options)) {
|
|
5843
5982
|
resolutions.push({
|
|
5844
5983
|
type: "conflictResolved",
|
|
@@ -5854,7 +5993,8 @@ class BaseMindooDB {
|
|
|
5854
5993
|
activeConflictPaths.delete(pathString);
|
|
5855
5994
|
}
|
|
5856
5995
|
}
|
|
5857
|
-
if (detectedPaths.size > 0 &&
|
|
5996
|
+
if (detectedPaths.size > 0 &&
|
|
5997
|
+
this.shouldEmitConflictObservation(change.entry, options)) {
|
|
5858
5998
|
conflicts.push({
|
|
5859
5999
|
docId,
|
|
5860
6000
|
location: {
|
|
@@ -5874,33 +6014,37 @@ class BaseMindooDB {
|
|
|
5874
6014
|
doc = afterDoc;
|
|
5875
6015
|
entriesApplied++;
|
|
5876
6016
|
const conflictPathCount = conflicts.reduce((count, conflict) => count + conflict.paths.length, 0);
|
|
5877
|
-
if (maxConflictsPerDoc !== undefined &&
|
|
6017
|
+
if (maxConflictsPerDoc !== undefined &&
|
|
6018
|
+
conflictPathCount >= maxConflictsPerDoc) {
|
|
5878
6019
|
break;
|
|
5879
6020
|
}
|
|
5880
6021
|
}
|
|
5881
6022
|
// If the document still has multiple active heads after replay, there may
|
|
5882
6023
|
// be unresolved conflicts with no later patch to surface them. Do one final
|
|
5883
6024
|
// tree scan in that bounded case so active conflicts are visible.
|
|
5884
|
-
if (plan.activeHeadEntryIds.length > 1
|
|
5885
|
-
|
|
5886
|
-
|
|
6025
|
+
if (plan.activeHeadEntryIds.length > 1 &&
|
|
6026
|
+
(maxConflictsPerDoc === undefined ||
|
|
6027
|
+
conflicts.reduce((count, conflict) => count + conflict.paths.length, 0) < maxConflictsPerDoc)) {
|
|
5887
6028
|
const alreadyReported = new Set(conflicts.flatMap((conflict) => conflict.paths.map((path) => path.pathString)));
|
|
5888
6029
|
const activeConflictPaths = this.collectDocumentConflictPaths(doc, options.detail === "values").filter((path) => !alreadyReported.has(path.pathString));
|
|
5889
6030
|
const remainingBudget = maxConflictsPerDoc === undefined
|
|
5890
6031
|
? activeConflictPaths.length
|
|
5891
|
-
: Math.max(0, maxConflictsPerDoc -
|
|
6032
|
+
: Math.max(0, maxConflictsPerDoc -
|
|
6033
|
+
conflicts.reduce((count, conflict) => count + conflict.paths.length, 0));
|
|
5892
6034
|
const boundedActiveConflictPaths = activeConflictPaths.slice(0, remainingBudget);
|
|
5893
6035
|
if (boundedActiveConflictPaths.length > 0) {
|
|
5894
6036
|
const activeHeadEntries = plan.activeHeadEntryIds
|
|
5895
6037
|
.map((entryId) => plan.replayById.get(entryId))
|
|
5896
6038
|
.filter((entry) => entry !== undefined);
|
|
5897
|
-
const latestActiveHeadEntry = activeHeadEntries
|
|
5898
|
-
.sort((left, right) => {
|
|
6039
|
+
const latestActiveHeadEntry = activeHeadEntries.sort((left, right) => {
|
|
5899
6040
|
const leftOrder = left.receiptOrder ?? left.createdAt;
|
|
5900
6041
|
const rightOrder = right.receiptOrder ?? right.createdAt;
|
|
5901
|
-
return leftOrder !== rightOrder
|
|
6042
|
+
return leftOrder !== rightOrder
|
|
6043
|
+
? rightOrder - leftOrder
|
|
6044
|
+
: right.id.localeCompare(left.id);
|
|
5902
6045
|
})[0];
|
|
5903
|
-
if (options.includeUnresolvedFromBefore ||
|
|
6046
|
+
if (options.includeUnresolvedFromBefore ||
|
|
6047
|
+
this.shouldEmitConflictObservation(latestActiveHeadEntry, options)) {
|
|
5904
6048
|
conflicts.push({
|
|
5905
6049
|
docId,
|
|
5906
6050
|
location: {
|
|
@@ -5932,8 +6076,8 @@ class BaseMindooDB {
|
|
|
5932
6076
|
if (!entry) {
|
|
5933
6077
|
return options.includeUnresolvedFromBefore === true;
|
|
5934
6078
|
}
|
|
5935
|
-
if (typeof entry.receiptOrder === "number"
|
|
5936
|
-
|
|
6079
|
+
if (typeof entry.receiptOrder === "number" &&
|
|
6080
|
+
typeof options.since.storeReceiptOrderAsOf === "number") {
|
|
5937
6081
|
return entry.receiptOrder > options.since.storeReceiptOrderAsOf;
|
|
5938
6082
|
}
|
|
5939
6083
|
return entry.createdAt > options.since.takenAt;
|
|
@@ -6245,13 +6389,15 @@ class BaseMindooDB {
|
|
|
6245
6389
|
* @returns JSON-safe scalar, array/object subset, or compact placeholder.
|
|
6246
6390
|
*/
|
|
6247
6391
|
toJsonSafeConflictValue(value, depth = 0, seen = new WeakSet()) {
|
|
6248
|
-
if (value === null
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6392
|
+
if (value === null ||
|
|
6393
|
+
typeof value === "string" ||
|
|
6394
|
+
typeof value === "number" ||
|
|
6395
|
+
typeof value === "boolean") {
|
|
6252
6396
|
return value;
|
|
6253
6397
|
}
|
|
6254
|
-
if (value === undefined ||
|
|
6398
|
+
if (value === undefined ||
|
|
6399
|
+
typeof value === "function" ||
|
|
6400
|
+
typeof value === "symbol") {
|
|
6255
6401
|
return undefined;
|
|
6256
6402
|
}
|
|
6257
6403
|
if (value instanceof Uint8Array) {
|
|
@@ -6268,7 +6414,9 @@ class BaseMindooDB {
|
|
|
6268
6414
|
}
|
|
6269
6415
|
seen.add(value);
|
|
6270
6416
|
if (Array.isArray(value)) {
|
|
6271
|
-
return value
|
|
6417
|
+
return value
|
|
6418
|
+
.slice(0, 50)
|
|
6419
|
+
.map((item) => this.toJsonSafeConflictValue(item, depth + 1, seen));
|
|
6272
6420
|
}
|
|
6273
6421
|
const result = {};
|
|
6274
6422
|
for (const [key, child] of Object.entries(value).slice(0, 50)) {
|
|
@@ -6379,9 +6527,9 @@ class BaseMindooDB {
|
|
|
6379
6527
|
// and, on a time-travel instance, already reflects existence and deletion
|
|
6380
6528
|
// at the cutoff (every scan feeding it is bounded by timeTravelDate). So
|
|
6381
6529
|
// the scan below only has to supply dates, never lifecycle state.
|
|
6382
|
-
const candidates = this.index.filter((entry) => entry.accessState === "visible"
|
|
6383
|
-
|
|
6384
|
-
|
|
6530
|
+
const candidates = this.index.filter((entry) => entry.accessState === "visible" &&
|
|
6531
|
+
matchesLifecycle(entry.isDeleted) &&
|
|
6532
|
+
(idPrefix === undefined || (0, idGeneration_1.matchesDocIdPrefix)(entry.docId, idPrefix)));
|
|
6385
6533
|
if (candidates.length === 0) {
|
|
6386
6534
|
return [];
|
|
6387
6535
|
}
|
|
@@ -6463,11 +6611,13 @@ class BaseMindooDB {
|
|
|
6463
6611
|
continue;
|
|
6464
6612
|
}
|
|
6465
6613
|
const createEntry = lifecycleEntries.find((entry) => entry.entryType === "doc_create");
|
|
6466
|
-
if (createEntry &&
|
|
6614
|
+
if (createEntry &&
|
|
6615
|
+
!(await this.tenant.hasDecryptionKey(createEntry.decryptionKeyId))) {
|
|
6467
6616
|
continue;
|
|
6468
6617
|
}
|
|
6469
|
-
const terminalEntries = lifecycleEntries.filter((entry) => (entry.entryType === "doc_delete" ||
|
|
6470
|
-
|
|
6618
|
+
const terminalEntries = lifecycleEntries.filter((entry) => (entry.entryType === "doc_delete" ||
|
|
6619
|
+
entry.entryType === "doc_undelete") &&
|
|
6620
|
+
entry.createdAt <= timestamp);
|
|
6471
6621
|
if (terminalEntries.length === 0) {
|
|
6472
6622
|
// Fast path (the vast majority of documents): no delete/undelete ever
|
|
6473
6623
|
// happened up to `timestamp`, so the document is alive and we never
|
|
@@ -6483,9 +6633,12 @@ class BaseMindooDB {
|
|
|
6483
6633
|
// decided by id tie-break, which can disagree with causality and with
|
|
6484
6634
|
// getDocumentAtTimestamp for the same instant. The extra per-doc
|
|
6485
6635
|
// metadata scan is bounded by the number of ever-deleted documents.
|
|
6486
|
-
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
6487
|
-
|
|
6488
|
-
|
|
6636
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, {
|
|
6637
|
+
docId,
|
|
6638
|
+
});
|
|
6639
|
+
const relevantEntries = (0, DocumentDagAnalysis_1.pruneToGroundedEntries)(allEntryMetadata.filter((entry) => entry.createdAt <= timestamp &&
|
|
6640
|
+
(this.isDocumentReplayEntry(entry) ||
|
|
6641
|
+
entry.entryType === "doc_snapshot")));
|
|
6489
6642
|
if (!this.computeIsDeletedFromMetadata(relevantEntries)) {
|
|
6490
6643
|
docIds.push(docId);
|
|
6491
6644
|
}
|
|
@@ -6517,7 +6670,8 @@ class BaseMindooDB {
|
|
|
6517
6670
|
// One gate call per document id: inside `userdirectory` the applicable rule
|
|
6518
6671
|
// depends on the id (personal documents are owned by their creator), so a
|
|
6519
6672
|
// single batch-wide check would deny an owner deleting their own data.
|
|
6520
|
-
if ((0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(this.effectiveBuiltinDbId()) ||
|
|
6673
|
+
if ((0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(this.effectiveBuiltinDbId()) ||
|
|
6674
|
+
this._isAdminOnlyDb) {
|
|
6521
6675
|
for (const docId of docIds) {
|
|
6522
6676
|
await this.assertLifecycleMutationAllowed("doc_delete", options.signingKeyPair, options.signingKeyPassword, undefined, docId);
|
|
6523
6677
|
}
|
|
@@ -6625,7 +6779,9 @@ class BaseMindooDB {
|
|
|
6625
6779
|
mutableDoc._lastModified = now;
|
|
6626
6780
|
}));
|
|
6627
6781
|
if (automerge_adapter_1.Automerge.getHeads(newDoc).every((head) => headsBefore.includes(head))) {
|
|
6628
|
-
newDoc = automerge_adapter_1.Automerge.emptyChange(newDoc, {
|
|
6782
|
+
newDoc = automerge_adapter_1.Automerge.emptyChange(newDoc, {
|
|
6783
|
+
time: now,
|
|
6784
|
+
});
|
|
6629
6785
|
}
|
|
6630
6786
|
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
6631
6787
|
if (!changeBytes) {
|
|
@@ -6669,7 +6825,9 @@ class BaseMindooDB {
|
|
|
6669
6825
|
signature,
|
|
6670
6826
|
originalSize: changeBytes.length,
|
|
6671
6827
|
encryptedSize: encryptedPayload.length,
|
|
6672
|
-
attachmentRefs: lifecycleAttachmentRefs.length > 0
|
|
6828
|
+
attachmentRefs: lifecycleAttachmentRefs.length > 0
|
|
6829
|
+
? lifecycleAttachmentRefs
|
|
6830
|
+
: undefined,
|
|
6673
6831
|
entryVersion: types_1.CURRENT_STORE_ENTRY_VERSION,
|
|
6674
6832
|
};
|
|
6675
6833
|
// Bind the metadata with the author signature (audit finding #5).
|
|
@@ -6716,7 +6874,7 @@ class BaseMindooDB {
|
|
|
6716
6874
|
* per-document flush-before-evict write storm during large batches.
|
|
6717
6875
|
*/
|
|
6718
6876
|
async applyLifecycleEntryLocally(prepared, options) {
|
|
6719
|
-
const { internalDoc, entryType, newDoc, automergeHash, entryId, entryMetadata, now } = prepared;
|
|
6877
|
+
const { internalDoc, entryType, newDoc, automergeHash, entryId, entryMetadata, now, } = prepared;
|
|
6720
6878
|
const docId = internalDoc.id;
|
|
6721
6879
|
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
6722
6880
|
// Keep the loaded document, cache, index, and sync dirty tracking in step
|
|
@@ -6726,10 +6884,12 @@ class BaseMindooDB {
|
|
|
6726
6884
|
internalDoc.lastModified = now;
|
|
6727
6885
|
// The delete/undelete entry just written is versioned and un-witnessed, so
|
|
6728
6886
|
// the document is awaiting witness until pushed + stamped with a receipt.
|
|
6729
|
-
internalDoc.awaitingWitness =
|
|
6887
|
+
internalDoc.awaitingWitness =
|
|
6888
|
+
(0, storeEntryTime_1.isProvisional)(entryMetadata) || (internalDoc.awaitingWitness ?? false);
|
|
6730
6889
|
// The doc is now witness-era (we just wrote a versioned entry), so it is
|
|
6731
6890
|
// witnessed iff it is no longer awaiting a receipt.
|
|
6732
|
-
internalDoc.witnessed =
|
|
6891
|
+
internalDoc.witnessed =
|
|
6892
|
+
(0, storeEntryTime_1.isVersioned)(entryMetadata) && !internalDoc.awaitingWitness;
|
|
6733
6893
|
// Force the changeSeq bump: a lifecycle entry was just written, so the
|
|
6734
6894
|
// changefeed must re-emit the doc even if all tracked index fields happen
|
|
6735
6895
|
// to match the previous state in the same millisecond.
|
|
@@ -6751,7 +6911,7 @@ class BaseMindooDB {
|
|
|
6751
6911
|
*/
|
|
6752
6912
|
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6753
6913
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6754
|
-
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ?
|
|
6914
|
+
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? " using custom signing key" : ""}`);
|
|
6755
6915
|
await this.assertLifecycleMutationAllowed("doc_delete", signingKeyPair, signingKeyPassword, undefined, docId);
|
|
6756
6916
|
// Get current document
|
|
6757
6917
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
@@ -6763,7 +6923,7 @@ class BaseMindooDB {
|
|
|
6763
6923
|
}
|
|
6764
6924
|
async undeleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6765
6925
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6766
|
-
this.logger.debug(`Undeleting document ${docId}${useCustomSigningKey ?
|
|
6926
|
+
this.logger.debug(`Undeleting document ${docId}${useCustomSigningKey ? " using custom signing key" : ""}`);
|
|
6767
6927
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
6768
6928
|
if (!internalDoc) {
|
|
6769
6929
|
throw new errors_1.DocumentNotFoundError(docId);
|
|
@@ -6780,7 +6940,10 @@ class BaseMindooDB {
|
|
|
6780
6940
|
return this.changeDocInternal(doc, changeFunc, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
6781
6941
|
}
|
|
6782
6942
|
async changeDocWithSigningKey(doc, changeFunc, signingKeyPair, signingKeyPassword) {
|
|
6783
|
-
return this.changeDoc(doc, changeFunc, {
|
|
6943
|
+
return this.changeDoc(doc, changeFunc, {
|
|
6944
|
+
signingKeyPair,
|
|
6945
|
+
signingKeyPassword,
|
|
6946
|
+
});
|
|
6784
6947
|
}
|
|
6785
6948
|
async applyTextPatch(doc, patch) {
|
|
6786
6949
|
this.assertWritable("applyTextPatch");
|
|
@@ -7182,7 +7345,7 @@ class BaseMindooDB {
|
|
|
7182
7345
|
throw new Error("changeDoc: signingKeyPair and signingKeyPassword must be provided together");
|
|
7183
7346
|
}
|
|
7184
7347
|
const useCustomKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
7185
|
-
this.logger.debug(`===== ${useCustomKey ?
|
|
7348
|
+
this.logger.debug(`===== ${useCustomKey ? "changeDocWithSigningKey" : "changeDoc"} called for document ${docId} =====`);
|
|
7186
7349
|
// Get internal document from cache or load it
|
|
7187
7350
|
let internalDoc = this.getCachedDocument(docId);
|
|
7188
7351
|
if (!internalDoc) {
|
|
@@ -7192,7 +7355,7 @@ class BaseMindooDB {
|
|
|
7192
7355
|
throw new errors_1.DocumentNotFoundError(docId);
|
|
7193
7356
|
}
|
|
7194
7357
|
internalDoc = loadedDoc;
|
|
7195
|
-
this.logger.debug(`Successfully loaded document ${docId} from store for ${useCustomKey ?
|
|
7358
|
+
this.logger.debug(`Successfully loaded document ${docId} from store for ${useCustomKey ? "changeDocWithSigningKey" : "changeDoc"}`);
|
|
7196
7359
|
}
|
|
7197
7360
|
else {
|
|
7198
7361
|
this.logger.debug(`Document ${docId} found in cache`);
|
|
@@ -7210,7 +7373,7 @@ class BaseMindooDB {
|
|
|
7210
7373
|
// Automerge.change() requires synchronous modifications, so we'll:
|
|
7211
7374
|
// 1. Execute the async callback to do any async work (like signing)
|
|
7212
7375
|
// 2. Apply document modifications synchronously within Automerge.change()
|
|
7213
|
-
//
|
|
7376
|
+
//
|
|
7214
7377
|
// We use a two-phase approach: the callback can do async work and collect
|
|
7215
7378
|
// what needs to be changed, then we apply those changes in Automerge.change()
|
|
7216
7379
|
const pendingChanges = new Map();
|
|
@@ -7246,16 +7409,16 @@ class BaseMindooDB {
|
|
|
7246
7409
|
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId),
|
|
7247
7410
|
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
7248
7411
|
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
7249
|
-
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
7250
|
-
|
|
7412
|
+
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
7413
|
+
(0, recipients_1.isPayloadEncryptedFor)(internalDoc.doc, internalDoc.sealedRecipients, Array.isArray(users) ? users : [users]),
|
|
7251
7414
|
getHeads: () => automerge_adapter_1.Automerge.getHeads(internalDoc.doc),
|
|
7252
7415
|
getData: () => {
|
|
7253
7416
|
// Return a proxy that collects property assignments and deletions
|
|
7254
7417
|
const currentData = internalDoc.doc;
|
|
7255
7418
|
return new Proxy(currentData, {
|
|
7256
7419
|
set: (target, prop, value) => {
|
|
7257
|
-
throwIfCallbackInactive(
|
|
7258
|
-
if (typeof prop ===
|
|
7420
|
+
throwIfCallbackInactive("set property");
|
|
7421
|
+
if (typeof prop === "string") {
|
|
7259
7422
|
// If this property was marked for deletion, remove it from deletions
|
|
7260
7423
|
pendingDeletions.delete(prop);
|
|
7261
7424
|
// Track the change
|
|
@@ -7266,8 +7429,8 @@ class BaseMindooDB {
|
|
|
7266
7429
|
return true;
|
|
7267
7430
|
},
|
|
7268
7431
|
deleteProperty: (target, prop) => {
|
|
7269
|
-
throwIfCallbackInactive(
|
|
7270
|
-
if (typeof prop ===
|
|
7432
|
+
throwIfCallbackInactive("delete property");
|
|
7433
|
+
if (typeof prop === "string") {
|
|
7271
7434
|
// Mark for deletion
|
|
7272
7435
|
pendingDeletions.add(prop);
|
|
7273
7436
|
// Remove from pending changes if it was there
|
|
@@ -7279,55 +7442,55 @@ class BaseMindooDB {
|
|
|
7279
7442
|
},
|
|
7280
7443
|
get: (target, prop) => {
|
|
7281
7444
|
// If marked for deletion, return undefined
|
|
7282
|
-
if (typeof prop ===
|
|
7445
|
+
if (typeof prop === "string" && pendingDeletions.has(prop)) {
|
|
7283
7446
|
return undefined;
|
|
7284
7447
|
}
|
|
7285
7448
|
// Check pending changes first, then target
|
|
7286
|
-
if (typeof prop ===
|
|
7449
|
+
if (typeof prop === "string" && pendingChanges.has(prop)) {
|
|
7287
7450
|
return pendingChanges.get(prop);
|
|
7288
7451
|
}
|
|
7289
7452
|
return target[prop];
|
|
7290
7453
|
},
|
|
7291
7454
|
has: (target, prop) => {
|
|
7292
7455
|
// If marked for deletion, it doesn't exist
|
|
7293
|
-
if (typeof prop ===
|
|
7456
|
+
if (typeof prop === "string" && pendingDeletions.has(prop)) {
|
|
7294
7457
|
return false;
|
|
7295
7458
|
}
|
|
7296
7459
|
// Check pending changes first, then target
|
|
7297
|
-
if (typeof prop ===
|
|
7460
|
+
if (typeof prop === "string" && pendingChanges.has(prop)) {
|
|
7298
7461
|
return true;
|
|
7299
7462
|
}
|
|
7300
7463
|
return prop in target;
|
|
7301
|
-
}
|
|
7464
|
+
},
|
|
7302
7465
|
});
|
|
7303
7466
|
},
|
|
7304
7467
|
// ========== Attachment Write Methods (work in changeDoc context) ==========
|
|
7305
7468
|
addAttachment: async (fileData, fileName, mimeType, keyId) => {
|
|
7306
|
-
throwIfCallbackInactive(
|
|
7469
|
+
throwIfCallbackInactive("addAttachment");
|
|
7307
7470
|
const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
|
|
7308
7471
|
const ref = await db.addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, now);
|
|
7309
7472
|
pendingAttachmentAdditions.push(ref);
|
|
7310
7473
|
return ref;
|
|
7311
7474
|
},
|
|
7312
7475
|
addAttachmentStream: async (dataStream, fileName, mimeType, keyId) => {
|
|
7313
|
-
throwIfCallbackInactive(
|
|
7476
|
+
throwIfCallbackInactive("addAttachmentStream");
|
|
7314
7477
|
const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
|
|
7315
7478
|
const ref = await db.addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, now);
|
|
7316
7479
|
pendingAttachmentAdditions.push(ref);
|
|
7317
7480
|
return ref;
|
|
7318
7481
|
},
|
|
7319
7482
|
removeAttachment: async (attachmentId) => {
|
|
7320
|
-
throwIfCallbackInactive(
|
|
7483
|
+
throwIfCallbackInactive("removeAttachment");
|
|
7321
7484
|
// Check if attachment exists (either in current doc or pending additions)
|
|
7322
7485
|
const payload = internalDoc.doc;
|
|
7323
7486
|
const existingAttachments = payload._attachments || [];
|
|
7324
|
-
const existsInDoc = existingAttachments.some(a => a.attachmentId === attachmentId);
|
|
7325
|
-
const existsInPending = pendingAttachmentAdditions.some(a => a.attachmentId === attachmentId);
|
|
7487
|
+
const existsInDoc = existingAttachments.some((a) => a.attachmentId === attachmentId);
|
|
7488
|
+
const existsInPending = pendingAttachmentAdditions.some((a) => a.attachmentId === attachmentId);
|
|
7326
7489
|
if (!existsInDoc && !existsInPending) {
|
|
7327
7490
|
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
7328
7491
|
}
|
|
7329
7492
|
// If it was added in this same changeDoc call, just remove from pending
|
|
7330
|
-
const pendingIndex = pendingAttachmentAdditions.findIndex(a => a.attachmentId === attachmentId);
|
|
7493
|
+
const pendingIndex = pendingAttachmentAdditions.findIndex((a) => a.attachmentId === attachmentId);
|
|
7331
7494
|
if (pendingIndex >= 0) {
|
|
7332
7495
|
const [pendingRef] = pendingAttachmentAdditions.splice(pendingIndex, 1);
|
|
7333
7496
|
await db.cleanupIncompleteAttachmentUpload(docId, pendingRef.attachmentId);
|
|
@@ -7362,28 +7525,33 @@ class BaseMindooDB {
|
|
|
7362
7525
|
* this changeDoc call), or when invoked after the callback finished.
|
|
7363
7526
|
*/
|
|
7364
7527
|
setAttachmentExtractedText: (attachmentId, result) => {
|
|
7365
|
-
throwIfCallbackInactive(
|
|
7528
|
+
throwIfCallbackInactive("setAttachmentExtractedText");
|
|
7366
7529
|
const payload = internalDoc.doc;
|
|
7367
7530
|
const existingAttachments = payload._attachments || [];
|
|
7368
|
-
const existsInDoc = existingAttachments.some((a) => a.attachmentId === attachmentId &&
|
|
7531
|
+
const existsInDoc = existingAttachments.some((a) => a.attachmentId === attachmentId &&
|
|
7532
|
+
!pendingAttachmentRemovals.has(attachmentId));
|
|
7369
7533
|
const existsInPending = pendingAttachmentAdditions.some((a) => a.attachmentId === attachmentId);
|
|
7370
7534
|
if (!existsInDoc && !existsInPending) {
|
|
7371
7535
|
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
7372
7536
|
}
|
|
7373
|
-
const cappedText = typeof result.text === "string" &&
|
|
7537
|
+
const cappedText = typeof result.text === "string" &&
|
|
7538
|
+
result.text.length > types_1.ATTACHMENT_EXTRACTED_TEXT_MAX_CHARS
|
|
7374
7539
|
? result.text.slice(0, types_1.ATTACHMENT_EXTRACTED_TEXT_MAX_CHARS)
|
|
7375
7540
|
: result.text;
|
|
7376
|
-
pendingAttachmentTextUpdates.set(attachmentId, {
|
|
7541
|
+
pendingAttachmentTextUpdates.set(attachmentId, {
|
|
7542
|
+
...result,
|
|
7543
|
+
text: cappedText,
|
|
7544
|
+
});
|
|
7377
7545
|
},
|
|
7378
7546
|
appendToAttachment: async (attachmentId, data) => {
|
|
7379
|
-
throwIfCallbackInactive(
|
|
7547
|
+
throwIfCallbackInactive("appendToAttachment");
|
|
7380
7548
|
// Find the attachment reference
|
|
7381
7549
|
const payload = internalDoc.doc;
|
|
7382
7550
|
const existingAttachments = payload._attachments || [];
|
|
7383
|
-
let ref = existingAttachments.find(a => a.attachmentId === attachmentId);
|
|
7551
|
+
let ref = existingAttachments.find((a) => a.attachmentId === attachmentId);
|
|
7384
7552
|
// Also check pending additions
|
|
7385
7553
|
if (!ref) {
|
|
7386
|
-
ref = pendingAttachmentAdditions.find(a => a.attachmentId === attachmentId);
|
|
7554
|
+
ref = pendingAttachmentAdditions.find((a) => a.attachmentId === attachmentId);
|
|
7387
7555
|
}
|
|
7388
7556
|
if (!ref) {
|
|
7389
7557
|
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
@@ -7403,7 +7571,10 @@ class BaseMindooDB {
|
|
|
7403
7571
|
existingAppend.sizeIncrease += sizeIncrease;
|
|
7404
7572
|
}
|
|
7405
7573
|
else {
|
|
7406
|
-
pendingAttachmentAppends.set(attachmentId, {
|
|
7574
|
+
pendingAttachmentAppends.set(attachmentId, {
|
|
7575
|
+
lastChunkId,
|
|
7576
|
+
sizeIncrease,
|
|
7577
|
+
});
|
|
7407
7578
|
}
|
|
7408
7579
|
},
|
|
7409
7580
|
// ========== Attachment Read Methods (also work in changeDoc context) ==========
|
|
@@ -7411,7 +7582,7 @@ class BaseMindooDB {
|
|
|
7411
7582
|
const payload = internalDoc.doc;
|
|
7412
7583
|
const existing = payload._attachments || [];
|
|
7413
7584
|
// Filter out removals and add pending additions
|
|
7414
|
-
const filtered = existing.filter(a => !pendingAttachmentRemovals.has(a.attachmentId));
|
|
7585
|
+
const filtered = existing.filter((a) => !pendingAttachmentRemovals.has(a.attachmentId));
|
|
7415
7586
|
return [...filtered, ...pendingAttachmentAdditions];
|
|
7416
7587
|
},
|
|
7417
7588
|
getAttachment: async (attachmentId) => {
|
|
@@ -7459,14 +7630,14 @@ class BaseMindooDB {
|
|
|
7459
7630
|
const attachments = automergeDoc._attachments;
|
|
7460
7631
|
// Remove attachments marked for removal
|
|
7461
7632
|
for (const attachmentId of pendingAttachmentRemovals) {
|
|
7462
|
-
const index = attachments.findIndex(a => a.attachmentId === attachmentId);
|
|
7633
|
+
const index = attachments.findIndex((a) => a.attachmentId === attachmentId);
|
|
7463
7634
|
if (index >= 0) {
|
|
7464
7635
|
attachments.splice(index, 1);
|
|
7465
7636
|
}
|
|
7466
7637
|
}
|
|
7467
7638
|
// Apply appends (update lastChunkId and size)
|
|
7468
|
-
for (const [attachmentId, { lastChunkId, sizeIncrease }] of pendingAttachmentAppends) {
|
|
7469
|
-
const attachment = attachments.find(a => a.attachmentId === attachmentId);
|
|
7639
|
+
for (const [attachmentId, { lastChunkId, sizeIncrease },] of pendingAttachmentAppends) {
|
|
7640
|
+
const attachment = attachments.find((a) => a.attachmentId === attachmentId);
|
|
7470
7641
|
if (attachment) {
|
|
7471
7642
|
attachment.lastChunkId = lastChunkId;
|
|
7472
7643
|
attachment.size += sizeIncrease;
|
|
@@ -7478,7 +7649,7 @@ class BaseMindooDB {
|
|
|
7478
7649
|
}
|
|
7479
7650
|
// Apply extraction results (setAttachmentExtractedText)
|
|
7480
7651
|
for (const [attachmentId, update] of pendingAttachmentTextUpdates) {
|
|
7481
|
-
const attachment = attachments.find(a => a.attachmentId === attachmentId);
|
|
7652
|
+
const attachment = attachments.find((a) => a.attachmentId === attachmentId);
|
|
7482
7653
|
if (!attachment) {
|
|
7483
7654
|
continue; // removed in the same changeDoc call
|
|
7484
7655
|
}
|
|
@@ -7525,7 +7696,9 @@ class BaseMindooDB {
|
|
|
7525
7696
|
useCustomKey: Boolean(useCustomKey),
|
|
7526
7697
|
signingKeyPair,
|
|
7527
7698
|
signingKeyPassword,
|
|
7528
|
-
successMessage: useCustomKey
|
|
7699
|
+
successMessage: useCustomKey
|
|
7700
|
+
? "changed with custom signing key"
|
|
7701
|
+
: "changed",
|
|
7529
7702
|
attachmentIds: pendingAttachmentAdditions.map((ref) => ref.attachmentId),
|
|
7530
7703
|
bypassPrecheck,
|
|
7531
7704
|
recipients: internalDoc.sealedRecipients,
|
|
@@ -7562,13 +7735,13 @@ class BaseMindooDB {
|
|
|
7562
7735
|
}
|
|
7563
7736
|
}
|
|
7564
7737
|
validateJsonPatch(patch) {
|
|
7565
|
-
const operationCount = (patch.set?.length ?? 0)
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7738
|
+
const operationCount = (patch.set?.length ?? 0) +
|
|
7739
|
+
(patch.unset?.length ?? 0) +
|
|
7740
|
+
(patch.listDelete?.length ?? 0) +
|
|
7741
|
+
(patch.listInsert?.length ?? 0) +
|
|
7742
|
+
(patch.textSplice?.length ?? 0) +
|
|
7743
|
+
(patch.textMark?.length ?? 0) +
|
|
7744
|
+
(patch.textUnmark?.length ?? 0);
|
|
7572
7745
|
if (operationCount === 0) {
|
|
7573
7746
|
throw new Error("JSON patch must include at least one operation");
|
|
7574
7747
|
}
|
|
@@ -7583,7 +7756,8 @@ class BaseMindooDB {
|
|
|
7583
7756
|
if (!Number.isInteger(operation.index) || operation.index < 0) {
|
|
7584
7757
|
throw new Error("JSON listDelete index must be a non-negative integer");
|
|
7585
7758
|
}
|
|
7586
|
-
if (!Number.isInteger(operation.deleteCount) ||
|
|
7759
|
+
if (!Number.isInteger(operation.deleteCount) ||
|
|
7760
|
+
operation.deleteCount < 0) {
|
|
7587
7761
|
throw new Error("JSON listDelete deleteCount must be a non-negative integer");
|
|
7588
7762
|
}
|
|
7589
7763
|
}
|
|
@@ -7601,10 +7775,12 @@ class BaseMindooDB {
|
|
|
7601
7775
|
if (!Number.isInteger(operation.index) || operation.index < 0) {
|
|
7602
7776
|
throw new Error("JSON textSplice index must be a non-negative integer");
|
|
7603
7777
|
}
|
|
7604
|
-
if (!Number.isInteger(operation.deleteCount) ||
|
|
7778
|
+
if (!Number.isInteger(operation.deleteCount) ||
|
|
7779
|
+
operation.deleteCount < 0) {
|
|
7605
7780
|
throw new Error("JSON textSplice deleteCount must be a non-negative integer");
|
|
7606
7781
|
}
|
|
7607
|
-
if (operation.insert !== undefined &&
|
|
7782
|
+
if (operation.insert !== undefined &&
|
|
7783
|
+
typeof operation.insert !== "string") {
|
|
7608
7784
|
throw new Error("JSON textSplice insert must be a string");
|
|
7609
7785
|
}
|
|
7610
7786
|
}
|
|
@@ -7616,7 +7792,9 @@ class BaseMindooDB {
|
|
|
7616
7792
|
if (!Number.isInteger(operation.length) || operation.length <= 0) {
|
|
7617
7793
|
throw new Error("JSON textMark length must be a positive integer");
|
|
7618
7794
|
}
|
|
7619
|
-
if (!operation.marks ||
|
|
7795
|
+
if (!operation.marks ||
|
|
7796
|
+
typeof operation.marks !== "object" ||
|
|
7797
|
+
Array.isArray(operation.marks)) {
|
|
7620
7798
|
throw new Error("JSON textMark marks must be an object");
|
|
7621
7799
|
}
|
|
7622
7800
|
}
|
|
@@ -7628,7 +7806,8 @@ class BaseMindooDB {
|
|
|
7628
7806
|
if (!Number.isInteger(operation.length) || operation.length <= 0) {
|
|
7629
7807
|
throw new Error("JSON textUnmark length must be a positive integer");
|
|
7630
7808
|
}
|
|
7631
|
-
if (!Array.isArray(operation.names) ||
|
|
7809
|
+
if (!Array.isArray(operation.names) ||
|
|
7810
|
+
operation.names.some((name) => typeof name !== "string" || !name)) {
|
|
7632
7811
|
throw new Error("JSON textUnmark names must be non-empty strings");
|
|
7633
7812
|
}
|
|
7634
7813
|
}
|
|
@@ -7647,7 +7826,8 @@ class BaseMindooDB {
|
|
|
7647
7826
|
this.validateRichTextSpans(patch.spans);
|
|
7648
7827
|
return;
|
|
7649
7828
|
}
|
|
7650
|
-
if (!Array.isArray(patch.spansSequence) ||
|
|
7829
|
+
if (!Array.isArray(patch.spansSequence) ||
|
|
7830
|
+
patch.spansSequence.length === 0) {
|
|
7651
7831
|
throw new Error("Rich-text patch spansSequence must be a non-empty array");
|
|
7652
7832
|
}
|
|
7653
7833
|
for (const spans of patch.spansSequence) {
|
|
@@ -7669,7 +7849,9 @@ class BaseMindooDB {
|
|
|
7669
7849
|
continue;
|
|
7670
7850
|
}
|
|
7671
7851
|
if (span.type === "block") {
|
|
7672
|
-
if (!span.value ||
|
|
7852
|
+
if (!span.value ||
|
|
7853
|
+
typeof span.value !== "object" ||
|
|
7854
|
+
Array.isArray(span.value)) {
|
|
7673
7855
|
throw new Error("Rich-text block span value must be an object");
|
|
7674
7856
|
}
|
|
7675
7857
|
continue;
|
|
@@ -7705,7 +7887,9 @@ class BaseMindooDB {
|
|
|
7705
7887
|
if (!Number.isInteger(markRange.length) || markRange.length <= 0) {
|
|
7706
7888
|
throw new Error("Rich-text mark range length must be a positive integer");
|
|
7707
7889
|
}
|
|
7708
|
-
if (!markRange.marks ||
|
|
7890
|
+
if (!markRange.marks ||
|
|
7891
|
+
typeof markRange.marks !== "object" ||
|
|
7892
|
+
Array.isArray(markRange.marks)) {
|
|
7709
7893
|
throw new Error("Rich-text mark range marks must be an object");
|
|
7710
7894
|
}
|
|
7711
7895
|
}
|
|
@@ -8032,7 +8216,7 @@ class BaseMindooDB {
|
|
|
8032
8216
|
}
|
|
8033
8217
|
}
|
|
8034
8218
|
async persistDocumentChange(options) {
|
|
8035
|
-
const { internalDoc, newDoc, now, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients } = options;
|
|
8219
|
+
const { internalDoc, newDoc, now, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients, } = options;
|
|
8036
8220
|
const docId = internalDoc.id;
|
|
8037
8221
|
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
8038
8222
|
const changesSincePreviousHeads = headsBeforeChange
|
|
@@ -8063,9 +8247,10 @@ class BaseMindooDB {
|
|
|
8063
8247
|
const { internalDoc, newDoc, now, changeBytesList, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients, } = options;
|
|
8064
8248
|
const docId = internalDoc.id;
|
|
8065
8249
|
await this.assertCurrentSealedAccess(internalDoc);
|
|
8066
|
-
const resolvedChangeBytesList = changeBytesList ??
|
|
8067
|
-
|
|
8068
|
-
|
|
8250
|
+
const resolvedChangeBytesList = changeBytesList ??
|
|
8251
|
+
(headsBeforeChange
|
|
8252
|
+
? automerge_adapter_1.Automerge.getChangesSince(newDoc, headsBeforeChange)
|
|
8253
|
+
: []);
|
|
8069
8254
|
if (resolvedChangeBytesList.length === 0) {
|
|
8070
8255
|
throw new Error("No Automerge changes to persist");
|
|
8071
8256
|
}
|
|
@@ -8118,12 +8303,13 @@ class BaseMindooDB {
|
|
|
8118
8303
|
internalDoc.lastModified = now;
|
|
8119
8304
|
// The change entries just written are versioned and un-witnessed, so the
|
|
8120
8305
|
// document is awaiting witness (until pushed + stamped with a receipt).
|
|
8121
|
-
internalDoc.awaitingWitness =
|
|
8306
|
+
internalDoc.awaitingWitness =
|
|
8307
|
+
entries.some(storeEntryTime_1.isProvisional) || (internalDoc.awaitingWitness ?? false);
|
|
8122
8308
|
// `versioned` is sticky: any versioned entry (new or already reflected by a
|
|
8123
8309
|
// prior awaiting/witnessed flag) keeps the doc in the witness-aware era.
|
|
8124
|
-
const versioned = entries.some(storeEntryTime_1.isVersioned)
|
|
8125
|
-
|
|
8126
|
-
|
|
8310
|
+
const versioned = entries.some(storeEntryTime_1.isVersioned) ||
|
|
8311
|
+
internalDoc.awaitingWitness ||
|
|
8312
|
+
(internalDoc.witnessed ?? false);
|
|
8127
8313
|
internalDoc.witnessed = versioned && !internalDoc.awaitingWitness;
|
|
8128
8314
|
await this.storeCachedDocument(internalDoc);
|
|
8129
8315
|
// Force the changeSeq bump: new change entries were just written, so the
|
|
@@ -8139,11 +8325,15 @@ class BaseMindooDB {
|
|
|
8139
8325
|
? signingKeyPair?.publicKey
|
|
8140
8326
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
8141
8327
|
await this.maybeWriteSnapshotForDocument(internalDoc, useCustomKey
|
|
8142
|
-
? {
|
|
8328
|
+
? {
|
|
8329
|
+
signingKeyPair,
|
|
8330
|
+
signingKeyPassword,
|
|
8331
|
+
createdByPublicKey: createdByPublicKey ?? "",
|
|
8332
|
+
}
|
|
8143
8333
|
: { createdByPublicKey: createdByPublicKey ?? "" });
|
|
8144
8334
|
}
|
|
8145
8335
|
async buildDocChangeStoreEntry(options) {
|
|
8146
|
-
const { internalDoc, changeBytes, now, useCustomKey, signingKeyPair, signingKeyPassword, attachmentIds, attachmentRefs, recipients } = options;
|
|
8336
|
+
const { internalDoc, changeBytes, now, useCustomKey, signingKeyPair, signingKeyPassword, attachmentIds, attachmentRefs, recipients, } = options;
|
|
8147
8337
|
const docId = internalDoc.id;
|
|
8148
8338
|
this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
|
|
8149
8339
|
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
@@ -8181,7 +8371,9 @@ class BaseMindooDB {
|
|
|
8181
8371
|
originalSize: changeBytes.length,
|
|
8182
8372
|
encryptedSize: encryptedPayload.length,
|
|
8183
8373
|
attachmentIds: attachmentIds && attachmentIds.length > 0 ? attachmentIds : undefined,
|
|
8184
|
-
attachmentRefs: attachmentRefs && attachmentRefs.length > 0
|
|
8374
|
+
attachmentRefs: attachmentRefs && attachmentRefs.length > 0
|
|
8375
|
+
? attachmentRefs
|
|
8376
|
+
: undefined,
|
|
8185
8377
|
recipients,
|
|
8186
8378
|
entryVersion: types_1.CURRENT_STORE_ENTRY_VERSION,
|
|
8187
8379
|
};
|
|
@@ -8259,13 +8451,19 @@ class BaseMindooDB {
|
|
|
8259
8451
|
signature,
|
|
8260
8452
|
originalSize: snapshotBytes.length,
|
|
8261
8453
|
encryptedSize: encryptedPayload.length,
|
|
8262
|
-
attachmentRefs: snapshotAttachmentRefs.length > 0
|
|
8454
|
+
attachmentRefs: snapshotAttachmentRefs.length > 0
|
|
8455
|
+
? snapshotAttachmentRefs
|
|
8456
|
+
: undefined,
|
|
8263
8457
|
encryptedData: encryptedPayload,
|
|
8264
8458
|
};
|
|
8265
8459
|
// Bind the metadata with the author signature (audit finding #5).
|
|
8266
|
-
snapshotEntry.metadataSignature =
|
|
8267
|
-
|
|
8268
|
-
|
|
8460
|
+
snapshotEntry.metadataSignature =
|
|
8461
|
+
await this.computeEntryMetadataSignature(snapshotEntry, options.signingKeyPair && options.signingKeyPassword
|
|
8462
|
+
? {
|
|
8463
|
+
signingKeyPair: options.signingKeyPair,
|
|
8464
|
+
signingKeyPassword: options.signingKeyPassword,
|
|
8465
|
+
}
|
|
8466
|
+
: undefined);
|
|
8269
8467
|
await this.store.putEntries([snapshotEntry]);
|
|
8270
8468
|
this.writesSinceSnapshotCheck.set(docId, 0);
|
|
8271
8469
|
this.logger.debug(`Created snapshot for document ${docId} with ${headHashes.length} heads and ${changesSinceSnapshot} changes since previous snapshot`);
|
|
@@ -8524,7 +8722,9 @@ class BaseMindooDB {
|
|
|
8524
8722
|
if (!serialized) {
|
|
8525
8723
|
return null;
|
|
8526
8724
|
}
|
|
8527
|
-
return serialized.length > 80
|
|
8725
|
+
return serialized.length > 80
|
|
8726
|
+
? `${serialized.slice(0, 77)}...`
|
|
8727
|
+
: serialized;
|
|
8528
8728
|
}
|
|
8529
8729
|
catch {
|
|
8530
8730
|
return String(value);
|
|
@@ -8582,10 +8782,16 @@ class BaseMindooDB {
|
|
|
8582
8782
|
}
|
|
8583
8783
|
return;
|
|
8584
8784
|
}
|
|
8585
|
-
if (this.isPlainDiffObject(beforeValue) ||
|
|
8586
|
-
|
|
8785
|
+
if (this.isPlainDiffObject(beforeValue) ||
|
|
8786
|
+
this.isPlainDiffObject(afterValue)) {
|
|
8787
|
+
const beforeObject = this.isPlainDiffObject(beforeValue)
|
|
8788
|
+
? beforeValue
|
|
8789
|
+
: {};
|
|
8587
8790
|
const afterObject = this.isPlainDiffObject(afterValue) ? afterValue : {};
|
|
8588
|
-
const childKeys = new Set([
|
|
8791
|
+
const childKeys = new Set([
|
|
8792
|
+
...Object.keys(beforeObject),
|
|
8793
|
+
...Object.keys(afterObject),
|
|
8794
|
+
]);
|
|
8589
8795
|
if (childKeys.size === 0 && basePath) {
|
|
8590
8796
|
results.add(basePath);
|
|
8591
8797
|
return;
|
|
@@ -8604,10 +8810,10 @@ class BaseMindooDB {
|
|
|
8604
8810
|
* Returns true for diffable object literals and false for arrays/binary payloads.
|
|
8605
8811
|
*/
|
|
8606
8812
|
isPlainDiffObject(value) {
|
|
8607
|
-
return value !== null
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8813
|
+
return (value !== null &&
|
|
8814
|
+
typeof value === "object" &&
|
|
8815
|
+
!Array.isArray(value) &&
|
|
8816
|
+
!(value instanceof Uint8Array));
|
|
8611
8817
|
}
|
|
8612
8818
|
/**
|
|
8613
8819
|
* Extracts the terminal field name from a normalized path for loose key/path matching.
|
|
@@ -8625,7 +8831,9 @@ class BaseMindooDB {
|
|
|
8625
8831
|
* the full decoded structure.
|
|
8626
8832
|
*/
|
|
8627
8833
|
summarizeDecodedChange(decodedChange) {
|
|
8628
|
-
const operations = Array.isArray(decodedChange.ops)
|
|
8834
|
+
const operations = Array.isArray(decodedChange.ops)
|
|
8835
|
+
? decodedChange.ops
|
|
8836
|
+
: [];
|
|
8629
8837
|
const actionCounts = {};
|
|
8630
8838
|
const touchedKeys = new Set();
|
|
8631
8839
|
const summarizedOperations = operations.slice(0, 12).map((operation) => {
|
|
@@ -8660,7 +8868,9 @@ class BaseMindooDB {
|
|
|
8660
8868
|
actorId: typeof decodedChange.actor === "string" ? decodedChange.actor : null,
|
|
8661
8869
|
hash: typeof decodedChange.hash === "string" ? decodedChange.hash : null,
|
|
8662
8870
|
seq: typeof decodedChange.seq === "number" ? decodedChange.seq : null,
|
|
8663
|
-
message: typeof decodedChange.message === "string"
|
|
8871
|
+
message: typeof decodedChange.message === "string"
|
|
8872
|
+
? decodedChange.message
|
|
8873
|
+
: null,
|
|
8664
8874
|
dependencyHashes: Array.isArray(decodedChange.deps)
|
|
8665
8875
|
? decodedChange.deps.filter((dep) => typeof dep === "string")
|
|
8666
8876
|
: [],
|
|
@@ -8686,7 +8896,9 @@ class BaseMindooDB {
|
|
|
8686
8896
|
}
|
|
8687
8897
|
const metadataById = new Map(allEntryMetadata.map((entry) => [entry.id, entry]));
|
|
8688
8898
|
let startFromSnapshot = snapshotEntryId !== null;
|
|
8689
|
-
const snapshotMeta = snapshotEntryId
|
|
8899
|
+
const snapshotMeta = snapshotEntryId
|
|
8900
|
+
? metadataById.get(snapshotEntryId) || null
|
|
8901
|
+
: null;
|
|
8690
8902
|
if (startFromSnapshot && !snapshotMeta) {
|
|
8691
8903
|
this.logger.warn(`Materialization referenced snapshot ${snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
|
|
8692
8904
|
startFromSnapshot = false;
|
|
@@ -8758,16 +8970,24 @@ class BaseMindooDB {
|
|
|
8758
8970
|
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
8759
8971
|
}
|
|
8760
8972
|
}
|
|
8761
|
-
const orderedReplayEntries = [...replayEntriesForState].sort((left, right) => left.createdAt !== right.createdAt
|
|
8973
|
+
const orderedReplayEntries = [...replayEntriesForState].sort((left, right) => left.createdAt !== right.createdAt
|
|
8974
|
+
? left.createdAt - right.createdAt
|
|
8975
|
+
: left.id.localeCompare(right.id));
|
|
8762
8976
|
const firstReplayEntry = orderedReplayEntries[0] ?? null;
|
|
8763
8977
|
const lastReplayEntry = orderedReplayEntries[orderedReplayEntries.length - 1] ?? null;
|
|
8764
8978
|
const witnessState = (0, storeEntryTime_1.metadataWitnessState)(replayEntriesForState);
|
|
8765
8979
|
return {
|
|
8766
8980
|
id: docId,
|
|
8767
8981
|
doc,
|
|
8768
|
-
createdAt: firstReplayEntry?.createdAt ??
|
|
8769
|
-
|
|
8770
|
-
|
|
8982
|
+
createdAt: firstReplayEntry?.createdAt ??
|
|
8983
|
+
snapshotMeta?.createdAt ??
|
|
8984
|
+
fallbackTimestamp,
|
|
8985
|
+
lastModified: lastReplayEntry?.createdAt ??
|
|
8986
|
+
snapshotMeta?.createdAt ??
|
|
8987
|
+
fallbackTimestamp,
|
|
8988
|
+
decryptionKeyId: firstReplayEntry?.decryptionKeyId ??
|
|
8989
|
+
snapshotMeta?.decryptionKeyId ??
|
|
8990
|
+
"default",
|
|
8771
8991
|
isDeleted: this.computeIsDeletedFromMetadata(replayEntriesForState),
|
|
8772
8992
|
awaitingWitness: witnessState.awaitingWitness,
|
|
8773
8993
|
witnessed: witnessState.witnessed,
|
|
@@ -8854,7 +9074,11 @@ class BaseMindooDB {
|
|
|
8854
9074
|
const startedAt = Date.now();
|
|
8855
9075
|
// Default to initial cursor if null is provided.
|
|
8856
9076
|
// Prefer deterministic sequence-based cursoring; keep legacy fallback compatibility.
|
|
8857
|
-
const actualCursor = cursor ?? {
|
|
9077
|
+
const actualCursor = cursor ?? {
|
|
9078
|
+
changeSeq: 0,
|
|
9079
|
+
lastModified: 0,
|
|
9080
|
+
docId: "",
|
|
9081
|
+
};
|
|
8858
9082
|
const idPrefix = this.normalizeIdPrefixFilter(options?.idPrefix);
|
|
8859
9083
|
this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
|
|
8860
9084
|
// Use a stable snapshot of the index for this generator run so concurrent
|
|
@@ -8896,7 +9120,10 @@ class BaseMindooDB {
|
|
|
8896
9120
|
docId: entry.docId,
|
|
8897
9121
|
};
|
|
8898
9122
|
yieldedDocuments++;
|
|
8899
|
-
yield {
|
|
9123
|
+
yield {
|
|
9124
|
+
doc: this.buildInaccessibleDoc(entry),
|
|
9125
|
+
cursor: inaccessibleCursor,
|
|
9126
|
+
};
|
|
8900
9127
|
await prefetchFrom(i + 1);
|
|
8901
9128
|
continue;
|
|
8902
9129
|
}
|
|
@@ -9098,8 +9325,7 @@ class BaseMindooDB {
|
|
|
9098
9325
|
// Update metadata
|
|
9099
9326
|
const payload = updatedDoc;
|
|
9100
9327
|
const lastEntryCreatedAt = entries.reduce((maxCreatedAt, entry) => Math.max(maxCreatedAt, entry.createdAt), cachedDoc.lastModified);
|
|
9101
|
-
const lastModified = payload._lastModified ||
|
|
9102
|
-
lastEntryCreatedAt;
|
|
9328
|
+
const lastModified = payload._lastModified || lastEntryCreatedAt;
|
|
9103
9329
|
const isDeleted = this.computeIsDeletedFromMetadata(allDocMetadata);
|
|
9104
9330
|
const updatedInternalDoc = {
|
|
9105
9331
|
id: docId,
|
|
@@ -9550,7 +9776,8 @@ class BaseMindooDB {
|
|
|
9550
9776
|
* re-enters the directory for them.
|
|
9551
9777
|
*/
|
|
9552
9778
|
async resolveCreateKeyId(options, docId) {
|
|
9553
|
-
if (options.recipients !== undefined &&
|
|
9779
|
+
if (options.recipients !== undefined &&
|
|
9780
|
+
options.decryptionKeyId !== undefined) {
|
|
9554
9781
|
throw new Error("createDocument: recipients and decryptionKeyId are mutually exclusive");
|
|
9555
9782
|
}
|
|
9556
9783
|
if (options.recipients !== undefined) {
|
|
@@ -9588,7 +9815,10 @@ class BaseMindooDB {
|
|
|
9588
9815
|
docId,
|
|
9589
9816
|
keyId,
|
|
9590
9817
|
includeSelf: options.recipientOptions?.includeSelf !== false,
|
|
9591
|
-
userTargets: resolved.targets.map((target) => ({
|
|
9818
|
+
userTargets: resolved.targets.map((target) => ({
|
|
9819
|
+
kind: target.kind,
|
|
9820
|
+
id: target.id,
|
|
9821
|
+
})),
|
|
9592
9822
|
skipped: resolved.skipped,
|
|
9593
9823
|
});
|
|
9594
9824
|
const recipients = await (0, recipients_1.sealToTargets)({
|
|
@@ -9662,7 +9892,8 @@ class BaseMindooDB {
|
|
|
9662
9892
|
*/
|
|
9663
9893
|
async canCreate(options = {}) {
|
|
9664
9894
|
const keyId = await this.resolveCreateKeyId(options);
|
|
9665
|
-
const useCustomSigningKey = options.signingKeyPair !== undefined &&
|
|
9895
|
+
const useCustomSigningKey = options.signingKeyPair !== undefined &&
|
|
9896
|
+
options.signingKeyPassword !== undefined;
|
|
9666
9897
|
const signerKey = useCustomSigningKey
|
|
9667
9898
|
? options.signingKeyPair.publicKey
|
|
9668
9899
|
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
@@ -9713,7 +9944,9 @@ class BaseMindooDB {
|
|
|
9713
9944
|
}
|
|
9714
9945
|
async canChangeRecipients(doc, next) {
|
|
9715
9946
|
const after = { ...doc.getData() };
|
|
9716
|
-
after[sealedTypes_1.ENCRYPT_FOR_FIELD] = {
|
|
9947
|
+
after[sealedTypes_1.ENCRYPT_FOR_FIELD] = {
|
|
9948
|
+
...(0, recipients_1.readEncryptFor)(after),
|
|
9949
|
+
};
|
|
9717
9950
|
return this.canChange(doc, after);
|
|
9718
9951
|
}
|
|
9719
9952
|
async mutateRecipients(doc, mode, specs, options) {
|
|
@@ -9728,8 +9961,8 @@ class BaseMindooDB {
|
|
|
9728
9961
|
throw new errors_1.DocumentNotFoundError(docId);
|
|
9729
9962
|
}
|
|
9730
9963
|
const now = (0, timeSource_1.semanticNow)();
|
|
9731
|
-
const signer = options?.signingKeyPair?.publicKey
|
|
9732
|
-
|
|
9964
|
+
const signer = options?.signingKeyPair?.publicKey ??
|
|
9965
|
+
(await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
9733
9966
|
const resolved = await (0, recipients_1.resolveRecipientSpecs)({
|
|
9734
9967
|
tenant,
|
|
9735
9968
|
specs,
|
|
@@ -9741,15 +9974,18 @@ class BaseMindooDB {
|
|
|
9741
9974
|
now,
|
|
9742
9975
|
});
|
|
9743
9976
|
const currentMap = (0, recipients_1.readEncryptFor)(internalDoc.doc);
|
|
9744
|
-
const activeIds = new Set(Object.entries(currentMap)
|
|
9977
|
+
const activeIds = new Set(Object.entries(currentMap)
|
|
9978
|
+
.filter(([, e]) => !e.removedAt)
|
|
9979
|
+
.map(([id]) => id.split("#")[0]));
|
|
9745
9980
|
const addedTargets = [];
|
|
9746
9981
|
const removedStableIds = [];
|
|
9747
9982
|
const matchesTarget = (id, entry, target) => {
|
|
9748
9983
|
const base = id.split("#")[0];
|
|
9749
|
-
return (target.stableId === base
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9984
|
+
return (target.stableId === base ||
|
|
9985
|
+
target.id === base ||
|
|
9986
|
+
(!!entry.label && target.id === entry.label) ||
|
|
9987
|
+
(!!entry.keyFingerprint &&
|
|
9988
|
+
target.keyFingerprint === entry.keyFingerprint));
|
|
9753
9989
|
};
|
|
9754
9990
|
if (mode === "add" || mode === "set") {
|
|
9755
9991
|
for (const target of resolved.targets) {
|
|
@@ -9785,7 +10021,9 @@ class BaseMindooDB {
|
|
|
9785
10021
|
let generations = currentGens;
|
|
9786
10022
|
let epoch = internalDoc.sealedRecipients?.epoch ?? currentGens.length;
|
|
9787
10023
|
if (rotate) {
|
|
9788
|
-
const nextDek = tenant
|
|
10024
|
+
const nextDek = tenant
|
|
10025
|
+
.getCryptoAdapter()
|
|
10026
|
+
.getRandomValues(new Uint8Array(32));
|
|
9789
10027
|
generations = [nextDek, ...currentGens];
|
|
9790
10028
|
epoch += 1;
|
|
9791
10029
|
}
|
|
@@ -9811,7 +10049,9 @@ class BaseMindooDB {
|
|
|
9811
10049
|
for (const id of removedStableIds) {
|
|
9812
10050
|
remainingByStable.delete(id);
|
|
9813
10051
|
for (const [stableId, target] of [...remainingByStable.entries()]) {
|
|
9814
|
-
if (target.id === id ||
|
|
10052
|
+
if (target.id === id ||
|
|
10053
|
+
target.stableId === id ||
|
|
10054
|
+
target.keyFingerprint === id) {
|
|
9815
10055
|
remainingByStable.delete(stableId);
|
|
9816
10056
|
}
|
|
9817
10057
|
}
|
|
@@ -9874,7 +10114,9 @@ class BaseMindooDB {
|
|
|
9874
10114
|
return {
|
|
9875
10115
|
epoch,
|
|
9876
10116
|
rotated: rotate,
|
|
9877
|
-
added: wrapped
|
|
10117
|
+
added: wrapped
|
|
10118
|
+
.getRecipients()
|
|
10119
|
+
.filter((r) => addedTargets.some((t) => t.keyFingerprint === r.keyFingerprint)),
|
|
9878
10120
|
removed: Object.entries((0, recipients_1.readEncryptFor)(internalDoc.doc))
|
|
9879
10121
|
.filter(([id, e]) => e.removedAt && removedStableIds.includes(id.split("#")[0]))
|
|
9880
10122
|
.map(([id, e]) => ({
|
|
@@ -10025,7 +10267,9 @@ class BaseMindooDB {
|
|
|
10025
10267
|
// monotonicity across the entries we accept, in trusted-time order (§5.4).
|
|
10026
10268
|
// A verify-only provider (no signer) routes receipts to the right scheme.
|
|
10027
10269
|
const subtle = this.getSubtle();
|
|
10028
|
-
const receiptVerifier = new Ed25519WitnessProvider_1.Ed25519WitnessProvider({
|
|
10270
|
+
const receiptVerifier = new Ed25519WitnessProvider_1.Ed25519WitnessProvider({
|
|
10271
|
+
subtle,
|
|
10272
|
+
}).createVerifier();
|
|
10029
10273
|
const nowMs = (0, timeSource_1.semanticNow)();
|
|
10030
10274
|
// Lifecycle entries only, folded in deterministic causal order. Un-witnessed
|
|
10031
10275
|
// entries (versioned or legacy) order by their author time `createdAt`, NOT
|
|
@@ -10103,10 +10347,15 @@ class BaseMindooDB {
|
|
|
10103
10347
|
// from a currently-trusted witness, carry a valid signature, and respect
|
|
10104
10348
|
// per-witness monotonicity and the future-time bound. Entries with no
|
|
10105
10349
|
// receipt are local/not-yet-witnessed and pass this check.
|
|
10106
|
-
if (getTrustedWitnessKeysAt &&
|
|
10350
|
+
if (getTrustedWitnessKeysAt &&
|
|
10351
|
+
(meta.receivedAt !== undefined || meta.receivedByPublicKey)) {
|
|
10107
10352
|
try {
|
|
10108
10353
|
const trustedWitnessKeys = await getTrustedWitnessKeysAt(meta.receivedAt ?? meta.createdAt);
|
|
10109
|
-
const receiptResult = await receiptVerifier.validate(meta, {
|
|
10354
|
+
const receiptResult = await receiptVerifier.validate(meta, {
|
|
10355
|
+
dbid,
|
|
10356
|
+
trustedWitnessKeys,
|
|
10357
|
+
nowMs,
|
|
10358
|
+
});
|
|
10110
10359
|
if (!receiptResult.ok) {
|
|
10111
10360
|
quarantine(meta, "invalid_witness_receipt", receiptResult.reason ?? "invalid witness receipt");
|
|
10112
10361
|
continue;
|
|
@@ -10216,7 +10465,8 @@ class BaseMindooDB {
|
|
|
10216
10465
|
const payload = currentDoc;
|
|
10217
10466
|
const isDeleted = this.computeIsDeletedFromMetadata(acceptedMeta);
|
|
10218
10467
|
const lastAccepted = acceptedMeta[acceptedMeta.length - 1];
|
|
10219
|
-
const lastModified = payload._lastModified ||
|
|
10468
|
+
const lastModified = payload._lastModified ||
|
|
10469
|
+
(lastAccepted ? lastAccepted.createdAt : (0, timeSource_1.semanticNow)());
|
|
10220
10470
|
const witnessState = (0, storeEntryTime_1.metadataWitnessState)(acceptedMeta);
|
|
10221
10471
|
return {
|
|
10222
10472
|
doc: {
|
|
@@ -10244,7 +10494,8 @@ class BaseMindooDB {
|
|
|
10244
10494
|
// contract of `loadDocumentInternal` aligned with `getDocument` /
|
|
10245
10495
|
// `getAllDocumentIds`.
|
|
10246
10496
|
const indexEntryIdx = this.getDocIndexPosition(docId);
|
|
10247
|
-
if (indexEntryIdx !== undefined &&
|
|
10497
|
+
if (indexEntryIdx !== undefined &&
|
|
10498
|
+
this.index[indexEntryIdx].accessState === "inaccessible") {
|
|
10248
10499
|
return null;
|
|
10249
10500
|
}
|
|
10250
10501
|
if (this.docCache.has(docId)) {
|
|
@@ -10331,13 +10582,16 @@ class BaseMindooDB {
|
|
|
10331
10582
|
return null;
|
|
10332
10583
|
}
|
|
10333
10584
|
// Log all entry types
|
|
10334
|
-
const entryTypes = allEntryMetadata
|
|
10585
|
+
const entryTypes = allEntryMetadata
|
|
10586
|
+
.map((em) => `${em.entryType}@${em.createdAt}`)
|
|
10587
|
+
.join(", ");
|
|
10335
10588
|
this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
|
|
10336
|
-
await this.tenant.ingestSealedFromEntries?.(allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
10337
|
-
?? allEntryMetadata[0].decryptionKeyId, allEntryMetadata);
|
|
10338
|
-
const loadKeyId = allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
10339
|
-
?? allEntryMetadata[0].decryptionKeyId;
|
|
10340
|
-
if ((0, sealedTypes_1.isSealedKeyId)(loadKeyId) &&
|
|
10589
|
+
await this.tenant.ingestSealedFromEntries?.(allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
10590
|
+
?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId, allEntryMetadata);
|
|
10591
|
+
const loadKeyId = allEntryMetadata.find((em) => em.entryType === "doc_create")
|
|
10592
|
+
?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId;
|
|
10593
|
+
if ((0, sealedTypes_1.isSealedKeyId)(loadKeyId) &&
|
|
10594
|
+
!(await this.tenant.hasDecryptionKey(loadKeyId))) {
|
|
10341
10595
|
this.logger.debug(`Document ${docId} is sealed and not unwrapable; treating as missing`);
|
|
10342
10596
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
10343
10597
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
@@ -10398,7 +10652,7 @@ class BaseMindooDB {
|
|
|
10398
10652
|
});
|
|
10399
10653
|
let startFromSnapshot = materializationPlan.snapshotEntryId !== null;
|
|
10400
10654
|
const snapshotMeta = materializationPlan.snapshotEntryId
|
|
10401
|
-
?
|
|
10655
|
+
? metadataById.get(materializationPlan.snapshotEntryId) || null
|
|
10402
10656
|
: null;
|
|
10403
10657
|
if (startFromSnapshot && !snapshotMeta) {
|
|
10404
10658
|
this.logger.warn(`Planner referenced snapshot ${materializationPlan.snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
|
|
@@ -10487,10 +10741,10 @@ class BaseMindooDB {
|
|
|
10487
10741
|
// Load and apply all entries
|
|
10488
10742
|
this.logger.debug(`Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
|
|
10489
10743
|
const entryLoadStartedAt = Date.now();
|
|
10490
|
-
const entries = await this.fetchEntries(entriesToLoad.map(em => em.id));
|
|
10744
|
+
const entries = await this.fetchEntries(entriesToLoad.map((em) => em.id));
|
|
10491
10745
|
entryLoadTime += Date.now() - entryLoadStartedAt;
|
|
10492
10746
|
this.logger.debug(`Retrieved ${entries.length} entries from store for document ${docId}`);
|
|
10493
|
-
this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ?
|
|
10747
|
+
this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? "starting from snapshot" : "starting from scratch"})`);
|
|
10494
10748
|
// Log current document state before applying entries
|
|
10495
10749
|
if (this.isDebugEnabled())
|
|
10496
10750
|
this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
@@ -10608,8 +10862,10 @@ class BaseMindooDB {
|
|
|
10608
10862
|
this.logger.debug(`Document ${docId} isDeleted: ${isDeleted}`);
|
|
10609
10863
|
// The authoritative decryptionKeyId comes from the doc_create entry's metadata,
|
|
10610
10864
|
// not from the Automerge payload (which does not store encryption metadata).
|
|
10611
|
-
const createEntry = allEntryMetadata.find(em => em.entryType === "doc_create");
|
|
10612
|
-
const decryptionKeyId = createEntry
|
|
10865
|
+
const createEntry = allEntryMetadata.find((em) => em.entryType === "doc_create");
|
|
10866
|
+
const decryptionKeyId = createEntry
|
|
10867
|
+
? createEntry.decryptionKeyId
|
|
10868
|
+
: "default";
|
|
10613
10869
|
// Get lastModified from payload, or use the timestamp of the last entry
|
|
10614
10870
|
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
10615
10871
|
const lastModified = payload._lastModified ||
|
|
@@ -10659,16 +10915,16 @@ class BaseMindooDB {
|
|
|
10659
10915
|
convertAutomergeToJS(doc) {
|
|
10660
10916
|
// Check if this document has a native Automerge handle attached
|
|
10661
10917
|
// The native implementation attaches metadata with Symbol.for('_am_meta')
|
|
10662
|
-
const STATE = Symbol.for(
|
|
10918
|
+
const STATE = Symbol.for("_am_meta");
|
|
10663
10919
|
const meta = doc[STATE];
|
|
10664
|
-
if (meta && meta.handle && typeof meta.handle.materialize ===
|
|
10920
|
+
if (meta && meta.handle && typeof meta.handle.materialize === "function") {
|
|
10665
10921
|
// Use native materialize() which properly converts Text objects to strings
|
|
10666
10922
|
try {
|
|
10667
|
-
const materialized = meta.handle.materialize(
|
|
10923
|
+
const materialized = meta.handle.materialize("/");
|
|
10668
10924
|
return materialized;
|
|
10669
10925
|
}
|
|
10670
10926
|
catch (error) {
|
|
10671
|
-
console.error(
|
|
10927
|
+
console.error("[MindooDB] Failed to materialize document:", error);
|
|
10672
10928
|
// Fall through to direct access
|
|
10673
10929
|
}
|
|
10674
10930
|
}
|
|
@@ -10680,7 +10936,9 @@ class BaseMindooDB {
|
|
|
10680
10936
|
convertAutomergeValueToJS(value, depth) {
|
|
10681
10937
|
if (value === null || value === undefined)
|
|
10682
10938
|
return value;
|
|
10683
|
-
if (typeof value === "string" ||
|
|
10939
|
+
if (typeof value === "string" ||
|
|
10940
|
+
typeof value === "number" ||
|
|
10941
|
+
typeof value === "boolean")
|
|
10684
10942
|
return value;
|
|
10685
10943
|
if (value instanceof Uint8Array)
|
|
10686
10944
|
return value;
|
|
@@ -10695,10 +10953,10 @@ class BaseMindooDB {
|
|
|
10695
10953
|
if (typeof value !== "object")
|
|
10696
10954
|
return value;
|
|
10697
10955
|
const record = value;
|
|
10698
|
-
if (typeof record.length === "number"
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10956
|
+
if (typeof record.length === "number" &&
|
|
10957
|
+
typeof value.concat === "function" &&
|
|
10958
|
+
record.kind === undefined &&
|
|
10959
|
+
typeof value.toString === "function") {
|
|
10702
10960
|
const asString = String(value);
|
|
10703
10961
|
if (asString && asString !== "[object Object]")
|
|
10704
10962
|
return asString;
|
|
@@ -10730,7 +10988,10 @@ class BaseMindooDB {
|
|
|
10730
10988
|
get: (target, prop) => {
|
|
10731
10989
|
const value = target[prop];
|
|
10732
10990
|
// Recursively wrap nested objects (but not arrays or special types)
|
|
10733
|
-
if (value !== null &&
|
|
10991
|
+
if (value !== null &&
|
|
10992
|
+
typeof value === "object" &&
|
|
10993
|
+
!Array.isArray(value) &&
|
|
10994
|
+
!(value instanceof Uint8Array)) {
|
|
10734
10995
|
return createReadOnlyProxy(value);
|
|
10735
10996
|
}
|
|
10736
10997
|
return value;
|
|
@@ -10750,8 +11011,8 @@ class BaseMindooDB {
|
|
|
10750
11011
|
isSealed: () => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId),
|
|
10751
11012
|
getRecipients: () => (0, recipients_1.resolveRecipientsFromPayload)(internalDoc.doc, internalDoc.sealedRecipients),
|
|
10752
11013
|
getRecipientEpoch: () => internalDoc.sealedRecipients?.epoch ?? 0,
|
|
10753
|
-
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId)
|
|
10754
|
-
|
|
11014
|
+
isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
|
|
11015
|
+
(0, recipients_1.isPayloadEncryptedFor)(internalDoc.doc, internalDoc.sealedRecipients, Array.isArray(users) ? users : [users]),
|
|
10755
11016
|
getHeads: () => automerge_adapter_1.Automerge.getHeads(internalDoc.doc),
|
|
10756
11017
|
getData: () => {
|
|
10757
11018
|
// Convert Automerge document to plain JS object, converting Text
|
|
@@ -10759,7 +11020,8 @@ class BaseMindooDB {
|
|
|
10759
11020
|
// the document's Automerge heads, so repeated getData() calls on an
|
|
10760
11021
|
// unchanged doc skip the full re-materialization.
|
|
10761
11022
|
const headsKey = automerge_adapter_1.Automerge.getHeads(internalDoc.doc).join(",");
|
|
10762
|
-
if (internalDoc.jsCache === undefined ||
|
|
11023
|
+
if (internalDoc.jsCache === undefined ||
|
|
11024
|
+
internalDoc.jsCacheHeads !== headsKey) {
|
|
10763
11025
|
internalDoc.jsCache = this.convertAutomergeToJS(internalDoc.doc);
|
|
10764
11026
|
internalDoc.jsCacheHeads = headsKey;
|
|
10765
11027
|
}
|
|
@@ -10849,7 +11111,7 @@ class BaseMindooDB {
|
|
|
10849
11111
|
* Get an attachment reference by ID from a document's _attachments array.
|
|
10850
11112
|
*/
|
|
10851
11113
|
async getAttachmentRefInternal(docId, attachmentId) {
|
|
10852
|
-
const internalDoc = this.getCachedDocument(docId) ?? await this.loadDocumentInternal(docId);
|
|
11114
|
+
const internalDoc = this.getCachedDocument(docId) ?? (await this.loadDocumentInternal(docId));
|
|
10853
11115
|
if (!internalDoc) {
|
|
10854
11116
|
throw new Error(`Document ${docId} not found while reading attachment ${attachmentId}`);
|
|
10855
11117
|
}
|
|
@@ -10858,7 +11120,7 @@ class BaseMindooDB {
|
|
|
10858
11120
|
getAttachmentRefFromDoc(internalDoc, attachmentId) {
|
|
10859
11121
|
const payload = internalDoc.doc;
|
|
10860
11122
|
const attachments = payload._attachments || [];
|
|
10861
|
-
const ref = attachments.find(a => a.attachmentId === attachmentId);
|
|
11123
|
+
const ref = attachments.find((a) => a.attachmentId === attachmentId);
|
|
10862
11124
|
if (!ref) {
|
|
10863
11125
|
throw new Error(`Attachment ${attachmentId} not found in document ${internalDoc.id}`);
|
|
10864
11126
|
}
|
|
@@ -10908,7 +11170,9 @@ class BaseMindooDB {
|
|
|
10908
11170
|
async getAttachmentForRefInternal(ref) {
|
|
10909
11171
|
const store = this.getEffectiveAttachmentStore();
|
|
10910
11172
|
// Resolve dependency chain to get all chunk IDs in order (oldest first)
|
|
10911
|
-
const chunkIds = await store.resolveDependencies(ref.lastChunkId, {
|
|
11173
|
+
const chunkIds = await store.resolveDependencies(ref.lastChunkId, {
|
|
11174
|
+
includeStart: true,
|
|
11175
|
+
});
|
|
10912
11176
|
this.logger.debug(`Resolved ${chunkIds.length} chunks for attachment ${ref.attachmentId}`);
|
|
10913
11177
|
// Fetch all chunks
|
|
10914
11178
|
const chunks = await store.getEntries(chunkIds);
|
|
@@ -11060,7 +11324,8 @@ class BaseMindooDB {
|
|
|
11060
11324
|
encryptedData,
|
|
11061
11325
|
};
|
|
11062
11326
|
// Bind the metadata with the author signature (audit finding #5).
|
|
11063
|
-
chunkEntry.metadataSignature =
|
|
11327
|
+
chunkEntry.metadataSignature =
|
|
11328
|
+
await this.computeEntryMetadataSignature(chunkEntry);
|
|
11064
11329
|
chunks.push(chunkEntry);
|
|
11065
11330
|
prevChunkId = chunkId;
|
|
11066
11331
|
}
|
|
@@ -11122,7 +11387,8 @@ class BaseMindooDB {
|
|
|
11122
11387
|
encryptedData,
|
|
11123
11388
|
};
|
|
11124
11389
|
// Bind the metadata with the author signature (audit finding #5).
|
|
11125
|
-
ledgerEntry.metadataSignature =
|
|
11390
|
+
ledgerEntry.metadataSignature =
|
|
11391
|
+
await this.computeEntryMetadataSignature(ledgerEntry);
|
|
11126
11392
|
await this.putEntriesWithRetry(this.store, [ledgerEntry], `pending ledger ${attachmentId}`);
|
|
11127
11393
|
}
|
|
11128
11394
|
async clearPendingAttachmentUploadLedger(attachmentId) {
|
|
@@ -11130,7 +11396,9 @@ class BaseMindooDB {
|
|
|
11130
11396
|
if (!deleteEntriesById) {
|
|
11131
11397
|
return;
|
|
11132
11398
|
}
|
|
11133
|
-
await deleteEntriesById.call(this.store, [
|
|
11399
|
+
await deleteEntriesById.call(this.store, [
|
|
11400
|
+
this.pendingAttachmentUploadLedgerId(attachmentId),
|
|
11401
|
+
]);
|
|
11134
11402
|
}
|
|
11135
11403
|
async cleanupIncompleteAttachmentUpload(docId, attachmentId) {
|
|
11136
11404
|
const store = this.getEffectiveAttachmentStore();
|
|
@@ -11200,7 +11468,8 @@ class BaseMindooDB {
|
|
|
11200
11468
|
encryptedData,
|
|
11201
11469
|
};
|
|
11202
11470
|
// Bind the metadata with the author signature (audit finding #5).
|
|
11203
|
-
chunkEntry.metadataSignature =
|
|
11471
|
+
chunkEntry.metadataSignature =
|
|
11472
|
+
await this.computeEntryMetadataSignature(chunkEntry);
|
|
11204
11473
|
pendingEntries.push(chunkEntry);
|
|
11205
11474
|
pendingEncryptedBytes += encryptedData.length;
|
|
11206
11475
|
chunkCount++;
|
|
@@ -11299,7 +11568,8 @@ class BaseMindooDB {
|
|
|
11299
11568
|
encryptedData,
|
|
11300
11569
|
};
|
|
11301
11570
|
// Bind the metadata with the author signature (audit finding #5).
|
|
11302
|
-
chunkEntry.metadataSignature =
|
|
11571
|
+
chunkEntry.metadataSignature =
|
|
11572
|
+
await this.computeEntryMetadataSignature(chunkEntry);
|
|
11303
11573
|
chunks.push(chunkEntry);
|
|
11304
11574
|
prevChunkId = chunkId;
|
|
11305
11575
|
}
|
|
@@ -11316,10 +11586,13 @@ class BaseMindooDB {
|
|
|
11316
11586
|
* Accepts either a raw store or a MindooDB instance (calls getStore()).
|
|
11317
11587
|
*/
|
|
11318
11588
|
getStoreForKind(storeKind = types_2.StoreKind.docs) {
|
|
11319
|
-
return storeKind === types_2.StoreKind.attachments
|
|
11589
|
+
return storeKind === types_2.StoreKind.attachments
|
|
11590
|
+
? this.attachmentStore
|
|
11591
|
+
: this.store;
|
|
11320
11592
|
}
|
|
11321
11593
|
resolveStore(remote, storeKind = types_2.StoreKind.docs) {
|
|
11322
|
-
if (
|
|
11594
|
+
if ("getStore" in remote &&
|
|
11595
|
+
typeof remote.getStore === "function") {
|
|
11323
11596
|
return storeKind === types_2.StoreKind.attachments
|
|
11324
11597
|
? remote.getAttachmentStore()
|
|
11325
11598
|
: remote.getStore();
|
|
@@ -11415,7 +11688,8 @@ class BaseMindooDB {
|
|
|
11415
11688
|
},
|
|
11416
11689
|
hasWriteContentRules: async (op) => {
|
|
11417
11690
|
const directory = await this.tenant.openDirectory();
|
|
11418
|
-
return (await directory.hasWriteContentRules?.(op, this.store.getId())) ??
|
|
11691
|
+
return ((await directory.hasWriteContentRules?.(op, this.store.getId())) ??
|
|
11692
|
+
false);
|
|
11419
11693
|
},
|
|
11420
11694
|
remapAttachmentPointers: (docId, lastChunkIdByAttachmentId, signing) => this.remapAttachmentPointers(docId, lastChunkIdByAttachmentId, signing),
|
|
11421
11695
|
};
|
|
@@ -11481,8 +11755,11 @@ class BaseMindooDB {
|
|
|
11481
11755
|
this.registerAutomergeHashMapping(docId, automerge_adapter_1.Automerge.decodeChange(changeBytes).hash, entry.id);
|
|
11482
11756
|
internalDoc.doc = newDoc;
|
|
11483
11757
|
internalDoc.lastModified = now;
|
|
11484
|
-
internalDoc.awaitingWitness =
|
|
11485
|
-
|
|
11758
|
+
internalDoc.awaitingWitness =
|
|
11759
|
+
(0, storeEntryTime_1.isProvisional)(entry) || (internalDoc.awaitingWitness ?? false);
|
|
11760
|
+
const versioned = (0, storeEntryTime_1.isVersioned)(entry) ||
|
|
11761
|
+
internalDoc.awaitingWitness ||
|
|
11762
|
+
(internalDoc.witnessed ?? false);
|
|
11486
11763
|
internalDoc.witnessed = versioned && !internalDoc.awaitingWitness;
|
|
11487
11764
|
await this.storeCachedDocument(internalDoc);
|
|
11488
11765
|
this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted, internalDoc.decryptionKeyId, "visible", internalDoc.awaitingWitness, internalDoc.witnessed, true);
|
|
@@ -11567,7 +11844,8 @@ class BaseMindooDB {
|
|
|
11567
11844
|
const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
|
|
11568
11845
|
const localStore = this.getStoreForKind(storeKind);
|
|
11569
11846
|
const remoteStore = this.resolveStore(remote, storeKind);
|
|
11570
|
-
if (localStore.getId() !== remoteStore.getId() ||
|
|
11847
|
+
if (localStore.getId() !== remoteStore.getId() ||
|
|
11848
|
+
localStore.getStoreKind() !== remoteStore.getStoreKind()) {
|
|
11571
11849
|
throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
|
|
11572
11850
|
}
|
|
11573
11851
|
this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}/${remoteStore.getStoreKind()}`);
|
|
@@ -11590,10 +11868,15 @@ class BaseMindooDB {
|
|
|
11590
11868
|
if (storeKind === types_2.StoreKind.docs) {
|
|
11591
11869
|
await this.reconcileAfterDocsPull();
|
|
11592
11870
|
}
|
|
11593
|
-
return {
|
|
11871
|
+
return {
|
|
11872
|
+
transferredEntries: 0,
|
|
11873
|
+
transferredBytes,
|
|
11874
|
+
scannedEntries: syncResult.scanned,
|
|
11875
|
+
cancelled: false,
|
|
11876
|
+
};
|
|
11594
11877
|
}
|
|
11595
11878
|
options?.onProgress?.({
|
|
11596
|
-
phase:
|
|
11879
|
+
phase: "processing",
|
|
11597
11880
|
message: `Processing ${syncResult.transferred} new entries...`,
|
|
11598
11881
|
transferredEntries: syncResult.transferred,
|
|
11599
11882
|
transferredBytes,
|
|
@@ -11625,7 +11908,7 @@ class BaseMindooDB {
|
|
|
11625
11908
|
}
|
|
11626
11909
|
this.logger.info(`Pull complete, synced ${syncResult.transferred} entries`);
|
|
11627
11910
|
options?.onProgress?.({
|
|
11628
|
-
phase:
|
|
11911
|
+
phase: "complete",
|
|
11629
11912
|
message: `Pull complete: ${syncResult.transferred} ${storeKind} entries synced`,
|
|
11630
11913
|
transferredEntries: syncResult.transferred,
|
|
11631
11914
|
transferredBytes,
|
|
@@ -11682,7 +11965,8 @@ class BaseMindooDB {
|
|
|
11682
11965
|
const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
|
|
11683
11966
|
const localStore = this.getStoreForKind(storeKind);
|
|
11684
11967
|
const remoteStore = this.resolveStore(remote, storeKind);
|
|
11685
|
-
if (localStore.getId() !== remoteStore.getId() ||
|
|
11968
|
+
if (localStore.getId() !== remoteStore.getId() ||
|
|
11969
|
+
localStore.getStoreKind() !== remoteStore.getStoreKind()) {
|
|
11686
11970
|
throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
|
|
11687
11971
|
}
|
|
11688
11972
|
this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}/${remoteStore.getStoreKind()}`);
|
|
@@ -11713,7 +11997,7 @@ class BaseMindooDB {
|
|
|
11713
11997
|
this.logger.info(`Pushed ${syncResult.transferred} entries to remote store`);
|
|
11714
11998
|
}
|
|
11715
11999
|
options?.onProgress?.({
|
|
11716
|
-
phase:
|
|
12000
|
+
phase: "complete",
|
|
11717
12001
|
message: `Push complete: ${syncResult.transferred} entries transferred`,
|
|
11718
12002
|
transferredEntries: syncResult.transferred,
|
|
11719
12003
|
transferredBytes,
|