mindoodb 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-server.md +1 -1
- package/README.md +1 -0
- package/dist/browser/index.mjs +5 -0
- package/dist/core/BaseMindooDB.d.ts +16 -6
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +145 -41
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +3 -2
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +6 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts +14 -0
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
- package/dist/core/appendonlystores/syncStores.js +39 -2
- package/dist/core/appendonlystores/syncStores.js.map +1 -1
- package/dist/core/changeAttribution.d.ts +66 -0
- package/dist/core/changeAttribution.d.ts.map +1 -0
- package/dist/core/changeAttribution.js +109 -0
- package/dist/core/changeAttribution.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +58 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/userkeys/sealedTypes.d.ts +2 -0
- package/dist/core/userkeys/sealedTypes.d.ts.map +1 -1
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +18 -0
- package/dist/node/server/TenantManager.js.map +1 -1
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@ exports.BaseMindooDB = exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES = void 0;
|
|
|
7
7
|
const automerge_adapter_1 = require("./automerge-adapter");
|
|
8
8
|
const values_1 = require("./values");
|
|
9
9
|
const storeEntryTime_1 = require("./storeEntryTime");
|
|
10
|
+
const changeAttribution_1 = require("./changeAttribution");
|
|
10
11
|
const types_1 = require("./types");
|
|
11
12
|
const types_2 = require("./types");
|
|
12
13
|
const types_3 = require("./appendonlystores/types");
|
|
@@ -214,6 +215,14 @@ exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES = new Uint8Array([
|
|
|
214
215
|
function getCustomIdInitialChangeBytes() {
|
|
215
216
|
return exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES;
|
|
216
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether a document's entries contain what it starts from: its `doc_create`, or a
|
|
220
|
+
* snapshot (the only entry a dense sync may transfer). Change entries alone do not
|
|
221
|
+
* make a document; they wait for their create (docs/accesscontrol.md §10).
|
|
222
|
+
*/
|
|
223
|
+
function hasDocumentOrigin(metadata) {
|
|
224
|
+
return metadata.some((entry) => entry.entryType === "doc_create" || entry.entryType === "doc_snapshot");
|
|
225
|
+
}
|
|
217
226
|
/**
|
|
218
227
|
* BaseMindooDB is a platform-agnostic implementation of MindooDB
|
|
219
228
|
* that works in both browser and server environments.
|
|
@@ -2725,7 +2734,7 @@ class BaseMindooDB {
|
|
|
2725
2734
|
entry.entryType === "doc_delete" ||
|
|
2726
2735
|
entry.entryType === "doc_undelete" ||
|
|
2727
2736
|
entry.entryType === "doc_snapshot");
|
|
2728
|
-
if (lifecycleEntries.length === 0) {
|
|
2737
|
+
if (lifecycleEntries.length === 0 || !hasDocumentOrigin(lifecycleEntries)) {
|
|
2729
2738
|
return null;
|
|
2730
2739
|
}
|
|
2731
2740
|
lifecycleEntries.sort((left, right) => left.createdAt !== right.createdAt
|
|
@@ -3280,7 +3289,24 @@ class BaseMindooDB {
|
|
|
3280
3289
|
// Before indexing, verify the user has the decryption key so that
|
|
3281
3290
|
// documents the user cannot access do not appear in getAllDocumentIds.
|
|
3282
3291
|
const representativeEntry = docLifecycleEntries[0];
|
|
3283
|
-
|
|
3292
|
+
// A sealed doc's wrap list rides only on its create and recipient
|
|
3293
|
+
// mutation entries, so an incremental batch of plain changes has
|
|
3294
|
+
// none. With a cold session cache (fresh session, warm start that
|
|
3295
|
+
// skipped reconcileKeyVisibility) the key must come from the doc's
|
|
3296
|
+
// full history, or the doc is wrongly flipped to "inaccessible"
|
|
3297
|
+
// and that state is persisted with the next cache flush.
|
|
3298
|
+
let fullDocMetadata;
|
|
3299
|
+
let sealedIngestEntries = docLifecycleEntries;
|
|
3300
|
+
if (!coldSync &&
|
|
3301
|
+
(0, sealedTypes_1.isSealedKeyId)(representativeEntry.decryptionKeyId) &&
|
|
3302
|
+
!(0, recipients_1.newestRecipientBlock)(docLifecycleEntries) &&
|
|
3303
|
+
!(await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId))) {
|
|
3304
|
+
fullDocMetadata = await this.scanAllMetadata(this.store, {
|
|
3305
|
+
docId,
|
|
3306
|
+
});
|
|
3307
|
+
sealedIngestEntries = fullDocMetadata;
|
|
3308
|
+
}
|
|
3309
|
+
await this.tenant.ingestSealedFromEntries?.(representativeEntry.decryptionKeyId, sealedIngestEntries);
|
|
3284
3310
|
const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
|
|
3285
3311
|
if (!keyAvailable) {
|
|
3286
3312
|
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
@@ -3305,7 +3331,16 @@ class BaseMindooDB {
|
|
|
3305
3331
|
// have recent entries, so re-scan that doc for prior lifecycle state.
|
|
3306
3332
|
const allDocMetadata = coldSync
|
|
3307
3333
|
? entryMetadataList
|
|
3308
|
-
:
|
|
3334
|
+
: (fullDocMetadata ??
|
|
3335
|
+
(await this.scanAllMetadata(this.store, { docId })));
|
|
3336
|
+
if (!hasDocumentOrigin(allDocMetadata)) {
|
|
3337
|
+
// Changes whose create has not arrived (parallel transfer
|
|
3338
|
+
// batches) or never will (refused by the server's access
|
|
3339
|
+
// rules). Indexing them would list an id `getDocument` cannot
|
|
3340
|
+
// load; the create's arrival indexes the document then.
|
|
3341
|
+
this.logger.debug(`Skipping metadata-first index for doc ${docId} — no doc_create (or snapshot) yet`);
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3309
3344
|
const mutationEntries = allDocMetadata.filter((e) => this.isDocumentReplayEntry(e));
|
|
3310
3345
|
// Snapshots compact replay history but should not change the user-visible
|
|
3311
3346
|
// modification time when the original create/change/delete/undelete entries still exist.
|
|
@@ -4020,6 +4055,7 @@ class BaseMindooDB {
|
|
|
4020
4055
|
}
|
|
4021
4056
|
this.validateIdPrefixOption("createDocuments", options);
|
|
4022
4057
|
this.validateAssumeUniqueIdOption("createDocuments", options);
|
|
4058
|
+
(0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
4023
4059
|
}
|
|
4024
4060
|
const results = new Array(inputs.length).fill(null);
|
|
4025
4061
|
const prepared = [];
|
|
@@ -4046,7 +4082,7 @@ class BaseMindooDB {
|
|
|
4046
4082
|
for (const [key, value] of valueEntries) {
|
|
4047
4083
|
data[key] = value;
|
|
4048
4084
|
}
|
|
4049
|
-
}, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
4085
|
+
}, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck, undefined, undefined, options.attribution);
|
|
4050
4086
|
}
|
|
4051
4087
|
results[index] = existingDoc;
|
|
4052
4088
|
continue;
|
|
@@ -4163,6 +4199,9 @@ class BaseMindooDB {
|
|
|
4163
4199
|
// of concurrent same-id creates is then not a concern and the create can
|
|
4164
4200
|
// follow the generated-ID path (initialValues baked into doc_create).
|
|
4165
4201
|
const useDeterministicSeed = useCustomDocId && !options.assumeUniqueId;
|
|
4202
|
+
// Validated before anything is written. The deterministic seed change of a
|
|
4203
|
+
// convergent custom id is fixed bytes and cannot carry it.
|
|
4204
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
4166
4205
|
// Sanitize caller-provided initial values: internal/reserved fields (those
|
|
4167
4206
|
// starting with "_", e.g. "_attachments") are managed by MindooDB and must
|
|
4168
4207
|
// not be seeded by callers. `undefined` values are treated as "not set"
|
|
@@ -4210,7 +4249,7 @@ class BaseMindooDB {
|
|
|
4210
4249
|
if (existing) {
|
|
4211
4250
|
if (existing.isDeleted) {
|
|
4212
4251
|
this.logger.debug(`Document ${docId} exists as a tombstone; undeleting existing custom-id document`);
|
|
4213
|
-
await this.undeleteDocInternal(docId, signingKeyPair, signingKeyPassword);
|
|
4252
|
+
await this.undeleteDocInternal(docId, signingKeyPair, signingKeyPassword, undefined, options.attribution);
|
|
4214
4253
|
const undeleted = await this.loadDocumentInternal(docId);
|
|
4215
4254
|
if (!undeleted || undeleted.isDeleted) {
|
|
4216
4255
|
throw new Error(`Document ${docId} could not be undeleted`);
|
|
@@ -4253,7 +4292,7 @@ class BaseMindooDB {
|
|
|
4253
4292
|
this.logger.debug(`Creating initial Automerge change for document ${docId}`);
|
|
4254
4293
|
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
4255
4294
|
try {
|
|
4256
|
-
newDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
|
|
4295
|
+
newDoc = automerge_adapter_1.Automerge.change(initialDoc, changeMessage ? { message: changeMessage } : {}, (doc) => {
|
|
4257
4296
|
// Store metadata in the document payload
|
|
4258
4297
|
// We need to modify the document to ensure a change is created
|
|
4259
4298
|
doc._attachments = [];
|
|
@@ -4429,6 +4468,7 @@ class BaseMindooDB {
|
|
|
4429
4468
|
// out of the deterministic seed, so initialValues fold into doc_create
|
|
4430
4469
|
// and the document costs a single store entry.
|
|
4431
4470
|
const useDeterministicSeed = useCustomDocId && !options.assumeUniqueId;
|
|
4471
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
4432
4472
|
if (useDeterministicSeed && options.recipients !== undefined) {
|
|
4433
4473
|
throw new Error("createDocuments: recipients is not supported together with a convergent custom id; pass assumeUniqueId");
|
|
4434
4474
|
}
|
|
@@ -4463,7 +4503,7 @@ class BaseMindooDB {
|
|
|
4463
4503
|
}
|
|
4464
4504
|
else {
|
|
4465
4505
|
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
4466
|
-
seedDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
|
|
4506
|
+
seedDoc = automerge_adapter_1.Automerge.change(initialDoc, changeMessage ? { message: changeMessage } : {}, (doc) => {
|
|
4467
4507
|
doc._attachments = [];
|
|
4468
4508
|
if (sealedCreate) {
|
|
4469
4509
|
(0, recipients_1.applyEncryptForCreate)(doc, sealedCreate.encryptFor);
|
|
@@ -4487,7 +4527,7 @@ class BaseMindooDB {
|
|
|
4487
4527
|
let finalDoc = seedDoc;
|
|
4488
4528
|
let valueChangeBytes = null;
|
|
4489
4529
|
if (useDeterministicSeed && initialValueEntries.length > 0) {
|
|
4490
|
-
finalDoc = automerge_adapter_1.Automerge.change(seedDoc, { time: now }, (doc) => {
|
|
4530
|
+
finalDoc = automerge_adapter_1.Automerge.change(seedDoc, { time: now, message: changeMessage }, (doc) => {
|
|
4491
4531
|
for (const [key, value] of initialValueEntries) {
|
|
4492
4532
|
this.assignHydrated(doc, key, value);
|
|
4493
4533
|
}
|
|
@@ -4923,12 +4963,14 @@ class BaseMindooDB {
|
|
|
4923
4963
|
};
|
|
4924
4964
|
// Wrap and yield (including lifecycle terminal entries)
|
|
4925
4965
|
const wrappedDoc = this.wrapDocument(internalDoc);
|
|
4966
|
+
const attribution = this.attributionFromChangePayload(entryMetadata.entryType, decryptedPayload);
|
|
4926
4967
|
yield {
|
|
4927
4968
|
changeEntryId: entryMetadata.id,
|
|
4928
4969
|
doc: wrappedDoc,
|
|
4929
4970
|
changeCreatedAt: entryMetadata.createdAt,
|
|
4930
4971
|
changeCreatedByPublicKey: entryMetadata.createdByPublicKey,
|
|
4931
4972
|
changeDependencyIds: [...entryMetadata.dependencyIds],
|
|
4973
|
+
...(attribution ? { attribution } : {}),
|
|
4932
4974
|
};
|
|
4933
4975
|
}
|
|
4934
4976
|
}
|
|
@@ -5393,6 +5435,40 @@ class BaseMindooDB {
|
|
|
5393
5435
|
this.revisionFeedDocCache.delete(oldest);
|
|
5394
5436
|
}
|
|
5395
5437
|
}
|
|
5438
|
+
/**
|
|
5439
|
+
* The agent attribution carried by a document change payload (the Automerge
|
|
5440
|
+
* change message), if any. Snapshots and undecodable payloads yield none.
|
|
5441
|
+
*/
|
|
5442
|
+
attributionFromChangePayload(entryType, payload) {
|
|
5443
|
+
if (entryType !== "doc_create" &&
|
|
5444
|
+
entryType !== "doc_change" &&
|
|
5445
|
+
entryType !== "doc_delete" &&
|
|
5446
|
+
entryType !== "doc_undelete") {
|
|
5447
|
+
return undefined;
|
|
5448
|
+
}
|
|
5449
|
+
try {
|
|
5450
|
+
return (0, changeAttribution_1.decodeChangeAttribution)(automerge_adapter_1.Automerge.decodeChange(payload).message);
|
|
5451
|
+
}
|
|
5452
|
+
catch {
|
|
5453
|
+
return undefined;
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
/**
|
|
5457
|
+
* Verify, decrypt and read the attribution of one stored entry. Entries with
|
|
5458
|
+
* an invalid signature or a payload this replica cannot decrypt yield none.
|
|
5459
|
+
*/
|
|
5460
|
+
async readStoreEntryAttribution(entry) {
|
|
5461
|
+
try {
|
|
5462
|
+
if (!(await this.tenant.verifyEntrySignature(entry, entry.encryptedData))) {
|
|
5463
|
+
return undefined;
|
|
5464
|
+
}
|
|
5465
|
+
const payload = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
|
|
5466
|
+
return this.attributionFromChangePayload(entry.entryType, payload);
|
|
5467
|
+
}
|
|
5468
|
+
catch {
|
|
5469
|
+
return undefined;
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5396
5472
|
async getDocumentHistoryPage(docId, options) {
|
|
5397
5473
|
const startedAt = Date.now();
|
|
5398
5474
|
const limit = Math.max(1, Math.floor(options?.limit ?? 100));
|
|
@@ -5422,6 +5498,21 @@ class BaseMindooDB {
|
|
|
5422
5498
|
// order for same-millisecond entries.
|
|
5423
5499
|
isDeleted: this.computeIsDeletedFromMetadata(relevantEntries.slice(0, offset + sliceIndex + 1)),
|
|
5424
5500
|
}));
|
|
5501
|
+
if (options?.includeAttribution && slice.length > 0) {
|
|
5502
|
+
// Opt-in: costs one load + signature check + decrypt per returned entry,
|
|
5503
|
+
// still bounded by the page size.
|
|
5504
|
+
await this.ingestSealedKeyFromEntries(slice);
|
|
5505
|
+
const loaded = await this.store.getEntries(slice.map((entry) => entry.id));
|
|
5506
|
+
const loadedById = new Map(loaded.map((entry) => [entry.id, entry]));
|
|
5507
|
+
for (const pageEntry of entries) {
|
|
5508
|
+
const stored = loadedById.get(pageEntry.entryId);
|
|
5509
|
+
const attribution = stored
|
|
5510
|
+
? await this.readStoreEntryAttribution(stored)
|
|
5511
|
+
: undefined;
|
|
5512
|
+
if (attribution)
|
|
5513
|
+
pageEntry.attribution = attribution;
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5425
5516
|
const nextOffset = offset + entries.length;
|
|
5426
5517
|
const hasMore = nextOffset < relevantEntries.length;
|
|
5427
5518
|
const result = {
|
|
@@ -7112,7 +7203,7 @@ class BaseMindooDB {
|
|
|
7112
7203
|
return docIds;
|
|
7113
7204
|
}
|
|
7114
7205
|
async deleteDocument(docId, options = {}) {
|
|
7115
|
-
return this.deleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
7206
|
+
return this.deleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck, options.attribution);
|
|
7116
7207
|
}
|
|
7117
7208
|
/**
|
|
7118
7209
|
* Bulk-delete documents in a single append-only store write.
|
|
@@ -7133,6 +7224,7 @@ class BaseMindooDB {
|
|
|
7133
7224
|
if (docIds.length === 0) {
|
|
7134
7225
|
return;
|
|
7135
7226
|
}
|
|
7227
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7136
7228
|
// One gate call per document id: inside `userdirectory` the applicable rule
|
|
7137
7229
|
// depends on the id (personal documents are owned by their creator), so a
|
|
7138
7230
|
// single batch-wide check would deny an owner deleting their own data.
|
|
@@ -7169,7 +7261,7 @@ class BaseMindooDB {
|
|
|
7169
7261
|
this.logger.debug(`Skipping bulk delete for ${docId} — not found or already deleted`);
|
|
7170
7262
|
continue;
|
|
7171
7263
|
}
|
|
7172
|
-
preparedByIndex[index] = await this.prepareLifecycleEntry(internalDoc, "doc_delete", options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
7264
|
+
preparedByIndex[index] = await this.prepareLifecycleEntry(internalDoc, "doc_delete", options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck, changeMessage);
|
|
7173
7265
|
}
|
|
7174
7266
|
}));
|
|
7175
7267
|
const prepared = preparedByIndex.filter((p) => p !== null);
|
|
@@ -7189,7 +7281,7 @@ class BaseMindooDB {
|
|
|
7189
7281
|
return this.deleteDocument(docId, { signingKeyPair, signingKeyPassword });
|
|
7190
7282
|
}
|
|
7191
7283
|
async undeleteDocument(docId, options = {}) {
|
|
7192
|
-
return this.undeleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
7284
|
+
return this.undeleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck, options.attribution);
|
|
7193
7285
|
}
|
|
7194
7286
|
async assertLifecycleMutationAllowed(op, signingKeyPair, signingKeyPassword, documentUsernameHash, docId) {
|
|
7195
7287
|
this.assertWritable("document lifecycle mutation");
|
|
@@ -7207,8 +7299,8 @@ class BaseMindooDB {
|
|
|
7207
7299
|
* change that advances `_lastModified`, wraps it in the requested lifecycle
|
|
7208
7300
|
* StoreEntry type, signs/encrypts it, and updates local caches/indexes.
|
|
7209
7301
|
*/
|
|
7210
|
-
async writeLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
7211
|
-
const prepared = await this.prepareLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck);
|
|
7302
|
+
async writeLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck, attribution) {
|
|
7303
|
+
const prepared = await this.prepareLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck, (0, changeAttribution_1.encodeChangeAttribution)(attribution));
|
|
7212
7304
|
await this.putLocalEntries([prepared.fullEntry]);
|
|
7213
7305
|
await this.applyLifecycleEntryLocally(prepared, { markDirty: true });
|
|
7214
7306
|
}
|
|
@@ -7218,7 +7310,7 @@ class BaseMindooDB {
|
|
|
7218
7310
|
* returned `fullEntry` (single or batched `putEntries`) and then call
|
|
7219
7311
|
* {@link applyLifecycleEntryLocally} to update caches and indexes.
|
|
7220
7312
|
*/
|
|
7221
|
-
async prepareLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
7313
|
+
async prepareLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck, changeMessage) {
|
|
7222
7314
|
// Delete and undelete are encoded as normal Automerge changes so the DAG
|
|
7223
7315
|
// keeps causal ancestry, while the StoreEntry type carries lifecycle intent.
|
|
7224
7316
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
@@ -7238,7 +7330,7 @@ class BaseMindooDB {
|
|
|
7238
7330
|
// `emptyChange`, which always advances the heads with a fresh hash while
|
|
7239
7331
|
// keeping causal ancestry intact.
|
|
7240
7332
|
const headsBefore = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
7241
|
-
let newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => automerge_adapter_1.Automerge.change(doc, { time: now }, (mutableDoc) => {
|
|
7333
|
+
let newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => automerge_adapter_1.Automerge.change(doc, { time: now, message: changeMessage }, (mutableDoc) => {
|
|
7242
7334
|
if (!Array.isArray(mutableDoc._attachments)) {
|
|
7243
7335
|
mutableDoc._attachments = [];
|
|
7244
7336
|
}
|
|
@@ -7247,6 +7339,7 @@ class BaseMindooDB {
|
|
|
7247
7339
|
if (automerge_adapter_1.Automerge.getHeads(newDoc).every((head) => headsBefore.includes(head))) {
|
|
7248
7340
|
newDoc = automerge_adapter_1.Automerge.emptyChange(newDoc, {
|
|
7249
7341
|
time: now,
|
|
7342
|
+
message: changeMessage,
|
|
7250
7343
|
});
|
|
7251
7344
|
}
|
|
7252
7345
|
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
@@ -7373,7 +7466,8 @@ class BaseMindooDB {
|
|
|
7373
7466
|
* Handles both regular deletion (signed by current user) and
|
|
7374
7467
|
* deletion with a custom signing key (e.g., for directory operations).
|
|
7375
7468
|
*/
|
|
7376
|
-
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
7469
|
+
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck, attribution) {
|
|
7470
|
+
(0, changeAttribution_1.encodeChangeAttribution)(attribution);
|
|
7377
7471
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
7378
7472
|
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? " using custom signing key" : ""}`);
|
|
7379
7473
|
await this.assertLifecycleMutationAllowed("doc_delete", signingKeyPair, signingKeyPassword, undefined, docId);
|
|
@@ -7382,10 +7476,11 @@ class BaseMindooDB {
|
|
|
7382
7476
|
if (!internalDoc || internalDoc.isDeleted) {
|
|
7383
7477
|
throw new Error(`Document ${docId} not found or already deleted`);
|
|
7384
7478
|
}
|
|
7385
|
-
await this.writeLifecycleEntry(internalDoc, "doc_delete", signingKeyPair, signingKeyPassword, bypassPrecheck);
|
|
7479
|
+
await this.writeLifecycleEntry(internalDoc, "doc_delete", signingKeyPair, signingKeyPassword, bypassPrecheck, attribution);
|
|
7386
7480
|
this.logger.info(`Document ${docId} deleted successfully`);
|
|
7387
7481
|
}
|
|
7388
|
-
async undeleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
7482
|
+
async undeleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck, attribution) {
|
|
7483
|
+
(0, changeAttribution_1.encodeChangeAttribution)(attribution);
|
|
7389
7484
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
7390
7485
|
this.logger.debug(`Undeleting document ${docId}${useCustomSigningKey ? " using custom signing key" : ""}`);
|
|
7391
7486
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
@@ -7397,11 +7492,11 @@ class BaseMindooDB {
|
|
|
7397
7492
|
this.logger.debug(`Document ${docId} is already alive; undelete is a no-op`);
|
|
7398
7493
|
return;
|
|
7399
7494
|
}
|
|
7400
|
-
await this.writeLifecycleEntry(internalDoc, "doc_undelete", signingKeyPair, signingKeyPassword, bypassPrecheck);
|
|
7495
|
+
await this.writeLifecycleEntry(internalDoc, "doc_undelete", signingKeyPair, signingKeyPassword, bypassPrecheck, attribution);
|
|
7401
7496
|
this.logger.info(`Document ${docId} undeleted successfully`);
|
|
7402
7497
|
}
|
|
7403
7498
|
async changeDoc(doc, changeFunc, options = {}) {
|
|
7404
|
-
return this.changeDocInternal(doc, changeFunc, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
7499
|
+
return this.changeDocInternal(doc, changeFunc, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck, undefined, undefined, options.attribution);
|
|
7405
7500
|
}
|
|
7406
7501
|
async changeDocWithSigningKey(doc, changeFunc, signingKeyPair, signingKeyPassword) {
|
|
7407
7502
|
return this.changeDoc(doc, changeFunc, {
|
|
@@ -7409,8 +7504,9 @@ class BaseMindooDB {
|
|
|
7409
7504
|
signingKeyPassword,
|
|
7410
7505
|
});
|
|
7411
7506
|
}
|
|
7412
|
-
async applyTextPatch(doc, patch) {
|
|
7507
|
+
async applyTextPatch(doc, patch, options = {}) {
|
|
7413
7508
|
this.assertWritable("applyTextPatch");
|
|
7509
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7414
7510
|
const docId = doc.getId();
|
|
7415
7511
|
this.logger.debug(`===== applyTextPatch called for document ${docId} =====`);
|
|
7416
7512
|
let internalDoc = this.getCachedDocument(docId);
|
|
@@ -7439,10 +7535,10 @@ class BaseMindooDB {
|
|
|
7439
7535
|
try {
|
|
7440
7536
|
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
|
|
7441
7537
|
if (baseHeads && baseHeads.length > 0) {
|
|
7442
|
-
const result = automerge_adapter_1.Automerge.changeAt(doc, baseHeads, applyEdits);
|
|
7538
|
+
const result = automerge_adapter_1.Automerge.changeAt(doc, baseHeads, { message: changeMessage }, applyEdits);
|
|
7443
7539
|
return result.newDoc;
|
|
7444
7540
|
}
|
|
7445
|
-
return automerge_adapter_1.Automerge.change(doc, applyEdits);
|
|
7541
|
+
return automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, applyEdits);
|
|
7446
7542
|
});
|
|
7447
7543
|
if (this.isDebugEnabled())
|
|
7448
7544
|
this.logger.debug(`Successfully applied text patch, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
@@ -7466,8 +7562,9 @@ class BaseMindooDB {
|
|
|
7466
7562
|
data: wrapped.getData(),
|
|
7467
7563
|
};
|
|
7468
7564
|
}
|
|
7469
|
-
async applyRichTextPatch(doc, patch) {
|
|
7565
|
+
async applyRichTextPatch(doc, patch, options = {}) {
|
|
7470
7566
|
this.assertWritable("applyRichTextPatch");
|
|
7567
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7471
7568
|
const docId = doc.getId();
|
|
7472
7569
|
this.logger.debug(`===== applyRichTextPatch called for document ${docId} =====`);
|
|
7473
7570
|
let internalDoc = this.getCachedDocument(docId);
|
|
@@ -7507,9 +7604,9 @@ class BaseMindooDB {
|
|
|
7507
7604
|
try {
|
|
7508
7605
|
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
|
|
7509
7606
|
if (patch.baseHeads && patch.baseHeads.length > 0) {
|
|
7510
|
-
return this.changeOnForkAt(doc, patch.baseHeads, applySpans);
|
|
7607
|
+
return this.changeOnForkAt(doc, patch.baseHeads, applySpans, changeMessage);
|
|
7511
7608
|
}
|
|
7512
|
-
return automerge_adapter_1.Automerge.change(doc, applySpans);
|
|
7609
|
+
return automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, applySpans);
|
|
7513
7610
|
});
|
|
7514
7611
|
if (this.isDebugEnabled())
|
|
7515
7612
|
this.logger.debug(`Successfully applied rich-text patch, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
@@ -7533,8 +7630,9 @@ class BaseMindooDB {
|
|
|
7533
7630
|
data: wrapped.getData(),
|
|
7534
7631
|
};
|
|
7535
7632
|
}
|
|
7536
|
-
async applyDocumentUpdate(doc, update) {
|
|
7633
|
+
async applyDocumentUpdate(doc, update, options = {}) {
|
|
7537
7634
|
this.assertWritable("applyDocumentUpdate");
|
|
7635
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7538
7636
|
const docId = doc.getId();
|
|
7539
7637
|
let internalDoc = this.getCachedDocument(docId);
|
|
7540
7638
|
if (!internalDoc) {
|
|
@@ -7587,8 +7685,8 @@ class BaseMindooDB {
|
|
|
7587
7685
|
let newDoc;
|
|
7588
7686
|
try {
|
|
7589
7687
|
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (current) => baseHeads
|
|
7590
|
-
? this.changeOnForkAt(current, baseHeads, applyAll)
|
|
7591
|
-
: automerge_adapter_1.Automerge.change(current, applyAll));
|
|
7688
|
+
? this.changeOnForkAt(current, baseHeads, applyAll, changeMessage)
|
|
7689
|
+
: automerge_adapter_1.Automerge.change(current, { message: changeMessage }, applyAll));
|
|
7592
7690
|
}
|
|
7593
7691
|
catch (error) {
|
|
7594
7692
|
this.logger.error(`Error applying document update for document ${docId}:`, error);
|
|
@@ -7647,8 +7745,9 @@ class BaseMindooDB {
|
|
|
7647
7745
|
}
|
|
7648
7746
|
}
|
|
7649
7747
|
}
|
|
7650
|
-
async applyRichTextStepsPatch(doc, patch) {
|
|
7748
|
+
async applyRichTextStepsPatch(doc, patch, options = {}) {
|
|
7651
7749
|
this.assertWritable("applyRichTextStepsPatch");
|
|
7750
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7652
7751
|
const docId = doc.getId();
|
|
7653
7752
|
this.logger.debug(`===== applyRichTextStepsPatch called for document ${docId} =====`);
|
|
7654
7753
|
let internalDoc = this.getCachedDocument(docId);
|
|
@@ -7700,10 +7799,10 @@ class BaseMindooDB {
|
|
|
7700
7799
|
try {
|
|
7701
7800
|
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
|
|
7702
7801
|
if (patch.baseHeads && patch.baseHeads.length > 0) {
|
|
7703
|
-
const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, applySteps);
|
|
7802
|
+
const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, { message: changeMessage }, applySteps);
|
|
7704
7803
|
return result.newDoc;
|
|
7705
7804
|
}
|
|
7706
|
-
return automerge_adapter_1.Automerge.change(doc, applySteps);
|
|
7805
|
+
return automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, applySteps);
|
|
7707
7806
|
});
|
|
7708
7807
|
if (this.isDebugEnabled())
|
|
7709
7808
|
this.logger.debug(`Successfully applied rich-text steps, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
@@ -7943,8 +8042,9 @@ class BaseMindooDB {
|
|
|
7943
8042
|
}
|
|
7944
8043
|
return internalDoc;
|
|
7945
8044
|
}
|
|
7946
|
-
async applyJsonPatch(doc, patch) {
|
|
8045
|
+
async applyJsonPatch(doc, patch, options = {}) {
|
|
7947
8046
|
this.assertWritable("applyJsonPatch");
|
|
8047
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
|
|
7948
8048
|
const docId = doc.getId();
|
|
7949
8049
|
this.logger.debug(`===== applyJsonPatch called for document ${docId} =====`);
|
|
7950
8050
|
let internalDoc = this.getCachedDocument(docId);
|
|
@@ -7969,10 +8069,10 @@ class BaseMindooDB {
|
|
|
7969
8069
|
try {
|
|
7970
8070
|
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
|
|
7971
8071
|
if (patch.baseHeads && patch.baseHeads.length > 0) {
|
|
7972
|
-
const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, applyPatch);
|
|
8072
|
+
const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, { message: changeMessage }, applyPatch);
|
|
7973
8073
|
return result.newDoc;
|
|
7974
8074
|
}
|
|
7975
|
-
return automerge_adapter_1.Automerge.change(doc, applyPatch);
|
|
8075
|
+
return automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, applyPatch);
|
|
7976
8076
|
});
|
|
7977
8077
|
if (this.isDebugEnabled())
|
|
7978
8078
|
this.logger.debug(`Successfully applied JSON patch, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
@@ -7996,8 +8096,9 @@ class BaseMindooDB {
|
|
|
7996
8096
|
data: wrapped.getData(),
|
|
7997
8097
|
};
|
|
7998
8098
|
}
|
|
7999
|
-
async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword, bypassPrecheck, restoredAttachments = [], internalMutation) {
|
|
8099
|
+
async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword, bypassPrecheck, restoredAttachments = [], internalMutation, attribution) {
|
|
8000
8100
|
this.assertWritable("changeDoc");
|
|
8101
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(attribution);
|
|
8001
8102
|
const docId = doc.getId();
|
|
8002
8103
|
if ((signingKeyPair !== undefined) !== (signingKeyPassword !== undefined)) {
|
|
8003
8104
|
throw new Error("changeDoc: signingKeyPair and signingKeyPassword must be provided together");
|
|
@@ -8292,7 +8393,7 @@ class BaseMindooDB {
|
|
|
8292
8393
|
const headsBeforeChange = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
8293
8394
|
let newDoc;
|
|
8294
8395
|
try {
|
|
8295
|
-
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => automerge_adapter_1.Automerge.change(doc, (automergeDoc) => {
|
|
8396
|
+
newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, (automergeDoc) => {
|
|
8296
8397
|
// Apply all pending changes (sets/updates)
|
|
8297
8398
|
for (const [key, value] of pendingChanges) {
|
|
8298
8399
|
this.assignHydrated(automergeDoc, key, this.hydrateTypedValues(value));
|
|
@@ -9079,17 +9180,17 @@ class BaseMindooDB {
|
|
|
9079
9180
|
* after `baseHeads` (its next sequence number is taken then); in that case a
|
|
9080
9181
|
* fresh actor writes the one change.
|
|
9081
9182
|
*/
|
|
9082
|
-
changeOnForkAt(doc, baseHeads, apply) {
|
|
9183
|
+
changeOnForkAt(doc, baseHeads, apply, changeMessage) {
|
|
9083
9184
|
const current = automerge_adapter_1.Automerge.getHeads(doc);
|
|
9084
9185
|
if (current.length === baseHeads.length &&
|
|
9085
9186
|
current.every((head) => baseHeads.includes(head))) {
|
|
9086
|
-
return automerge_adapter_1.Automerge.change(doc, apply);
|
|
9187
|
+
return automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, apply);
|
|
9087
9188
|
}
|
|
9088
9189
|
const base = automerge_adapter_1.Automerge.view(doc, baseHeads);
|
|
9089
9190
|
const actor = automerge_adapter_1.Automerge.getActorId(doc);
|
|
9090
9191
|
const ownChangeSince = automerge_adapter_1.Automerge.getChanges(base, doc).some((change) => automerge_adapter_1.Automerge.decodeChange(change).actor === actor);
|
|
9091
9192
|
const fork = automerge_adapter_1.Automerge.clone(base, ownChangeSince ? undefined : { actor });
|
|
9092
|
-
const changed = automerge_adapter_1.Automerge.change(fork, apply);
|
|
9193
|
+
const changed = automerge_adapter_1.Automerge.change(fork, { message: changeMessage }, apply);
|
|
9093
9194
|
return automerge_adapter_1.Automerge.merge(doc, changed);
|
|
9094
9195
|
}
|
|
9095
9196
|
runChangeWithOutdatedDocRecovery(internalDoc, apply) {
|
|
@@ -9840,6 +9941,7 @@ class BaseMindooDB {
|
|
|
9840
9941
|
message: typeof decodedChange.message === "string"
|
|
9841
9942
|
? decodedChange.message
|
|
9842
9943
|
: null,
|
|
9944
|
+
attribution: (0, changeAttribution_1.decodeChangeAttribution)(typeof decodedChange.message === "string" ? decodedChange.message : null),
|
|
9843
9945
|
dependencyHashes: Array.isArray(decodedChange.deps)
|
|
9844
9946
|
? decodedChange.deps.filter((dep) => typeof dep === "string")
|
|
9845
9947
|
: [],
|
|
@@ -11044,6 +11146,7 @@ class BaseMindooDB {
|
|
|
11044
11146
|
}
|
|
11045
11147
|
async mutateRecipients(doc, mode, specs, options) {
|
|
11046
11148
|
this.assertWritable("changeDoc");
|
|
11149
|
+
const changeMessage = (0, changeAttribution_1.encodeChangeAttribution)(options?.attribution);
|
|
11047
11150
|
if (!(0, sealedTypes_1.isSealedKeyId)(doc.getDecryptionKeyId())) {
|
|
11048
11151
|
throw new Error("Recipient mutators require a document created with `recipients`");
|
|
11049
11152
|
}
|
|
@@ -11161,7 +11264,7 @@ class BaseMindooDB {
|
|
|
11161
11264
|
});
|
|
11162
11265
|
tenant.rememberSealedGenerations(internalDoc.decryptionKeyId, generations);
|
|
11163
11266
|
const headsBeforeChange = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
11164
|
-
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (draft) => {
|
|
11267
|
+
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, { message: changeMessage }, (draft) => {
|
|
11165
11268
|
if (addedTargets.length > 0) {
|
|
11166
11269
|
const addMap = {};
|
|
11167
11270
|
for (const target of addedTargets) {
|
|
@@ -11201,6 +11304,7 @@ class BaseMindooDB {
|
|
|
11201
11304
|
signingKeyPair: options.signingKeyPair,
|
|
11202
11305
|
signingKeyPassword: options.signingKeyPassword,
|
|
11203
11306
|
bypassAccessControlPrecheck: options.bypassAccessControlPrecheck,
|
|
11307
|
+
attribution: options.attribution,
|
|
11204
11308
|
});
|
|
11205
11309
|
}
|
|
11206
11310
|
const wrapped = this.wrapDocument(internalDoc);
|