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.
Files changed (34) hide show
  1. package/README-server.md +1 -1
  2. package/README.md +1 -0
  3. package/dist/browser/index.mjs +5 -0
  4. package/dist/core/BaseMindooDB.d.ts +16 -6
  5. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  6. package/dist/core/BaseMindooDB.js +145 -41
  7. package/dist/core/BaseMindooDB.js.map +1 -1
  8. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  9. package/dist/core/accesscontrol/types.js +3 -2
  10. package/dist/core/accesscontrol/types.js.map +1 -1
  11. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  12. package/dist/core/appendonlystores/network/AuthenticationService.js +6 -0
  13. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  14. package/dist/core/appendonlystores/syncStores.d.ts +14 -0
  15. package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
  16. package/dist/core/appendonlystores/syncStores.js +39 -2
  17. package/dist/core/appendonlystores/syncStores.js.map +1 -1
  18. package/dist/core/changeAttribution.d.ts +66 -0
  19. package/dist/core/changeAttribution.d.ts.map +1 -0
  20. package/dist/core/changeAttribution.js +109 -0
  21. package/dist/core/changeAttribution.js.map +1 -0
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/index.js +1 -0
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/types.d.ts +58 -5
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/core/types.js.map +1 -1
  29. package/dist/core/userkeys/sealedTypes.d.ts +2 -0
  30. package/dist/core/userkeys/sealedTypes.d.ts.map +1 -1
  31. package/dist/node/server/TenantManager.d.ts.map +1 -1
  32. package/dist/node/server/TenantManager.js +18 -0
  33. package/dist/node/server/TenantManager.js.map +1 -1
  34. 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
- await this.tenant.ingestSealedFromEntries?.(representativeEntry.decryptionKeyId, docLifecycleEntries);
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
- : await this.scanAllMetadata(this.store, { docId });
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);