mindoodb 0.0.85 → 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 (319) hide show
  1. package/README-server.md +6 -2
  2. package/README.md +6 -4
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +2 -1
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +151 -12
  7. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +340 -24
  9. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  10. package/dist/browser/index.mjs +14 -0
  11. package/dist/cloudflare/DurableObjectSql.d.ts +56 -0
  12. package/dist/cloudflare/DurableObjectSql.d.ts.map +1 -0
  13. package/dist/cloudflare/DurableObjectSql.js +180 -0
  14. package/dist/cloudflare/DurableObjectSql.js.map +1 -0
  15. package/dist/cloudflare/MindooDatabaseObject.d.ts +86 -0
  16. package/dist/cloudflare/MindooDatabaseObject.d.ts.map +1 -0
  17. package/dist/cloudflare/MindooDatabaseObject.js +614 -0
  18. package/dist/cloudflare/MindooDatabaseObject.js.map +1 -0
  19. package/dist/cloudflare/MindooServerObject.d.ts +63 -0
  20. package/dist/cloudflare/MindooServerObject.d.ts.map +1 -0
  21. package/dist/cloudflare/MindooServerObject.js +521 -0
  22. package/dist/cloudflare/MindooServerObject.js.map +1 -0
  23. package/dist/cloudflare/WorkersCryptoAdapter.d.ts +7 -0
  24. package/dist/cloudflare/WorkersCryptoAdapter.d.ts.map +1 -0
  25. package/dist/cloudflare/WorkersCryptoAdapter.js +14 -0
  26. package/dist/cloudflare/WorkersCryptoAdapter.js.map +1 -0
  27. package/dist/cloudflare/host.d.ts +49 -0
  28. package/dist/cloudflare/host.d.ts.map +1 -0
  29. package/dist/cloudflare/host.js +104 -0
  30. package/dist/cloudflare/host.js.map +1 -0
  31. package/dist/cloudflare/http.d.ts +91 -0
  32. package/dist/cloudflare/http.d.ts.map +1 -0
  33. package/dist/cloudflare/http.js +266 -0
  34. package/dist/cloudflare/http.js.map +1 -0
  35. package/dist/cloudflare/index.d.ts +12 -0
  36. package/dist/cloudflare/index.d.ts.map +1 -0
  37. package/dist/cloudflare/index.js +20 -0
  38. package/dist/cloudflare/index.js.map +1 -0
  39. package/dist/cloudflare/storeRpc.d.ts +22 -0
  40. package/dist/cloudflare/storeRpc.d.ts.map +1 -0
  41. package/dist/cloudflare/storeRpc.js +85 -0
  42. package/dist/cloudflare/storeRpc.js.map +1 -0
  43. package/dist/cloudflare/types.d.ts +101 -0
  44. package/dist/cloudflare/types.d.ts.map +1 -0
  45. package/dist/cloudflare/types.js +10 -0
  46. package/dist/cloudflare/types.js.map +1 -0
  47. package/dist/cloudflare/worker.d.ts +25 -0
  48. package/dist/cloudflare/worker.d.ts.map +1 -0
  49. package/dist/cloudflare/worker.js +170 -0
  50. package/dist/cloudflare/worker.js.map +1 -0
  51. package/dist/core/BaseMindooDB.d.ts +95 -10
  52. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  53. package/dist/core/BaseMindooDB.js +441 -98
  54. package/dist/core/BaseMindooDB.js.map +1 -1
  55. package/dist/core/BaseMindooTenant.d.ts +43 -0
  56. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  57. package/dist/core/BaseMindooTenant.js +157 -6
  58. package/dist/core/BaseMindooTenant.js.map +1 -1
  59. package/dist/core/BaseMindooTenantDirectory.d.ts +103 -0
  60. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  61. package/dist/core/BaseMindooTenantDirectory.js +371 -8
  62. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  63. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  64. package/dist/core/BaseMindooTenantFactory.js +2 -1
  65. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  66. package/dist/core/ChangeDocDraft.d.ts +133 -0
  67. package/dist/core/ChangeDocDraft.d.ts.map +1 -0
  68. package/dist/core/ChangeDocDraft.js +492 -0
  69. package/dist/core/ChangeDocDraft.js.map +1 -0
  70. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
  71. package/dist/core/MindooDBServerAdmin.js +2 -1
  72. package/dist/core/MindooDBServerAdmin.js.map +1 -1
  73. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  74. package/dist/core/accesscontrol/directoryProjection.js +19 -0
  75. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  76. package/dist/core/accesscontrol/evaluate.d.ts +14 -0
  77. package/dist/core/accesscontrol/evaluate.d.ts.map +1 -1
  78. package/dist/core/accesscontrol/evaluate.js +42 -1
  79. package/dist/core/accesscontrol/evaluate.js.map +1 -1
  80. package/dist/core/accesscontrol/types.d.ts +88 -4
  81. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  82. package/dist/core/accesscontrol/types.js +114 -1
  83. package/dist/core/accesscontrol/types.js.map +1 -1
  84. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +14 -0
  85. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  86. package/dist/core/appendonlystores/network/AuthenticationService.js +27 -7
  87. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  88. package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
  89. package/dist/core/appendonlystores/network/IrohNetworkTransport.js +12 -0
  90. package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
  91. package/dist/core/appendonlystores/network/IrohStreamIO.d.ts.map +1 -1
  92. package/dist/core/appendonlystores/network/IrohStreamIO.js +3 -23
  93. package/dist/core/appendonlystores/network/IrohStreamIO.js.map +1 -1
  94. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts +6 -0
  95. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts.map +1 -1
  96. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js +20 -1
  97. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js.map +1 -1
  98. package/dist/core/appendonlystores/rpc/storeRpcCodec.d.ts.map +1 -1
  99. package/dist/core/appendonlystores/rpc/storeRpcCodec.js +3 -23
  100. package/dist/core/appendonlystores/rpc/storeRpcCodec.js.map +1 -1
  101. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts +7 -0
  102. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
  103. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +39 -30
  104. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
  105. package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts +37 -1
  106. package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts.map +1 -1
  107. package/dist/core/appendonlystores/sqlite/SqliteDriver.js +99 -40
  108. package/dist/core/appendonlystores/sqlite/SqliteDriver.js.map +1 -1
  109. package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
  110. package/dist/core/appendonlystores/syncStores.js +25 -5
  111. package/dist/core/appendonlystores/syncStores.js.map +1 -1
  112. package/dist/core/appendonlystores/types.d.ts +7 -1
  113. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  114. package/dist/core/appendonlystores/types.js +1 -0
  115. package/dist/core/appendonlystores/types.js.map +1 -1
  116. package/dist/core/builtinDbInvariants.d.ts +49 -0
  117. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  118. package/dist/core/builtinDbInvariants.js +86 -1
  119. package/dist/core/builtinDbInvariants.js.map +1 -1
  120. package/dist/core/crypto/AuthChallenge.d.ts +68 -0
  121. package/dist/core/crypto/AuthChallenge.d.ts.map +1 -0
  122. package/dist/core/crypto/AuthChallenge.js +118 -0
  123. package/dist/core/crypto/AuthChallenge.js.map +1 -0
  124. package/dist/core/crypto/DomainStatement.d.ts +5 -3
  125. package/dist/core/crypto/DomainStatement.d.ts.map +1 -1
  126. package/dist/core/crypto/DomainStatement.js +5 -3
  127. package/dist/core/crypto/DomainStatement.js.map +1 -1
  128. package/dist/core/crypto/encryptedFields.d.ts +11 -0
  129. package/dist/core/crypto/encryptedFields.d.ts.map +1 -1
  130. package/dist/core/crypto/encryptedFields.js +18 -0
  131. package/dist/core/crypto/encryptedFields.js.map +1 -1
  132. package/dist/core/expressions/evaluateExpression.d.ts +8 -0
  133. package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
  134. package/dist/core/expressions/evaluateExpression.js +29 -4
  135. package/dist/core/expressions/evaluateExpression.js.map +1 -1
  136. package/dist/core/expressions/metadata.js +4 -4
  137. package/dist/core/expressions/metadata.js.map +1 -1
  138. package/dist/core/index.d.ts +3 -3
  139. package/dist/core/index.d.ts.map +1 -1
  140. package/dist/core/index.js +17 -6
  141. package/dist/core/index.js.map +1 -1
  142. package/dist/core/query/executeQuery.d.ts.map +1 -1
  143. package/dist/core/query/executeQuery.js +5 -1
  144. package/dist/core/query/executeQuery.js.map +1 -1
  145. package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
  146. package/dist/core/query/executeQueryInclude.js +2 -0
  147. package/dist/core/query/executeQueryInclude.js.map +1 -1
  148. package/dist/core/query/queryView.d.ts.map +1 -1
  149. package/dist/core/query/queryView.js +5 -1
  150. package/dist/core/query/queryView.js.map +1 -1
  151. package/dist/core/types.d.ts +204 -10
  152. package/dist/core/types.d.ts.map +1 -1
  153. package/dist/core/types.js.map +1 -1
  154. package/dist/core/uri/MindooURI.d.ts.map +1 -1
  155. package/dist/core/uri/MindooURI.js +3 -24
  156. package/dist/core/uri/MindooURI.js.map +1 -1
  157. package/dist/core/userkeys/index.d.ts +1 -0
  158. package/dist/core/userkeys/index.d.ts.map +1 -1
  159. package/dist/core/userkeys/index.js +5 -1
  160. package/dist/core/userkeys/index.js.map +1 -1
  161. package/dist/core/userkeys/sealedValues.d.ts +54 -0
  162. package/dist/core/userkeys/sealedValues.d.ts.map +1 -0
  163. package/dist/core/userkeys/sealedValues.js +67 -0
  164. package/dist/core/userkeys/sealedValues.js.map +1 -0
  165. package/dist/core/utils/base64.d.ts +17 -0
  166. package/dist/core/utils/base64.d.ts.map +1 -0
  167. package/dist/core/utils/base64.js +54 -0
  168. package/dist/core/utils/base64.js.map +1 -0
  169. package/dist/core/utils/dynamicImportInterop.d.ts +10 -0
  170. package/dist/core/utils/dynamicImportInterop.d.ts.map +1 -0
  171. package/dist/core/utils/dynamicImportInterop.js +22 -0
  172. package/dist/core/utils/dynamicImportInterop.js.map +1 -0
  173. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  174. package/dist/core/utils/idGeneration.js +14 -3
  175. package/dist/core/utils/idGeneration.js.map +1 -1
  176. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  177. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +7 -0
  178. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  179. package/dist/node/server/FsServerDataStore.d.ts +23 -0
  180. package/dist/node/server/FsServerDataStore.d.ts.map +1 -0
  181. package/dist/node/server/FsServerDataStore.js +59 -0
  182. package/dist/node/server/FsServerDataStore.js.map +1 -0
  183. package/dist/node/server/IrohServerEndpoint.d.ts +1 -1
  184. package/dist/node/server/IrohServerEndpoint.d.ts.map +1 -1
  185. package/dist/node/server/IrohServerRpc.d.ts +5 -5
  186. package/dist/node/server/IrohServerRpc.d.ts.map +1 -1
  187. package/dist/node/server/IrohServerRpc.js +1 -1
  188. package/dist/node/server/IrohServerRpc.js.map +1 -1
  189. package/dist/node/server/MindooDBServer.d.ts +29 -150
  190. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  191. package/dist/node/server/MindooDBServer.js +160 -1573
  192. package/dist/node/server/MindooDBServer.js.map +1 -1
  193. package/dist/node/server/PurgedDocRegistry.d.ts +4 -27
  194. package/dist/node/server/PurgedDocRegistry.d.ts.map +1 -1
  195. package/dist/node/server/PurgedDocRegistry.js +6 -83
  196. package/dist/node/server/PurgedDocRegistry.js.map +1 -1
  197. package/dist/node/server/StoreFactory.d.ts +1 -1
  198. package/dist/node/server/StoreFactory.d.ts.map +1 -1
  199. package/dist/node/server/TenantManager.d.ts +16 -284
  200. package/dist/node/server/TenantManager.d.ts.map +1 -1
  201. package/dist/node/server/TenantManager.js +35 -1168
  202. package/dist/node/server/TenantManager.js.map +1 -1
  203. package/dist/node/server/add-to-network.js +1 -1
  204. package/dist/node/server/add-to-network.js.map +1 -1
  205. package/dist/node/server/config.d.ts +11 -15
  206. package/dist/node/server/config.d.ts.map +1 -1
  207. package/dist/node/server/config.js +30 -245
  208. package/dist/node/server/config.js.map +1 -1
  209. package/dist/node/server/expressAdapter.d.ts +23 -0
  210. package/dist/node/server/expressAdapter.d.ts.map +1 -0
  211. package/dist/node/server/expressAdapter.js +108 -0
  212. package/dist/node/server/expressAdapter.js.map +1 -0
  213. package/dist/node/server/nativeIrohStreamIO.d.ts +1 -1
  214. package/dist/node/server/nativeIrohStreamIO.d.ts.map +1 -1
  215. package/dist/node/server/peer/FileClusterAuditStorage.d.ts +16 -0
  216. package/dist/node/server/peer/FileClusterAuditStorage.d.ts.map +1 -0
  217. package/dist/node/server/peer/FileClusterAuditStorage.js +39 -0
  218. package/dist/node/server/peer/FileClusterAuditStorage.js.map +1 -0
  219. package/dist/node/server/peer/IrohPeerLink.d.ts +1 -1
  220. package/dist/node/server/peer/IrohPeerLink.d.ts.map +1 -1
  221. package/dist/node/server/peer/routes.d.ts +10 -44
  222. package/dist/node/server/peer/routes.d.ts.map +1 -1
  223. package/dist/node/server/peer/routes.js +33 -275
  224. package/dist/node/server/peer/routes.js.map +1 -1
  225. package/dist/node/server/resolveServerPassword.js +1 -1
  226. package/dist/node/server/resolveServerPassword.js.map +1 -1
  227. package/dist/node/server/server.js +1 -1
  228. package/dist/node/server/server.js.map +1 -1
  229. package/dist/node/server/serverinit.js +1 -1
  230. package/dist/node/server/serverinit.js.map +1 -1
  231. package/dist/server/CapabilityMatcher.d.ts +60 -0
  232. package/dist/server/CapabilityMatcher.d.ts.map +1 -0
  233. package/dist/server/CapabilityMatcher.js +138 -0
  234. package/dist/server/CapabilityMatcher.js.map +1 -0
  235. package/dist/server/DeviceDiscoveryService.d.ts +36 -0
  236. package/dist/server/DeviceDiscoveryService.d.ts.map +1 -0
  237. package/dist/server/DeviceDiscoveryService.js +164 -0
  238. package/dist/server/DeviceDiscoveryService.js.map +1 -0
  239. package/dist/server/PurgedDocRegistry.d.ts +35 -0
  240. package/dist/server/PurgedDocRegistry.d.ts.map +1 -0
  241. package/dist/server/PurgedDocRegistry.js +90 -0
  242. package/dist/server/PurgedDocRegistry.js.map +1 -0
  243. package/dist/server/ServerDataStore.d.ts +67 -0
  244. package/dist/server/ServerDataStore.d.ts.map +1 -0
  245. package/dist/server/ServerDataStore.js +88 -0
  246. package/dist/server/ServerDataStore.js.map +1 -0
  247. package/dist/server/SyncEventBus.d.ts +39 -0
  248. package/dist/server/SyncEventBus.d.ts.map +1 -0
  249. package/dist/server/SyncEventBus.js +41 -0
  250. package/dist/server/SyncEventBus.js.map +1 -0
  251. package/dist/server/SystemAdminAuth.d.ts +82 -0
  252. package/dist/server/SystemAdminAuth.d.ts.map +1 -0
  253. package/dist/server/SystemAdminAuth.js +227 -0
  254. package/dist/server/SystemAdminAuth.js.map +1 -0
  255. package/dist/server/TenantManager.d.ts +366 -0
  256. package/dist/server/TenantManager.d.ts.map +1 -0
  257. package/dist/server/TenantManager.js +1416 -0
  258. package/dist/server/TenantManager.js.map +1 -0
  259. package/dist/server/http/MindooDBServerCore.d.ts +313 -0
  260. package/dist/server/http/MindooDBServerCore.d.ts.map +1 -0
  261. package/dist/server/http/MindooDBServerCore.js +1410 -0
  262. package/dist/server/http/MindooDBServerCore.js.map +1 -0
  263. package/dist/server/http/types.d.ts +89 -0
  264. package/dist/server/http/types.d.ts.map +1 -0
  265. package/dist/server/http/types.js +29 -0
  266. package/dist/server/http/types.js.map +1 -0
  267. package/dist/server/index.d.ts +32 -0
  268. package/dist/server/index.d.ts.map +1 -0
  269. package/dist/server/index.js +76 -0
  270. package/dist/server/index.js.map +1 -0
  271. package/dist/server/peer/ClusterAuditLog.d.ts +71 -0
  272. package/dist/server/peer/ClusterAuditLog.d.ts.map +1 -0
  273. package/dist/server/peer/ClusterAuditLog.js +81 -0
  274. package/dist/server/peer/ClusterAuditLog.js.map +1 -0
  275. package/dist/server/peer/ClusterJobRunner.d.ts +43 -0
  276. package/dist/server/peer/ClusterJobRunner.d.ts.map +1 -0
  277. package/dist/server/peer/ClusterJobRunner.js +119 -0
  278. package/dist/server/peer/ClusterJobRunner.js.map +1 -0
  279. package/dist/server/peer/ClusterManager.d.ts +149 -0
  280. package/dist/server/peer/ClusterManager.d.ts.map +1 -0
  281. package/dist/server/peer/ClusterManager.js +292 -0
  282. package/dist/server/peer/ClusterManager.js.map +1 -0
  283. package/dist/server/peer/PeerAuthService.d.ts +69 -0
  284. package/dist/server/peer/PeerAuthService.d.ts.map +1 -0
  285. package/dist/server/peer/PeerAuthService.js +207 -0
  286. package/dist/server/peer/PeerAuthService.js.map +1 -0
  287. package/dist/server/peer/PeerReplicator.d.ts +303 -0
  288. package/dist/server/peer/PeerReplicator.d.ts.map +1 -0
  289. package/dist/server/peer/PeerReplicator.js +1072 -0
  290. package/dist/server/peer/PeerReplicator.js.map +1 -0
  291. package/dist/server/peer/clusterHttp.d.ts +113 -0
  292. package/dist/server/peer/clusterHttp.d.ts.map +1 -0
  293. package/dist/server/peer/clusterHttp.js +290 -0
  294. package/dist/server/peer/clusterHttp.js.map +1 -0
  295. package/dist/server/peer/types.d.ts +244 -0
  296. package/dist/server/peer/types.d.ts.map +1 -0
  297. package/dist/server/peer/types.js +189 -0
  298. package/dist/server/peer/types.js.map +1 -0
  299. package/dist/server/rateLimits.d.ts +119 -0
  300. package/dist/server/rateLimits.d.ts.map +1 -0
  301. package/dist/server/rateLimits.js +159 -0
  302. package/dist/server/rateLimits.js.map +1 -0
  303. package/dist/server/serverConfig.d.ts +37 -0
  304. package/dist/server/serverConfig.d.ts.map +1 -0
  305. package/dist/server/serverConfig.js +251 -0
  306. package/dist/server/serverConfig.js.map +1 -0
  307. package/dist/server/timestampProxy.d.ts +129 -0
  308. package/dist/server/timestampProxy.d.ts.map +1 -0
  309. package/dist/server/timestampProxy.js +361 -0
  310. package/dist/server/timestampProxy.js.map +1 -0
  311. package/dist/server/types.d.ts +267 -0
  312. package/dist/server/types.d.ts.map +1 -0
  313. package/dist/server/types.js +56 -0
  314. package/dist/server/types.js.map +1 -0
  315. package/dist/server/validation.d.ts +43 -0
  316. package/dist/server/validation.d.ts.map +1 -0
  317. package/dist/server/validation.js +104 -0
  318. package/dist/server/validation.js.map +1 -0
  319. package/package.json +9 -1
@@ -5,6 +5,7 @@ exports.BaseMindooDB = exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES = void 0;
5
5
  // React Native: native Rust (react-native-automerge-generated)
6
6
  // Browser/Node.js: WASM (@automerge/automerge/slim)
7
7
  const automerge_adapter_1 = require("./automerge-adapter");
8
+ const ChangeDocDraft_1 = require("./ChangeDocDraft");
8
9
  const values_1 = require("./values");
9
10
  const storeEntryTime_1 = require("./storeEntryTime");
10
11
  const changeAttribution_1 = require("./changeAttribution");
@@ -364,6 +365,25 @@ class BaseMindooDB {
364
365
  */
365
366
  this.revisionFeedDocCache = new Map();
366
367
  this.revisionFeedCacheLimit = 256;
368
+ /**
369
+ * Revisions of documents whose fold suffix holds un-witnessed entries, keyed
370
+ * by the exact fold (entry order, witness state and prefix). The feed
371
+ * re-discovers such entries on every resume, since their trusted time is the
372
+ * provisional `now`; while nothing about the document changed, only that
373
+ * trusted time differs, so the revisions are re-emitted from here instead
374
+ * of being loaded, verified, decrypted and folded again. Without it, every
375
+ * pass re-folded every un-witnessed document: registering N users offline
376
+ * cost O(N) per registration. An entry is replaced on any change and
377
+ * dropped once the document has no un-witnessed entries left.
378
+ *
379
+ * Each revision holds its own document state, so the cache is bounded: a
380
+ * document with a longer suffix, or one arriving when the cache is full, is
381
+ * folded every pass as before.
382
+ */
383
+ this.provisionalRevisionCache = new Map();
384
+ this.provisionalRevisionCacheSize = 0;
385
+ this.provisionalRevisionCacheLimit = 4096;
386
+ this.provisionalRevisionCacheDocLimit = 32;
367
387
  /**
368
388
  * Window size for batching encrypted-payload loads while folding a document's
369
389
  * revision suffix ({@link foldDocRevisions}): one {@link ContentAddressedStore.getEntries}
@@ -462,6 +482,16 @@ class BaseMindooDB {
462
482
  this.syncStoreChangesInFlight = null;
463
483
  /** Shared follow-up run for callers that arrive mid-sync. */
464
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;
465
495
  this.changeListeners = new Set();
466
496
  this.pendingChangeNotifications = new Map();
467
497
  this.changeNotifyTimer = null;
@@ -764,16 +794,97 @@ class BaseMindooDB {
764
794
  * — a peer sync — and therefore have to apply the admin/owner rules
765
795
  * themselves.
766
796
  *
767
- * Deliberately the same check the load path uses, so an entry admitted here
768
- * is one that will also materialize. The alternative, accepting on ingest
769
- * and dropping on load, leaves the entry sitting in the store and pushes it
770
- * 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`.
771
809
  *
772
810
  * Returns false for databases without a builtin invariant, so callers can
773
811
  * ask unconditionally.
774
812
  */
775
- async violatesBuiltinWriteInvariant(entry) {
776
- 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
+ }
777
888
  }
778
889
  async ensureSignerUsernameHash(signerKey) {
779
890
  if (this.signerUsernameHashByKey.has(signerKey)) {
@@ -2808,6 +2919,93 @@ class BaseMindooDB {
2808
2919
  }
2809
2920
  return outcome;
2810
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
+ }
2811
3009
  /**
2812
3010
  * Drop everything this instance derived from a document whose store entries
2813
3011
  * were purged: the L1/L2 caches, summary values and full-text tokens (via
@@ -2887,7 +3085,7 @@ class BaseMindooDB {
2887
3085
  }
2888
3086
  const existingIndex = this.getDocIndexPosition(docId);
2889
3087
  const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
2890
- await this.tenant.ingestSealedFromEntries?.(visibility.decryptionKeyId, metadataByDoc.get(docId));
3088
+ await this.ingestSealed(visibility.decryptionKeyId, metadataByDoc.get(docId));
2891
3089
  const canRead = await this.tenant.hasDecryptionKey(visibility.decryptionKeyId);
2892
3090
  if (canRead) {
2893
3091
  // This scan is the authority on visibility, so it also heals a tally
@@ -3167,6 +3365,10 @@ class BaseMindooDB {
3167
3365
  * {@link syncStoreChangesInFlight}).
3168
3366
  */
3169
3367
  async syncStoreChanges() {
3368
+ if (this.sealedIngestDepth > 0) {
3369
+ // Called back from our own sealed ingest (see sealedIngestDepth).
3370
+ return;
3371
+ }
3170
3372
  if (this.syncStoreChangesInFlight) {
3171
3373
  if (!this.syncStoreChangesQueued) {
3172
3374
  // The running scan may have started before the entries this caller
@@ -3308,7 +3510,7 @@ class BaseMindooDB {
3308
3510
  });
3309
3511
  sealedIngestEntries = fullDocMetadata;
3310
3512
  }
3311
- await this.tenant.ingestSealedFromEntries?.(representativeEntry.decryptionKeyId, sealedIngestEntries);
3513
+ await this.ingestSealed(representativeEntry.decryptionKeyId, sealedIngestEntries);
3312
3514
  const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
3313
3515
  if (!keyAvailable) {
3314
3516
  this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
@@ -4057,6 +4259,9 @@ class BaseMindooDB {
4057
4259
  }
4058
4260
  this.validateIdPrefixOption("createDocuments", options);
4059
4261
  this.validateAssumeUniqueIdOption("createDocuments", options);
4262
+ if ((options.attachments?.length ?? 0) > 0) {
4263
+ throw new Error("createDocuments: attachments are only supported by createDocument()");
4264
+ }
4060
4265
  (0, changeAttribution_1.encodeChangeAttribution)(options.attribution);
4061
4266
  }
4062
4267
  const results = new Array(inputs.length).fill(null);
@@ -4235,6 +4440,10 @@ class BaseMindooDB {
4235
4440
  throw new Error("createDocument: recipients is not supported together with a convergent custom id; " +
4236
4441
  "pass assumeUniqueId for random ids.");
4237
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
+ }
4238
4447
  }
4239
4448
  const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
4240
4449
  const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
@@ -4293,11 +4502,17 @@ class BaseMindooDB {
4293
4502
  else {
4294
4503
  this.logger.debug(`Creating initial Automerge change for document ${docId}`);
4295
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
+ }
4296
4511
  try {
4297
4512
  newDoc = automerge_adapter_1.Automerge.change(initialDoc, changeMessage ? { message: changeMessage } : {}, (doc) => {
4298
4513
  // Store metadata in the document payload
4299
4514
  // We need to modify the document to ensure a change is created
4300
- doc._attachments = [];
4515
+ doc._attachments = createAttachments.map((ref) => ({ ...ref }));
4301
4516
  if (sealedCreate) {
4302
4517
  (0, recipients_1.applyEncryptForCreate)(doc, sealedCreate.encryptFor);
4303
4518
  }
@@ -4804,6 +5019,22 @@ class BaseMindooDB {
4804
5019
  });
4805
5020
  return this.wrapDocument(internalDoc);
4806
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
+ }
4807
5038
  /**
4808
5039
  * Unwrap a `$sealed:` DEK from entry recipient wraps into the tenant session
4809
5040
  * cache. `loadDocumentInternal` already does this; history, timestamp, and
@@ -4816,7 +5047,7 @@ class BaseMindooDB {
4816
5047
  return;
4817
5048
  const keyId = entries.find((entry) => entry.entryType === "doc_create")
4818
5049
  ?.decryptionKeyId ?? entries[0].decryptionKeyId;
4819
- await this.tenant.ingestSealedFromEntries?.(keyId, entries);
5050
+ await this.ingestSealed(keyId, entries);
4820
5051
  }
4821
5052
  /**
4822
5053
  * Re-ingest the newest sealed wrap list. When this replica can no longer
@@ -5176,10 +5407,48 @@ class BaseMindooDB {
5176
5407
  if (firstChangedIdx < 0) {
5177
5408
  return;
5178
5409
  }
5410
+ // Unchanged document whose suffix is still provisional: re-emit the
5411
+ // previous fold with the current trusted times (see provisionalRevisionCache).
5412
+ const suffix = ttSorted.slice(firstChangedIdx);
5413
+ const hasProvisional = suffix.some((r) => !r.witnessed);
5414
+ const foldKey = [
5415
+ ttSorted.slice(0, firstChangedIdx).map((r) => r.meta.id).join(","),
5416
+ suffix.map((r) => `${r.meta.id}:${r.meta.receivedAt ?? "-"}`).join(","),
5417
+ ].join("|");
5418
+ const cachedFold = this.provisionalRevisionCache.get(docId);
5419
+ if (cachedFold && cachedFold.key === foldKey) {
5420
+ for (let i = 0; i < cachedFold.revisions.length; i++) {
5421
+ yield { ...cachedFold.revisions[i], trustedTime: suffix[i].tt, cursor: resumeCursor };
5422
+ }
5423
+ return;
5424
+ }
5425
+ if (cachedFold) {
5426
+ this.provisionalRevisionCache.delete(docId);
5427
+ this.provisionalRevisionCacheSize -= cachedFold.revisions.length;
5428
+ }
5429
+ const foldedRevisions = [];
5430
+ let foldComplete = true;
5179
5431
  // Origin time and key id come from the document's create entry, independent
5180
5432
  // of fold order (under the trusted-time rule un-witnessed entries can sort
5181
5433
  // before the create, so we must not rely on seeing the create first).
5182
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
+ }
5183
5452
  // Seed the running doc to the merge of the unchanged prefix.
5184
5453
  const prefix = ttSorted.slice(0, firstChangedIdx).map((r) => r.meta);
5185
5454
  const seed = await this.seedRevisionFold(docId, prefix, docAllMeta);
@@ -5225,6 +5494,7 @@ class BaseMindooDB {
5225
5494
  // Absent from the loaded window means missing in the store or it failed
5226
5495
  // signature verification (already logged during the parallel prepare).
5227
5496
  this.logger.warn(`Revision entry ${meta.id} unavailable (missing or invalid), skipping`);
5497
+ foldComplete = false;
5228
5498
  continue;
5229
5499
  }
5230
5500
  const entryData = ready.entry;
@@ -5249,16 +5519,24 @@ class BaseMindooDB {
5249
5519
  // this document's replay entries seen up to and including this one.
5250
5520
  isDeleted: this.computeIsDeletedFromMetadata([...seenMeta]),
5251
5521
  };
5252
- yield {
5522
+ const revision = {
5253
5523
  docId,
5254
5524
  entryId: meta.id,
5255
5525
  doc: this.wrapDocument(internalDoc),
5256
5526
  entryType: meta.entryType,
5257
- trustedTime: rev.tt,
5258
5527
  witnessed: rev.witnessed,
5259
5528
  createdByPublicKey: meta.createdByPublicKey,
5260
- cursor: resumeCursor,
5261
5529
  };
5530
+ foldedRevisions.push(revision);
5531
+ yield { ...revision, trustedTime: rev.tt, cursor: resumeCursor };
5532
+ }
5533
+ // Reached only when the consumer took every revision of this document.
5534
+ if (hasProvisional &&
5535
+ foldComplete &&
5536
+ foldedRevisions.length <= this.provisionalRevisionCacheDocLimit &&
5537
+ this.provisionalRevisionCacheSize + foldedRevisions.length <= this.provisionalRevisionCacheLimit) {
5538
+ this.provisionalRevisionCache.set(docId, { key: foldKey, revisions: foldedRevisions });
5539
+ this.provisionalRevisionCacheSize += foldedRevisions.length;
5262
5540
  }
5263
5541
  // Cache the doc's full merged state for the next incremental advance.
5264
5542
  this.putRevisionFeedCache(docId, {
@@ -8137,8 +8415,6 @@ class BaseMindooDB {
8137
8415
  //
8138
8416
  // We use a two-phase approach: the callback can do async work and collect
8139
8417
  // what needs to be changed, then we apply those changes in Automerge.change()
8140
- const pendingChanges = new Map();
8141
- const pendingDeletions = new Set();
8142
8418
  // Track pending attachment operations
8143
8419
  const pendingAttachmentAdditions = [];
8144
8420
  const pendingAttachmentRemovals = new Set();
@@ -8146,19 +8422,6 @@ class BaseMindooDB {
8146
8422
  const pendingAttachmentAppends = new Map();
8147
8423
  // Map of attachmentId -> extraction result for setAttachmentExtractedText
8148
8424
  const pendingAttachmentTextUpdates = new Map();
8149
- // incrementCounter() calls, applied in call order after the field
8150
- // assignments so they can target counters assigned in the same callback.
8151
- const pendingCounterIncrements = [];
8152
- // Assignments and deletions are applied before the increments, so an
8153
- // assignment or deletion of a field supersedes the increments queued on it
8154
- // (or below it) earlier in the callback; later increments still apply.
8155
- const dropPendingCounterIncrements = (field) => {
8156
- for (let i = pendingCounterIncrements.length - 1; i >= 0; i--) {
8157
- if (pendingCounterIncrements[i].path[0] === field) {
8158
- pendingCounterIncrements.splice(i, 1);
8159
- }
8160
- }
8161
- };
8162
8425
  // Reference to db for closures
8163
8426
  const db = this;
8164
8427
  // Guard flag to prevent operations after callback completes
@@ -8169,6 +8432,15 @@ class BaseMindooDB {
8169
8432
  throw new Error(`${methodName}() cannot be called after changeDoc() callback has completed. Document changes can only be made within the callback.`);
8170
8433
  }
8171
8434
  };
8435
+ // getData() edits a lazily built shadow copy and logs every edit, at any
8436
+ // depth, for replay inside Automerge.change() below.
8437
+ const draft = new ChangeDocDraft_1.ChangeDocDraft(internalDoc.doc, {
8438
+ scalarKind: (value) => db.automergeScalarKind(value),
8439
+ readScalar: (value) => db.readAutomergeScalar(value),
8440
+ makeCounter: (value) => new automerge_adapter_1.Automerge.Counter(value),
8441
+ isRichTextInBase: (path) => db.isRichTextAt(internalDoc.doc, path),
8442
+ assertActive: throwIfCallbackInactive,
8443
+ });
8172
8444
  // Create a document wrapper that collects changes
8173
8445
  const collectingDoc = {
8174
8446
  getDatabase: () => this,
@@ -8186,62 +8458,7 @@ class BaseMindooDB {
8186
8458
  isEncryptedFor: (users) => (0, sealedTypes_1.isSealedKeyId)(internalDoc.decryptionKeyId) &&
8187
8459
  (0, recipients_1.isPayloadEncryptedFor)(internalDoc.doc, internalDoc.sealedRecipients, Array.isArray(users) ? users : [users]),
8188
8460
  getHeads: () => automerge_adapter_1.Automerge.getHeads(internalDoc.doc),
8189
- getData: () => {
8190
- // Return a proxy that collects property assignments and deletions
8191
- const currentData = internalDoc.doc;
8192
- return new Proxy(currentData, {
8193
- set: (target, prop, value) => {
8194
- throwIfCallbackInactive("set property");
8195
- if (typeof prop === "string") {
8196
- dropPendingCounterIncrements(prop);
8197
- // If this property was marked for deletion, remove it from deletions
8198
- pendingDeletions.delete(prop);
8199
- // Track the change
8200
- pendingChanges.set(prop, value);
8201
- // NOTE: Don't set on target immediately - Automerge requires changes
8202
- // to be made inside Automerge.change(). Pending changes are applied later.
8203
- }
8204
- return true;
8205
- },
8206
- deleteProperty: (target, prop) => {
8207
- throwIfCallbackInactive("delete property");
8208
- if (typeof prop === "string") {
8209
- dropPendingCounterIncrements(prop);
8210
- // Mark for deletion
8211
- pendingDeletions.add(prop);
8212
- // Remove from pending changes if it was there
8213
- pendingChanges.delete(prop);
8214
- // NOTE: Don't delete from target immediately - Automerge requires changes
8215
- // to be made inside Automerge.change(). Pending deletions are applied later.
8216
- }
8217
- return true;
8218
- },
8219
- get: (target, prop) => {
8220
- // If marked for deletion, return undefined
8221
- if (typeof prop === "string" && pendingDeletions.has(prop)) {
8222
- return undefined;
8223
- }
8224
- // Check pending changes first, then target
8225
- if (typeof prop === "string" && pendingChanges.has(prop)) {
8226
- return pendingChanges.get(prop);
8227
- }
8228
- // Counters read as numbers and immutable strings as strings here
8229
- // too; change counters with incrementCounter().
8230
- return db.readAutomergeScalar(target[prop]);
8231
- },
8232
- has: (target, prop) => {
8233
- // If marked for deletion, it doesn't exist
8234
- if (typeof prop === "string" && pendingDeletions.has(prop)) {
8235
- return false;
8236
- }
8237
- // Check pending changes first, then target
8238
- if (typeof prop === "string" && pendingChanges.has(prop)) {
8239
- return true;
8240
- }
8241
- return prop in target;
8242
- },
8243
- });
8244
- },
8461
+ getData: () => draft.getRoot(),
8245
8462
  // ========== Attachment Write Methods (work in changeDoc context) ==========
8246
8463
  addAttachment: async (fileData, fileName, mimeType, keyId) => {
8247
8464
  throwIfCallbackInactive("addAttachment");
@@ -8356,12 +8573,25 @@ class BaseMindooDB {
8356
8573
  }
8357
8574
  },
8358
8575
  // ========== Counter Write Methods ==========
8576
+ updateText: (path, value) => {
8577
+ const segments = typeof path === "string" ? [path] : [...path];
8578
+ db.validateJsonPath(segments, "updateText");
8579
+ if (draft.getFieldType(segments) === "richText") {
8580
+ throw new Error(`updateText: '${segments.map(String).join(".")}' is rich text (marks or blocks); use applyRichTextPatch() or applyRichTextStepsPatch()`);
8581
+ }
8582
+ draft.updateText(segments, value);
8583
+ },
8584
+ getFieldType: (path) => {
8585
+ const segments = typeof path === "string" ? [path] : [...path];
8586
+ db.validateJsonPath(segments, "getFieldType");
8587
+ return draft.getFieldType(segments);
8588
+ },
8359
8589
  incrementCounter: (path, delta) => {
8360
8590
  throwIfCallbackInactive("incrementCounter");
8361
8591
  const segments = typeof path === "string" ? [path] : [...path];
8362
8592
  db.validateJsonPath(segments, "incrementCounter");
8363
8593
  (0, values_1.assertCounterAmount)(delta, "incrementCounter delta");
8364
- pendingCounterIncrements.push({ path: segments, delta });
8594
+ draft.incrementCounter(segments, delta);
8365
8595
  },
8366
8596
  // ========== Attachment Read Methods (also work in changeDoc context) ==========
8367
8597
  getAttachments: () => {
@@ -8396,20 +8626,14 @@ class BaseMindooDB {
8396
8626
  let newDoc;
8397
8627
  try {
8398
8628
  newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => automerge_adapter_1.Automerge.change(doc, { message: changeMessage }, (automergeDoc) => {
8399
- // Apply all pending changes (sets/updates)
8400
- for (const [key, value] of pendingChanges) {
8401
- this.assignHydrated(automergeDoc, key, this.hydrateTypedValues(value));
8402
- }
8403
- // Apply all pending deletions
8404
- for (const key of pendingDeletions) {
8405
- delete automergeDoc[key];
8406
- }
8629
+ // Replay the callback's field edits and counter increments in the
8630
+ // order they were made: an assignment after an increment replaces
8631
+ // it, an increment after an assignment adds to the new counter, and
8632
+ // a list index means the element it meant at that moment.
8633
+ this.applyChangeDocDraftOps(automergeDoc, draft.ops);
8407
8634
  // Engine-internal edits that need the raw Automerge document, e.g.
8408
8635
  // restoreDocumentToEntry's rich-text spans.
8409
8636
  internalMutation?.(automergeDoc);
8410
- for (const { path, delta } of pendingCounterIncrements) {
8411
- this.incrementCounterAtPath(automergeDoc, path, delta);
8412
- }
8413
8637
  // Apply pending attachment changes
8414
8638
  if (pendingAttachmentAdditions.length > 0 ||
8415
8639
  pendingAttachmentRemovals.size > 0 ||
@@ -8514,6 +8738,96 @@ class BaseMindooDB {
8514
8738
  }
8515
8739
  await Promise.all(pendingAttachmentAdditions.map((ref) => this.clearPendingAttachmentUploadLedger(ref.attachmentId)));
8516
8740
  }
8741
+ /** "counter" / "atomic" for the CRDT scalars that read as plain values, otherwise null. */
8742
+ automergeScalarKind(value) {
8743
+ if (this.isAutomergeCounter(value))
8744
+ return "counter";
8745
+ if (this.isAutomergeImmutableString(value))
8746
+ return "atomic";
8747
+ return null;
8748
+ }
8749
+ /** Whether the text at `path` carries marks or block markers (rich text). */
8750
+ isRichTextAt(automergeDoc, path) {
8751
+ if (typeof this.readValueAtPath(automergeDoc, path) !== "string")
8752
+ return false;
8753
+ const spans = automerge_adapter_1.Automerge.spans(automergeDoc, path);
8754
+ return spans.some((span) => span.type === "block" || (span.marks !== undefined && Object.keys(span.marks).length > 0));
8755
+ }
8756
+ fieldTypeAt(automergeDoc, path) {
8757
+ if (path.length === 0)
8758
+ return "map";
8759
+ let parent = automergeDoc;
8760
+ for (const segment of path.slice(0, -1)) {
8761
+ parent = parent?.[segment];
8762
+ if (parent === null || typeof parent !== "object" || this.automergeScalarKind(parent) !== null)
8763
+ return undefined;
8764
+ }
8765
+ const key = path[path.length - 1];
8766
+ if (!Object.prototype.hasOwnProperty.call(parent, key))
8767
+ return undefined;
8768
+ const value = parent[key];
8769
+ if (typeof value === "string")
8770
+ return this.isRichTextAt(automergeDoc, path) ? "richText" : "text";
8771
+ return (0, ChangeDocDraft_1.fieldTypeOf)(value, { scalarKind: (v) => this.automergeScalarKind(v) });
8772
+ }
8773
+ async getFieldType(doc, path) {
8774
+ const segments = typeof path === "string" ? [path] : [...path];
8775
+ this.validateJsonPath(segments, "getFieldType");
8776
+ const docId = doc.getId();
8777
+ let internalDoc = this.wrappedInternalDocs.get(doc) ?? this.getCachedDocument(docId);
8778
+ if (!internalDoc) {
8779
+ const loadedDoc = await this.loadDocumentInternal(docId);
8780
+ if (!loadedDoc) {
8781
+ throw new errors_1.DocumentNotFoundError(docId);
8782
+ }
8783
+ internalDoc = loadedDoc;
8784
+ }
8785
+ return this.fieldTypeAt(internalDoc.doc, segments);
8786
+ }
8787
+ /** Replays the edits a changeDoc() callback made through getData(), see ChangeDocDraft. */
8788
+ applyChangeDocDraftOps(automergeDoc, ops) {
8789
+ const containerAt = (path) => {
8790
+ let current = automergeDoc;
8791
+ for (const segment of path)
8792
+ current = current[segment];
8793
+ return current;
8794
+ };
8795
+ for (const op of ops) {
8796
+ if (op.kind === "counter") {
8797
+ this.incrementCounterAtPath(automergeDoc, op.path, op.delta);
8798
+ continue;
8799
+ }
8800
+ if (op.kind === "splice") {
8801
+ const list = containerAt(op.path);
8802
+ const values = this.hydrateTypedValues(op.items);
8803
+ list.splice(op.index, op.deleteCount, ...this.withoutNestedFloats(values));
8804
+ values.forEach((value, offset) => {
8805
+ if (value instanceof automerge_adapter_1.Automerge.Float64)
8806
+ list[op.index + offset] = new automerge_adapter_1.Automerge.Float64(value.value);
8807
+ else
8808
+ this.restoreNestedFloats(list[op.index + offset], value);
8809
+ });
8810
+ continue;
8811
+ }
8812
+ const parent = containerAt(op.path.slice(0, -1));
8813
+ const key = op.path[op.path.length - 1];
8814
+ if (op.kind === "delete") {
8815
+ delete parent[key];
8816
+ continue;
8817
+ }
8818
+ const current = parent[key];
8819
+ if (op.kind === "text" && typeof current === "string") {
8820
+ // Splice only the changed middle, so the edit merges with concurrent
8821
+ // typing instead of replacing the whole text.
8822
+ const splice = (0, ChangeDocDraft_1.diffTextSplice)(current, op.value);
8823
+ if (splice) {
8824
+ automerge_adapter_1.Automerge.splice(automergeDoc, op.path, splice.index, splice.deleteCount, splice.insert);
8825
+ }
8826
+ continue;
8827
+ }
8828
+ this.assignHydrated(parent, key, this.hydrateTypedValues(op.value));
8829
+ }
8830
+ }
8517
8831
  validateTextPatch(patch) {
8518
8832
  if (!Array.isArray(patch.path) || patch.path.length === 0) {
8519
8833
  throw new Error("Text patch path must contain at least one segment");
@@ -11882,7 +12196,7 @@ class BaseMindooDB {
11882
12196
  .map((em) => `${em.entryType}@${em.createdAt}`)
11883
12197
  .join(", ");
11884
12198
  this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
11885
- await this.tenant.ingestSealedFromEntries?.(allEntryMetadata.find((em) => em.entryType === "doc_create")
12199
+ await this.ingestSealed(allEntryMetadata.find((em) => em.entryType === "doc_create")
11886
12200
  ?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId, allEntryMetadata);
11887
12201
  const loadKeyId = allEntryMetadata.find((em) => em.entryType === "doc_create")
11888
12202
  ?.decryptionKeyId ?? allEntryMetadata[0].decryptionKeyId;
@@ -12367,6 +12681,14 @@ class BaseMindooDB {
12367
12681
  setAttachmentExtractedText: () => {
12368
12682
  throw new Error("setAttachmentExtractedText() can only be called within changeDoc() callback");
12369
12683
  },
12684
+ getFieldType: (path) => {
12685
+ const segments = typeof path === "string" ? [path] : [...path];
12686
+ this.validateJsonPath(segments, "getFieldType");
12687
+ return this.fieldTypeAt(internalDoc.doc, segments);
12688
+ },
12689
+ updateText: () => {
12690
+ throw new Error("updateText() can only be called within changeDoc() callback; use applyJsonPatch() with textSplice outside of it");
12691
+ },
12370
12692
  incrementCounter: () => {
12371
12693
  throw new Error("incrementCounter() can only be called within changeDoc() callback; use applyJsonPatch() with counterIncrement outside of it");
12372
12694
  },
@@ -13181,9 +13503,19 @@ class BaseMindooDB {
13181
13503
  // erased history back (docs/accesscontrol.md §13). Never applied to the
13182
13504
  // directory, which carries the purge requests themselves.
13183
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;
13184
13508
  const localStore = dbId === "directory"
13185
13509
  ? this.getStoreForKind(storeKind)
13186
- : (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);
13187
13519
  if (localStore.getId() !== remoteStore.getId() ||
13188
13520
  localStore.getStoreKind() !== remoteStore.getStoreKind()) {
13189
13521
  throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
@@ -13291,6 +13623,17 @@ class BaseMindooDB {
13291
13623
  await this.tenant.reconcileUserKeysSafe?.();
13292
13624
  await this.tenant.reconcileKeyDistributionsForCurrentUserSafe();
13293
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
+ }
13294
13637
  }
13295
13638
  /**
13296
13639
  * Push changes to a remote content-addressed store or another MindooDB instance.