@bevel-software/platform-core-backend 0.20.0 → 0.21.0

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 (576) hide show
  1. package/dist/core/catalog-cache-invalidation.d.ts +7 -2
  2. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  3. package/dist/core/catalog-cache-invalidation.js +8 -9
  4. package/dist/core/catalog-cache-invalidation.js.map +1 -1
  5. package/dist/core/core-ports.d.ts +19 -0
  6. package/dist/core/core-ports.d.ts.map +1 -1
  7. package/dist/core/core-ports.js.map +1 -1
  8. package/dist/core/create-core-server.d.ts +2 -0
  9. package/dist/core/create-core-server.d.ts.map +1 -1
  10. package/dist/core/create-core-server.js +23 -105
  11. package/dist/core/create-core-server.js.map +1 -1
  12. package/dist/core/create-core-services.d.ts +30 -5
  13. package/dist/core/create-core-services.d.ts.map +1 -1
  14. package/dist/core/create-core-services.js +136 -81
  15. package/dist/core/create-core-services.js.map +1 -1
  16. package/dist/core/lifecycle.d.ts +74 -28
  17. package/dist/core/lifecycle.d.ts.map +1 -1
  18. package/dist/core/lifecycle.js +157 -20
  19. package/dist/core/lifecycle.js.map +1 -1
  20. package/dist/core/public-config.d.ts +5 -1
  21. package/dist/core/public-config.d.ts.map +1 -1
  22. package/dist/core/public-config.js +7 -5
  23. package/dist/core/public-config.js.map +1 -1
  24. package/dist/core-config.d.ts +95 -1
  25. package/dist/core-config.d.ts.map +1 -1
  26. package/dist/core-config.js +24 -8
  27. package/dist/core-config.js.map +1 -1
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/modules/access/access-control.service.d.ts +18 -1
  33. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  34. package/dist/modules/access/access-control.service.js +19 -8
  35. package/dist/modules/access/access-control.service.js.map +1 -1
  36. package/dist/modules/access/access.routes.d.ts +2 -1
  37. package/dist/modules/access/access.routes.d.ts.map +1 -1
  38. package/dist/modules/access/access.routes.js +10 -10
  39. package/dist/modules/access/access.routes.js.map +1 -1
  40. package/dist/modules/access/change-read-gate.d.ts +6 -3
  41. package/dist/modules/access/change-read-gate.d.ts.map +1 -1
  42. package/dist/modules/access/change-read-gate.js +9 -6
  43. package/dist/modules/access/change-read-gate.js.map +1 -1
  44. package/dist/modules/access/creator-access.d.ts +4 -2
  45. package/dist/modules/access/creator-access.d.ts.map +1 -1
  46. package/dist/modules/access/creator-access.js +7 -4
  47. package/dist/modules/access/creator-access.js.map +1 -1
  48. package/dist/modules/agent-instructions/read-preamble.d.ts +2 -1
  49. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -1
  50. package/dist/modules/agent-instructions/read-preamble.js +6 -9
  51. package/dist/modules/agent-instructions/read-preamble.js.map +1 -1
  52. package/dist/modules/auth/account-admission.d.ts +40 -0
  53. package/dist/modules/auth/account-admission.d.ts.map +1 -0
  54. package/dist/modules/auth/account-admission.js +17 -0
  55. package/dist/modules/auth/account-admission.js.map +1 -0
  56. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  57. package/dist/modules/auth/account.routes.js +4 -1
  58. package/dist/modules/auth/account.routes.js.map +1 -1
  59. package/dist/modules/auth/auth.service.d.ts +19 -1
  60. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  61. package/dist/modules/auth/auth.service.js +33 -1
  62. package/dist/modules/auth/auth.service.js.map +1 -1
  63. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  64. package/dist/modules/auth/oidc-auth-provider.js +9 -0
  65. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  66. package/dist/modules/connection-probe/connection-probe.service.d.ts +13 -1
  67. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  68. package/dist/modules/connection-probe/connection-probe.service.js +10 -2
  69. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  70. package/dist/modules/database/advisory-lock.d.ts +17 -0
  71. package/dist/modules/database/advisory-lock.d.ts.map +1 -1
  72. package/dist/modules/database/advisory-lock.js +24 -3
  73. package/dist/modules/database/advisory-lock.js.map +1 -1
  74. package/dist/modules/database/connection.d.ts +67 -2
  75. package/dist/modules/database/connection.d.ts.map +1 -1
  76. package/dist/modules/database/connection.js +118 -12
  77. package/dist/modules/database/connection.js.map +1 -1
  78. package/dist/modules/database/migrate.d.ts +1 -1
  79. package/dist/modules/database/migrate.d.ts.map +1 -1
  80. package/dist/modules/database/migrate.js +27 -3
  81. package/dist/modules/database/migrate.js.map +1 -1
  82. package/dist/modules/kb-fs/branch-name.d.ts +0 -1
  83. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  84. package/dist/modules/kb-fs/branch-name.js +3 -3
  85. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  86. package/dist/modules/kb-fs/clone-config.d.ts +9 -8
  87. package/dist/modules/kb-fs/clone-config.d.ts.map +1 -1
  88. package/dist/modules/kb-fs/clone-config.js +14 -12
  89. package/dist/modules/kb-fs/clone-config.js.map +1 -1
  90. package/dist/modules/mcp/mcp.service.d.ts +7 -0
  91. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  92. package/dist/modules/mcp/mcp.service.js +4 -4
  93. package/dist/modules/mcp/mcp.service.js.map +1 -1
  94. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +4 -4
  95. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
  96. package/dist/modules/plugins/compile/marketplace-compiler.service.js +7 -10
  97. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
  98. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +4 -1
  99. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
  100. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +5 -3
  101. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
  102. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +6 -1
  103. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
  104. package/dist/modules/plugins/discovery/kb-plugin-source.js +14 -9
  105. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
  106. package/dist/modules/plugins/discovery/native.source.d.ts +4 -1
  107. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
  108. package/dist/modules/plugins/discovery/native.source.js +4 -2
  109. package/dist/modules/plugins/discovery/native.source.js.map +1 -1
  110. package/dist/modules/plugins/index.d.ts +1 -1
  111. package/dist/modules/plugins/index.d.ts.map +1 -1
  112. package/dist/modules/plugins/index.js +1 -1
  113. package/dist/modules/plugins/index.js.map +1 -1
  114. package/dist/modules/plugins/join-request-jobs.service.d.ts +3 -1
  115. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  116. package/dist/modules/plugins/join-request-jobs.service.js +6 -6
  117. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  118. package/dist/modules/plugins/join-requests.service.d.ts +3 -1
  119. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.js +9 -8
  121. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  122. package/dist/modules/plugins/plugin-links.d.ts +65 -10
  123. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  124. package/dist/modules/plugins/plugin-links.js +136 -31
  125. package/dist/modules/plugins/plugin-links.js.map +1 -1
  126. package/dist/modules/plugins/plugin-links.service.d.ts +107 -5
  127. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
  128. package/dist/modules/plugins/plugin-links.service.js +151 -21
  129. package/dist/modules/plugins/plugin-links.service.js.map +1 -1
  130. package/dist/modules/plugins/plugin-provision.service.d.ts +10 -3
  131. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  132. package/dist/modules/plugins/plugin-provision.service.js +33 -26
  133. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  134. package/dist/modules/plugins/plugin-rename.service.d.ts +4 -2
  135. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
  136. package/dist/modules/plugins/plugin-rename.service.js +10 -12
  137. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  138. package/dist/modules/plugins/plugins.routes.d.ts +3 -0
  139. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  140. package/dist/modules/plugins/plugins.routes.js +45 -15
  141. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  142. package/dist/modules/plugins/plugins.service.d.ts +4 -4
  143. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  144. package/dist/modules/plugins/plugins.service.js +7 -10
  145. package/dist/modules/plugins/plugins.service.js.map +1 -1
  146. package/dist/modules/plugins/teams.routes.d.ts +4 -1
  147. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  148. package/dist/modules/plugins/teams.routes.js +11 -9
  149. package/dist/modules/plugins/teams.routes.js.map +1 -1
  150. package/dist/modules/secrets-vault/index.d.ts +1 -1
  151. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  152. package/dist/modules/secrets-vault/index.js +1 -1
  153. package/dist/modules/secrets-vault/index.js.map +1 -1
  154. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts +29 -11
  155. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  156. package/dist/modules/secrets-vault/secrets-variable-loader.js +46 -21
  157. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  158. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +3 -0
  159. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  160. package/dist/modules/secrets-vault/secrets-vault.routes.js +4 -6
  161. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  162. package/dist/modules/settings/deployment-settings.service.d.ts +12 -8
  163. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  164. package/dist/modules/settings/deployment-settings.service.js +20 -22
  165. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  166. package/dist/modules/settings/setup.routes.d.ts +9 -2
  167. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  168. package/dist/modules/settings/setup.routes.js +20 -14
  169. package/dist/modules/settings/setup.routes.js.map +1 -1
  170. package/dist/modules/skills/allowed-tools-check.d.ts +4 -3
  171. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -1
  172. package/dist/modules/skills/allowed-tools-check.js +8 -8
  173. package/dist/modules/skills/allowed-tools-check.js.map +1 -1
  174. package/dist/modules/skills/pending-skills.service.d.ts +3 -1
  175. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  176. package/dist/modules/skills/pending-skills.service.js +9 -6
  177. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  178. package/dist/modules/skills/skill-access-requests.routes.d.ts +2 -1
  179. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  180. package/dist/modules/skills/skill-access-requests.routes.js +6 -6
  181. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  182. package/dist/modules/skills/skills.routes.d.ts +4 -1
  183. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  184. package/dist/modules/skills/skills.routes.js +4 -4
  185. package/dist/modules/skills/skills.routes.js.map +1 -1
  186. package/dist/modules/skills/skills.service.d.ts +4 -2
  187. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  188. package/dist/modules/skills/skills.service.js +13 -11
  189. package/dist/modules/skills/skills.service.js.map +1 -1
  190. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +2 -1
  191. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  192. package/dist/modules/tool-manuals/mcp-json-discovery.js +6 -5
  193. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  194. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +4 -2
  195. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  196. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -10
  197. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  198. package/dist/modules/tool-manuals/pending-tools.service.d.ts +3 -1
  199. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -1
  200. package/dist/modules/tool-manuals/pending-tools.service.js +19 -16
  201. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -1
  202. package/dist/modules/tool-manuals/tool-delete.service.d.ts +4 -2
  203. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -1
  204. package/dist/modules/tool-manuals/tool-delete.service.js +14 -12
  205. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -1
  206. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +3 -1
  207. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  208. package/dist/modules/tool-manuals/tool-manuals.routes.js +7 -7
  209. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  210. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -2
  211. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  212. package/dist/modules/tool-manuals/tool-manuals.service.js +15 -12
  213. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  214. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +3 -0
  215. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  216. package/dist/modules/tool-manuals/tool-manuals.tools.js +4 -6
  217. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  218. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +3 -2
  219. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  220. package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -9
  221. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  222. package/dist/modules/workflow/git/git.service.d.ts +11 -5
  223. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  224. package/dist/modules/workflow/git/git.service.js +23 -14
  225. package/dist/modules/workflow/git/git.service.js.map +1 -1
  226. package/dist/modules/workflow/git/node-git-runner.d.ts +26 -2
  227. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  228. package/dist/modules/workflow/git/node-git-runner.js +35 -5
  229. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  230. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  231. package/dist/modules/workflow/review-workflow/review-workflow.service.js +9 -4
  232. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  233. package/dist/modules/workflow/session-ontology.service.d.ts +6 -1
  234. package/dist/modules/workflow/session-ontology.service.d.ts.map +1 -1
  235. package/dist/modules/workflow/session-ontology.service.js +6 -2
  236. package/dist/modules/workflow/session-ontology.service.js.map +1 -1
  237. package/dist/modules/workflow/workflow.service.d.ts +8 -3
  238. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  239. package/dist/modules/workflow/workflow.service.js +33 -23
  240. package/dist/modules/workflow/workflow.service.js.map +1 -1
  241. package/dist/modules/workspace/session-ontology.gate.d.ts +3 -1
  242. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  243. package/dist/modules/workspace/session-ontology.gate.js +3 -3
  244. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  245. package/dist/modules/workspace/startup/kb-git.d.ts +4 -3
  246. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  247. package/dist/modules/workspace/startup/kb-git.js +15 -13
  248. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  249. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +5 -8
  250. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  251. package/dist/modules/workspace/startup/kb-startup-runner.js +35 -38
  252. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  253. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +7 -1
  254. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  255. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +23 -16
  256. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  257. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +7 -1
  258. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -1
  259. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +12 -5
  260. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -1
  261. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +7 -1
  262. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -1
  263. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +12 -5
  264. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -1
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +12 -5
  268. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +4 -1
  270. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  271. package/dist/modules/workspace/startup/steps/seed-tree.js +14 -11
  272. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  273. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +17 -3
  274. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  275. package/dist/modules/workspace/startup/steps/template-files.step.js +36 -30
  276. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  277. package/dist/modules/workspace/startup/steps/template-source.d.ts +6 -1
  278. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  279. package/dist/modules/workspace/startup/steps/template-source.js +10 -6
  280. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  281. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  282. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  283. package/dist/modules/workspace/workspace.routes.js +8 -7
  284. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  285. package/dist/modules/workspace/workspace.service.d.ts +18 -12
  286. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  287. package/dist/modules/workspace/workspace.service.js +32 -28
  288. package/dist/modules/workspace/workspace.service.js.map +1 -1
  289. package/dist/modules/workspace/workspace.tools.d.ts +2 -1
  290. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  291. package/dist/modules/workspace/workspace.tools.js +20 -18
  292. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  293. package/dist/modules/workspace/write-denial.d.ts +3 -2
  294. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  295. package/dist/modules/workspace/write-denial.js +6 -5
  296. package/dist/modules/workspace/write-denial.js.map +1 -1
  297. package/dist/shared/git.contract.d.ts +52 -7
  298. package/dist/shared/git.contract.d.ts.map +1 -1
  299. package/dist/shared/git.contract.js +30 -8
  300. package/dist/shared/git.contract.js.map +1 -1
  301. package/dist/shared/kb-context.d.ts +76 -0
  302. package/dist/shared/kb-context.d.ts.map +1 -0
  303. package/dist/shared/kb-context.js +108 -0
  304. package/dist/shared/kb-context.js.map +1 -0
  305. package/dist/shared/kb-layout.d.ts +3 -2
  306. package/dist/shared/kb-layout.d.ts.map +1 -1
  307. package/dist/shared/kb-layout.js +4 -3
  308. package/dist/shared/kb-layout.js.map +1 -1
  309. package/dist/shared/kb-layout.test.js +35 -27
  310. package/dist/shared/kb-layout.test.js.map +1 -1
  311. package/dist/shared/pending-proposals.d.ts +4 -1
  312. package/dist/shared/pending-proposals.d.ts.map +1 -1
  313. package/dist/shared/pending-proposals.js +3 -4
  314. package/dist/shared/pending-proposals.js.map +1 -1
  315. package/dist/shared/redact-secret.d.ts +5 -5
  316. package/dist/shared/redact-secret.js +5 -5
  317. package/dist/tenancy/index.d.ts +13 -0
  318. package/dist/tenancy/index.d.ts.map +1 -0
  319. package/dist/tenancy/index.js +13 -0
  320. package/dist/tenancy/index.js.map +1 -0
  321. package/dist/tenancy/static-tenant-source.d.ts +98 -0
  322. package/dist/tenancy/static-tenant-source.d.ts.map +1 -0
  323. package/dist/tenancy/static-tenant-source.js +220 -0
  324. package/dist/tenancy/static-tenant-source.js.map +1 -0
  325. package/dist/tenancy/tenant-host.d.ts +80 -0
  326. package/dist/tenancy/tenant-host.d.ts.map +1 -0
  327. package/dist/tenancy/tenant-host.js +217 -0
  328. package/dist/tenancy/tenant-host.js.map +1 -0
  329. package/dist/tenancy/tenant-runtime.d.ts +91 -0
  330. package/dist/tenancy/tenant-runtime.d.ts.map +1 -0
  331. package/dist/tenancy/tenant-runtime.js +174 -0
  332. package/dist/tenancy/tenant-runtime.js.map +1 -0
  333. package/dist/tenancy/tenant-secrets.d.ts +24 -0
  334. package/dist/tenancy/tenant-secrets.d.ts.map +1 -0
  335. package/dist/tenancy/tenant-secrets.js +30 -0
  336. package/dist/tenancy/tenant-secrets.js.map +1 -0
  337. package/dist/tenancy/tenant-source.contract.d.ts +45 -0
  338. package/dist/tenancy/tenant-source.contract.d.ts.map +1 -0
  339. package/dist/tenancy/tenant-source.contract.js +21 -0
  340. package/dist/tenancy/tenant-source.contract.js.map +1 -0
  341. package/migrations/0000_core_init.sql +14 -14
  342. package/migrations/0003_deployment_settings.sql +1 -1
  343. package/migrations/0005_github_facade.sql +1 -1
  344. package/migrations/0012_agent_audit.sql +6 -6
  345. package/package.json +3 -3
  346. package/src/__tests__/branch-model.test.ts +7 -6
  347. package/src/__tests__/kb-context.ts +30 -0
  348. package/src/__tests__/kb-layout-config.test.ts +35 -30
  349. package/src/core/__tests__/catalog-cache-invalidation.test.ts +2 -1
  350. package/src/core/__tests__/lifecycle.test.ts +78 -1
  351. package/src/core/__tests__/public-config.test.ts +5 -1
  352. package/src/core/catalog-cache-invalidation.ts +16 -11
  353. package/src/core/core-ports.ts +19 -0
  354. package/src/core/create-core-server.ts +27 -106
  355. package/src/core/create-core-services.ts +180 -95
  356. package/src/core/lifecycle.ts +214 -29
  357. package/src/core/public-config.ts +8 -5
  358. package/src/core-config.ts +107 -8
  359. package/src/index.ts +25 -2
  360. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +2 -1
  361. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -1
  362. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -1
  363. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +2 -1
  364. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  365. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +2 -1
  366. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -1
  367. package/src/modules/access/__tests__/access.routes.revoke.test.ts +2 -1
  368. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -1
  369. package/src/modules/access/__tests__/change-read-gate.test.ts +13 -11
  370. package/src/modules/access/__tests__/creator-access.test.ts +3 -2
  371. package/src/modules/access/__tests__/roles.routes.test.ts +2 -1
  372. package/src/modules/access/access-control.service.ts +20 -6
  373. package/src/modules/access/access.routes.ts +10 -13
  374. package/src/modules/access/change-read-gate.ts +10 -4
  375. package/src/modules/access/creator-access.ts +7 -2
  376. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +13 -12
  377. package/src/modules/agent-instructions/read-preamble.ts +10 -12
  378. package/src/modules/auth/__tests__/account.routes.test.ts +15 -0
  379. package/src/modules/auth/__tests__/auth.service.test.ts +60 -0
  380. package/src/modules/auth/account-admission.ts +44 -0
  381. package/src/modules/auth/account.routes.ts +4 -1
  382. package/src/modules/auth/auth.service.ts +33 -1
  383. package/src/modules/auth/oidc-auth-provider.ts +9 -0
  384. package/src/modules/connection-probe/connection-probe.service.ts +7 -1
  385. package/src/modules/database/__tests__/advisory-lock.test.ts +23 -1
  386. package/src/modules/database/__tests__/connection.test.ts +98 -0
  387. package/src/modules/database/__tests__/migrations-unqualified.test.ts +32 -0
  388. package/src/modules/database/advisory-lock.ts +28 -4
  389. package/src/modules/database/connection.ts +143 -13
  390. package/src/modules/database/migrate.ts +48 -14
  391. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  392. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  393. package/src/modules/kb-fs/__tests__/branch-name.test.ts +6 -6
  394. package/src/modules/kb-fs/__tests__/clone-config.test.ts +24 -20
  395. package/src/modules/kb-fs/branch-name.ts +3 -3
  396. package/src/modules/kb-fs/clone-config.ts +15 -12
  397. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +2 -1
  398. package/src/modules/mcp/mcp.service.ts +11 -4
  399. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +12 -11
  400. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +2 -1
  401. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +6 -4
  402. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +2 -1
  403. package/src/modules/plugins/__tests__/join-requests.service.test.ts +2 -1
  404. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +4 -3
  405. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +6 -4
  406. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +220 -5
  407. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +3 -1
  408. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +7 -6
  409. package/src/modules/plugins/__tests__/plugins.routes.test.ts +11 -2
  410. package/src/modules/plugins/__tests__/teams.routes.test.ts +2 -1
  411. package/src/modules/plugins/compile/marketplace-compiler.service.ts +7 -9
  412. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +20 -16
  413. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +9 -2
  414. package/src/modules/plugins/discovery/kb-plugin-source.ts +15 -8
  415. package/src/modules/plugins/discovery/native.source.ts +4 -1
  416. package/src/modules/plugins/index.ts +1 -1
  417. package/src/modules/plugins/join-request-jobs.service.ts +8 -7
  418. package/src/modules/plugins/join-requests.service.ts +7 -7
  419. package/src/modules/plugins/plugin-links.service.ts +192 -19
  420. package/src/modules/plugins/plugin-links.ts +147 -27
  421. package/src/modules/plugins/plugin-provision.service.ts +54 -27
  422. package/src/modules/plugins/plugin-rename.service.ts +10 -10
  423. package/src/modules/plugins/plugins.routes.ts +45 -15
  424. package/src/modules/plugins/plugins.service.ts +7 -11
  425. package/src/modules/plugins/teams.routes.ts +16 -8
  426. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +7 -0
  427. package/src/modules/secrets-vault/__tests__/secrets-variable-loader.test.ts +40 -0
  428. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +2 -0
  429. package/src/modules/secrets-vault/index.ts +2 -0
  430. package/src/modules/secrets-vault/secrets-variable-loader.ts +51 -21
  431. package/src/modules/secrets-vault/secrets-vault.routes.ts +7 -6
  432. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +24 -11
  433. package/src/modules/settings/__tests__/git-root-folders.test.ts +2 -1
  434. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +2 -1
  435. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +7 -4
  436. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +2 -0
  437. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +2 -0
  438. package/src/modules/settings/__tests__/setup.routes.test.ts +33 -52
  439. package/src/modules/settings/deployment-settings.service.ts +21 -20
  440. package/src/modules/settings/setup.routes.ts +26 -16
  441. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +11 -10
  442. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +4 -3
  443. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -1
  444. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +2 -1
  445. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +2 -1
  446. package/src/modules/skills/__tests__/skills.service.test.ts +3 -2
  447. package/src/modules/skills/allowed-tools-check.ts +7 -6
  448. package/src/modules/skills/pending-skills.service.ts +9 -8
  449. package/src/modules/skills/skill-access-requests.routes.ts +7 -7
  450. package/src/modules/skills/skills.routes.ts +4 -3
  451. package/src/modules/skills/skills.service.ts +13 -9
  452. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +3 -2
  453. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -2
  454. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +17 -12
  455. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  456. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +4 -1
  457. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +2 -1
  458. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +2 -1
  459. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +5 -4
  460. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +2 -1
  461. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +4 -2
  462. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +13 -5
  463. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -1
  464. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +9 -1
  465. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +3 -2
  466. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +2 -0
  467. package/src/modules/tool-manuals/mcp-json-discovery.ts +6 -4
  468. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -11
  469. package/src/modules/tool-manuals/pending-tools.service.ts +19 -16
  470. package/src/modules/tool-manuals/tool-delete.service.ts +13 -10
  471. package/src/modules/tool-manuals/tool-manuals.routes.ts +10 -8
  472. package/src/modules/tool-manuals/tool-manuals.service.ts +15 -13
  473. package/src/modules/tool-manuals/tool-manuals.tools.ts +7 -6
  474. package/src/modules/workflow/__tests__/apply-failure.test.ts +2 -1
  475. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +14 -26
  476. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -1
  477. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +3 -2
  478. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +2 -1
  479. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +3 -10
  480. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -1
  481. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +3 -10
  482. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +28 -27
  483. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +2 -1
  484. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +2 -1
  485. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +3 -2
  486. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +2 -1
  487. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -4
  488. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +2 -1
  489. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +2 -1
  490. package/src/modules/workflow/agent-tools/workflow.tools.ts +11 -9
  491. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +5 -4
  492. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +12 -11
  493. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +9 -8
  494. package/src/modules/workflow/git/__tests__/git.service.commitFile.strayPath.test.ts +8 -7
  495. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +6 -5
  496. package/src/modules/workflow/git/__tests__/git.service.createBranch.test.ts +3 -2
  497. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +3 -2
  498. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +7 -6
  499. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +16 -15
  500. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +3 -2
  501. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +2 -1
  502. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +2 -1
  503. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +10 -9
  504. package/src/modules/workflow/git/__tests__/git.service.listTrackedFiles.test.ts +2 -1
  505. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +6 -5
  506. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +9 -8
  507. package/src/modules/workflow/git/__tests__/git.service.pendingChanges.test.ts +2 -1
  508. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +4 -3
  509. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +9 -8
  510. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +2 -1
  511. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +2 -1
  512. package/src/modules/workflow/git/__tests__/git.service.status.test.ts +4 -3
  513. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +8 -7
  514. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +41 -3
  515. package/src/modules/workflow/git/git.service.ts +24 -13
  516. package/src/modules/workflow/git/node-git-runner.ts +37 -4
  517. package/src/modules/workflow/review-workflow/review-workflow.service.ts +9 -6
  518. package/src/modules/workflow/session-ontology.service.ts +7 -2
  519. package/src/modules/workflow/workflow.service.ts +31 -24
  520. package/src/modules/workspace/__tests__/file-stat-access.test.ts +3 -2
  521. package/src/modules/workspace/__tests__/git-internals.security.test.ts +10 -9
  522. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +4 -2
  523. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -2
  524. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +2 -1
  525. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +2 -1
  526. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +2 -1
  527. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +2 -1
  528. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -1
  529. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +2 -1
  530. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +5 -4
  531. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +2 -1
  532. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +2 -1
  533. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  534. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
  535. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  536. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  537. package/src/modules/workspace/__tests__/workspace.service.test.ts +84 -50
  538. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
  539. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -7
  540. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +4 -3
  541. package/src/modules/workspace/__tests__/workspace.tools.test.ts +9 -8
  542. package/src/modules/workspace/__tests__/write-denial.test.ts +5 -4
  543. package/src/modules/workspace/session-ontology.gate.ts +6 -4
  544. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +6 -2
  545. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +29 -21
  546. package/src/modules/workspace/startup/kb-git.ts +15 -22
  547. package/src/modules/workspace/startup/kb-startup-runner.ts +40 -46
  548. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +104 -98
  549. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +24 -16
  550. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +14 -5
  551. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +13 -5
  552. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +14 -5
  553. package/src/modules/workspace/startup/steps/seed-tree.ts +11 -9
  554. package/src/modules/workspace/startup/steps/template-files.step.ts +43 -34
  555. package/src/modules/workspace/startup/steps/template-source.ts +8 -5
  556. package/src/modules/workspace/workspace.routes.ts +8 -7
  557. package/src/modules/workspace/workspace.service.ts +30 -27
  558. package/src/modules/workspace/workspace.tools.ts +21 -20
  559. package/src/modules/workspace/write-denial.ts +7 -5
  560. package/src/shared/__tests__/kb-layout.plugin.test.ts +19 -16
  561. package/src/shared/git.contract.ts +70 -8
  562. package/src/shared/kb-context.ts +130 -0
  563. package/src/shared/kb-layout.test.ts +37 -27
  564. package/src/shared/kb-layout.ts +4 -3
  565. package/src/shared/pending-proposals.ts +7 -4
  566. package/src/shared/redact-secret.ts +5 -5
  567. package/src/tenancy/__tests__/static-tenant-source.test.ts +177 -0
  568. package/src/tenancy/__tests__/tenant-host.test.ts +372 -0
  569. package/src/tenancy/__tests__/tenant-runtime.test.ts +221 -0
  570. package/src/tenancy/__tests__/tenant-secrets.test.ts +35 -0
  571. package/src/tenancy/index.ts +35 -0
  572. package/src/tenancy/static-tenant-source.ts +296 -0
  573. package/src/tenancy/tenant-host.ts +261 -0
  574. package/src/tenancy/tenant-runtime.ts +208 -0
  575. package/src/tenancy/tenant-secrets.ts +41 -0
  576. package/src/tenancy/tenant-source.contract.ts +59 -0
@@ -0,0 +1,296 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { resolveDatabaseUrl, type TenantConfig } from '../core-config.js';
4
+ import { DEFAULT_DB_SCHEMA, assertSchemaName } from '../modules/database/connection.js';
5
+ import { DEFAULT_GIT_TIMEOUT_MS } from '../modules/workflow/git/node-git-runner.js';
6
+ import { defaultKbTemplateDir } from '../assets.js';
7
+ import { DEFAULT_GIT_USERNAME } from '../shared/git.contract.js';
8
+ import { deriveTenantSecrets } from './tenant-secrets.js';
9
+ import { loopbackTenantBaseUrl } from './tenant-host.js';
10
+ import { assertTenantSlug, normalizeHost, type TenantDescriptor, type TenantSource } from './tenant-source.contract.js';
11
+
12
+ /**
13
+ * What every tenant of one process shares: the process's own facts, the one
14
+ * database, the roots its folders hang off, and the master key its secrets
15
+ * derive from. A tenant's own record ({@link TenantRecord}) adds what differs.
16
+ */
17
+ export interface TenantHostSettings {
18
+ readonly databaseUrl: string;
19
+ readonly port: number;
20
+ readonly nodeEnv: string;
21
+ readonly trustProxy: string;
22
+ /** Each tenant's workspaces live under `<workspacesRoot>/<slug>`; its backups, spills and caches beside them. */
23
+ readonly workspacesRoot: string;
24
+ readonly kbTemplateDir: string;
25
+ readonly gitTimeoutMs: number;
26
+ readonly ontologySessionBlock: boolean;
27
+ readonly updateCheckEnabled: boolean;
28
+ /** See `tenant-secrets.ts`. */
29
+ readonly masterKey: string;
30
+ }
31
+
32
+ /** One entry of the tenants file. Everything optional has the single-tenant default. */
33
+ export interface TenantRecord {
34
+ slug: string;
35
+ /** Host names (a port is tolerated and ignored) that reach this tenant. At least one. */
36
+ hosts: string[];
37
+ adminEmail: string;
38
+ /** The bootstrap password; omitted, password login is off for this tenant. */
39
+ adminPassword?: string;
40
+ /** Force password login on or off; default: on exactly when `adminPassword` is set. */
41
+ loginPassword?: boolean;
42
+ kbRepoUrl?: string;
43
+ gitToken?: string;
44
+ gitUsername?: string;
45
+ kbDirName?: string;
46
+ /** The branch model; absent, the tenant's admin enters it on the setup screen. */
47
+ defaultBranch?: string;
48
+ protectedBranches?: string | string[];
49
+ /** The bearer a git host's webhook presents to `POST /api/sync`. */
50
+ kbSyncSecret?: string;
51
+ /** Default `t_<slug>` with hyphens as underscores. */
52
+ dbSchema?: string;
53
+ /** The credential-prefix brand; default: the slug without hyphens. */
54
+ tenantId?: string;
55
+ /** Default `https://<first host>`. */
56
+ publicBackendUrl?: string;
57
+ /** Default: the backend URL (the backend serves the SPA). */
58
+ publicFrontendUrl?: string;
59
+ allowedEmailDomains?: string[];
60
+ oidc?: {
61
+ issuerUrl: string;
62
+ clientId: string;
63
+ clientSecret: string;
64
+ scopes?: string;
65
+ providerLabel?: string;
66
+ };
67
+ }
68
+
69
+ /** The file's shape: a list of records. */
70
+ export interface TenantsFile {
71
+ tenants: TenantRecord[];
72
+ }
73
+
74
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
75
+ const USERNAME_PATTERN = /^[A-Za-z0-9._-]+$/;
76
+
77
+ /**
78
+ * A tenant's {@link TenantConfig} from its record and the host's settings:
79
+ * the same values `CoreConfig` reads from the environment, resolved the same
80
+ * way, with the folders and the schema named after the slug so two tenants
81
+ * can never share a path or a table.
82
+ */
83
+ export function tenantConfigFrom(record: TenantRecord, settings: TenantHostSettings): TenantConfig {
84
+ const slug = assertTenantSlug(record.slug);
85
+ if (!Array.isArray(record.hosts) || record.hosts.length === 0) {
86
+ throw new Error(`Tenant "${slug}" names no host`);
87
+ }
88
+ const hosts = record.hosts.map(normalizeHost);
89
+ const adminEmail = (record.adminEmail ?? '').trim().toLowerCase();
90
+ if (!EMAIL_PATTERN.test(adminEmail)) {
91
+ throw new Error(`Tenant "${slug}": adminEmail is not a valid email: "${record.adminEmail ?? ''}"`);
92
+ }
93
+ const gitUsername = (record.gitUsername ?? DEFAULT_GIT_USERNAME).trim();
94
+ if (!USERNAME_PATTERN.test(gitUsername)) {
95
+ throw new Error(`Tenant "${slug}": gitUsername must match [A-Za-z0-9._-]+; got "${gitUsername}"`);
96
+ }
97
+ const tenantId = (record.tenantId ?? slug.replace(/-/g, '')).trim().toLowerCase();
98
+ if (!/^[a-z0-9]+$/.test(tenantId)) {
99
+ throw new Error(`Tenant "${slug}": tenantId must be lowercase alphanumeric; got "${tenantId}"`);
100
+ }
101
+ const dbSchema = assertSchemaName(record.dbSchema ?? `t_${slug.replace(/-/g, '_')}`);
102
+ // The default schema is where a single-tenant deployment on the same
103
+ // database keeps its tables and, in drizzle's own schema beside it, its
104
+ // migration ledger; a tenant there would be no tenant at all.
105
+ if (dbSchema === DEFAULT_DB_SCHEMA) {
106
+ throw new Error(`Tenant "${slug}": dbSchema must not be "${DEFAULT_DB_SCHEMA}"; a tenant needs a schema of its own`);
107
+ }
108
+ const adminPassword = record.adminPassword ?? '';
109
+ const loginPasswordEnabled = record.loginPassword ?? adminPassword !== '';
110
+ if (loginPasswordEnabled && !adminPassword) {
111
+ throw new Error(`Tenant "${slug}": loginPassword is on but no adminPassword is set`);
112
+ }
113
+ const publicBackendUrl = (record.publicBackendUrl ?? `https://${hosts[0]}`).trim().replace(/\/+$/, '');
114
+ const publicFrontendUrl = (record.publicFrontendUrl ?? publicBackendUrl).trim().replace(/\/+$/, '');
115
+ for (const [name, value] of [
116
+ ['publicBackendUrl', publicBackendUrl],
117
+ ['publicFrontendUrl', publicFrontendUrl],
118
+ ] as const) {
119
+ try {
120
+ new URL(value);
121
+ } catch {
122
+ throw new Error(`Tenant "${slug}": ${name} is not a valid URL: "${value}"`);
123
+ }
124
+ }
125
+ const secrets = deriveTenantSecrets(settings.masterKey, slug);
126
+ const besideWorkspaces = (name: string) => path.resolve(settings.workspacesRoot, '..', name, slug);
127
+ const allowedEmailDomains = (record.allowedEmailDomains ?? [])
128
+ .map((d) => d.trim().toLowerCase().replace(/^[@.]+/, ''))
129
+ .filter((d) => d.length > 0);
130
+ const protectedBranches = Array.isArray(record.protectedBranches)
131
+ ? record.protectedBranches.join(',')
132
+ : (record.protectedBranches ?? '');
133
+ // The record's settings, as the environment a single-tenant deployment
134
+ // would pin them in: only what the record names, so the setup screen
135
+ // still collects the rest. Nothing of the host process's environment.
136
+ const settingsEnv: NodeJS.ProcessEnv = {};
137
+ for (const [name, value] of [
138
+ ['KB_REPO_URL', record.kbRepoUrl],
139
+ ['GIT_TOKEN', record.gitToken],
140
+ ['GIT_USERNAME', record.gitUsername],
141
+ ['KB_DIR_NAME', record.kbDirName],
142
+ ['DEFAULT_BRANCH', record.defaultBranch],
143
+ ['PROTECTED_BRANCHES', protectedBranches],
144
+ ['KB_SYNC_SECRET', record.kbSyncSecret],
145
+ ['OIDC_ISSUER_URL', record.oidc?.issuerUrl],
146
+ ['OIDC_CLIENT_ID', record.oidc?.clientId],
147
+ ['OIDC_CLIENT_SECRET', record.oidc?.clientSecret],
148
+ ['OIDC_SCOPES', record.oidc?.scopes],
149
+ ['OIDC_PROVIDER_LABEL', record.oidc?.providerLabel],
150
+ ['ALLOWED_EMAIL_DOMAINS', allowedEmailDomains.join(',')],
151
+ ] as const) {
152
+ const trimmed = (value ?? '').trim();
153
+ if (trimmed) settingsEnv[name] = trimmed;
154
+ }
155
+ return {
156
+ port: settings.port,
157
+ databaseUrl: settings.databaseUrl,
158
+ dbSchema,
159
+ nodeEnv: settings.nodeEnv,
160
+ tenantId,
161
+ externalApiKeyPrefix: `${tenantId}_`,
162
+ internalTokenPrefix: `${tenantId}-int_`,
163
+ uploadTokenPrefix: `${tenantId}-up_`,
164
+ mcpOAuthTokenPrefix: `${tenantId}-mcp_`,
165
+ workspacesRoot: path.resolve(settings.workspacesRoot, slug),
166
+ backupsRoot: besideWorkspaces('backups'),
167
+ spillRoot: besideWorkspaces('tool-chain-spills'),
168
+ docExtractCacheRoot: besideWorkspaces('doc-extract-cache'),
169
+ jwtSecret: secrets.jwtSecret,
170
+ secretsEncKey: secrets.secretsEncKey,
171
+ internalTokenSecret: secrets.internalTokenSecret,
172
+ adminEmail,
173
+ adminPassword,
174
+ loginPasswordEnabled,
175
+ oidcIssuerUrl: (record.oidc?.issuerUrl ?? '').trim().replace(/\/+$/, ''),
176
+ oidcClientId: (record.oidc?.clientId ?? '').trim(),
177
+ oidcClientSecret: (record.oidc?.clientSecret ?? '').trim(),
178
+ oidcScopes: (record.oidc?.scopes ?? 'openid profile email').trim(),
179
+ oidcProviderLabel: (record.oidc?.providerLabel ?? 'Single sign-on').trim(),
180
+ kbRepoUrl: (record.kbRepoUrl ?? '').trim(),
181
+ kbDirName: (record.kbDirName ?? '').trim(),
182
+ gitUsername,
183
+ gitToken: (record.gitToken ?? '').trim(),
184
+ kbTemplateDir: settings.kbTemplateDir,
185
+ ontologySessionBlock: settings.ontologySessionBlock,
186
+ updateCheckEnabled: settings.updateCheckEnabled,
187
+ allowedEmailDomains,
188
+ trustProxy: settings.trustProxy,
189
+ gitTimeoutMs: settings.gitTimeoutMs,
190
+ publicBackendUrl,
191
+ publicFrontendUrl,
192
+ configuredPublicFrontendUrl: publicFrontendUrl,
193
+ loopbackBaseUrl: loopbackTenantBaseUrl(settings.port, slug),
194
+ settingsEnv,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * The reference {@link TenantSource}: every tenant known up front, from a
200
+ * JSON file (`TENANTS_FILE`). Enough for development, tests and a small
201
+ * self-hosted fleet; the cloud app brings its own source over its registry.
202
+ */
203
+ export class StaticTenantSource implements TenantSource {
204
+ private readonly bySlug = new Map<string, TenantDescriptor>();
205
+ private readonly byHost = new Map<string, TenantDescriptor>();
206
+
207
+ constructor(records: readonly TenantRecord[], settings: TenantHostSettings) {
208
+ for (const record of records) {
209
+ const config = tenantConfigFrom(record, settings);
210
+ const descriptor: TenantDescriptor = {
211
+ slug: record.slug,
212
+ hosts: record.hosts.map(normalizeHost),
213
+ config,
214
+ };
215
+ if (this.bySlug.has(descriptor.slug)) throw new Error(`Tenant "${descriptor.slug}" is listed twice`);
216
+ this.bySlug.set(descriptor.slug, descriptor);
217
+ for (const host of descriptor.hosts) {
218
+ const taken = this.byHost.get(host);
219
+ if (taken) throw new Error(`Host "${host}" is claimed by both "${taken.slug}" and "${descriptor.slug}"`);
220
+ this.byHost.set(host, descriptor);
221
+ }
222
+ }
223
+ }
224
+
225
+ static async fromFile(file: string, settings: TenantHostSettings): Promise<StaticTenantSource> {
226
+ const text = await fs.readFile(file, 'utf8');
227
+ let parsed: unknown;
228
+ try {
229
+ parsed = JSON.parse(text);
230
+ } catch (err) {
231
+ throw new Error(`${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
232
+ }
233
+ const tenants = (parsed as Partial<TenantsFile> | null)?.tenants;
234
+ if (!Array.isArray(tenants)) throw new Error(`${file} must be an object with a "tenants" array`);
235
+ return new StaticTenantSource(tenants as TenantRecord[], settings);
236
+ }
237
+
238
+ get slugs(): readonly string[] {
239
+ return [...this.bySlug.keys()];
240
+ }
241
+
242
+ async resolveByHost(host: string): Promise<TenantDescriptor | null> {
243
+ return this.byHost.get(normalizeHost(host)) ?? null;
244
+ }
245
+
246
+ async describe(slug: string): Promise<TenantDescriptor | null> {
247
+ return this.bySlug.get(slug) ?? null;
248
+ }
249
+ }
250
+
251
+ /** The process facts a tenant host reads from its environment — see `docs/multi-tenant.md`. */
252
+ export interface TenantHostEnv extends TenantHostSettings {
253
+ /** The tenants file, when the process runs as a host at all. */
254
+ readonly tenantsFile: string | null;
255
+ /** Minutes without a request after which a tenant's graph is stopped. */
256
+ readonly idleMinutes: number;
257
+ }
258
+
259
+ const MAX_TIMER_MS = 2_147_483_647;
260
+
261
+ /**
262
+ * The host's settings from the environment: the same variables a
263
+ * single-tenant deployment reads for these facts (`DATABASE_URL` and the
264
+ * `POSTGRES_*` parts, `PORT`, `TRUST_PROXY`, `WORKSPACES_ROOT`,
265
+ * `KB_TEMPLATE_DIR`, `GIT_TIMEOUT_MS`, …), plus the three that only a host
266
+ * has: `TENANTS_FILE`, `TENANT_MASTER_KEY` and `TENANT_IDLE_MINUTES`.
267
+ */
268
+ export function tenantHostEnv(env: NodeJS.ProcessEnv = process.env): TenantHostEnv {
269
+ const tenantsFile = (env.TENANTS_FILE || '').trim() || null;
270
+ const masterKey = (env.TENANT_MASTER_KEY || '').trim();
271
+ if (tenantsFile && !masterKey) {
272
+ throw new Error(
273
+ 'TENANT_MASTER_KEY is required with TENANTS_FILE — every tenant\'s secrets derive from it. Generate one with: ' +
274
+ `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`,
275
+ );
276
+ }
277
+ const gitTimeout = Number(env.GIT_TIMEOUT_MS);
278
+ const idle = Number(env.TENANT_IDLE_MINUTES);
279
+ const domain = (env.DOMAIN || '').trim();
280
+ const workspacesRoot = env.WORKSPACES_ROOT || path.resolve(process.cwd(), 'workspaces');
281
+ return {
282
+ tenantsFile,
283
+ masterKey,
284
+ databaseUrl: resolveDatabaseUrl(env),
285
+ port: parseInt(env.PORT || '3001', 10),
286
+ nodeEnv: env.NODE_ENV || 'development',
287
+ trustProxy: (env.TRUST_PROXY || (domain ? '1' : '')).trim(),
288
+ workspacesRoot,
289
+ kbTemplateDir: env.KB_TEMPLATE_DIR || defaultKbTemplateDir(),
290
+ gitTimeoutMs:
291
+ Number.isFinite(gitTimeout) && gitTimeout > 0 && gitTimeout <= MAX_TIMER_MS ? gitTimeout : DEFAULT_GIT_TIMEOUT_MS,
292
+ ontologySessionBlock: (env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false',
293
+ updateCheckEnabled: (env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false',
294
+ idleMinutes: Number.isFinite(idle) && idle > 0 ? idle : 30,
295
+ };
296
+ }
@@ -0,0 +1,261 @@
1
+ import express, { type Express, type Request } from 'express';
2
+ import path from 'node:path';
3
+ import { isIP } from 'node:net';
4
+ import type { ProcessConfig } from '../core-config.js';
5
+ import { GIT_SHA } from '../version.js';
6
+ import { logger } from '../shared/logging.js';
7
+ import { TenantRuntime, type TenantRuntimeDeps } from './tenant-runtime.js';
8
+ import { TENANT_SLUG_PATTERN, normalizeHost, type TenantDescriptor, type TenantSource } from './tenant-source.contract.js';
9
+
10
+ const log = logger('tenancy');
11
+
12
+ /**
13
+ * The path prefix under which a process reaches ONE of its own tenants over
14
+ * loopback: `http://127.0.0.1:<port>/_tenant/<slug>/api/...`. The MCP proxy
15
+ * and the UTCP manuals it seeds dial the tenant's own REST surface this way,
16
+ * because a loopback request carries no tenant host name and a header cannot
17
+ * ride a UTCP call (the proxy is a passthrough; it reshapes nothing).
18
+ *
19
+ * The host honours the prefix only for a request that came from this
20
+ * machine AND was forwarded by nobody: the socket peer is loopback and there
21
+ * is no `X-Forwarded-For`. A reverse proxy on the same host (nginx, a
22
+ * Coolify proxy) delivers every public request from a loopback socket, and
23
+ * every such proxy adds that header; the process's own dial never does.
24
+ * What the prefix could grant anyone who got past this is only what the
25
+ * tenant's host name grants already — sessions, keys and tokens are per
26
+ * tenant — but the addressing rule is stated so it can be relied on.
27
+ */
28
+ export const LOOPBACK_TENANT_PREFIX = '/_tenant';
29
+
30
+ /** The base URL a tenant's graph dials itself at — see {@link LOOPBACK_TENANT_PREFIX}. */
31
+ export function loopbackTenantBaseUrl(port: number, slug: string): string {
32
+ return `http://127.0.0.1:${port}${LOOPBACK_TENANT_PREFIX}/${slug}`;
33
+ }
34
+
35
+ /** Whether `address` is this machine talking to itself. */
36
+ export function isLoopbackAddress(address: string | undefined): boolean {
37
+ if (!address) return false;
38
+ const bare = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address;
39
+ if (bare === '::1') return true;
40
+ return isIP(bare) === 4 && bare.startsWith('127.');
41
+ }
42
+
43
+ /** How a request names its tenant: by the loopback prefix, or by its host. */
44
+ export type TenantSelector = { kind: 'slug'; slug: string; rest: string } | { kind: 'host'; host: string };
45
+
46
+ /**
47
+ * Which tenant a request is for. The loopback prefix wins, but only from a
48
+ * loopback peer that no proxy forwarded (no `X-Forwarded-For`); everything
49
+ * else is decided by the host name (the `X-Forwarded-Host` a trusted proxy
50
+ * set, else `Host`), lowercase and without its port.
51
+ */
52
+ export function selectTenant(req: {
53
+ hostname: string;
54
+ url: string;
55
+ peer: string | undefined;
56
+ forwardedFor?: string | string[] | undefined;
57
+ }): TenantSelector {
58
+ const forwarded = Array.isArray(req.forwardedFor) ? req.forwardedFor.length > 0 : Boolean(req.forwardedFor);
59
+ if (isLoopbackAddress(req.peer) && !forwarded && req.url.startsWith(`${LOOPBACK_TENANT_PREFIX}/`)) {
60
+ const afterPrefix = req.url.slice(LOOPBACK_TENANT_PREFIX.length + 1);
61
+ const end = afterPrefix.search(/[/?#]/);
62
+ const slug = end === -1 ? afterPrefix : afterPrefix.slice(0, end);
63
+ if (TENANT_SLUG_PATTERN.test(slug)) {
64
+ const rest = end === -1 ? '/' : afterPrefix.slice(end).replace(/^(?=[?#])/, '/');
65
+ return { kind: 'slug', slug, rest };
66
+ }
67
+ }
68
+ return { kind: 'host', host: normalizeHost(req.hostname || '') };
69
+ }
70
+
71
+ export interface TenantHostOptions {
72
+ source: TenantSource;
73
+ process: ProcessConfig;
74
+ /** The built SPA, served once for every resolved tenant. */
75
+ staticDir?: string;
76
+ /** Minutes without a request after which a tenant's graph is stopped. Default 30; 0 never evicts. */
77
+ idleMinutes?: number;
78
+ /** How long a request waits for an activation in flight before answering 503. Default 60s. */
79
+ activationWaitMs?: number;
80
+ /** Seams for the runtimes the host builds — a suite's fake graphs. */
81
+ runtime?: TenantRuntimeDeps;
82
+ /** How often idle tenants are looked for. Default: once a minute. */
83
+ sweepIntervalMs?: number;
84
+ }
85
+
86
+ export interface TenantHost {
87
+ /** The app the process listens on. */
88
+ app: Express;
89
+ /** The runtimes built so far, by slug. */
90
+ runtimes(): ReadonlyMap<string, TenantRuntime>;
91
+ /** Stop every active tenant; the host keeps serving and reactivates on demand. */
92
+ evictAll(): Promise<void>;
93
+ /** Stop the idle sweep and every tenant: the process's shutdown. */
94
+ stop(): Promise<void>;
95
+ }
96
+
97
+ const DEFAULT_ACTIVATION_WAIT_MS = 60_000;
98
+ const DEFAULT_IDLE_MINUTES = 30;
99
+ const DEFAULT_SWEEP_INTERVAL_MS = 60_000;
100
+
101
+ /** `promise`, or `null` once `ms` have passed. */
102
+ function within<T>(promise: Promise<T>, ms: number): Promise<T | null> {
103
+ let timer: NodeJS.Timeout | undefined;
104
+ const late = new Promise<null>((resolve) => {
105
+ timer = setTimeout(() => resolve(null), ms);
106
+ });
107
+ return Promise.race([promise, late]).finally(() => clearTimeout(timer));
108
+ }
109
+
110
+ /**
111
+ * One Express app in front of many tenants' apps. It answers process health
112
+ * itself, resolves each request to a tenant (see {@link selectTenant}),
113
+ * activates that tenant's graph on first use, hands the request to the
114
+ * tenant's app, and serves the SPA for whatever the tenant's app leaves
115
+ * unhandled. Nothing below it knows there are other tenants: each graph was
116
+ * built by the same composition root a single-tenant deployment uses.
117
+ */
118
+ export function createTenantHost(opts: TenantHostOptions): TenantHost {
119
+ const runtimes = new Map<string, TenantRuntime>();
120
+ const activationWaitMs = opts.activationWaitMs ?? DEFAULT_ACTIVATION_WAIT_MS;
121
+ const idleMs = (opts.idleMinutes ?? DEFAULT_IDLE_MINUTES) * 60_000;
122
+
123
+ const runtimeFor = (descriptor: TenantDescriptor): TenantRuntime => {
124
+ let runtime = runtimes.get(descriptor.slug);
125
+ if (!runtime) {
126
+ runtime = new TenantRuntime(descriptor, opts.runtime);
127
+ runtimes.set(descriptor.slug, runtime);
128
+ }
129
+ return runtime;
130
+ };
131
+
132
+ const app = express();
133
+ // Forwarded headers are honoured exactly as a single-tenant deployment
134
+ // honours them, and for the same reason: `req.hostname` is what picks the
135
+ // tenant, and behind a proxy it lives in `X-Forwarded-Host`.
136
+ if (opts.process.trustProxy) {
137
+ const raw = opts.process.trustProxy;
138
+ app.set('trust proxy', /^\d+$/.test(raw) ? Number(raw) : raw);
139
+ }
140
+
141
+ // The process's own health, before any tenant is involved: a load balancer
142
+ // asks with whatever host name it likes, and a process with no active
143
+ // tenant is still a healthy process.
144
+ app.get('/api/health', (_req, res) => {
145
+ res.json({ status: 'ok', sha: GIT_SHA, timestamp: Date.now(), tenants: runtimes.size });
146
+ });
147
+
148
+ app.use(async (req: Request, res, next) => {
149
+ const selector = selectTenant({
150
+ hostname: req.hostname,
151
+ url: req.url,
152
+ peer: req.socket.remoteAddress,
153
+ forwardedFor: req.headers['x-forwarded-for'],
154
+ });
155
+ let descriptor: TenantDescriptor | null;
156
+ try {
157
+ descriptor =
158
+ selector.kind === 'slug'
159
+ ? await opts.source.describe(selector.slug)
160
+ : await opts.source.resolveByHost(selector.host);
161
+ } catch (err) {
162
+ log.error('tenant lookup failed:', { err });
163
+ res.status(503).set('Retry-After', '5').json({ error: 'Tenant lookup failed' });
164
+ return;
165
+ }
166
+ if (!descriptor) {
167
+ if (req.path.startsWith('/api/') || req.path.startsWith(LOOPBACK_TENANT_PREFIX)) {
168
+ res.status(404).json({ error: 'No workspace at this address' });
169
+ } else {
170
+ res.status(404).type('text/plain').send('No workspace at this address.');
171
+ }
172
+ return;
173
+ }
174
+ if (selector.kind === 'slug') req.url = selector.rest;
175
+
176
+ const runtime = runtimeFor(descriptor);
177
+ // Counted from here, before the graph is even asked for, until the
178
+ // response closes however it ends — a 503 below included. An open
179
+ // response is what keeps a tenant from being evicted from under it.
180
+ // Left exactly once: a client that dropped during the tenant lookup
181
+ // above has had its `close` already, and that event will not come
182
+ // again, so the socket is asked directly; a socket destroyed but not
183
+ // yet closed would otherwise be left twice, once here and once by the
184
+ // listener, taking another request's count with it.
185
+ runtime.enter();
186
+ let left = false;
187
+ const leave = () => {
188
+ if (left) return;
189
+ left = true;
190
+ runtime.leave();
191
+ };
192
+ res.on('close', leave);
193
+ if (req.socket.destroyed) {
194
+ leave();
195
+ return;
196
+ }
197
+ let graph: Awaited<ReturnType<TenantRuntime['handle']>> | null;
198
+ try {
199
+ graph = await within(runtime.handle(), activationWaitMs);
200
+ } catch {
201
+ // Logged by the runtime; the caller learns only that it may try again.
202
+ res.status(503).set('Retry-After', '5').json({ error: 'This workspace could not be started; try again shortly' });
203
+ return;
204
+ }
205
+ if (!graph) {
206
+ res.status(503).set('Retry-After', '5').json({ error: 'This workspace is starting; try again shortly' });
207
+ return;
208
+ }
209
+ graph.app(req, res, next);
210
+ });
211
+
212
+ // Whatever a tenant's app left unhandled: the SPA, with the same rule a
213
+ // single-tenant deployment applies to the namespaces that must never
214
+ // resolve to `index.html` (see `createCoreServer`).
215
+ if (opts.staticDir) {
216
+ const frontendDist = opts.staticDir;
217
+ app.use(express.static(frontendDist));
218
+ app.get('{*path}', (req, res) => {
219
+ if (
220
+ req.path.startsWith('/.well-known/') ||
221
+ req.path.startsWith('/api/') ||
222
+ req.path.startsWith('/login/oauth/')
223
+ ) {
224
+ res.status(404).json({ error: 'Not found' });
225
+ return;
226
+ }
227
+ res.sendFile(path.join(frontendDist, 'index.html'));
228
+ });
229
+ }
230
+
231
+ // Idle tenants are stopped on a timer, never on the request that would
232
+ // have been their last: a graph whose last response closed `idleMs` ago,
233
+ // with none still open and no queued commit, is not worth its pool and
234
+ // its lease. `busy()` is what keeps an event stream's tenant alive.
235
+ const sweep = async () => {
236
+ for (const runtime of runtimes.values()) {
237
+ if (runtime.state !== 'active' || runtime.idleFor() < idleMs) continue;
238
+ if (await runtime.busy()) continue;
239
+ await runtime.evict().catch((err: unknown) => log.warn(`evicting tenant "${runtime.slug}" failed:`, { err }));
240
+ }
241
+ };
242
+ const sweeper =
243
+ idleMs > 0
244
+ ? setInterval(() => void sweep(), Math.min(opts.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS, idleMs))
245
+ : null;
246
+ sweeper?.unref();
247
+
248
+ const evictAll = async () => {
249
+ await Promise.all([...runtimes.values()].map((runtime) => runtime.evict().catch(() => undefined)));
250
+ };
251
+
252
+ return {
253
+ app,
254
+ runtimes: () => runtimes,
255
+ evictAll,
256
+ async stop() {
257
+ if (sweeper) clearInterval(sweeper);
258
+ await evictAll();
259
+ },
260
+ };
261
+ }