mindoodb 0.0.86 → 0.0.88

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