@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,372 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import express from 'express';
3
+ import http from 'node:http';
4
+ import net, { type AddressInfo } from 'node:net';
5
+ import type { TenantConfig } from '../../core-config.js';
6
+ import { createTenantHost, isLoopbackAddress, selectTenant, type TenantHost } from '../tenant-host.js';
7
+ import type { TenantGraph } from '../tenant-runtime.js';
8
+ import type { TenantDescriptor, TenantSource } from '../tenant-source.contract.js';
9
+
10
+ /**
11
+ * Two tenants behind one host, with a tiny app standing in for each graph:
12
+ * what these cases pin is the front door — which requests reach which
13
+ * tenant, what an unknown host gets, what happens while a tenant starts —
14
+ * and none of it depends on what a real graph does once it has the request.
15
+ */
16
+ const tenants: TenantDescriptor[] = [
17
+ { slug: 'acme', hosts: ['acme.test'], config: {} as TenantConfig },
18
+ { slug: 'globex', hosts: ['globex.test'], config: {} as TenantConfig },
19
+ ];
20
+
21
+ const source: TenantSource = {
22
+ async resolveByHost(host) {
23
+ return tenants.find((t) => t.hosts.includes(host)) ?? null;
24
+ },
25
+ async describe(slug) {
26
+ return tenants.find((t) => t.slug === slug) ?? null;
27
+ },
28
+ };
29
+
30
+ function tenantApp(slug: string) {
31
+ const app = express();
32
+ app.get('/api/whoami', (req, res) => {
33
+ res.json({ tenant: slug, path: req.path, query: req.query });
34
+ });
35
+ // Anything else: which tenant got it, and with what path.
36
+ app.use((req, res) => {
37
+ res.status(404).json({ tenant: slug, path: req.path, unhandled: true });
38
+ });
39
+ return app;
40
+ }
41
+
42
+ let server: http.Server | null = null;
43
+ let host: TenantHost | null = null;
44
+
45
+ async function listen(h: TenantHost): Promise<number> {
46
+ host = h;
47
+ server = await new Promise<http.Server>((resolve) => {
48
+ const s = h.app.listen(0, '127.0.0.1', () => resolve(s));
49
+ });
50
+ return (server.address() as AddressInfo).port;
51
+ }
52
+
53
+ /** A request with an explicit Host header — which `fetch` refuses to set. */
54
+ function request(
55
+ port: number,
56
+ path: string,
57
+ hostHeader: string,
58
+ headers: Record<string, string> = {},
59
+ ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
60
+ return new Promise((resolve, reject) => {
61
+ const req = http.request(
62
+ { host: '127.0.0.1', port, path, method: 'GET', headers: { ...headers, host: hostHeader } },
63
+ (res) => {
64
+ const chunks: Buffer[] = [];
65
+ res.on('data', (c: Buffer) => chunks.push(c));
66
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, headers: res.headers, body: Buffer.concat(chunks).toString() }));
67
+ },
68
+ );
69
+ req.on('error', reject);
70
+ req.end();
71
+ });
72
+ }
73
+
74
+ afterEach(async () => {
75
+ await host?.stop();
76
+ host = null;
77
+ if (server) await new Promise<void>((resolve) => server!.close(() => resolve()));
78
+ server = null;
79
+ });
80
+
81
+ describe('selectTenant', () => {
82
+ it('picks the tenant by host name, however the header spells it', () => {
83
+ expect(selectTenant({ hostname: 'Acme.Test', url: '/api/x', peer: '10.0.0.5' })).toEqual({ kind: 'host', host: 'acme.test' });
84
+ });
85
+
86
+ it('honours the loopback prefix only from a loopback peer, and strips it from the path', () => {
87
+ expect(selectTenant({ hostname: 'localhost', url: '/_tenant/acme/api/x?y=1', peer: '127.0.0.1' })).toEqual({
88
+ kind: 'slug',
89
+ slug: 'acme',
90
+ rest: '/api/x?y=1',
91
+ });
92
+ expect(selectTenant({ hostname: 'localhost', url: '/_tenant/acme', peer: '::ffff:127.0.0.1' })).toEqual({
93
+ kind: 'slug',
94
+ slug: 'acme',
95
+ rest: '/',
96
+ });
97
+ // From outside, the prefix is just a path nobody serves.
98
+ expect(selectTenant({ hostname: 'acme.test', url: '/_tenant/globex/api/x', peer: '10.0.0.5' })).toEqual({
99
+ kind: 'host',
100
+ host: 'acme.test',
101
+ });
102
+ // From a loopback peer that is a reverse proxy on the same machine — it
103
+ // forwarded someone, and says so — the prefix is not honoured either.
104
+ expect(
105
+ selectTenant({ hostname: 'acme.test', url: '/_tenant/globex/api/x', peer: '127.0.0.1', forwardedFor: '203.0.113.9' }),
106
+ ).toEqual({ kind: 'host', host: 'acme.test' });
107
+ expect(
108
+ selectTenant({ hostname: 'acme.test', url: '/_tenant/globex/api/x', peer: '127.0.0.1', forwardedFor: ['203.0.113.9'] }),
109
+ ).toEqual({ kind: 'host', host: 'acme.test' });
110
+ // An empty header is no header.
111
+ expect(selectTenant({ hostname: 'localhost', url: '/_tenant/acme/api/x', peer: '127.0.0.1', forwardedFor: '' })).toEqual({
112
+ kind: 'slug',
113
+ slug: 'acme',
114
+ rest: '/api/x',
115
+ });
116
+ // A prefix that names nothing a slug could be is not a selector either.
117
+ expect(selectTenant({ hostname: 'localhost', url: '/_tenant/../etc', peer: '127.0.0.1' })).toEqual({
118
+ kind: 'host',
119
+ host: 'localhost',
120
+ });
121
+ });
122
+
123
+ it('knows which peers are this machine', () => {
124
+ expect(isLoopbackAddress('127.0.0.1')).toBe(true);
125
+ expect(isLoopbackAddress('127.9.9.9')).toBe(true);
126
+ expect(isLoopbackAddress('::1')).toBe(true);
127
+ expect(isLoopbackAddress('::ffff:127.0.0.1')).toBe(true);
128
+ expect(isLoopbackAddress('10.0.0.5')).toBe(false);
129
+ expect(isLoopbackAddress('::ffff:10.0.0.5')).toBe(false);
130
+ expect(isLoopbackAddress(undefined)).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('createTenantHost', () => {
135
+ it('hands each host name to its own tenant, and activates each tenant once', async () => {
136
+ const activations: string[] = [];
137
+ const port = await listen(
138
+ createTenantHost({
139
+ source,
140
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
141
+ runtime: {
142
+ activate: async (d) => {
143
+ activations.push(d.slug);
144
+ return { core: {}, app: tenantApp(d.slug) } as unknown as TenantGraph;
145
+ },
146
+ stop: async () => undefined,
147
+ },
148
+ }),
149
+ );
150
+ const a = await request(port, '/api/whoami', 'acme.test');
151
+ const b = await request(port, '/api/whoami', 'globex.test:443');
152
+ const again = await request(port, '/api/whoami', 'ACME.TEST');
153
+ expect(JSON.parse(a.body)).toMatchObject({ tenant: 'acme' });
154
+ expect(JSON.parse(b.body)).toMatchObject({ tenant: 'globex' });
155
+ expect(JSON.parse(again.body)).toMatchObject({ tenant: 'acme' });
156
+ expect(activations).toEqual(['acme', 'globex']);
157
+ expect([...host!.runtimes().keys()]).toEqual(['acme', 'globex']);
158
+ });
159
+
160
+ it('answers 404 for a host no tenant claims, JSON under /api and a plain page elsewhere, building nothing', async () => {
161
+ let activations = 0;
162
+ const port = await listen(
163
+ createTenantHost({
164
+ source,
165
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
166
+ runtime: {
167
+ activate: async () => {
168
+ activations += 1;
169
+ return { core: {}, app: tenantApp('x') } as unknown as TenantGraph;
170
+ },
171
+ },
172
+ }),
173
+ );
174
+ const api = await request(port, '/api/whoami', 'nobody.test');
175
+ expect(api.status).toBe(404);
176
+ expect(JSON.parse(api.body)).toEqual({ error: 'No workspace at this address' });
177
+ const page = await request(port, '/some/page', 'nobody.test');
178
+ expect(page.status).toBe(404);
179
+ expect(page.headers['content-type']).toContain('text/plain');
180
+ expect(activations).toBe(0);
181
+ });
182
+
183
+ it('answers process health itself, whatever the host, and per-tenant paths through the tenant', async () => {
184
+ const port = await listen(
185
+ createTenantHost({
186
+ source,
187
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
188
+ runtime: { activate: async (d) => ({ core: {}, app: tenantApp(d.slug) }) as unknown as TenantGraph, stop: async () => undefined },
189
+ }),
190
+ );
191
+ const health = await request(port, '/api/health', 'nobody.test');
192
+ expect(health.status).toBe(200);
193
+ expect(JSON.parse(health.body)).toMatchObject({ status: 'ok', tenants: 0 });
194
+ });
195
+
196
+ it('reaches a tenant over loopback by the path prefix, with the prefix stripped', async () => {
197
+ const port = await listen(
198
+ createTenantHost({
199
+ source,
200
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
201
+ runtime: { activate: async (d) => ({ core: {}, app: tenantApp(d.slug) }) as unknown as TenantGraph, stop: async () => undefined },
202
+ }),
203
+ );
204
+ const res = await request(port, '/_tenant/globex/api/whoami?k=v', '127.0.0.1');
205
+ expect(res.status).toBe(200);
206
+ expect(JSON.parse(res.body)).toEqual({ tenant: 'globex', path: '/api/whoami', query: { k: 'v' } });
207
+ const unknown = await request(port, '/_tenant/nobody/api/whoami', '127.0.0.1');
208
+ expect(unknown.status).toBe(404);
209
+ // The same path through a same-machine reverse proxy: decided by host.
210
+ const proxied = await request(port, '/_tenant/globex/api/whoami', 'acme.test', { 'x-forwarded-for': '203.0.113.9' });
211
+ expect(JSON.parse(proxied.body)).toEqual({ tenant: 'acme', path: '/_tenant/globex/api/whoami', unhandled: true });
212
+ });
213
+
214
+ it('does not count a client that dropped while its tenant was being looked up', async () => {
215
+ // A registry-backed source: the lookup is a real round trip, and the
216
+ // client is gone before it answers.
217
+ const slowSource: TenantSource = {
218
+ resolveByHost: async (host) => {
219
+ await new Promise((resolve) => setTimeout(resolve, 150));
220
+ return source.resolveByHost(host);
221
+ },
222
+ describe: (slug) => source.describe(slug),
223
+ };
224
+ const port = await listen(
225
+ createTenantHost({
226
+ source: slowSource,
227
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
228
+ runtime: { activate: async (d) => ({ core: {}, app: tenantApp(d.slug) }) as unknown as TenantGraph, stop: async () => undefined },
229
+ }),
230
+ );
231
+ const socket = net.connect(port, '127.0.0.1');
232
+ await new Promise<void>((resolve) => socket.on('connect', resolve));
233
+ socket.write('GET /api/whoami HTTP/1.1\r\nHost: acme.test\r\n\r\n');
234
+ await new Promise((resolve) => setTimeout(resolve, 30));
235
+ socket.destroy();
236
+ await new Promise((resolve) => setTimeout(resolve, 200));
237
+ const runtime = host!.runtimes().get('acme');
238
+ expect(runtime?.open).toBe(0);
239
+ // ...and a client that stays is still counted, and left once it goes.
240
+ const served = await request(port, '/api/whoami', 'acme.test');
241
+ expect(served.status).toBe(200);
242
+ await new Promise((resolve) => setTimeout(resolve, 20));
243
+ expect(runtime?.open).toBe(0);
244
+ });
245
+
246
+ it('keeps a tenant with an open response alive through the sweep, and lets it go once that response closes', async () => {
247
+ const events: string[] = [];
248
+ const streams: import('express').Response[] = [];
249
+ let now = 0;
250
+ const port = await listen(
251
+ createTenantHost({
252
+ source,
253
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
254
+ idleMinutes: 1,
255
+ sweepIntervalMs: 10,
256
+ runtime: {
257
+ activate: async (d) => {
258
+ events.push(`activate:${d.slug}`);
259
+ const app = express();
260
+ // An event stream: headers sent, body held open until told.
261
+ app.get('/api/events', (_req, res) => {
262
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
263
+ res.write(': hello\n\n');
264
+ streams.push(res);
265
+ });
266
+ return { core: {}, app } as unknown as TenantGraph;
267
+ },
268
+ stop: async (g) => {
269
+ events.push('stop');
270
+ void g;
271
+ },
272
+ busy: async () => false,
273
+ now: () => now,
274
+ },
275
+ }),
276
+ );
277
+ const opened = new Promise<http.IncomingMessage>((resolve, reject) => {
278
+ const req = http.request({ host: '127.0.0.1', port, path: '/api/events', headers: { host: 'acme.test' } }, resolve);
279
+ req.on('error', reject);
280
+ req.end();
281
+ });
282
+ const stream = await opened;
283
+ expect(stream.statusCode).toBe(200);
284
+ expect(streams).toHaveLength(1);
285
+ expect(host!.runtimes().get('acme')?.open).toBe(1);
286
+ // An hour with no new request, and several sweeps: the stream keeps it.
287
+ now += 3_600_000;
288
+ await new Promise((resolve) => setTimeout(resolve, 60));
289
+ expect(host!.runtimes().get('acme')?.state).toBe('active');
290
+ expect(events).toEqual(['activate:acme']);
291
+ // The stream ends; idleness counts from here, so it is not evicted yet...
292
+ streams[0]!.end();
293
+ await new Promise<void>((resolve) => stream.on('end', resolve).resume());
294
+ await new Promise((resolve) => setTimeout(resolve, 60));
295
+ expect(host!.runtimes().get('acme')?.state).toBe('active');
296
+ // ...and a minute after it closed, it is.
297
+ now += 61_000;
298
+ await new Promise((resolve) => setTimeout(resolve, 60));
299
+ expect(host!.runtimes().get('acme')?.state).toBe('idle');
300
+ expect(events).toEqual(['activate:acme', 'stop']);
301
+ });
302
+
303
+ it('answers 503 with Retry-After while a tenant is still starting, and when its start failed', async () => {
304
+ let fail = true;
305
+ let release: (() => void) | null = null;
306
+ const port = await listen(
307
+ createTenantHost({
308
+ source,
309
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
310
+ activationWaitMs: 50,
311
+ runtime: {
312
+ // No retry window here: the second request must try again at once.
313
+ retry: { initialMs: 0 },
314
+ activate: async (d) => {
315
+ if (fail) throw new Error('no');
316
+ await new Promise<void>((resolve) => {
317
+ release = resolve;
318
+ });
319
+ return { core: {}, app: tenantApp(d.slug) } as unknown as TenantGraph;
320
+ },
321
+ stop: async () => undefined,
322
+ },
323
+ }),
324
+ );
325
+ const failed = await request(port, '/api/whoami', 'acme.test');
326
+ expect(failed.status).toBe(503);
327
+ expect(failed.headers['retry-after']).toBe('5');
328
+
329
+ fail = false;
330
+ const slow = await request(port, '/api/whoami', 'acme.test');
331
+ expect(slow.status).toBe(503);
332
+ // The activation keeps going behind the 503; once it lands, the next request is served.
333
+ release!();
334
+ const served = await request(port, '/api/whoami', 'acme.test');
335
+ expect(served.status).toBe(200);
336
+ });
337
+
338
+ it('evicts every tenant on stop, and an idle tenant on the sweep, then reactivates on demand', async () => {
339
+ const events: string[] = [];
340
+ let now = 0;
341
+ const port = await listen(
342
+ createTenantHost({
343
+ source,
344
+ process: { port: 0, nodeEnv: 'test', trustProxy: '' },
345
+ idleMinutes: 1,
346
+ sweepIntervalMs: 10,
347
+ runtime: {
348
+ activate: async (d) => {
349
+ events.push(`activate:${d.slug}`);
350
+ return { core: {}, app: tenantApp(d.slug) } as unknown as TenantGraph;
351
+ },
352
+ stop: async (g) => {
353
+ events.push(`stop:${(g.app as unknown as { slug?: string }).slug ?? ''}`);
354
+ },
355
+ busy: async () => false,
356
+ now: () => now,
357
+ },
358
+ }),
359
+ );
360
+ await request(port, '/api/whoami', 'acme.test');
361
+ expect(events).toEqual(['activate:acme']);
362
+ // A minute passes with no request: the sweep stops the graph.
363
+ now += 61_000;
364
+ await new Promise((resolve) => setTimeout(resolve, 60));
365
+ expect(host!.runtimes().get('acme')?.state).toBe('idle');
366
+ // ...and the next request brings it back.
367
+ await request(port, '/api/whoami', 'acme.test');
368
+ expect(events.filter((e) => e === 'activate:acme')).toHaveLength(2);
369
+ await host!.stop();
370
+ expect(host!.runtimes().get('acme')?.state).toBe('idle');
371
+ });
372
+ });
@@ -0,0 +1,221 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { TenantConfig } from '../../core-config.js';
3
+ import { TenantRuntime, type TenantGraph } from '../tenant-runtime.js';
4
+ import type { TenantDescriptor } from '../tenant-source.contract.js';
5
+
6
+ const descriptor: TenantDescriptor = { slug: 'acme', hosts: ['acme.test'], config: {} as TenantConfig };
7
+
8
+ /** A graph builder whose completion the test controls. */
9
+ function harness(opts: { fail?: boolean | (() => boolean); now?: () => number; retry?: { initialMs: number; maxMs: number } } = {}) {
10
+ const events: string[] = [];
11
+ let release: (() => void) | null = null;
12
+ let activations = 0;
13
+ const runtime = new TenantRuntime(descriptor, {
14
+ activate: async () => {
15
+ activations += 1;
16
+ events.push(`activate:${activations}`);
17
+ await new Promise<void>((resolve) => {
18
+ release = resolve;
19
+ });
20
+ if (typeof opts.fail === 'function' ? opts.fail() : opts.fail) throw new Error('the remote said no');
21
+ return { core: { id: activations } } as unknown as TenantGraph;
22
+ },
23
+ stop: async (graph) => {
24
+ events.push(`stop:${(graph.core as unknown as { id: number }).id}`);
25
+ },
26
+ busy: async () => false,
27
+ now: opts.now,
28
+ retry: opts.retry,
29
+ });
30
+ return { runtime, events, finish: () => release?.(), activations: () => activations };
31
+ }
32
+
33
+ const settle = () => new Promise((resolve) => setImmediate(resolve));
34
+
35
+ describe('TenantRuntime', () => {
36
+ it('builds the graph once for a burst of first requests, and hands every one the same graph', async () => {
37
+ const h = harness();
38
+ expect(h.runtime.state).toBe('idle');
39
+ const first = h.runtime.handle();
40
+ const second = h.runtime.handle();
41
+ const third = h.runtime.handle();
42
+ await settle();
43
+ expect(h.runtime.state).toBe('activating');
44
+ h.finish();
45
+ const graphs = await Promise.all([first, second, third]);
46
+ expect(h.activations()).toBe(1);
47
+ expect(graphs[1]).toBe(graphs[0]);
48
+ expect(graphs[2]).toBe(graphs[0]);
49
+ expect(h.runtime.state).toBe('active');
50
+ // Once active, a request is answered from the running graph.
51
+ expect(await h.runtime.handle()).toBe(graphs[0]);
52
+ expect(h.activations()).toBe(1);
53
+ });
54
+
55
+ it('remembers a failed activation for a growing window, and tries again once it has passed', async () => {
56
+ let now = 0;
57
+ const h = harness({ fail: true, now: () => now, retry: { initialMs: 100, maxMs: 300 } });
58
+ const first = h.runtime.handle();
59
+ await settle();
60
+ h.finish();
61
+ await expect(first).rejects.toThrow('the remote said no');
62
+ expect(h.runtime.state).toBe('idle');
63
+
64
+ // Inside the window: the same failure, from memory, without a build.
65
+ now = 99;
66
+ await expect(h.runtime.handle()).rejects.toThrow('the remote said no');
67
+ expect(h.activations()).toBe(1);
68
+
69
+ // Past it: tried again; a second failure doubles the window.
70
+ now = 100;
71
+ const second = h.runtime.handle();
72
+ await settle();
73
+ expect(h.activations()).toBe(2);
74
+ h.finish();
75
+ await expect(second).rejects.toThrow();
76
+ now = 299;
77
+ await expect(h.runtime.handle()).rejects.toThrow();
78
+ expect(h.activations()).toBe(2);
79
+
80
+ // The window is capped: 100, 200, then 300 and no more.
81
+ now = 300;
82
+ const third = h.runtime.handle();
83
+ await settle();
84
+ h.finish();
85
+ await expect(third).rejects.toThrow();
86
+ now = 599;
87
+ await expect(h.runtime.handle()).rejects.toThrow();
88
+ now = 600;
89
+ const fourth = h.runtime.handle();
90
+ await settle();
91
+ expect(h.activations()).toBe(4);
92
+ h.finish();
93
+ await expect(fourth).rejects.toThrow();
94
+ now = 899;
95
+ await expect(h.runtime.handle()).rejects.toThrow();
96
+ expect(h.activations()).toBe(4);
97
+ });
98
+
99
+ it('forgets the failures once an activation succeeds', async () => {
100
+ let now = 0;
101
+ let fail = true;
102
+ const h = harness({ fail: () => fail, now: () => now, retry: { initialMs: 100, maxMs: 300 } });
103
+ const first = h.runtime.handle();
104
+ await settle();
105
+ h.finish();
106
+ await expect(first).rejects.toThrow();
107
+ fail = false;
108
+ now = 100;
109
+ const ok = h.runtime.handle();
110
+ await settle();
111
+ h.finish();
112
+ await ok;
113
+ expect(h.runtime.state).toBe('active');
114
+ // A later failure, after an eviction, starts the window from the beginning.
115
+ await h.runtime.evict();
116
+ fail = true;
117
+ const again = h.runtime.handle();
118
+ await settle();
119
+ h.finish();
120
+ await expect(again).rejects.toThrow();
121
+ now = 199;
122
+ await expect(h.runtime.handle()).rejects.toThrow();
123
+ now = 200;
124
+ const retried = h.runtime.handle();
125
+ await settle();
126
+ expect(h.activations()).toBe(4);
127
+ h.finish();
128
+ await expect(retried).rejects.toThrow();
129
+ });
130
+
131
+ it('evicts a running graph, and builds a fresh one on the next request', async () => {
132
+ const h = harness();
133
+ const pending = h.runtime.handle();
134
+ await settle();
135
+ h.finish();
136
+ await pending;
137
+ await h.runtime.evict();
138
+ expect(h.events).toEqual(['activate:1', 'stop:1']);
139
+ expect(h.runtime.state).toBe('idle');
140
+ const next = h.runtime.handle();
141
+ await settle();
142
+ h.finish();
143
+ await next;
144
+ expect(h.events).toEqual(['activate:1', 'stop:1', 'activate:2']);
145
+ });
146
+
147
+ it('an eviction that lands during an activation waits for it and stops what it built', async () => {
148
+ const h = harness();
149
+ const pending = h.runtime.handle();
150
+ await settle();
151
+ const evicting = h.runtime.evict();
152
+ expect(h.runtime.state).toBe('evicting');
153
+ h.finish();
154
+ await pending;
155
+ await evicting;
156
+ expect(h.events).toEqual(['activate:1', 'stop:1']);
157
+ expect(h.runtime.state).toBe('idle');
158
+ });
159
+
160
+ it('a second eviction joins the one under way, and evicting an idle runtime stops nothing', async () => {
161
+ const h = harness();
162
+ await h.runtime.evict();
163
+ expect(h.events).toEqual([]);
164
+ const pending = h.runtime.handle();
165
+ await settle();
166
+ h.finish();
167
+ await pending;
168
+ const a = h.runtime.evict();
169
+ const b = h.runtime.evict();
170
+ await Promise.all([a, b]);
171
+ expect(h.events).toEqual(['activate:1', 'stop:1']);
172
+ });
173
+
174
+ it('measures idleness from the last request, and reports busy only for a running graph', async () => {
175
+ let now = 1_000;
176
+ let busy = false;
177
+ const runtime = new TenantRuntime(descriptor, {
178
+ activate: async () => ({ core: {} } as unknown as TenantGraph),
179
+ stop: async () => undefined,
180
+ busy: async () => busy,
181
+ now: () => now,
182
+ });
183
+ expect(await runtime.busy()).toBe(false);
184
+ await runtime.handle();
185
+ now += 5_000;
186
+ expect(runtime.idleFor()).toBe(5_000);
187
+ await runtime.handle();
188
+ expect(runtime.idleFor()).toBe(0);
189
+ busy = true;
190
+ expect(await runtime.busy()).toBe(true);
191
+ });
192
+
193
+ it('is busy while any response is open, and counts idleness from the last one that closed', async () => {
194
+ let now = 1_000;
195
+ const runtime = new TenantRuntime(descriptor, {
196
+ activate: async () => ({ core: {} } as unknown as TenantGraph),
197
+ stop: async () => undefined,
198
+ busy: async () => false,
199
+ now: () => now,
200
+ });
201
+ await runtime.handle();
202
+ // An event stream a browser holds: entered long ago, never left.
203
+ runtime.enter();
204
+ runtime.enter();
205
+ now += 3_600_000;
206
+ expect(runtime.idleFor()).toBe(3_600_000);
207
+ expect(await runtime.busy()).toBe(true);
208
+ expect(runtime.open).toBe(2);
209
+ runtime.leave();
210
+ expect(await runtime.busy()).toBe(true);
211
+ // The last response closing is what idleness counts from.
212
+ now += 10;
213
+ runtime.leave();
214
+ expect(runtime.open).toBe(0);
215
+ expect(runtime.idleFor()).toBe(0);
216
+ expect(await runtime.busy()).toBe(false);
217
+ // A stray leave cannot make the count negative.
218
+ runtime.leave();
219
+ expect(runtime.open).toBe(0);
220
+ });
221
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deriveTenantSecrets } from '../tenant-secrets.js';
3
+
4
+ const MASTER = 'a-master-key-that-is-long-enough-to-count-as-random';
5
+
6
+ describe('deriveTenantSecrets', () => {
7
+ it('is reproducible: the same master key and slug give the same secrets, on the day of an export too', () => {
8
+ expect(deriveTenantSecrets(MASTER, 'acme')).toEqual(deriveTenantSecrets(MASTER, 'acme'));
9
+ });
10
+
11
+ it('gives each tenant, and each of the three secrets, a value of its own', () => {
12
+ const acme = deriveTenantSecrets(MASTER, 'acme');
13
+ const globex = deriveTenantSecrets(MASTER, 'globex');
14
+ expect(acme.jwtSecret).not.toBe(globex.jwtSecret);
15
+ expect(acme.secretsEncKey).not.toBe(globex.secretsEncKey);
16
+ expect(acme.internalTokenSecret).not.toBe(globex.internalTokenSecret);
17
+ expect(new Set([acme.jwtSecret, acme.secretsEncKey, acme.internalTokenSecret]).size).toBe(3);
18
+ });
19
+
20
+ it('derives 32-byte keys, which is what the vault encryption requires', () => {
21
+ const { secretsEncKey } = deriveTenantSecrets(MASTER, 'acme');
22
+ expect(Buffer.from(secretsEncKey, 'base64')).toHaveLength(32);
23
+ });
24
+
25
+ it('changes every secret when the master key changes', () => {
26
+ const a = deriveTenantSecrets(MASTER, 'acme');
27
+ const b = deriveTenantSecrets(`${MASTER}-rotated`, 'acme');
28
+ expect(a.jwtSecret).not.toBe(b.jwtSecret);
29
+ });
30
+
31
+ it('refuses a master key too short to be random, and a missing slug', () => {
32
+ expect(() => deriveTenantSecrets('short', 'acme')).toThrow(/TENANT_MASTER_KEY/);
33
+ expect(() => deriveTenantSecrets(MASTER, '')).toThrow(/slug/);
34
+ });
35
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Serving several knowledge bases from one process — see
3
+ * `docs/multi-tenant.md` and `openspec/changes/multi-tenant-runtime`.
4
+ *
5
+ * The contract (`TenantSource`) and the mechanism (runtime, host) are core's;
6
+ * where tenants come from is the caller's. Core ships a file-based source.
7
+ */
8
+ export {
9
+ assertTenantSlug,
10
+ normalizeHost,
11
+ TENANT_SLUG_PATTERN,
12
+ type TenantDescriptor,
13
+ type TenantSource,
14
+ } from './tenant-source.contract.js';
15
+ export { deriveTenantSecrets, type TenantSecrets } from './tenant-secrets.js';
16
+ export {
17
+ StaticTenantSource,
18
+ tenantConfigFrom,
19
+ tenantHostEnv,
20
+ type TenantHostEnv,
21
+ type TenantHostSettings,
22
+ type TenantRecord,
23
+ type TenantsFile,
24
+ } from './static-tenant-source.js';
25
+ export { TenantRuntime, type TenantGraph, type TenantRuntimeDeps, type TenantState } from './tenant-runtime.js';
26
+ export {
27
+ createTenantHost,
28
+ isLoopbackAddress,
29
+ loopbackTenantBaseUrl,
30
+ selectTenant,
31
+ LOOPBACK_TENANT_PREFIX,
32
+ type TenantHost,
33
+ type TenantHostOptions,
34
+ type TenantSelector,
35
+ } from './tenant-host.js';