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
@@ -2,6 +2,12 @@
2
2
  /**
3
3
  * MindooDB Server - Express HTTP server implementing the sync API.
4
4
  *
5
+ * The route handlers themselves are platform-neutral and live in
6
+ * {@link MindooDBServerCore} (`src/server/http`); this class is the Node host:
7
+ * it wires the server's components, mounts the handlers on Express and adds
8
+ * what only a Node process provides — HTTP/2 + TLS, static files, Iroh, the
9
+ * peer and cluster routers, and the middleware stack below.
10
+ *
5
11
  * Security features:
6
12
  * - Capabilities-based system admin authorization via config.json
7
13
  * - Challenge/response (Ed25519) authentication for /system/* endpoints
@@ -55,104 +61,29 @@ const http2_1 = __importDefault(require("http2"));
55
61
  const path_1 = __importDefault(require("path"));
56
62
  const fs_1 = require("fs");
57
63
  const helmet_1 = __importDefault(require("helmet"));
58
- const errors_1 = require("../../core/errors");
59
64
  const cors_1 = __importDefault(require("cors"));
60
65
  const express_rate_limit_1 = __importStar(require("express-rate-limit"));
61
66
  const compressionMiddleware_1 = require("./compressionMiddleware");
62
- const binaryEntryFraming_1 = require("../../core/appendonlystores/network/binaryEntryFraming");
63
- const entryWireCodec_1 = require("../../core/appendonlystores/network/entryWireCodec");
64
- const SyncEventBus_1 = require("./SyncEventBus");
65
- const ClusterManager_1 = require("./peer/ClusterManager");
67
+ const SyncEventBus_1 = require("../../server/SyncEventBus");
68
+ const ClusterManager_1 = require("../../server/peer/ClusterManager");
69
+ const FileClusterAuditStorage_1 = require("./peer/FileClusterAuditStorage");
70
+ const IrohPeerLink_1 = require("./peer/IrohPeerLink");
66
71
  const routes_1 = require("./peer/routes");
67
- const types_1 = require("./peer/types");
68
- const types_2 = require("../../core/appendonlystores/types");
69
- const types_3 = require("../../core/appendonlystores/network/types");
70
72
  const TenantManager_1 = require("./TenantManager");
71
- const CapabilityMatcher_1 = require("./CapabilityMatcher");
72
- const SystemAdminAuth_1 = require("./SystemAdminAuth");
73
- const DeviceDiscoveryService_1 = require("./DeviceDiscoveryService");
73
+ const CapabilityMatcher_1 = require("../../server/CapabilityMatcher");
74
+ const SystemAdminAuth_1 = require("../../server/SystemAdminAuth");
75
+ const DeviceDiscoveryService_1 = require("../../server/DeviceDiscoveryService");
74
76
  const config_1 = require("./config");
75
77
  const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
76
78
  const IrohServerRpc_1 = require("./IrohServerRpc");
77
79
  const IrohServerEndpoint_1 = require("./IrohServerEndpoint");
78
- const validation_1 = require("./validation");
79
- const urlSafety_1 = require("../../core/utils/urlSafety");
80
- const types_4 = require("./types");
81
- const timestampProxy_1 = require("./timestampProxy");
82
- const rateLimits_1 = require("./rateLimits");
83
- /**
84
- * Wire-format types for the sync HTTP API live in
85
- * {@link ../../core/appendonlystores/network/entryWireCodec}: binary fields
86
- * (signature, encryptedData, rsaEncryptedPayload) are base64 strings in
87
- * transit. Client and server share that codec so signed trailing blocks
88
- * cannot drift off the wire.
89
- */
90
- /**
91
- * Rate-limit defaults. Auth and sync have separate tiers because a single
92
- * challenge/authenticate handshake is two requests; with multiple tenants,
93
- * store kinds, and token renewals the auth budget exhausts quickly if set
94
- * too low. These can be overridden in config.json under `rateLimits.auth`
95
- * and `rateLimits.sync`.
96
- */
97
- const DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS = 60000;
98
- const DEFAULT_AUTH_RATE_LIMIT_MAX = 120;
99
- const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
100
- /**
101
- * Sync is keyed per authenticated principal, and a client legitimately opens
102
- * several databases at once: a host like Teacher's Desk reads six, and a cold
103
- * pass over one of them costs a few hundred batched requests. A budget sized
104
- * for a single database turns that normal open into a throttled one.
105
- */
106
- const DEFAULT_SYNC_RATE_LIMIT_MAX = 3000;
107
- /**
108
- * The global limiter is a coarse net for unauthenticated floods and unknown
109
- * routes; the per-tier limiters are the real budgets. It runs first, so a
110
- * global max below a tier max silently replaces that tier — the sync budget
111
- * was unreachable while this was a hardcoded 500. The default is therefore
112
- * derived from the tiers, and an override that would shadow one is honoured
113
- * but flagged at startup rather than quietly applied.
114
- */
115
- const DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS = 60000;
116
- const GLOBAL_RATE_LIMIT_TIER_HEADROOM = 500;
117
- /**
118
- * Timestamping gets its own budget rather than sharing the sync one. Sync is
119
- * chatty and high-volume; sealing a document is a deliberate, rare user action.
120
- * Sharing a bucket would let ordinary sync traffic starve the seal endpoint, and
121
- * a runaway seal loop degrade sync.
122
- *
123
- * The daily cap matters more than the burst cap: proxying collapses every user
124
- * onto the server's IP, so one tenant can exhaust an upstream's per-IP allowance
125
- * (open-tsa.eu publishes 60/minute) and lock out everyone else on this server.
126
- */
127
- const DEFAULT_TIMESTAMP_RATE_LIMIT_WINDOW_MS = 60000;
128
- const DEFAULT_TIMESTAMP_RATE_LIMIT_MAX = 10;
129
- const DEFAULT_TIMESTAMP_DAILY_LIMIT_MAX = 500;
130
- const TIMESTAMP_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;
80
+ const types_1 = require("../../server/types");
81
+ const timestampProxy_1 = require("../../server/timestampProxy");
82
+ const rateLimits_1 = require("../../server/rateLimits");
83
+ const MindooDBServerCore_1 = require("../../server/http/MindooDBServerCore");
84
+ const expressAdapter_1 = require("./expressAdapter");
85
+ const types_2 = require("../../server/http/types");
131
86
  const DEFAULT_SERVER_SOCKET_TIMEOUT_MS = 120000;
132
- /** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
133
- function parseBodySizeLimitToBytes(limit) {
134
- const normalized = limit.trim().toLowerCase();
135
- if (!normalized) {
136
- return null;
137
- }
138
- if (/^\d+$/.test(normalized)) {
139
- return Number(normalized);
140
- }
141
- const match = normalized.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
142
- if (!match) {
143
- return null;
144
- }
145
- const value = Number(match[1]);
146
- const unit = match[2];
147
- const multiplier = unit === "b"
148
- ? 1
149
- : unit === "kb"
150
- ? 1024
151
- : unit === "mb"
152
- ? 1024 * 1024
153
- : 1024 * 1024 * 1024;
154
- return Math.round(value * multiplier);
155
- }
156
87
  class MindooDBServer {
157
88
  /**
158
89
  * @param dataDir Root directory for on-disk tenant data and server identity
@@ -171,9 +102,9 @@ class MindooDBServer {
171
102
  this.staticDir = staticDir;
172
103
  this.configPath = configPath ?? path_1.default.join(dataDir, "config.json");
173
104
  this.jsonBodyLimit = process.env.MINDOODB_JSON_BODY_LIMIT?.trim() || "5mb";
174
- this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(this.jsonBodyLimit);
175
- this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[types_4.ENV_VARS.TSA_PROVIDERS], {
176
- allowInsecureUrls: /^(1|true)$/i.test(process.env[types_4.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? ""),
105
+ const allowInsecureSyncUrls = /^(1|true)$/i.test(process.env[types_1.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
106
+ this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[types_1.ENV_VARS.TSA_PROVIDERS], {
107
+ allowInsecureUrls: allowInsecureSyncUrls,
177
108
  });
178
109
  if (this.tsaProviders.length > 0) {
179
110
  console.log(`[MindooDBServer] RFC 3161 timestamp proxy enabled with providers: ${this.tsaProviders
@@ -191,7 +122,7 @@ class MindooDBServer {
191
122
  this.deviceDiscovery = new DeviceDiscoveryService_1.DeviceDiscoveryService(this.tenantManager, cryptoAdapter);
192
123
  this.cluster = new ClusterManager_1.ClusterManager({
193
124
  cryptoAdapter,
194
- dataDir,
125
+ auditStorage: new FileClusterAuditStorage_1.FileClusterAuditStorage(dataDir),
195
126
  eventBus: this.syncEventBus,
196
127
  listTrustedServers: () => this.tenantManager.listTrustedServers(),
197
128
  localServerName: () => this.tenantManager.getServerPublicInfo()?.name ?? null,
@@ -205,6 +136,7 @@ class MindooDBServer {
205
136
  onDirectoryPulled: (tenantId) => this.tenantManager.executePendingPurges(tenantId),
206
137
  localRole: this.serverConfig.cluster?.role,
207
138
  getIrohStreamIO: () => this.irohEndpoint.getStreamIO(),
139
+ openIrohLink: (io, ticket) => new IrohPeerLink_1.IrohPeerLink(io, ticket),
208
140
  });
209
141
  // Peer tokens are accepted by the tenant sync routes so a peer can mirror
210
142
  // the encrypted stores through the ordinary protocol; the store bypasses
@@ -225,6 +157,24 @@ class MindooDBServer {
225
157
  },
226
158
  trustedWitnessResolver: (receivedByPublicKey) => this.cluster.auth.findTrustedServerByKey(receivedByPublicKey) !== null,
227
159
  });
160
+ this.core = new MindooDBServerCore_1.MindooDBServerCore({
161
+ tenantManager: this.tenantManager,
162
+ systemAdminAuth: this.systemAdminAuth,
163
+ capabilityMatcher: this.capabilityMatcher,
164
+ deviceDiscovery: this.deviceDiscovery,
165
+ syncEvents: this.syncEventBus,
166
+ configRepository: new config_1.FileServerConfigRepository(this.configPath),
167
+ getServerConfig: () => this.serverConfig,
168
+ reloadConfig: (newConfig) => this.reloadConfig(newConfig),
169
+ tsaProviders: this.tsaProviders,
170
+ jsonBodyLimit: this.jsonBodyLimit,
171
+ allowInsecureSyncUrls,
172
+ cluster: {
173
+ mirrorsForTenant: (tenantId) => this.cluster.mirrorsForTenant(tenantId),
174
+ syncReplicators: () => this.cluster.syncReplicators(),
175
+ resolvePeerName: (token) => this.cluster.auth.validateToken(token).then((payload) => payload?.sub),
176
+ },
177
+ });
228
178
  const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
229
179
  if (principalCount === 0) {
230
180
  console.log(`[MindooDBServer] WARNING: No system admin principals configured. All /system/* endpoints are locked down.`);
@@ -328,10 +278,7 @@ class MindooDBServer {
328
278
  config: irohConfig,
329
279
  handler: (0, IrohServerRpc_1.createMindooDBServerIrohHandler)({
330
280
  getServerPublicInfo: () => this.tenantManager.getServerPublicInfo(),
331
- getJsonBodyLimit: () => ({
332
- limit: this.jsonBodyLimit,
333
- bytes: this.jsonBodyLimitBytes,
334
- }),
281
+ getJsonBodyLimit: () => this.core.getJsonBodyLimit(),
335
282
  getClusterRole: () => this.serverConfig.cluster?.role ?? "peer",
336
283
  listTenantPublicInfosFingerprints: (tenantId) => this.tenantManager.listTenantPublicInfosFingerprints(tenantId),
337
284
  getAuthService: (tenantId) => this.tenantManager.getAuthService(tenantId),
@@ -369,6 +316,10 @@ class MindooDBServer {
369
316
  console.error("[MindooDBServer] Failed to start Iroh listener:", error);
370
317
  }
371
318
  }
319
+ /** Every limiter tier, resolved against the current config.json. */
320
+ rateLimitPolicy() {
321
+ return (0, rateLimits_1.resolveServerRateLimits)(this.serverConfig.rateLimits);
322
+ }
372
323
  /**
373
324
  * Global middleware stack applied to every request, in order:
374
325
  * 1. helmet — security headers (CSP, X-Frame-Options, etc.)
@@ -412,7 +363,6 @@ class MindooDBServer {
412
363
  next();
413
364
  });
414
365
  }
415
- /** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
416
366
  /**
417
367
  * Teach Express which upstream hops to trust for `X-Forwarded-For`, so
418
368
  * `req.ip` is the client rather than the proxy. Every rate limiter keys on
@@ -427,7 +377,7 @@ class MindooDBServer {
427
377
  * list over `true`, which trusts the whole chain.
428
378
  */
429
379
  applyTrustProxySetting() {
430
- const raw = process.env[types_4.ENV_VARS.TRUST_PROXY]?.trim();
380
+ const raw = process.env[types_1.ENV_VARS.TRUST_PROXY]?.trim();
431
381
  if (!raw || /^(0|false)$/i.test(raw)) {
432
382
  return;
433
383
  }
@@ -447,6 +397,7 @@ class MindooDBServer {
447
397
  console.log(`[MindooDBServer] trust proxy set to ${JSON.stringify(value)}`);
448
398
  }
449
399
  }
400
+ /** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
450
401
  readCorsOriginsFromEnv() {
451
402
  const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
452
403
  if (!raw) {
@@ -471,6 +422,7 @@ class MindooDBServer {
471
422
  * /:tenantId/* — per-tenant sync & auth API
472
423
  */
473
424
  setupRoutes() {
425
+ const core = this.core;
474
426
  this.app.get("/", (req, res) => {
475
427
  if (this.staticDir && (0, fs_1.existsSync)(path_1.default.join(this.staticDir, "index.html"))) {
476
428
  res.redirect(302, "/statics/index.html");
@@ -479,99 +431,24 @@ class MindooDBServer {
479
431
  res.status(404).json({ error: "Not found" });
480
432
  }
481
433
  });
482
- this.app.get("/health", (req, res) => {
483
- res.json({ status: "ok", timestamp: Date.now() });
484
- });
434
+ this.app.get("/health", (0, expressAdapter_1.route)(() => core.health()));
485
435
  // Server-wide device discovery (before /:tenantId so "device" is not a tenant id).
486
436
  // Aggressive rate limit: challenge+discover scans every local tenant.
437
+ const deviceLimit = this.rateLimitPolicy().device;
487
438
  const deviceDiscoveryRateLimit = (0, express_rate_limit_1.default)({
488
- windowMs: 60000,
489
- max: 10,
439
+ windowMs: deviceLimit.windowMs,
440
+ max: deviceLimit.max,
490
441
  standardHeaders: true,
491
442
  legacyHeaders: false,
492
443
  message: { error: "Too many device discovery requests, please try again later" },
493
444
  });
494
- this.app.post("/device/challenge", deviceDiscoveryRateLimit, this.handleDeviceChallenge.bind(this));
495
- this.app.post("/device/discover", deviceDiscoveryRateLimit, this.handleDeviceDiscover.bind(this));
445
+ this.app.post("/device/challenge", deviceDiscoveryRateLimit, (0, expressAdapter_1.route)((req) => core.deviceChallenge(req)));
446
+ this.app.post("/device/discover", deviceDiscoveryRateLimit, (0, expressAdapter_1.route)((req) => core.deviceDiscover(req)));
496
447
  // .well-known endpoints are unauthenticated so clients can discover
497
448
  // server identity and tenant availability before attempting to authenticate.
498
- this.app.get("/.well-known/mindoodb-server-info", (req, res) => {
499
- const info = this.tenantManager.getServerPublicInfo();
500
- if (!info) {
501
- res.status(503).json({ error: "Server identity not initialized" });
502
- return;
503
- }
504
- res.json({
505
- ...info,
506
- // Its own mesh role, so `add-to-network` can propagate it into the
507
- // other side's trusted-servers entry instead of asking the operator to
508
- // repeat it on every node.
509
- clusterRole: this.serverConfig.cluster?.role ?? "peer",
510
- maxJsonRequestBodyLimit: this.jsonBodyLimit,
511
- maxJsonRequestBodyBytes: this.jsonBodyLimitBytes ?? undefined,
512
- // Lets a client decide between calling a browser-reachable TSA directly
513
- // and routing through this server, per tenant and without a probe.
514
- timestamping: {
515
- enabled: this.tsaProviders.length > 0,
516
- providers: (0, timestampProxy_1.toPublicTsaProviders)(this.tsaProviders),
517
- },
518
- });
519
- });
520
- /**
521
- * Mirrors of one tenant, for client failover.
522
- *
523
- * Scoped to the tenant in the query on purpose: a client asking about
524
- * tenant A must not learn which other tenants this server replicates, or
525
- * to whom. Unauthenticated like the other discovery endpoints — a client
526
- * needs it precisely when it cannot reach a server to authenticate with.
527
- */
528
- this.app.get("/.well-known/mindoodb-cluster", (req, res) => {
529
- const tenantId = typeof req.query.tenantId === "string" ? req.query.tenantId.toLowerCase() : "";
530
- if (!tenantId) {
531
- res.status(400).json({ error: "tenantId query parameter is required" });
532
- return;
533
- }
534
- try {
535
- (0, validation_1.validateTenantId)(tenantId);
536
- }
537
- catch {
538
- res.status(400).json({ error: "Invalid tenantId format" });
539
- return;
540
- }
541
- if (!this.tenantManager.tenantExists(tenantId)) {
542
- res.status(404).json({ error: "Tenant not found on server" });
543
- return;
544
- }
545
- res.json({ tenantId, mirrors: this.cluster.mirrorsForTenant(tenantId) });
546
- });
547
- this.app.get("/.well-known/mindoodb-tenants/:tenantId/publicinfos-fingerprints", async (req, res) => {
548
- const tenantId = req.params.tenantId.toLowerCase();
549
- try {
550
- (0, validation_1.validateTenantId)(tenantId);
551
- const fingerprints = await this.tenantManager.listTenantPublicInfosFingerprints(tenantId);
552
- if (fingerprints.length === 0) {
553
- res.status(404).json({ error: "Tenant not found on server" });
554
- return;
555
- }
556
- const response = {
557
- tenantId,
558
- fingerprints,
559
- };
560
- res.json(response);
561
- }
562
- catch (error) {
563
- if (error instanceof validation_1.ValidationError) {
564
- res.status(400).json({ error: error.message });
565
- return;
566
- }
567
- if (error instanceof Error && error.message.includes("not found")) {
568
- res.status(404).json({ error: "Tenant not found on server" });
569
- return;
570
- }
571
- console.error("[MindooDBServer] Error reading tenant publicInfos fingerprints:", error);
572
- res.status(500).json({ error: "Failed to read tenant publicInfos fingerprints" });
573
- }
574
- });
449
+ this.app.get("/.well-known/mindoodb-server-info", (0, expressAdapter_1.route)(() => core.serverInfo()));
450
+ this.app.get("/.well-known/mindoodb-cluster", (0, expressAdapter_1.route)((req) => core.clusterMirrors(req)));
451
+ this.app.get("/.well-known/mindoodb-tenants/:tenantId/publicinfos-fingerprints", (0, expressAdapter_1.route)((req) => core.publicInfosFingerprints(req)));
575
452
  // Static file serving
576
453
  if (this.staticDir) {
577
454
  const resolvedStaticDir = path_1.default.resolve(this.staticDir);
@@ -592,9 +469,10 @@ class MindooDBServer {
592
469
  if (!(0, SystemIpAllowlist_1.isSystemIpAllowListDisabled)(systemIpRaw)) {
593
470
  console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
594
471
  }
472
+ const systemLimit = this.rateLimitPolicy().system;
595
473
  const systemRateLimit = (0, express_rate_limit_1.default)({
596
- windowMs: this.serverConfig.rateLimits?.system?.windowMs ?? 60000,
597
- max: this.serverConfig.rateLimits?.system?.max ?? 30,
474
+ windowMs: systemLimit.windowMs,
475
+ max: systemLimit.max,
598
476
  standardHeaders: true,
599
477
  legacyHeaders: false,
600
478
  message: { error: "Too many system requests, please try again later" },
@@ -604,9 +482,10 @@ class MindooDBServer {
604
482
  // would lock out the operator's own cluster, and 30 req/min would throttle
605
483
  // replication to a crawl. They are protected by the peer JWT, which is only
606
484
  // issued to a key in trusted-servers.json.
485
+ const peerLimit = this.rateLimitPolicy().peer;
607
486
  this.app.use("/system/peer", (0, express_rate_limit_1.default)({
608
- windowMs: 60000,
609
- max: 600,
487
+ windowMs: peerLimit.windowMs,
488
+ max: peerLimit.max,
610
489
  standardHeaders: true,
611
490
  legacyHeaders: false,
612
491
  message: { error: "Too many peer requests, please try again later" },
@@ -619,93 +498,12 @@ class MindooDBServer {
619
498
  const systemRouter = (0, express_1.Router)();
620
499
  this.setupSystemRoutes(systemRouter);
621
500
  this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
622
- // Tenant-scoped routes: tenantMiddleware validates the :tenantId param
501
+ // Tenant-scoped routes: the tenant guard validates the :tenantId param
623
502
  // and rejects unknown tenants before any handler runs.
624
- this.app.use("/:tenantId", this.tenantMiddleware.bind(this), this.createTenantRouter());
503
+ this.app.use("/:tenantId", (0, expressAdapter_1.guard)((req) => core.tenantGuard(req)), this.createTenantRouter());
625
504
  // Express error handler — must be registered last and have the 4-arg signature
626
505
  this.app.use(this.errorHandler.bind(this));
627
506
  }
628
- // ==================== System Admin Auth Middleware ====================
629
- /**
630
- * JWT-based middleware for /system/* routes (excluding /system/auth/*).
631
- * Extracts and validates the bearer token, then checks capabilities
632
- * for the current (method, path, username, publicsignkey).
633
- *
634
- * `DELETE /system/tenants/:tenantId` is also allowed when the JWT signing
635
- * key is that tenant's registered `adminSigningPublicKey` (own-tenant
636
- * delete). Sub-resources and other tenants stay capability-gated.
637
- */
638
- async systemAdminMiddleware(req, res, next) {
639
- const auth = req.headers.authorization;
640
- if (!auth || !auth.startsWith("Bearer ")) {
641
- res.status(401).json({ error: "Missing or invalid Authorization header" });
642
- return;
643
- }
644
- const token = auth.substring(7);
645
- const payload = await this.systemAdminAuth.validateToken(token);
646
- if (!payload) {
647
- res.status(401).json({ error: "Invalid or expired token" });
648
- return;
649
- }
650
- // The original request path relative to the /system mount is in req.path,
651
- // but we need the full path for capability matching.
652
- const fullPath = `/system${req.path}`;
653
- const method = req.method.toUpperCase();
654
- const capabilityAuthorized = this.capabilityMatcher.isAuthorized(method, fullPath, payload.sub, payload.publicsignkey);
655
- const ownerDeleteAuthorized = !capabilityAuthorized &&
656
- this.isOwnTenantDeleteAuthorized(method, fullPath, payload.publicsignkey);
657
- if (!capabilityAuthorized && !ownerDeleteAuthorized) {
658
- res.status(403).json({ error: "Forbidden: insufficient capabilities" });
659
- return;
660
- }
661
- req.systemAdmin = {
662
- username: payload.sub,
663
- publicsignkey: payload.publicsignkey,
664
- };
665
- next();
666
- }
667
- /**
668
- * Owner bypass for deleting the caller's own tenant. Matches only
669
- * `DELETE /system/tenants/:tenantId` (no sub-resource) and requires the
670
- * JWT public key to equal that tenant's stored admin signing key.
671
- */
672
- isOwnTenantDeleteAuthorized(method, fullPath, publicsignkey) {
673
- if (method !== "DELETE") {
674
- return false;
675
- }
676
- const tenantId = (0, CapabilityMatcher_1.parseSystemTenantItemPath)(fullPath);
677
- if (!tenantId) {
678
- return false;
679
- }
680
- try {
681
- (0, validation_1.validateTenantIdFormat)(tenantId);
682
- }
683
- catch {
684
- return false;
685
- }
686
- return this.tenantManager.isTenantAdminSigningKeyFor(tenantId, publicsignkey);
687
- }
688
- // ==================== Tenant Middleware ====================
689
- tenantMiddleware(req, res, next) {
690
- const rawTenantId = req.params.tenantId?.toLowerCase();
691
- if (!rawTenantId) {
692
- res.status(400).json({ error: "Tenant ID required" });
693
- return;
694
- }
695
- try {
696
- (0, validation_1.validateTenantIdFormat)(rawTenantId);
697
- }
698
- catch {
699
- res.status(400).json({ error: "Invalid tenant ID format" });
700
- return;
701
- }
702
- if (!this.tenantManager.tenantExists(rawTenantId)) {
703
- res.status(404).json({ error: this.formatTenantNotFoundOnServerError(rawTenantId) });
704
- return;
705
- }
706
- req.tenantId = rawTenantId;
707
- next();
708
- }
709
507
  // ==================== System Routes ====================
710
508
  /**
711
509
  * Wire up all /system/* routes. Auth endpoints sit outside the JWT
@@ -714,379 +512,39 @@ class MindooDBServer {
714
512
  * passes through the CapabilityMatcher for fine-grained authorization.
715
513
  */
716
514
  setupSystemRoutes(router) {
515
+ const core = this.core;
717
516
  const challengeRateLimit = this.createAuthRateLimit("system", "challenge");
718
517
  const authenticateRateLimit = this.createAuthRateLimit("system", "authenticate");
719
- router.post("/auth/challenge", challengeRateLimit, this.handleSystemChallenge.bind(this));
720
- router.post("/auth/authenticate", authenticateRateLimit, this.handleSystemAuthenticate.bind(this));
721
- const authMiddleware = this.systemAdminMiddleware.bind(this);
518
+ router.post("/auth/challenge", challengeRateLimit, (0, expressAdapter_1.route)((req) => core.systemChallenge(req)));
519
+ router.post("/auth/authenticate", authenticateRateLimit, (0, expressAdapter_1.route)((req) => core.systemAuthenticate(req)));
520
+ // Capabilities are matched against the full path below /system. req.path
521
+ // is relative to the mount the middleware runs under, so a middleware
522
+ // mounted below /system names its own prefix.
523
+ const authMiddlewareFor = (prefix) => (req, res, next) => (0, expressAdapter_1.guard)((serverRequest) => core.systemAdminGuard(serverRequest, `${prefix}${req.path}`))(req, res, next);
524
+ const authMiddleware = authMiddlewareFor("/system");
722
525
  // Cluster administration. Behind the same JWT + capability middleware as
723
526
  // the rest of /system, so config.json can grant an auditor principal the
724
527
  // GET routes without granting the POST/PATCH/DELETE ones.
725
- router.use("/cluster", authMiddleware, (0, routes_1.createClusterRouter)({
528
+ router.use("/cluster", authMiddlewareFor("/system/cluster"), (0, routes_1.createClusterRouter)({
726
529
  cluster: this.cluster,
727
530
  listTrustedServers: () => this.tenantManager.listTrustedServers(),
728
531
  saveTrustedServer: (peer) => this.tenantManager.saveTrustedServer(peer),
729
532
  removeTrustedServer: (name) => this.tenantManager.removeTrustedServer(name),
730
533
  }));
731
534
  // Tenant CRUD
732
- router.get("/tenants", authMiddleware, (req, res) => {
733
- try {
734
- const tenants = this.tenantManager.listTenants();
735
- const response = { tenants };
736
- res.json(response);
737
- }
738
- catch (error) {
739
- console.error("[MindooDBServer] Error listing tenants:", error);
740
- res.status(500).json({ error: "Failed to list tenants" });
741
- }
742
- });
743
- // Tenant registration is idempotent: re-POSTing with the same tenantId
744
- // and matching $publicinfos key returns 200 instead of 409.
745
- router.post("/tenants/:tenantId", authMiddleware, async (req, res) => {
746
- try {
747
- const tenantId = req.params.tenantId.toLowerCase();
748
- try {
749
- (0, validation_1.validateTenantId)(tenantId);
750
- }
751
- catch (validationError) {
752
- res.status(400).json({
753
- error: validationError instanceof validation_1.ValidationError
754
- ? validationError.message
755
- : "Invalid tenantId format",
756
- });
757
- return;
758
- }
759
- const request = {
760
- ...req.body,
761
- tenantId,
762
- };
763
- if (!request.adminSigningPublicKey) {
764
- res.status(400).json({ error: "adminSigningPublicKey is required" });
765
- return;
766
- }
767
- if (!request.adminEncryptionPublicKey) {
768
- res.status(400).json({ error: "adminEncryptionPublicKey is required" });
769
- return;
770
- }
771
- if (request.adminUsername !== undefined) {
772
- (0, validation_1.validateUsername)(request.adminUsername);
773
- }
774
- (0, validation_1.validateStringLength)(request.adminSigningPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminSigningPublicKey");
775
- (0, validation_1.validateStringLength)(request.adminEncryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminEncryptionPublicKey");
776
- if (!request.encryptedPublicInfosKey && !request.publicInfosKey) {
777
- res.status(400).json({ error: "encryptedPublicInfosKey or publicInfosKey is required" });
778
- return;
779
- }
780
- if (request.publicInfosKey !== undefined) {
781
- (0, validation_1.validateStringLength)(request.publicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "publicInfosKey");
782
- }
783
- if (request.encryptedPublicInfosKey !== undefined) {
784
- (0, validation_1.validateStringLength)(request.encryptedPublicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptedPublicInfosKey");
785
- }
786
- const result = await this.tenantManager.registerTenant(request);
787
- const response = {
788
- success: true,
789
- tenantId: result.context.tenantId,
790
- created: result.created,
791
- message: result.created
792
- ? `Tenant ${result.context.tenantId} registered successfully`
793
- : `Tenant ${result.context.tenantId} already exists with matching $publicinfos key`,
794
- };
795
- res.status(result.created ? 201 : 200).json(response);
796
- }
797
- catch (error) {
798
- if (error instanceof validation_1.ValidationError) {
799
- res.status(400).json({ error: error.message });
800
- return;
801
- }
802
- if (error instanceof Error && error.message.includes("different $publicinfos key")) {
803
- res.status(409).json({ error: error.message });
804
- return;
805
- }
806
- console.error("[MindooDBServer] Error registering tenant:", error);
807
- res.status(500).json({ error: "Failed to register tenant" });
808
- }
809
- });
810
- router.put("/tenants/:tenantId", authMiddleware, (req, res) => {
811
- try {
812
- const tenantId = req.params.tenantId.toLowerCase();
813
- try {
814
- (0, validation_1.validateTenantId)(tenantId);
815
- }
816
- catch {
817
- res.status(400).json({ error: "Invalid tenantId format" });
818
- return;
819
- }
820
- if (!this.tenantManager.tenantExists(tenantId)) {
821
- res.status(404).json({ error: "Tenant not found" });
822
- return;
823
- }
824
- this.tenantManager.updateTenantConfig(tenantId, req.body);
825
- res.json({ success: true, message: `Tenant ${tenantId} updated` });
826
- }
827
- catch (error) {
828
- if (error instanceof validation_1.ValidationError) {
829
- res.status(400).json({ error: error.message });
830
- return;
831
- }
832
- console.error("[MindooDBServer] Error updating tenant:", error);
833
- res.status(500).json({ error: "Failed to update tenant" });
834
- }
835
- });
836
- router.delete("/tenants/:tenantId", authMiddleware, async (req, res) => {
837
- try {
838
- const tenantId = req.params.tenantId.toLowerCase();
839
- try {
840
- (0, validation_1.validateTenantId)(tenantId);
841
- }
842
- catch {
843
- res.status(400).json({ error: "Invalid tenantId format" });
844
- return;
845
- }
846
- if (!this.tenantManager.tenantExists(tenantId)) {
847
- res.status(404).json({ error: "Tenant not found" });
848
- return;
849
- }
850
- await this.tenantManager.removeTenant(tenantId);
851
- res.json({ success: true, message: `Tenant ${tenantId} removed` });
852
- }
853
- catch (error) {
854
- console.error("[MindooDBServer] Error removing tenant:", error);
855
- res.status(500).json({ error: "Failed to remove tenant" });
856
- }
857
- });
535
+ router.get("/tenants", authMiddleware, (0, expressAdapter_1.route)(() => core.listTenants()));
536
+ router.post("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.registerTenant(req)));
537
+ router.put("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.updateTenant(req)));
538
+ router.delete("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.deleteTenant(req)));
858
539
  // Trusted server management
859
- router.get("/trusted-servers", authMiddleware, (req, res) => {
860
- try {
861
- res.json({ servers: this.tenantManager.listTrustedServers() });
862
- }
863
- catch (error) {
864
- console.error("[MindooDBServer] Error listing trusted servers:", error);
865
- res.status(500).json({ error: "Failed to list trusted servers" });
866
- }
867
- });
868
- router.post("/trusted-servers", authMiddleware, (req, res) => {
869
- try {
870
- const { name, signingPublicKey, encryptionPublicKey, url, direction, role, attachments } = req.body;
871
- if (!name || !signingPublicKey || !encryptionPublicKey) {
872
- res.status(400).json({ error: "name, signingPublicKey, and encryptionPublicKey are required" });
873
- return;
874
- }
875
- (0, validation_1.validateStringLength)(name, 256, "name");
876
- (0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
877
- (0, validation_1.validateStringLength)(encryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptionPublicKey");
878
- const server = { name, signingPublicKey, encryptionPublicKey };
879
- // Replication settings are optional: an entry without a url is a trust
880
- // relationship only (inbound peers may authenticate, we never dial).
881
- if (url !== undefined) {
882
- (0, validation_1.validateStringLength)(url, 2048, "url");
883
- const allowInsecure = /^(1|true)$/i.test(process.env[types_4.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
884
- try {
885
- (0, urlSafety_1.assertSafeSyncUrl)(url, {
886
- requireHttps: !allowInsecure,
887
- allowPrivate: allowInsecure,
888
- });
889
- }
890
- catch (e) {
891
- if (e instanceof urlSafety_1.UnsafeUrlError) {
892
- res.status(400).json({ error: e.message });
893
- return;
894
- }
895
- throw e;
896
- }
897
- server.url = url;
898
- }
899
- for (const [field, value, guard] of [
900
- ["direction", direction, types_1.isPeerSyncDirection],
901
- ["role", role, types_1.isPeerRole],
902
- ["attachments", attachments, types_1.isPeerAttachmentMode],
903
- ]) {
904
- if (value === undefined)
905
- continue;
906
- if (!guard(value)) {
907
- res.status(400).json({ error: `Invalid ${field}: ${String(value)}` });
908
- return;
909
- }
910
- Object.assign(server, { [field]: value });
911
- }
912
- this.tenantManager.addTrustedServer(server);
913
- this.cluster.syncReplicators();
914
- res.status(201).json({ success: true, message: `Trusted server "${name}" added` });
915
- }
916
- catch (error) {
917
- if (error instanceof validation_1.ValidationError) {
918
- res.status(400).json({ error: error.message });
919
- return;
920
- }
921
- if (error instanceof Error && error.message.includes("already exists")) {
922
- res.status(409).json({ error: error.message });
923
- return;
924
- }
925
- console.error("[MindooDBServer] Error adding trusted server:", error);
926
- res.status(500).json({ error: "Failed to add trusted server" });
927
- }
928
- });
929
- router.delete("/trusted-servers/:serverName", authMiddleware, (req, res) => {
930
- try {
931
- const serverName = req.params.serverName;
932
- (0, validation_1.validateStringLength)(serverName, 256, "serverName");
933
- const removed = this.tenantManager.removeTrustedServer(serverName);
934
- if (!removed) {
935
- res.status(404).json({ error: "Trusted server not found" });
936
- return;
937
- }
938
- // Revoking trust must also stop replicating with that server.
939
- this.cluster.syncReplicators();
940
- res.json({ success: true, message: `Trusted server "${serverName}" removed` });
941
- }
942
- catch (error) {
943
- if (error instanceof validation_1.ValidationError) {
944
- res.status(400).json({ error: error.message });
945
- return;
946
- }
947
- console.error("[MindooDBServer] Error removing trusted server:", error);
948
- res.status(500).json({ error: "Failed to remove trusted server" });
949
- }
950
- });
540
+ router.get("/trusted-servers", authMiddleware, (0, expressAdapter_1.route)(() => core.listTrustedServers()));
541
+ router.post("/trusted-servers", authMiddleware, (0, expressAdapter_1.route)((req) => core.addTrustedServer(req)));
542
+ router.delete("/trusted-servers/:serverName", authMiddleware, (0, expressAdapter_1.route)((req) => core.removeTrustedServer(req)));
951
543
  // Runtime config management
952
- router.get("/config/backups", authMiddleware, (req, res) => {
953
- try {
954
- res.json({ backups: (0, config_1.listConfigBackups)(this.configPath) });
955
- }
956
- catch (error) {
957
- console.error("[MindooDBServer] Error listing config backups:", error);
958
- res.status(500).json({ error: "Failed to list config backups" });
959
- }
960
- });
961
- router.get("/config/backups/:backupFile", authMiddleware, (req, res) => {
962
- try {
963
- const backupFile = decodeURIComponent(req.params.backupFile);
964
- if (!(0, config_1.isConfigBackupFilename)(backupFile)) {
965
- res.status(400).json({ error: "Invalid config backup filename" });
966
- return;
967
- }
968
- const config = (0, config_1.loadConfigBackup)(this.configPath, backupFile);
969
- res.json({ file: backupFile, config });
970
- }
971
- catch (error) {
972
- if (error instanceof Error && error.message.startsWith("Config backup not found:")) {
973
- res.status(404).json({ error: "Config backup not found" });
974
- return;
975
- }
976
- if (error instanceof Error && error.message.startsWith("Invalid config backup filename:")) {
977
- res.status(400).json({ error: "Invalid config backup filename" });
978
- return;
979
- }
980
- console.error("[MindooDBServer] Error reading config backup:", error);
981
- res.status(500).json({ error: "Failed to read config backup" });
982
- }
983
- });
984
- router.get("/config", authMiddleware, (req, res) => {
985
- res.json(this.serverConfig);
986
- });
987
- router.put("/config", authMiddleware, this.handleUpdateConfig.bind(this));
988
- }
989
- // ==================== System Auth Handlers ====================
990
- async handleSystemChallenge(req, res) {
991
- try {
992
- const { username, publicsignkey } = req.body;
993
- if (!username || typeof username !== "string") {
994
- res.status(400).json({ error: "username is required" });
995
- return;
996
- }
997
- if (!publicsignkey || typeof publicsignkey !== "string") {
998
- res.status(400).json({ error: "publicsignkey is required" });
999
- return;
1000
- }
1001
- (0, validation_1.validateUsername)(username);
1002
- (0, validation_1.validateStringLength)(publicsignkey, validation_1.MAX_PEM_KEY_LENGTH, "publicsignkey");
1003
- const challenge = await this.systemAdminAuth.generateChallenge(username, publicsignkey);
1004
- res.json({ challenge });
1005
- }
1006
- catch (error) {
1007
- if (error instanceof Error && error.message === "Unknown system admin principal") {
1008
- res.status(404).json({ error: "System admin principal not found" });
1009
- return;
1010
- }
1011
- if (error instanceof validation_1.ValidationError) {
1012
- res.status(400).json({ error: error.message });
1013
- return;
1014
- }
1015
- console.error("[MindooDBServer] System challenge error:", error);
1016
- res.status(500).json({ error: "Internal server error" });
1017
- }
1018
- }
1019
- async handleSystemAuthenticate(req, res) {
1020
- try {
1021
- const { challenge, signature } = req.body;
1022
- if (!challenge || !signature) {
1023
- res.status(400).json({ error: "challenge and signature are required" });
1024
- return;
1025
- }
1026
- (0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
1027
- (0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
1028
- const signatureBytes = this.base64ToUint8Array(signature);
1029
- const result = await this.systemAdminAuth.authenticate(challenge, signatureBytes);
1030
- // Return 401 on a failed authentication instead of 200 (audit, Low).
1031
- if (!result.success) {
1032
- res.status(401).json(result);
1033
- return;
1034
- }
1035
- res.json(result);
1036
- }
1037
- catch (error) {
1038
- if (error instanceof validation_1.ValidationError) {
1039
- res.status(400).json({ error: error.message });
1040
- return;
1041
- }
1042
- console.error("[MindooDBServer] System authenticate error:", error);
1043
- res.status(500).json({ error: "Internal server error" });
1044
- }
1045
- }
1046
- // ==================== Config Update Handler ====================
1047
- async handleUpdateConfig(req, res) {
1048
- try {
1049
- let newConfig;
1050
- try {
1051
- newConfig = (0, config_1.validateServerConfig)(req.body, "request body");
1052
- }
1053
- catch (err) {
1054
- res.status(400).json({
1055
- error: err instanceof Error ? err.message : "Invalid config format",
1056
- });
1057
- return;
1058
- }
1059
- // Self-lockout protection: simulate the proposed config against the calling
1060
- // admin's identity — reject if they would lose their own config access.
1061
- const callingAdmin = req.systemAdmin;
1062
- const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
1063
- if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
1064
- res.status(400).json({
1065
- error: "This change would remove your own access to PUT /system/config",
1066
- });
1067
- return;
1068
- }
1069
- // Backup the current config before overwriting
1070
- const { existsSync: fsExists } = await import("fs");
1071
- let backupFile = null;
1072
- if (fsExists(this.configPath)) {
1073
- backupFile = (0, config_1.backupConfig)(this.configPath);
1074
- }
1075
- // Persist new config to disk
1076
- (0, config_1.writeConfig)(this.configPath, newConfig);
1077
- // Hot-swap in-memory state
1078
- this.reloadConfig(newConfig);
1079
- console.log(`[MindooDBServer] Config updated by ${callingAdmin.username}` +
1080
- (backupFile ? ` (backup: ${backupFile})` : ""));
1081
- res.json({
1082
- success: true,
1083
- ...(backupFile ? { backupFile } : {}),
1084
- });
1085
- }
1086
- catch (error) {
1087
- console.error("[MindooDBServer] Error updating config:", error);
1088
- res.status(500).json({ error: "Failed to update config" });
1089
- }
544
+ router.get("/config/backups", authMiddleware, (0, expressAdapter_1.route)(() => core.listConfigBackups()));
545
+ router.get("/config/backups/:backupFile", authMiddleware, (0, expressAdapter_1.route)((req) => core.getConfigBackup(req)));
546
+ router.get("/config", authMiddleware, (0, expressAdapter_1.route)(() => core.getConfig()));
547
+ router.put("/config", authMiddleware, (0, expressAdapter_1.route)((req) => core.updateConfig(req)));
1090
548
  }
1091
549
  // ==================== Tenant Routes ====================
1092
550
  /**
@@ -1095,52 +553,47 @@ class MindooDBServer {
1095
553
  * createSyncRateLimit), sync with a much higher ceiling.
1096
554
  *
1097
555
  * Both "docs" and "attachments" store kinds expose the same set of sync
1098
- * operations, mounted at /sync/docs/* and /sync/attachments/*.
556
+ * operations (SYNC_ROUTES), mounted at /sync/docs/* and /sync/attachments/*.
1099
557
  */
1100
558
  createTenantRouter() {
559
+ const core = this.core;
1101
560
  const router = (0, express_1.Router)({ mergeParams: true });
1102
561
  const authRateLimitConfig = this.getAuthRateLimitConfig();
1103
562
  const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
1104
563
  const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
1105
564
  // Identity resolution precedes the limiter so it can key on the caller.
1106
565
  const syncGuards = [
1107
- this.syncPrincipalMiddleware.bind(this),
566
+ (0, expressAdapter_1.guard)(async (req) => {
567
+ await core.resolveSyncPrincipal(req);
568
+ return null;
569
+ }),
1108
570
  this.createSyncRateLimit(),
1109
571
  ];
1110
572
  console.log(`[MindooDBServer] Tenant auth rate limit configured windowMs=${authRateLimitConfig.windowMs} max=${authRateLimitConfig.max}`);
1111
- router.post("/auth/challenge", challengeRateLimit, this.handleChallenge.bind(this));
1112
- router.post("/auth/authenticate", authenticateRateLimit, this.handleAuthenticate.bind(this));
573
+ router.post("/auth/challenge", challengeRateLimit, (0, expressAdapter_1.route)((req) => core.challenge(req)));
574
+ router.post("/auth/authenticate", authenticateRateLimit, (0, expressAdapter_1.route)((req) => core.authenticate(req)));
1113
575
  // RFC 3161 proxy. Authentication runs *before* the limiters so they can key
1114
576
  // on the identity rather than only the IP — otherwise everyone behind one
1115
577
  // NAT shares a budget, and a single tenant can lock out the rest.
1116
- router.post("/timestamps/rfc3161", this.timestampAuthMiddleware.bind(this), this.createTimestampRateLimit("burst"), this.createTimestampRateLimit("daily"), express_1.default.raw({ type: timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE, limit: timestampProxy_1.MAX_TIMESTAMP_REQUEST_BYTES }), this.handleTimestampRequest.bind(this));
1117
- for (const storeKind of ["docs", "attachments"]) {
1118
- const syncBase = `/sync/${storeKind}`;
1119
- router.post(`${syncBase}/findNewEntries`, syncGuards, this.handleFindNewEntries.bind(this));
1120
- router.post(`${syncBase}/findNewEntriesForDoc`, syncGuards, this.handleFindNewEntriesForDoc.bind(this));
1121
- router.post(`${syncBase}/findEntries`, syncGuards, this.handleFindEntries.bind(this));
1122
- router.post(`${syncBase}/scanEntriesSince`, syncGuards, this.handleScanEntriesSince.bind(this));
1123
- router.post(`${syncBase}/getStoreHead`, syncGuards, this.handleGetStoreHead.bind(this));
1124
- router.post(`${syncBase}/getIdBloomSummary`, syncGuards, this.handleGetIdBloomSummary.bind(this));
1125
- router.post(`${syncBase}/getCompactionStatus`, syncGuards, this.handleGetCompactionStatus.bind(this));
1126
- router.get(`${syncBase}/capabilities`, syncGuards, this.handleGetCapabilities.bind(this));
1127
- // SSE live change feed (sync-v5, phase 5): one long-lived GET per
1128
- // subscriber; putEntries publishes into the in-process event bus.
1129
- router.get(`${syncBase}/events`, syncGuards, this.handleSyncEvents.bind(this));
1130
- router.post(`${syncBase}/getEntries`, syncGuards, this.handleGetEntries.bind(this));
1131
- // Binary wire format v2 (sync-v5, phase 3): octet-stream framing for the
1132
- // two payload-heavy endpoints. putEntriesBinary needs its own raw body
1133
- // parser (the global JSON parser only handles application/json).
1134
- router.post(`${syncBase}/getEntriesBinary`, syncGuards, this.handleGetEntriesBinary.bind(this));
1135
- router.post(`${syncBase}/putEntriesBinary`, syncGuards, express_1.default.raw({ type: binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE, limit: this.jsonBodyLimit }), this.handlePutEntriesBinary.bind(this));
1136
- router.post(`${syncBase}/getEntryMetadata`, syncGuards, this.handleGetEntryMetadata.bind(this));
1137
- router.post(`${syncBase}/putEntries`, syncGuards, this.handlePutEntries.bind(this));
1138
- router.post(`${syncBase}/hasEntries`, syncGuards, this.handleHasEntries.bind(this));
1139
- router.get(`${syncBase}/getAllIds`, syncGuards, this.handleGetAllIds.bind(this));
1140
- router.post(`${syncBase}/resolveDependencies`, syncGuards, this.handleResolveDependencies.bind(this));
1141
- router.post(`${syncBase}/planDocumentMaterialization`, syncGuards, this.handlePlanDocumentMaterialization.bind(this));
1142
- router.post(`${syncBase}/planDocumentMaterializationBatch`, syncGuards, this.handlePlanDocumentMaterializationBatch.bind(this));
1143
- router.post(`${syncBase}/planAttachmentReadByWalkingMetadata`, syncGuards, this.handlePlanAttachmentReadByWalkingMetadata.bind(this));
578
+ router.post("/timestamps/rfc3161", (0, expressAdapter_1.guard)((req) => core.timestampAuthGuard(req)), this.createTimestampRateLimit("burst"), this.createTimestampRateLimit("daily"), express_1.default.raw({ type: timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE, limit: timestampProxy_1.MAX_TIMESTAMP_REQUEST_BYTES }), (0, expressAdapter_1.route)((req) => core.timestampRequest(req)));
579
+ for (const storeKind of MindooDBServerCore_1.SYNC_STORE_KINDS) {
580
+ for (const syncRoute of MindooDBServerCore_1.SYNC_ROUTES) {
581
+ const routePath = `/sync/${storeKind}/${syncRoute.name}`;
582
+ const handlers = [...syncGuards];
583
+ if ("rawBodyType" in syncRoute) {
584
+ // The global JSON parser only handles application/json.
585
+ handlers.push(express_1.default.raw({ type: syncRoute.rawBodyType, limit: this.jsonBodyLimit }));
586
+ }
587
+ handlers.push(syncRoute.name === "events"
588
+ ? (req, res, next) => this.handleSyncEvents(storeKind, req, res).catch(next)
589
+ : (0, expressAdapter_1.route)((req) => core.sync(syncRoute.name, req, storeKind)));
590
+ if (syncRoute.method === "GET") {
591
+ router.get(routePath, handlers);
592
+ }
593
+ else {
594
+ router.post(routePath, handlers);
595
+ }
596
+ }
1144
597
  }
1145
598
  return router;
1146
599
  }
@@ -1150,12 +603,7 @@ class MindooDBServer {
1150
603
  * the net catches only traffic no tier claims.
1151
604
  */
1152
605
  getGlobalRateLimitConfig() {
1153
- const resolved = (0, rateLimits_1.resolveGlobalRateLimit)(this.serverConfig.rateLimits?.global, {
1154
- sync: this.getSyncRateLimitConfig(),
1155
- auth: this.getAuthRateLimitConfig(),
1156
- defaultWindowMs: DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS,
1157
- headroom: GLOBAL_RATE_LIMIT_TIER_HEADROOM,
1158
- });
606
+ const resolved = this.rateLimitPolicy().global;
1159
607
  if (resolved.shadowsTiers) {
1160
608
  console.warn(`[MindooDBServer] rateLimits.global.max=${resolved.max} is below the ${resolved.tierFloor} ` +
1161
609
  "requests the sync and auth tiers may spend in the same window; the global limiter " +
@@ -1165,49 +613,26 @@ class MindooDBServer {
1165
613
  }
1166
614
  /** Resolved auth rate-limit config; falls back to defaults when not set in config.json. */
1167
615
  getAuthRateLimitConfig() {
1168
- return {
1169
- windowMs: this.serverConfig.rateLimits?.auth?.windowMs ?? DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS,
1170
- max: this.serverConfig.rateLimits?.auth?.max ?? DEFAULT_AUTH_RATE_LIMIT_MAX,
1171
- };
1172
- }
1173
- /** Resolved sync rate-limit config; falls back to defaults when not set in config.json. */
1174
- getSyncRateLimitConfig() {
1175
- return {
1176
- windowMs: this.serverConfig.rateLimits?.sync?.windowMs ?? DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS,
1177
- max: this.serverConfig.rateLimits?.sync?.max ?? DEFAULT_SYNC_RATE_LIMIT_MAX,
1178
- };
1179
- }
1180
- /** Resolved timestamp rate-limit config; falls back to defaults when not set in config.json. */
1181
- getTimestampRateLimitConfig() {
1182
- const configured = this.serverConfig.rateLimits?.timestamps;
1183
- return {
1184
- windowMs: configured?.windowMs ?? DEFAULT_TIMESTAMP_RATE_LIMIT_WINDOW_MS,
1185
- max: configured?.max ?? DEFAULT_TIMESTAMP_RATE_LIMIT_MAX,
1186
- dailyMax: configured?.dailyMax ?? DEFAULT_TIMESTAMP_DAILY_LIMIT_MAX,
1187
- };
616
+ return this.rateLimitPolicy().auth;
1188
617
  }
1189
618
  /**
1190
619
  * Two limiters guard the timestamp proxy: a short burst window that keeps a
1191
620
  * misbehaving client from hammering an upstream, and a daily cap that bounds
1192
621
  * total consumption of a shared (often free) upstream allowance.
1193
622
  *
1194
- * Both key on tenant + principal + IP. The principal is present because
1195
- * {@link timestampAuthMiddleware} runs first.
623
+ * Both key on tenant + principal + IP. The principal is present because the
624
+ * timestamp auth guard runs first.
1196
625
  */
1197
626
  createTimestampRateLimit(tier) {
1198
- const config = this.getTimestampRateLimitConfig();
1199
- const windowMs = tier === "burst" ? config.windowMs : TIMESTAMP_DAILY_WINDOW_MS;
627
+ const config = this.rateLimitPolicy().timestamp;
628
+ const windowMs = tier === "burst" ? config.windowMs : rateLimits_1.TIMESTAMP_DAILY_WINDOW_MS;
1200
629
  const max = tier === "burst" ? config.max : config.dailyMax;
1201
630
  return (0, express_rate_limit_1.default)({
1202
631
  windowMs,
1203
632
  max,
1204
633
  standardHeaders: true,
1205
634
  legacyHeaders: false,
1206
- keyGenerator: (req) => {
1207
- const tenantId = req.tenantId ?? "unknown";
1208
- const principal = req.timestampPrincipal ?? "anonymous";
1209
- return `timestamp:${tier}:${tenantId}:${principal}:${this.getClientIpForRateLimit(req)}`;
1210
- },
635
+ keyGenerator: (req) => (0, rateLimits_1.buildTimestampRateLimitKey)(tier, req.tenantId, req.timestampPrincipal, this.getClientIpForRateLimit(req)),
1211
636
  handler: (req, res, _next, options) => {
1212
637
  console.warn(`[MindooDBServer] Timestamp rate limit hit (${tier}) tenant=${req.tenantId} ip=${req.ip}`);
1213
638
  res.status(options.statusCode).json(options.message);
@@ -1219,30 +644,6 @@ class MindooDBServer {
1219
644
  },
1220
645
  });
1221
646
  }
1222
- /**
1223
- * Resolve the caller of a sync request so {@link createSyncRateLimit} can
1224
- * bucket per identity instead of per address.
1225
- *
1226
- * Unlike {@link timestampAuthMiddleware} this never rejects: the route
1227
- * handlers do their own token validation and own the 401, and this pass only
1228
- * needs to know whose budget to spend. An unverifiable token simply falls
1229
- * back to address-only bucketing — a forged one must not be able to conjure
1230
- * a fresh budget, which is why the token is verified rather than decoded.
1231
- */
1232
- async syncPrincipalMiddleware(req, _res, next) {
1233
- try {
1234
- const token = this.extractToken(req);
1235
- const authService = await this.tenantManager.getAuthService(req.tenantId);
1236
- const payload = await authService.validateToken(token);
1237
- if (payload && payload.tenantId === req.tenantId) {
1238
- req.syncPrincipal = payload.deviceSigningKey ?? payload.sub;
1239
- }
1240
- }
1241
- catch {
1242
- // No usable token; fall through to address-only bucketing.
1243
- }
1244
- next();
1245
- }
1246
647
  /**
1247
648
  * Sync budget, keyed on tenant + principal + IP like the auth and timestamp
1248
649
  * tiers. Address-only keying would put every user behind one NAT or reverse
@@ -1250,7 +651,7 @@ class MindooDBServer {
1250
651
  * throttle everyone else on that address.
1251
652
  */
1252
653
  createSyncRateLimit() {
1253
- const config = this.getSyncRateLimitConfig();
654
+ const config = this.rateLimitPolicy().sync;
1254
655
  console.log(`[MindooDBServer] Tenant sync rate limit configured windowMs=${config.windowMs} max=${config.max}`);
1255
656
  return (0, express_rate_limit_1.default)({
1256
657
  windowMs: config.windowMs,
@@ -1266,91 +667,12 @@ class MindooDBServer {
1266
667
  message: { error: "Too many sync requests, please try again later" },
1267
668
  });
1268
669
  }
1269
- /**
1270
- * Authenticate a timestamp request with the tenant's existing bearer token and
1271
- * record the principal for rate-limit bucketing.
1272
- *
1273
- * The proxy makes outbound requests on the caller's behalf, so it must never
1274
- * be open: unauthenticated it would be a free relay to third-party services.
1275
- */
1276
- async timestampAuthMiddleware(req, res, next) {
1277
- try {
1278
- const token = this.extractToken(req);
1279
- const authService = await this.tenantManager.getAuthService(req.tenantId);
1280
- const payload = await authService.validateToken(token);
1281
- if (!payload || payload.tenantId !== req.tenantId) {
1282
- res.status(401).json({ error: "Invalid or expired token" });
1283
- return;
1284
- }
1285
- req.timestampPrincipal = payload.deviceSigningKey ?? payload.sub;
1286
- next();
1287
- }
1288
- catch (error) {
1289
- this.handleRequestError(error, res);
1290
- }
1291
- }
1292
- /**
1293
- * Forward a DER TimeStampReq to a configured TSA and return the DER
1294
- * TimeStampResp verbatim.
1295
- *
1296
- * The server neither parses nor validates the ASN.1: the client checks that
1297
- * the returned token's `messageImprint` matches the digest it sent and that
1298
- * the token chains to a trusted root. That is what reduces a hostile proxy —
1299
- * ours or anyone's — from a forgery risk to a denial-of-service risk.
1300
- */
1301
- async handleTimestampRequest(req, res) {
1302
- if (this.tsaProviders.length === 0) {
1303
- res.status(503).json({ error: "Timestamping is not enabled on this server" });
1304
- return;
1305
- }
1306
- const body = req.body;
1307
- if (!Buffer.isBuffer(body)) {
1308
- res.status(415).json({
1309
- error: `Timestamp requests must be sent as ${timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE}`,
1310
- });
1311
- return;
1312
- }
1313
- // The provider is named by id and resolved against the env allowlist; a
1314
- // client-supplied URL would make this an SSRF gadget.
1315
- const requestedProvider = req.query.provider;
1316
- let preferredProviderId;
1317
- if (typeof requestedProvider === "string" && requestedProvider.length > 0) {
1318
- preferredProviderId = requestedProvider.toLowerCase();
1319
- }
1320
- else if (requestedProvider !== undefined) {
1321
- res.status(400).json({ error: "provider must be a single string value" });
1322
- return;
1323
- }
1324
- try {
1325
- const result = await (0, timestampProxy_1.requestTimestampToken)({
1326
- providers: this.tsaProviders,
1327
- request: new Uint8Array(body),
1328
- preferredProviderId,
1329
- });
1330
- res.setHeader("content-type", timestampProxy_1.TIMESTAMP_REPLY_CONTENT_TYPE);
1331
- res.setHeader("x-mindoodb-tsa-provider", result.providerId);
1332
- res.status(200).send(Buffer.from(result.response));
1333
- }
1334
- catch (error) {
1335
- if (error instanceof timestampProxy_1.TimestampProxyError) {
1336
- if (error.attempts.length > 0) {
1337
- console.warn(`[MindooDBServer] Timestamp request failed for tenant ${req.tenantId}: ${error.attempts
1338
- .map((attempt) => `${attempt.providerId}=${attempt.error ?? "ok"}`)
1339
- .join("; ")}`);
1340
- }
1341
- res.status(error.status).json({ error: error.message });
1342
- return;
1343
- }
1344
- console.error("[MindooDBServer] Unexpected timestamp proxy error:", error);
1345
- res.status(500).json({ error: "Failed to obtain a timestamp" });
1346
- }
1347
- }
1348
670
  /**
1349
671
  * Create a rate-limiter for authentication endpoints. Challenge and
1350
672
  * authenticate get separate limiters so that one phase can't starve the
1351
- * other. The custom keyGenerator buckets by (scope, tenant, phase, IP,
1352
- * username) — this prevents users behind a shared IP (NAT / reverse
1353
- * proxy) from exhausting each other's budgets.
673
+ * other. The key buckets by (scope, tenant, phase, IP, username) — this
674
+ * prevents users behind a shared IP (NAT / reverse proxy) from exhausting
675
+ * each other's budgets.
1354
676
  */
1355
677
  createAuthRateLimit(scope, phase) {
1356
678
  const authRateLimitConfig = this.getAuthRateLimitConfig();
@@ -1359,7 +681,7 @@ class MindooDBServer {
1359
681
  max: authRateLimitConfig.max,
1360
682
  standardHeaders: true,
1361
683
  legacyHeaders: false,
1362
- keyGenerator: (req) => this.getAuthRateLimitKey(req, scope, phase),
684
+ keyGenerator: (req) => (0, rateLimits_1.buildAuthRateLimitKey)(scope, phase, req.tenantId ?? req.params.tenantId?.toLowerCase(), this.getClientIpForRateLimit(req), req.body),
1363
685
  handler: (req, res, _next, options) => {
1364
686
  this.logAuthRateLimitHit(req, scope, phase);
1365
687
  res.status(options.statusCode).json(options.message);
@@ -1367,30 +689,6 @@ class MindooDBServer {
1367
689
  message: { error: "Too many authentication attempts, please try again later" },
1368
690
  });
1369
691
  }
1370
- /**
1371
- * Composite rate-limit key. Challenge requests include the username so
1372
- * that brute-forcing one account doesn't block legitimate users on the
1373
- * same IP. Authenticate requests omit username because the challenge
1374
- * token is already scoped to a single principal.
1375
- */
1376
- getAuthRateLimitKey(req, scope, phase) {
1377
- const tenantId = scope === "tenant"
1378
- ? req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"
1379
- : "system";
1380
- const clientIp = this.getClientIpForRateLimit(req);
1381
- if (phase === "challenge") {
1382
- // Bucket by username when supplied, otherwise by the signing public key a
1383
- // key-based client identifies with, so key-based clients still get a
1384
- // stable per-principal bucket instead of all sharing "-".
1385
- const principal = typeof req.body?.username === "string" && req.body.username.trim()
1386
- ? req.body.username.trim().toLowerCase()
1387
- : typeof req.body?.signingPublicKey === "string" && req.body.signingPublicKey.trim()
1388
- ? req.body.signingPublicKey.trim()
1389
- : "-";
1390
- return `${scope}:${tenantId}:${phase}:${clientIp}:${principal}`;
1391
- }
1392
- return `${scope}:${tenantId}:${phase}:${clientIp}`;
1393
- }
1394
692
  /**
1395
693
  * Emit a structured warning when an auth rate limit is triggered.
1396
694
  * The forwarded-for header is logged to help diagnose situations where
@@ -1420,739 +718,47 @@ class MindooDBServer {
1420
718
  : req.socket?.remoteAddress) ?? "unknown";
1421
719
  return (0, express_rate_limit_1.ipKeyGenerator)(raw);
1422
720
  }
1423
- // ==================== Validation Helpers ====================
1424
- validateDbId(dbId) {
1425
- return (0, validation_1.validateIdentifier)(dbId, "dbId");
1426
- }
1427
- formatTenantNotFoundOnServerError(tenantId) {
1428
- const serverName = this.tenantManager.getServerPublicInfo()?.name ?? "unknown";
1429
- return `Tenant ${tenantId} not found on server ${serverName}`;
1430
- }
1431
- // ==================== Device Discovery (server-wide) ====================
1432
- async handleDeviceChallenge(req, res) {
1433
- try {
1434
- const { signingPublicKey } = req.body ?? {};
1435
- if (typeof signingPublicKey !== "string" || !signingPublicKey.trim()) {
1436
- res.status(400).json({ error: "signingPublicKey is required" });
1437
- return;
1438
- }
1439
- (0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
1440
- const challenge = this.deviceDiscovery.createChallenge(signingPublicKey);
1441
- res.json({ challenge });
1442
- }
1443
- catch (error) {
1444
- this.handleRequestError(error, res);
1445
- }
1446
- }
1447
- async handleDeviceDiscover(req, res) {
1448
- try {
1449
- const { challenge, signature } = req.body ?? {};
1450
- if (typeof challenge !== "string" || !challenge.trim()) {
1451
- res.status(400).json({ error: "challenge is required" });
1452
- return;
1453
- }
1454
- if (typeof signature !== "string" || !signature.trim()) {
1455
- res.status(400).json({ error: "signature is required" });
1456
- return;
1457
- }
1458
- (0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
1459
- (0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
1460
- const signatureBytes = this.base64ToUint8Array(signature);
1461
- const result = await this.deviceDiscovery.discover(challenge, signatureBytes);
1462
- res.json(result);
1463
- }
1464
- catch (error) {
1465
- if (error instanceof DeviceDiscoveryService_1.DeviceDiscoveryAuthError) {
1466
- res.status(401).json({ error: error.message });
1467
- return;
1468
- }
1469
- this.handleRequestError(error, res);
1470
- }
1471
- }
1472
- // ==================== Tenant Auth Handlers ====================
1473
- // Two-step challenge-response flow:
1474
- // 1. POST /auth/challenge → client sends username, server returns a random nonce
1475
- // 2. POST /auth/authenticate → client signs the nonce with its Ed25519 private key,
1476
- // server verifies the signature and returns a JWT for subsequent sync calls
1477
- async handleChallenge(req, res) {
1478
- try {
1479
- const { username, signingPublicKey } = req.body;
1480
- // The username is optional: a client may identify itself by its device
1481
- // signing public key instead, so the server never needs the cleartext
1482
- // name. Validate whichever was supplied.
1483
- if (username !== undefined && username !== null) {
1484
- (0, validation_1.validateUsername)(username);
1485
- }
1486
- if (signingPublicKey !== undefined && signingPublicKey !== null) {
1487
- (0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_SIGNATURE_LENGTH, "signingPublicKey");
1488
- }
1489
- const authService = await this.tenantManager.getAuthService(req.tenantId);
1490
- const challenge = await authService.generateChallenge(typeof username === "string" ? username : undefined, typeof signingPublicKey === "string" ? { signingPublicKey } : undefined);
1491
- res.json({ challenge });
1492
- }
1493
- catch (error) {
1494
- this.handleRequestError(error, res);
1495
- }
1496
- }
1497
- async handleAuthenticate(req, res) {
1498
- try {
1499
- const { challenge, signature } = req.body;
1500
- if (!challenge || !signature) {
1501
- res.status(400).json({ error: "challenge and signature are required" });
1502
- return;
1503
- }
1504
- (0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
1505
- (0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
1506
- const authService = await this.tenantManager.getAuthService(req.tenantId);
1507
- const signatureBytes = this.base64ToUint8Array(signature);
1508
- const result = await authService.authenticate(challenge, signatureBytes);
1509
- // Return 401 on a failed authentication instead of 200 (audit, Low): a
1510
- // failed credential check must not look like a success at the HTTP layer.
1511
- if (!result.success) {
1512
- res.status(401).json(result);
1513
- return;
1514
- }
1515
- res.json(result);
1516
- }
1517
- catch (error) {
1518
- this.handleRequestError(error, res);
1519
- }
1520
- }
1521
- // ==================== Sync Handlers ====================
1522
- async handleFindNewEntries(req, res) {
1523
- try {
1524
- const token = this.extractToken(req);
1525
- const { dbId, haveIds } = req.body;
1526
- const validDbId = this.validateDbId(dbId);
1527
- (0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
1528
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1529
- const entries = await serverStore.handleFindNewEntries(token, haveIds || []);
1530
- res.json({
1531
- entries: entries.map((e) => this.serializeEntryMetadata(e)),
1532
- });
1533
- }
1534
- catch (error) {
1535
- this.handleRequestError(error, res);
1536
- }
1537
- }
1538
- async handleFindNewEntriesForDoc(req, res) {
1539
- try {
1540
- const token = this.extractToken(req);
1541
- const { dbId, haveIds, docId } = req.body;
1542
- const validDbId = this.validateDbId(dbId);
1543
- if (!docId) {
1544
- res.status(400).json({ error: "docId is required" });
1545
- return;
1546
- }
1547
- (0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
1548
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1549
- const entries = await serverStore.handleFindNewEntriesForDoc(token, haveIds || [], docId);
1550
- res.json({
1551
- entries: entries.map((e) => this.serializeEntryMetadata(e)),
1552
- });
1553
- }
1554
- catch (error) {
1555
- this.handleRequestError(error, res);
1556
- }
1557
- }
1558
- async handleFindEntries(req, res) {
1559
- try {
1560
- const token = this.extractToken(req);
1561
- const { dbId, type, creationDateFrom, creationDateUntil } = req.body;
1562
- const validDbId = this.validateDbId(dbId);
1563
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1564
- const entries = await serverStore.handleFindEntries(token, type, creationDateFrom ?? null, creationDateUntil ?? null);
1565
- res.json({
1566
- entries: entries.map((e) => this.serializeEntryMetadata(e)),
1567
- });
1568
- }
1569
- catch (error) {
1570
- this.handleRequestError(error, res);
1571
- }
1572
- }
1573
- async handleScanEntriesSince(req, res) {
1574
- try {
1575
- const token = this.extractToken(req);
1576
- const { dbId, cursor, limit, filters } = req.body;
1577
- const validDbId = this.validateDbId(dbId);
1578
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1579
- const result = await serverStore.handleScanEntriesSince(token, cursor ?? null, limit, filters);
1580
- res.json({
1581
- entries: result.entries.map((e) => this.serializeEntryMetadata(e)),
1582
- nextCursor: result.nextCursor,
1583
- hasMore: result.hasMore,
1584
- });
1585
- }
1586
- catch (error) {
1587
- this.handleRequestError(error, res);
1588
- }
1589
- }
1590
- async handleGetStoreHead(req, res) {
1591
- try {
1592
- const token = this.extractToken(req);
1593
- const { dbId } = req.body;
1594
- const validDbId = this.validateDbId(dbId);
1595
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1596
- const head = await serverStore.handleGetStoreHead(token);
1597
- res.json({ head });
1598
- }
1599
- catch (error) {
1600
- this.handleRequestError(error, res);
1601
- }
1602
- }
1603
- async handleGetIdBloomSummary(req, res) {
1604
- try {
1605
- const token = this.extractToken(req);
1606
- const { dbId } = req.body;
1607
- const validDbId = this.validateDbId(dbId);
1608
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1609
- const summary = await serverStore.handleGetIdBloomSummary(token);
1610
- res.json({ summary: summary });
1611
- }
1612
- catch (error) {
1613
- this.handleRequestError(error, res);
1614
- }
1615
- }
1616
- async handleGetCapabilities(req, res) {
1617
- try {
1618
- const token = this.extractToken(req);
1619
- const validDbId = this.validateDbId(req.query.dbId);
1620
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1621
- const capabilities = await serverStore.handleGetCapabilities(token);
1622
- // HTTP-layer features on top of the transport-agnostic capability set:
1623
- // binary octet-stream framing routes (phase 3) and the SSE change feed
1624
- // (phase 5) exist only in this Express host.
1625
- res.json({
1626
- capabilities: {
1627
- ...capabilities,
1628
- supportsBinaryEntries: true,
1629
- supportsPayloadOnlyEntries: true,
1630
- supportsChangeEvents: true,
1631
- },
1632
- });
1633
- }
1634
- catch (error) {
1635
- this.handleRequestError(error, res);
1636
- }
1637
- }
1638
- /**
1639
- * SSE live change feed (sync-v5, phase 5).
1640
- *
1641
- * `GET /:tenantId/sync/:storeKind/events` — Bearer-authenticated like every
1642
- * other sync route (fetch-streaming clients can set headers; native
1643
- * EventSource cannot, which is why the SDK uses a fetch reader).
1644
- *
1645
- * The subscribe-time read gate runs via `handleGetCapabilities` against the
1646
- * database given in `?dbId=` (default `directory`, which every tenant member
1647
- * can read): it validates the token and runs the database-open gate. Events
1648
- * carry only `{dbId, storeKind, epoch, maxReceiptOrder}` — metadata, never
1649
- * content. A heartbeat comment every 30 s keeps proxies from closing the
1650
- * stream. Token expiry does not tear down an open stream; the events are
1651
- * non-sensitive change hints and a reconnect re-authenticates.
1652
- */
1653
- async handleSyncEvents(req, res) {
1654
- try {
1655
- const token = this.extractToken(req);
1656
- const tenantId = req.tenantId;
1657
- const storeKind = this.getStoreKindFromRequest(req);
1658
- const gateDbId = this.validateDbId(typeof req.query.dbId === "string" && req.query.dbId ? req.query.dbId : "directory");
1659
- // Validates the token and runs the database-open/read gate before any
1660
- // event is delivered.
1661
- const serverStore = await this.tenantManager.getServerStore(tenantId, gateDbId, storeKind);
1662
- await serverStore.handleGetCapabilities(token);
1663
- res.status(200);
1664
- res.setHeader("Content-Type", "text/event-stream");
1665
- res.setHeader("Cache-Control", "no-cache, no-transform");
1666
- res.setHeader("X-Accel-Buffering", "no");
1667
- if (!req.httpVersion.startsWith("2")) {
1668
- // Forbidden (and unnecessary) on HTTP/2 streams.
1669
- res.setHeader("Connection", "keep-alive");
1670
- }
1671
- res.flushHeaders();
1672
- res.write(`event: hello\ndata: ${JSON.stringify({ protocolVersion: "sync-v5" })}\n\n`);
1673
- const unsubscribe = this.syncEventBus.subscribe((event) => {
1674
- if (event.tenantId !== tenantId || event.storeKind !== storeKind) {
1675
- return;
1676
- }
1677
- res.write(`event: change\ndata: ${JSON.stringify({
1678
- dbId: event.dbId,
1679
- storeKind: event.storeKind,
1680
- epoch: event.epoch,
1681
- maxReceiptOrder: event.maxReceiptOrder,
1682
- })}\n\n`);
1683
- });
1684
- const heartbeat = setInterval(() => {
1685
- res.write(`: heartbeat ${Date.now()}\n\n`);
1686
- }, 30000);
1687
- const cleanup = () => {
1688
- clearInterval(heartbeat);
1689
- unsubscribe();
1690
- };
1691
- req.on("close", cleanup);
1692
- res.on("close", cleanup);
1693
- console.log(`[MindooDBServer] SSE subscriber connected tenant=${tenantId} storeKind=${storeKind} subscribers=${this.syncEventBus.listenerCount}`);
1694
- }
1695
- catch (error) {
1696
- this.handleRequestError(error, res);
1697
- }
1698
- }
721
+ // ==================== Sync Event Stream ====================
1699
722
  /**
1700
- * Publish a change event after an accepted putEntries write (sync-v5,
1701
- * phase 5). Fire-and-forget: head lookup failures must never affect the
1702
- * write path.
723
+ * SSE transport for the change feed: the core runs the subscribe-time read
724
+ * gate, this keeps the stream open on the in-process event bus with a
725
+ * heartbeat every 30 s.
1703
726
  */
1704
- publishSyncChangeEvent(tenantId, dbId, storeKind, serverStore, token) {
1705
- if (this.syncEventBus.listenerCount === 0) {
727
+ async handleSyncEvents(storeKind, req, res) {
728
+ const response = await this.core.sync("events", (0, expressAdapter_1.toServerRequest)(req), storeKind);
729
+ if (!(0, types_2.isEventStreamResponse)(response)) {
730
+ await (0, expressAdapter_1.sendServerResponse)(req, res, response);
1706
731
  return;
1707
732
  }
1708
- // Peer replicators subscribe to this bus, so a write that arrived from a
1709
- // peer has to be labelled: without the label it would be pushed straight
1710
- // back to its origin, and two bidirectional peers would ping-pong.
1711
- const originPeer = this.cluster?.auth
1712
- ? this.cluster.auth.validateToken(token).then((payload) => payload?.sub)
1713
- : Promise.resolve(undefined);
1714
- void Promise.all([serverStore.handleGetStoreHead(token).catch(() => null), originPeer]).then(([head, peerName]) => {
1715
- this.syncEventBus.publish({
1716
- tenantId,
1717
- dbId,
1718
- storeKind,
1719
- epoch: head?.epoch,
1720
- maxReceiptOrder: head?.maxReceiptOrder,
1721
- originPeer: peerName,
1722
- });
1723
- });
1724
- }
1725
- async handleGetCompactionStatus(req, res) {
1726
- try {
1727
- const token = this.extractToken(req);
1728
- const { dbId } = req.body;
1729
- const validDbId = this.validateDbId(dbId);
1730
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1731
- const status = await serverStore.handleGetCompactionStatus(token);
1732
- res.json({ status: status });
1733
- }
1734
- catch (error) {
1735
- this.handleRequestError(error, res);
1736
- }
1737
- }
1738
- async handleGetEntries(req, res) {
1739
- try {
1740
- const token = this.extractToken(req);
1741
- const { dbId, ids, sessionKeyWrap } = req.body;
1742
- const validDbId = this.validateDbId(dbId);
1743
- (0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
1744
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1745
- // Session-key transport format (sync-v5, phase 2): one RSA-wrapped
1746
- // AES-256-GCM key per response instead of per-entry RSA. Negotiated via
1747
- // the `sessionKeyWrap` request flag; legacy clients get the old format.
1748
- if (sessionKeyWrap === true) {
1749
- const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
1750
- res.json({
1751
- wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
1752
- entries: batch.entries.map((e) => ({
1753
- ...this.serializeEntryMetadata(e),
1754
- iv: this.uint8ArrayToBase64(e.iv),
1755
- sessionEncryptedPayload: this.uint8ArrayToBase64(e.sessionEncryptedPayload),
1756
- })),
1757
- });
1758
- return;
1759
- }
1760
- const entries = await serverStore.handleGetEntries(token, ids || []);
1761
- res.json({
1762
- entries: entries.map((e) => ({
1763
- ...this.serializeEntryMetadata(e),
1764
- rsaEncryptedPayload: this.uint8ArrayToBase64(e.rsaEncryptedPayload),
1765
- })),
1766
- });
1767
- }
1768
- catch (error) {
1769
- this.handleRequestError(error, res);
1770
- }
1771
- }
1772
- /**
1773
- * Binary variant of getEntries (sync-v5, phase 3): always session-key
1774
- * encrypted, response framed as `application/octet-stream` (see
1775
- * binaryEntryFraming.ts). The request stays JSON (small).
1776
- */
1777
- async handleGetEntriesBinary(req, res) {
1778
- try {
1779
- const token = this.extractToken(req);
1780
- const { dbId, ids, payloadOnly } = req.body;
1781
- const validDbId = this.validateDbId(dbId);
1782
- (0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
1783
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1784
- const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
1785
- // `payloadOnly` (capability supportsPayloadOnlyEntries): the client
1786
- // already holds the metadata from its scan, so each frame carries only
1787
- // what it cannot have yet. Same entries, same session-key encryption.
1788
- const body = (0, binaryEntryFraming_1.encodeBinaryEntryMessage)({
1789
- header: {
1790
- format: binaryEntryFraming_1.BINARY_GET_ENTRIES_FORMAT,
1791
- wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
1792
- ...(payloadOnly === true ? { payloadOnly: true } : {}),
1793
- },
1794
- entries: batch.entries.map((e) => ({
1795
- meta: payloadOnly === true
1796
- ? { id: e.id, iv: this.uint8ArrayToBase64(e.iv) }
1797
- : { ...this.serializeEntryMetadata(e), iv: this.uint8ArrayToBase64(e.iv) },
1798
- payload: e.sessionEncryptedPayload,
1799
- })),
1800
- });
1801
- const payloadBytes = batch.entries.reduce((sum, e) => sum + e.sessionEncryptedPayload.byteLength, 0);
1802
- res.setHeader("Content-Type", binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE);
1803
- await (0, compressionMiddleware_1.sendBinaryEntryBody)(req, res, body, body.byteLength - payloadBytes);
1804
- }
1805
- catch (error) {
1806
- this.handleRequestError(error, res);
1807
- }
1808
- }
1809
- async handleGetEntryMetadata(req, res) {
1810
- try {
1811
- const token = this.extractToken(req);
1812
- const { dbId, id } = req.body;
1813
- const validDbId = this.validateDbId(dbId);
1814
- if (!id) {
1815
- res.status(400).json({ error: "id is required" });
1816
- return;
1817
- }
1818
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1819
- const entry = await serverStore.handleGetEntryMetadata(token, id);
1820
- res.json({
1821
- entry: entry ? this.serializeEntryMetadata(entry) : null,
1822
- });
1823
- }
1824
- catch (error) {
1825
- this.handleRequestError(error, res);
1826
- }
1827
- }
1828
- /**
1829
- * Ingest encrypted entries from a client. The `stage` variable tracks
1830
- * progress through the pipeline so that error logs pinpoint the failing
1831
- * step (token extraction → body parsing → deserialization → storage).
1832
- */
1833
- async handlePutEntries(req, res) {
1834
- let stage = "extract-token";
1835
- try {
1836
- const token = this.extractToken(req);
1837
- stage = "read-body";
1838
- const { dbId, entries } = req.body;
1839
- const validDbId = this.validateDbId(dbId);
1840
- (0, validation_1.validateArraySize)(entries, validation_1.MAX_PUT_ENTRIES, "entries");
1841
- const serializedEntries = Array.isArray(entries) ? entries : [];
1842
- const serializedEntryTypeCounts = serializedEntries.reduce((acc, entry) => {
1843
- const key = entry?.entryType ?? "unknown";
1844
- acc[key] = (acc[key] ?? 0) + 1;
1845
- return acc;
1846
- }, {});
1847
- console.log(`[MindooDBServer] putEntries request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${serializedEntries.length} parsed-bytes=${req.jsonBodyBytes ?? "-"} content-length=${req.headers["content-length"] ?? "-"} types=${JSON.stringify(serializedEntryTypeCounts)}`);
1848
- stage = "load-server-store";
1849
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1850
- stage = "deserialize-entries";
1851
- const deserializedEntries = serializedEntries.map((entry, index) => {
1852
- try {
1853
- return this.deserializeEntry(entry);
1854
- }
1855
- catch (error) {
1856
- console.error("[MindooDBServer] putEntries failed while deserializing entry", {
1857
- index,
1858
- id: entry?.id ?? "-",
1859
- type: entry?.entryType ?? "-",
1860
- docId: entry?.docId ?? "-",
1861
- encryptedSize: entry?.encryptedSize ?? "-",
1862
- originalSize: entry?.originalSize ?? "-",
1863
- signatureLength: entry?.signature?.length ?? "-",
1864
- encryptedDataLength: entry?.encryptedData?.length ?? "-",
1865
- error,
1866
- });
1867
- throw error;
1868
- }
1869
- });
1870
- const totalEncryptedBytes = deserializedEntries.reduce((sum, entry) => sum + entry.encryptedData.byteLength, 0);
1871
- const totalOriginalBytes = deserializedEntries.reduce((sum, entry) => sum + entry.originalSize, 0);
1872
- console.log(`[MindooDBServer] putEntries deserialized tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${deserializedEntries.length} totalEncryptedBytes=${totalEncryptedBytes} totalOriginalBytes=${totalOriginalBytes}`);
1873
- stage = "store-entries";
1874
- const ack = await serverStore.handlePutEntries(token, deserializedEntries);
1875
- stage = "respond-success";
1876
- // Return witness receipts for accepted entries so the pushing client (and
1877
- // every replica it later syncs with) can persist the attestation
1878
- // (docs/accesscontrol.md §5.3), plus per-entry rejections for
1879
- // signature-class failures the server skipped. Older clients ignore the
1880
- // extra fields.
1881
- res.json({
1882
- success: true,
1883
- receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
1884
- rejected: ack.rejected,
1885
- });
1886
- // Announce the accepted write to SSE subscribers (sync-v5, phase 5).
1887
- if (ack.receipts.length > 0) {
1888
- this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
1889
- }
1890
- // A directory push may carry new admin-signed purge requests
1891
- // (docs/accesscontrol.md §13). Execute any pending purges after the
1892
- // response so the server physically removes purged history from its own
1893
- // stores and arms the re-push denylist. Fire-and-forget: failures are
1894
- // logged and retried on the next directory push.
1895
- if (validDbId === "directory") {
1896
- const tenantId = req.tenantId;
1897
- void this.tenantManager.executePendingPurges(tenantId).catch((error) => {
1898
- console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
1899
- });
1900
- }
1901
- }
1902
- catch (error) {
1903
- console.error("[MindooDBServer] putEntries failed", {
1904
- stage,
1905
- tenantId: req.tenantId ?? "-",
1906
- dbId: req.body?.dbId ?? "-",
1907
- parsedBytes: req.jsonBodyBytes ?? "-",
1908
- contentLength: req.headers["content-length"] ?? "-",
1909
- entryCount: Array.isArray(req.body?.entries) ? req.body.entries.length : "-",
1910
- error,
1911
- });
1912
- this.handleRequestError(error, res);
1913
- }
1914
- }
1915
- /**
1916
- * Binary variant of putEntries (sync-v5, phase 3). The request body is
1917
- * octet-stream framing; `dbId` travels in the message header JSON because
1918
- * there is no JSON body. Receipts are returned as JSON like putEntries.
1919
- */
1920
- async handlePutEntriesBinary(req, res) {
1921
- let stage = "extract-token";
1922
- try {
1923
- const token = this.extractToken(req);
1924
- stage = "decode-body";
1925
- if (!Buffer.isBuffer(req.body)) {
1926
- res.status(400).json({ error: `putEntriesBinary requires a ${binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE} body` });
1927
- return;
1928
- }
1929
- const message = (0, binaryEntryFraming_1.decodeBinaryEntryMessage)(new Uint8Array(req.body.buffer, req.body.byteOffset, req.body.byteLength));
1930
- if (message.header.format !== binaryEntryFraming_1.BINARY_PUT_ENTRIES_FORMAT) {
1931
- res.status(400).json({ error: `Unsupported binary entry format: ${String(message.header.format)}` });
1932
- return;
1933
- }
1934
- const validDbId = this.validateDbId(message.header.dbId);
1935
- (0, validation_1.validateArraySize)(message.entries, validation_1.MAX_PUT_ENTRIES, "entries");
1936
- console.log(`[MindooDBServer] putEntriesBinary request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${message.entries.length} content-length=${req.headers["content-length"] ?? "-"}`);
1937
- stage = "load-server-store";
1938
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1939
- stage = "deserialize-entries";
1940
- const deserializedEntries = message.entries.map((frame, index) => {
1941
- const meta = frame.meta;
1942
- try {
1943
- return {
1944
- ...this.deserializeEntryMetadata(meta),
1945
- encryptedData: frame.payload,
1946
- };
1947
- }
1948
- catch (error) {
1949
- console.error("[MindooDBServer] putEntriesBinary failed while deserializing entry", {
1950
- index,
1951
- id: meta?.id ?? "-",
1952
- type: meta?.entryType ?? "-",
1953
- docId: meta?.docId ?? "-",
1954
- payloadLength: frame.payload?.byteLength ?? "-",
1955
- error,
1956
- });
1957
- throw error;
1958
- }
1959
- });
1960
- stage = "store-entries";
1961
- const ack = await serverStore.handlePutEntries(token, deserializedEntries);
1962
- stage = "respond-success";
1963
- res.json({
1964
- success: true,
1965
- receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
1966
- rejected: ack.rejected,
1967
- });
1968
- // Announce the accepted write to SSE subscribers (sync-v5, phase 5).
1969
- if (ack.receipts.length > 0) {
1970
- this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
1971
- }
1972
- // Same post-response purge hook as the JSON putEntries path.
1973
- if (validDbId === "directory") {
1974
- const tenantId = req.tenantId;
1975
- void this.tenantManager.executePendingPurges(tenantId).catch((error) => {
1976
- console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
1977
- });
1978
- }
1979
- }
1980
- catch (error) {
1981
- console.error("[MindooDBServer] putEntriesBinary failed", {
1982
- stage,
1983
- tenantId: req.tenantId ?? "-",
1984
- contentLength: req.headers["content-length"] ?? "-",
1985
- error,
1986
- });
1987
- this.handleRequestError(error, res);
1988
- }
1989
- }
1990
- async handleHasEntries(req, res) {
1991
- try {
1992
- const token = this.extractToken(req);
1993
- const { dbId, ids } = req.body;
1994
- const validDbId = this.validateDbId(dbId);
1995
- (0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
1996
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1997
- const existingIds = await serverStore.handleHasEntries(token, ids || []);
1998
- res.json({ ids: existingIds });
1999
- }
2000
- catch (error) {
2001
- this.handleRequestError(error, res);
2002
- }
2003
- }
2004
- async handleGetAllIds(req, res) {
2005
- try {
2006
- const token = this.extractToken(req);
2007
- const validDbId = this.validateDbId(req.query.dbId);
2008
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
2009
- const ids = await serverStore.handleGetAllIds(token);
2010
- res.json({ ids });
2011
- }
2012
- catch (error) {
2013
- this.handleRequestError(error, res);
2014
- }
733
+ this.openSyncEventStream(req, res, response.events);
2015
734
  }
2016
- async handleResolveDependencies(req, res) {
2017
- try {
2018
- const token = this.extractToken(req);
2019
- const { dbId, startId, options } = req.body;
2020
- const validDbId = this.validateDbId(dbId);
2021
- if (!startId) {
2022
- res.status(400).json({ error: "startId is required" });
2023
- return;
2024
- }
2025
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
2026
- const ids = await serverStore.handleResolveDependencies(token, startId, options);
2027
- res.json({ ids });
2028
- }
2029
- catch (error) {
2030
- this.handleRequestError(error, res);
735
+ openSyncEventStream(req, res, subscription) {
736
+ res.status(200);
737
+ res.setHeader("Content-Type", "text/event-stream");
738
+ res.setHeader("Cache-Control", "no-cache, no-transform");
739
+ res.setHeader("X-Accel-Buffering", "no");
740
+ if (!req.httpVersion.startsWith("2")) {
741
+ // Forbidden (and unnecessary) on HTTP/2 streams.
742
+ res.setHeader("Connection", "keep-alive");
2031
743
  }
2032
- }
2033
- async handlePlanDocumentMaterialization(req, res) {
2034
- try {
2035
- const token = this.extractToken(req);
2036
- const { dbId, docId, options } = req.body;
2037
- const validDbId = this.validateDbId(dbId);
2038
- if (!docId) {
2039
- res.status(400).json({ error: "docId is required" });
2040
- return;
744
+ res.flushHeaders();
745
+ res.write((0, MindooDBServerCore_1.formatSyncHelloEvent)());
746
+ const unsubscribe = this.syncEventBus.subscribe((event) => {
747
+ const frame = (0, MindooDBServerCore_1.formatSyncChangeEvent)(subscription, event);
748
+ if (frame) {
749
+ res.write(frame);
2041
750
  }
2042
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
2043
- const plan = await serverStore.handlePlanDocumentMaterialization(token, docId, options);
2044
- res.json({ plan: plan });
2045
- }
2046
- catch (error) {
2047
- this.handleRequestError(error, res);
2048
- }
2049
- }
2050
- async handlePlanDocumentMaterializationBatch(req, res) {
2051
- try {
2052
- const token = this.extractToken(req);
2053
- const { dbId, docIds, options } = req.body;
2054
- const validDbId = this.validateDbId(dbId);
2055
- (0, validation_1.validateArraySize)(docIds, validation_1.MAX_HAVE_IDS, "docIds");
2056
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
2057
- const batchPlan = await serverStore.handlePlanDocumentMaterializationBatch(token, docIds || [], options);
2058
- res.json({ batchPlan: batchPlan });
2059
- }
2060
- catch (error) {
2061
- this.handleRequestError(error, res);
2062
- }
2063
- }
2064
- async handlePlanAttachmentReadByWalkingMetadata(req, res) {
2065
- try {
2066
- const token = this.extractToken(req);
2067
- const { dbId, lastChunkId, attachmentSize, options } = req.body;
2068
- const validDbId = this.validateDbId(dbId);
2069
- if (!lastChunkId) {
2070
- res.status(400).json({ error: "lastChunkId is required" });
2071
- return;
2072
- }
2073
- if (typeof attachmentSize !== "number" || !Number.isFinite(attachmentSize)) {
2074
- res.status(400).json({ error: "attachmentSize is required" });
2075
- return;
2076
- }
2077
- if (!options || typeof options.startByte !== "number") {
2078
- res.status(400).json({ error: "options.startByte is required" });
2079
- return;
2080
- }
2081
- const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
2082
- const plan = await serverStore.handlePlanAttachmentReadByWalkingMetadata(token, lastChunkId, attachmentSize, options);
2083
- res.json({ plan: plan });
2084
- }
2085
- catch (error) {
2086
- this.handleRequestError(error, res);
2087
- }
2088
- }
2089
- // ==================== Management Handlers ====================
2090
- // ==================== Helper Methods ====================
2091
- /**
2092
- * Derive the StoreKind from the request path. Both "docs" and
2093
- * "attachments" share the same handler implementations; the store kind
2094
- * selects which on-disk append-only store to operate against.
2095
- */
2096
- getStoreKindFromRequest(req) {
2097
- const trimmedPath = req.path.replace(/\/$/, "");
2098
- if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
2099
- return types_2.StoreKind.docs;
2100
- }
2101
- if (trimmedPath.startsWith("/sync/attachments/") || trimmedPath === "/sync/attachments/capabilities" || trimmedPath === "/sync/attachments/getAllIds") {
2102
- return types_2.StoreKind.attachments;
2103
- }
2104
- throw new Error(`Unsupported sync store kind for path: ${req.path}`);
2105
- }
2106
- extractToken(req) {
2107
- const auth = req.headers.authorization;
2108
- if (!auth || !auth.startsWith("Bearer ")) {
2109
- throw new types_3.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
2110
- }
2111
- return auth.substring(7);
2112
- }
2113
- /** Map domain errors to HTTP status codes; unknown errors become 500. */
2114
- handleRequestError(error, res) {
2115
- if (error instanceof validation_1.ValidationError) {
2116
- res.status(400).json({ error: error.message });
2117
- return;
2118
- }
2119
- if (error instanceof Error && error.name === "NetworkError") {
2120
- const networkError = error;
2121
- const status = this.getStatusForErrorType(networkError.type);
2122
- // The status alone loses the distinction the client needs: several
2123
- // NetworkErrorTypes map onto the same code (USER_REVOKED and
2124
- // ACCESS_DENIED both become 403), and a client that has to guess falls
2125
- // back to matching on the message text. Carry the type so it does not
2126
- // have to.
2127
- res.status(status).json({ error: networkError.message, type: networkError.type });
2128
- return;
2129
- }
2130
- if ((0, errors_1.isMissingDependencyError)(error)) {
2131
- // Not a server fault: the requested chain is incomplete in this store.
2132
- // Pass the details on so the caller can tell what is missing.
2133
- res.status(409).json({ error: error.message, type: "MISSING_DEPENDENCY" });
2134
- return;
2135
- }
2136
- console.error("[MindooDBServer] Request error:", error);
2137
- res.status(500).json({ error: "Internal server error" });
2138
- }
2139
- getStatusForErrorType(type) {
2140
- switch (type) {
2141
- case "INVALID_TOKEN":
2142
- return 401;
2143
- case "USER_REVOKED":
2144
- return 403;
2145
- case "USER_NOT_FOUND":
2146
- return 404;
2147
- case "INVALID_SIGNATURE":
2148
- return 403;
2149
- case "CHALLENGE_EXPIRED":
2150
- return 401;
2151
- case "ACCESS_DENIED":
2152
- return 403;
2153
- default:
2154
- return 500;
2155
- }
751
+ });
752
+ const heartbeat = setInterval(() => {
753
+ res.write((0, MindooDBServerCore_1.formatSyncHeartbeat)(Date.now()));
754
+ }, MindooDBServerCore_1.SYNC_EVENT_HEARTBEAT_MS);
755
+ const cleanup = () => {
756
+ clearInterval(heartbeat);
757
+ unsubscribe();
758
+ };
759
+ req.on("close", cleanup);
760
+ res.on("close", cleanup);
761
+ console.log(`[MindooDBServer] SSE subscriber connected tenant=${subscription.tenantId} storeKind=${subscription.storeKind} subscribers=${this.syncEventBus.listenerCount}`);
2156
762
  }
2157
763
  /**
2158
764
  * Express error handler (4-arg signature). Handles body-parser errors
@@ -2193,25 +799,6 @@ class MindooDBServer {
2193
799
  });
2194
800
  res.status(500).json({ error: "Internal server error" });
2195
801
  }
2196
- // ==================== Serialization ====================
2197
- // Entries are stored with binary fields (Uint8Array) internally but
2198
- // transmitted as base64 strings over JSON. Shared with the client
2199
- // (entryWireCodec) so signed trailing blocks cannot drift off the wire.
2200
- serializeEntryMetadata(metadata) {
2201
- return (0, entryWireCodec_1.serializeEntryMetadata)(metadata);
2202
- }
2203
- deserializeEntryMetadata(serialized) {
2204
- return (0, entryWireCodec_1.deserializeEntryMetadata)(serialized);
2205
- }
2206
- deserializeEntry(serialized) {
2207
- return (0, entryWireCodec_1.deserializeStoreEntry)(serialized);
2208
- }
2209
- uint8ArrayToBase64(bytes) {
2210
- return Buffer.from(bytes).toString("base64");
2211
- }
2212
- base64ToUint8Array(base64) {
2213
- return new Uint8Array(Buffer.from(base64, "base64"));
2214
- }
2215
802
  }
2216
803
  exports.MindooDBServer = MindooDBServer;
2217
804
  //# sourceMappingURL=MindooDBServer.js.map