@bevel-software/platform-core-backend 0.4.0 → 0.5.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 (305) hide show
  1. package/dist/core/core-ports.d.ts +15 -0
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +40 -6
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +17 -1
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +153 -29
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/core-config.d.ts +70 -21
  12. package/dist/core-config.d.ts.map +1 -1
  13. package/dist/core-config.js +134 -58
  14. package/dist/core-config.js.map +1 -1
  15. package/dist/modules/access/access-control.interface.d.ts +6 -0
  16. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.d.ts +74 -5
  18. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  19. package/dist/modules/access/access-control.service.js +116 -15
  20. package/dist/modules/access/access-control.service.js.map +1 -1
  21. package/dist/modules/access/access-declarations.d.ts +93 -0
  22. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  23. package/dist/modules/access/access-declarations.js +168 -0
  24. package/dist/modules/access/access-declarations.js.map +1 -0
  25. package/dist/modules/access/access-mutation.service.js +4 -4
  26. package/dist/modules/access/access-mutation.service.js.map +1 -1
  27. package/dist/modules/access/access-splice.d.ts +16 -1
  28. package/dist/modules/access/access-splice.d.ts.map +1 -1
  29. package/dist/modules/access/access-splice.js +40 -21
  30. package/dist/modules/access/access-splice.js.map +1 -1
  31. package/dist/modules/access/access.routes.d.ts.map +1 -1
  32. package/dist/modules/access/access.routes.js +51 -0
  33. package/dist/modules/access/access.routes.js.map +1 -1
  34. package/dist/modules/access/creator-access.d.ts +8 -0
  35. package/dist/modules/access/creator-access.d.ts.map +1 -1
  36. package/dist/modules/access/creator-access.js +21 -2
  37. package/dist/modules/access/creator-access.js.map +1 -1
  38. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  40. package/dist/modules/admin/admin-access.service.js +3 -2
  41. package/dist/modules/admin/admin-access.service.js.map +1 -1
  42. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.routes.js +50 -1
  44. package/dist/modules/auth/auth.routes.js.map +1 -1
  45. package/dist/modules/auth/auth.service.d.ts +39 -10
  46. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  47. package/dist/modules/auth/auth.service.js +38 -16
  48. package/dist/modules/auth/auth.service.js.map +1 -1
  49. package/dist/modules/database/core-schema.d.ts +124 -0
  50. package/dist/modules/database/core-schema.d.ts.map +1 -1
  51. package/dist/modules/database/core-schema.js +45 -0
  52. package/dist/modules/database/core-schema.js.map +1 -1
  53. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  54. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  55. package/dist/modules/groups/group-provision.service.js +288 -0
  56. package/dist/modules/groups/group-provision.service.js.map +1 -0
  57. package/dist/modules/groups/groups.contract.d.ts +106 -0
  58. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  59. package/dist/modules/groups/groups.contract.js +36 -0
  60. package/dist/modules/groups/groups.contract.js.map +1 -0
  61. package/dist/modules/groups/groups.routes.d.ts +42 -0
  62. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  63. package/dist/modules/groups/groups.routes.js +379 -0
  64. package/dist/modules/groups/groups.routes.js.map +1 -0
  65. package/dist/modules/groups/groups.service.d.ts +60 -0
  66. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  67. package/dist/modules/groups/groups.service.js +172 -0
  68. package/dist/modules/groups/groups.service.js.map +1 -0
  69. package/dist/modules/groups/index.d.ts +7 -0
  70. package/dist/modules/groups/index.d.ts.map +1 -0
  71. package/dist/modules/groups/index.js +6 -0
  72. package/dist/modules/groups/index.js.map +1 -0
  73. package/dist/modules/groups/join-proposals.d.ts +53 -0
  74. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  75. package/dist/modules/groups/join-proposals.js +67 -0
  76. package/dist/modules/groups/join-proposals.js.map +1 -0
  77. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  78. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  79. package/dist/modules/groups/join-requests.service.js +135 -0
  80. package/dist/modules/groups/join-requests.service.js.map +1 -0
  81. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  83. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  85. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  86. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  87. package/dist/modules/settings/deployment-settings.service.js +368 -0
  88. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  89. package/dist/modules/settings/setup.routes.d.ts +49 -0
  90. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  91. package/dist/modules/settings/setup.routes.js +292 -0
  92. package/dist/modules/settings/setup.routes.js.map +1 -0
  93. package/dist/modules/skills/index.d.ts +2 -1
  94. package/dist/modules/skills/index.d.ts.map +1 -1
  95. package/dist/modules/skills/index.js +1 -0
  96. package/dist/modules/skills/index.js.map +1 -1
  97. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  98. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  99. package/dist/modules/skills/pending-skills.service.js +147 -0
  100. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  101. package/dist/modules/skills/skills.contract.d.ts +32 -3
  102. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  103. package/dist/modules/skills/skills.contract.js +1 -1
  104. package/dist/modules/skills/skills.routes.d.ts +3 -2
  105. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  106. package/dist/modules/skills/skills.routes.js +18 -1
  107. package/dist/modules/skills/skills.routes.js.map +1 -1
  108. package/dist/modules/skills/skills.service.d.ts +22 -0
  109. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  110. package/dist/modules/skills/skills.service.js +24 -12
  111. package/dist/modules/skills/skills.service.js.map +1 -1
  112. package/dist/modules/tool-manuals/index.d.ts +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  114. package/dist/modules/tool-manuals/index.js.map +1 -1
  115. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  117. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  120. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  122. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  125. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  126. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  129. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  132. package/dist/modules/workflow/event-bus.d.ts +11 -0
  133. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  134. package/dist/modules/workflow/event-bus.js +22 -0
  135. package/dist/modules/workflow/event-bus.js.map +1 -1
  136. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  137. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  138. package/dist/modules/workflow/file-lock.service.js +16 -0
  139. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  140. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  142. package/dist/modules/workflow/git/branch-name.js +1 -1
  143. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  144. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  145. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  146. package/dist/modules/workflow/git/git.service.js +157 -54
  147. package/dist/modules/workflow/git/git.service.js.map +1 -1
  148. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  149. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  150. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  151. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  152. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  155. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.js +95 -14
  157. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  158. package/dist/modules/workflow/workflow.service.d.ts +97 -5
  159. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  160. package/dist/modules/workflow/workflow.service.js +393 -114
  161. package/dist/modules/workflow/workflow.service.js.map +1 -1
  162. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  163. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  164. package/dist/modules/workspace/bevel-ignore.js +5 -1
  165. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  166. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  167. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  168. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  169. package/dist/modules/workspace/kb-seed.service.js +228 -51
  170. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  171. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  173. package/dist/modules/workspace/workspace.routes.js +51 -18
  174. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  175. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  176. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  177. package/dist/modules/workspace/workspace.service.js +36 -9
  178. package/dist/modules/workspace/workspace.service.js.map +1 -1
  179. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.js +8 -3
  181. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  182. package/dist/shared/kb-layout.test.js +2 -2
  183. package/dist/test-setup.d.ts +2 -0
  184. package/dist/test-setup.d.ts.map +1 -0
  185. package/dist/test-setup.js +18 -0
  186. package/dist/test-setup.js.map +1 -0
  187. package/kb-template/.bevelignore +6 -2
  188. package/kb-template/AGENTS.md +272 -0
  189. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  190. package/kb-template/access.md +36 -36
  191. package/migrations/0002_user_onboarding_done.sql +1 -0
  192. package/migrations/0003_deployment_settings.sql +9 -0
  193. package/migrations/meta/0001_snapshot.json +1486 -0
  194. package/migrations/meta/0002_snapshot.json +1493 -0
  195. package/migrations/meta/0003_snapshot.json +1552 -0
  196. package/migrations/meta/_journal.json +15 -1
  197. package/package.json +3 -3
  198. package/src/__tests__/branch-model.test.ts +79 -0
  199. package/src/__tests__/core-config.admin.test.ts +114 -0
  200. package/src/__tests__/database-url.test.ts +70 -0
  201. package/src/core/core-ports.ts +15 -0
  202. package/src/core/create-core-server.ts +55 -6
  203. package/src/core/create-core-services.ts +201 -36
  204. package/src/core-config.ts +139 -59
  205. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  206. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  207. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  208. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  209. package/src/modules/access/access-control.interface.ts +11 -0
  210. package/src/modules/access/access-control.service.ts +113 -15
  211. package/src/modules/access/access-declarations.ts +251 -0
  212. package/src/modules/access/access-mutation.service.ts +4 -4
  213. package/src/modules/access/access-splice.ts +64 -21
  214. package/src/modules/access/access.routes.ts +61 -0
  215. package/src/modules/access/creator-access.ts +22 -2
  216. package/src/modules/admin/admin-access.service.ts +4 -3
  217. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  218. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  219. package/src/modules/auth/auth.routes.ts +50 -1
  220. package/src/modules/auth/auth.service.ts +55 -17
  221. package/src/modules/database/core-schema.ts +46 -0
  222. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  223. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  224. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  225. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  226. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  227. package/src/modules/groups/group-provision.service.ts +328 -0
  228. package/src/modules/groups/groups.contract.ts +107 -0
  229. package/src/modules/groups/groups.routes.ts +433 -0
  230. package/src/modules/groups/groups.service.ts +193 -0
  231. package/src/modules/groups/index.ts +17 -0
  232. package/src/modules/groups/join-proposals.ts +114 -0
  233. package/src/modules/groups/join-requests.service.ts +185 -0
  234. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  235. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  236. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  237. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  238. package/src/modules/settings/deployment-settings.service.ts +419 -0
  239. package/src/modules/settings/setup.routes.ts +317 -0
  240. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  241. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  242. package/src/modules/skills/index.ts +3 -0
  243. package/src/modules/skills/pending-skills.service.ts +168 -0
  244. package/src/modules/skills/skills.contract.ts +34 -3
  245. package/src/modules/skills/skills.routes.ts +23 -2
  246. package/src/modules/skills/skills.service.ts +24 -16
  247. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  251. package/src/modules/tool-manuals/index.ts +2 -0
  252. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  253. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  254. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  255. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  256. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  257. package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +82 -72
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +295 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +311 -306
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +172 -64
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +365 -19
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -6,6 +6,35 @@ import { defaultKbTemplateDir } from './assets.js';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
8
8
 
9
+ /**
10
+ * The Postgres connection string: `DATABASE_URL` if given, otherwise built
11
+ * from the `POSTGRES_*` parts.
12
+ *
13
+ * The composition lives HERE rather than in docker-compose because expressing
14
+ * it there needed a nested default — `${DATABASE_URL:-postgresql://${POSTGRES_USER:-…}…}`
15
+ * — and deployment UIs that scan compose with a regex rather than a YAML
16
+ * parser truncate that at the first closing brace, offering the operator a
17
+ * variable whose value is the fragment `postgresql://${POSTGRES_USER:-bevel`.
18
+ * A malformed connection string presented as configuration is worse than no
19
+ * configuration at all.
20
+ *
21
+ * It is also the better home for it: the parts are URL-ENCODED, which shell
22
+ * interpolation cannot do — a password containing `@`, `/` or `:` silently
23
+ * produced an unparseable URL, and "check your password" is not the error
24
+ * anyone got.
25
+ */
26
+ export function resolveDatabaseUrl(env: NodeJS.ProcessEnv = process.env): string {
27
+ const explicit = (env.DATABASE_URL || '').trim();
28
+ if (explicit) return explicit;
29
+ const user = encodeURIComponent(env.POSTGRES_USER || 'bevel');
30
+ const password = encodeURIComponent(env.POSTGRES_PASSWORD || 'bevel');
31
+ // `localhost` suits a local `pnpm dev`; compose passes the service name.
32
+ const host = (env.POSTGRES_HOST || 'localhost').trim();
33
+ const port = (env.POSTGRES_PORT || '5432').trim();
34
+ const database = encodeURIComponent(env.POSTGRES_DB || 'bevel');
35
+ return `postgresql://${user}:${password}@${host}:${port}/${database}`;
36
+ }
37
+
9
38
  /**
10
39
  * Configuration for the CORE platform: the git-backed workspace/workflow,
11
40
  * skills, tools, secrets vault, access control, and the MCP surface. Contains
@@ -44,16 +73,31 @@ export class CoreConfig {
44
73
  readonly spillRoot: string;
45
74
  readonly jwtSecret: string;
46
75
  /**
47
- * Bootstrap admin credential, checked directly against the environment at
48
- * login time (never stored). Lets a fresh deployment sign in before any
49
- * account exists; that admin (or any user with the Admin role in
50
- * `roles.yaml`) then creates per-user accounts from Roles & Members.
51
- * Remove EITHER variable and the credential stops working immediately —
52
- * accounts created in the database are unaffected. The email is also
53
- * always recognized as an admin (see AdminAccessService), so a fresh KB
54
- * whose `roles.yaml` doesn't list it yet is still administrable.
76
+ * The deployment's owner. REQUIRED — three separate jobs rest on it, and a
77
+ * deployment without one has no way to be administered:
78
+ *
79
+ * - always recognized as an admin (see AdminAccessService), whatever the
80
+ * sign-in method, so a KB whose `roles.yaml` does not list them yet is
81
+ * still administrable;
82
+ * - written as the sole Admin into the `roles.yaml` generated when an EMPTY
83
+ * KB remote is seeded (this replaced `SEED_ADMIN_EMAILS`, which asked for
84
+ * the same answer a second time);
85
+ * - half of the bootstrap password credential below.
55
86
  */
56
87
  readonly adminEmail: string;
88
+ /**
89
+ * Password half of the bootstrap credential, checked directly against the
90
+ * environment at login time (never stored) so a fresh deployment can sign in
91
+ * before any account exists. Captured ONCE here, so changing or unsetting it
92
+ * takes effect on the next restart rather than at once — accounts in the
93
+ * database are unaffected either way.
94
+ *
95
+ * Required only when password login is ENABLED. An SSO-only deployment
96
+ * (`LOGIN_PASSWORD=false`) would otherwise have to mint a shared password
97
+ * the server is configured to reject — an unused credential someone still
98
+ * has to store and rotate. The admin identity above is required either way,
99
+ * so "there is always an identifiable admin" holds regardless.
100
+ */
57
101
  readonly adminPassword: string;
58
102
  /**
59
103
  * Generic OIDC single sign-on (any spec-compliant provider: Entra, Okta,
@@ -89,13 +133,6 @@ export class CoreConfig {
89
133
  * existing one. Override with `KB_TEMPLATE_DIR`.
90
134
  */
91
135
  readonly kbTemplateDir: string;
92
- /**
93
- * Admin emails written into the generated `roles.yaml` when a fresh KB repo is
94
- * seeded. Comma/whitespace-separated. A freshly-seeded repo with no Admin is
95
- * unusable (access resolution requires ≥1 Admin), so seeding an EMPTY remote
96
- * fails fast when this is empty. Not needed once the remote is already seeded.
97
- */
98
- readonly seedAdminEmails: string[];
99
136
  /**
100
137
  * Ontology-session boundary kill-switch. When true (default), an agent run
101
138
  * that has read across more than one ontology can no longer write. A no-op on
@@ -115,10 +152,24 @@ export class CoreConfig {
115
152
  */
116
153
  readonly loginPasswordEnabled: boolean;
117
154
  /**
118
- * Optional allow-list of email domains. When non-empty, ONLY emails whose
119
- * domain matches (exactly, or as a subdomain) may log in via ANY method
120
- * (shared-password or SSO). Comma/whitespace-separated; empty (default)
121
- * allows any email. A leading `@` or `.` on an entry is tolerated, so
155
+ * Optional allow-list of email domains for SSO — part of the OIDC
156
+ * configuration, and only meaningful alongside it.
157
+ *
158
+ * SSO AUTO-PROVISIONS: `loginWithSso` upserts the account on first sign-in,
159
+ * with no admin approval step. Against a single-tenant issuer the issuer is
160
+ * already the boundary and this is belt-and-braces; against a multi-tenant
161
+ * one (Google, the Entra `common` endpoint, Auth0 with social connections)
162
+ * it is the ONLY boundary — without it, anyone the issuer will authenticate
163
+ * provisions themselves into the deployment.
164
+ *
165
+ * Deliberately NOT applied to the other two entry points. An account an
166
+ * admin created is vetted by the act of creating it, and password login can
167
+ * only reach an account that already exists — so a check there would gate
168
+ * nothing while being able to lock out a bootstrap admin whose own address
169
+ * sits outside the list.
170
+ *
171
+ * Comma/whitespace-separated; empty (default) allows any email. Matches the
172
+ * domain exactly or as a subdomain, and a leading `@` or `.` is tolerated:
122
173
  * `ALLOWED_EMAIL_DOMAINS=bevel.software, example.com` works as expected.
123
174
  */
124
175
  readonly allowedEmailDomains: string[];
@@ -127,8 +178,12 @@ export class CoreConfig {
127
178
  * secrets: the secrets-vault values and the MCP OAuth client secrets/tokens.
128
179
  * Read from `SECRETS_ENC_KEY`, falling back to the legacy
129
180
  * `CONNECTOR_CONFIG_ENC_KEY` → `SHAREPOINT_TOKEN_ENC_KEY` chain so existing
130
- * deploys keep decrypting without re-keying. Empty = secret values can't be
131
- * saved (a clear error surfaces only when a secret is actually written/read).
181
+ * deploys keep decrypting without re-keying.
182
+ *
183
+ * REQUIRED. It used to be optional, with the failure surfacing only when
184
+ * somebody wrote a secret — which now includes the git token typed into the
185
+ * setup screen, so an unset key turns the first-run flow into a dead end at
186
+ * the last step. Refusing to boot names the variable instead.
132
187
  */
133
188
  readonly secretsEncKey: string;
134
189
  /**
@@ -160,7 +215,7 @@ export class CoreConfig {
160
215
 
161
216
  constructor() {
162
217
  this.port = parseInt(process.env.PORT || '3001', 10);
163
- this.databaseUrl = process.env.DATABASE_URL || 'postgresql://bevel:bevel@localhost:5432/bevel';
218
+ this.databaseUrl = resolveDatabaseUrl();
164
219
  this.nodeEnv = process.env.NODE_ENV || 'development';
165
220
  this.tenantId = (process.env.TENANT_ID || 'bevel').trim().toLowerCase();
166
221
  if (!/^[a-z0-9]+$/.test(this.tenantId)) {
@@ -171,12 +226,42 @@ export class CoreConfig {
171
226
  this.workspacesRoot = process.env.WORKSPACES_ROOT || path.resolve(process.cwd(), 'workspaces');
172
227
  this.backupsRoot = process.env.BACKUPS_ROOT || path.resolve(this.workspacesRoot, '..', 'backups');
173
228
  this.spillRoot = process.env.SPILL_ROOT || path.resolve(this.workspacesRoot, '..', 'tool-chain-spills');
174
- this.jwtSecret = process.env.JWT_SECRET || '';
229
+ // Required, and checked BEFORE the auth routes it signs for are ever
230
+ // mounted. Empty, the first login attempt fails deep inside the JWT library
231
+ // with a message about a missing key — a boot error naming the variable is
232
+ // the same information, hours earlier.
233
+ this.jwtSecret = (process.env.JWT_SECRET || '').trim();
234
+ if (!this.jwtSecret) {
235
+ throw new Error(
236
+ 'JWT_SECRET is required — it signs login sessions. Generate one with: ' +
237
+ `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`,
238
+ );
239
+ }
240
+ // Parsed here rather than beside the other toggles below: whether the
241
+ // bootstrap PASSWORD is required depends on it.
242
+ this.loginPasswordEnabled =
243
+ (process.env.LOGIN_PASSWORD ?? 'true').trim().toLowerCase() !== 'false';
175
244
  this.adminEmail = (process.env.ADMIN_EMAIL || '').trim().toLowerCase();
176
245
  this.adminPassword = process.env.ADMIN_PASSWORD || '';
177
- if (this.adminEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.adminEmail)) {
246
+ if (!this.adminEmail) {
247
+ throw new Error(
248
+ 'ADMIN_EMAIL is required — the address that owns this deployment. It is ' +
249
+ 'always treated as an admin (whatever the sign-in method) and is written ' +
250
+ 'as the initial Admin when an empty knowledge-base repo is seeded.',
251
+ );
252
+ }
253
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.adminEmail)) {
178
254
  throw new Error(`ADMIN_EMAIL is not a valid email: "${this.adminEmail}"`);
179
255
  }
256
+ // Only when password login can actually be used. An SSO-only deployment
257
+ // has no use for this value and should not have to hold one.
258
+ if (this.loginPasswordEnabled && !this.adminPassword) {
259
+ throw new Error(
260
+ 'ADMIN_PASSWORD is required while password login is enabled — it is the ' +
261
+ 'credential that signs in before any account exists. Set it, or set ' +
262
+ 'LOGIN_PASSWORD=false if this deployment signs in through SSO only.',
263
+ );
264
+ }
180
265
  this.oidcIssuerUrl = (process.env.OIDC_ISSUER_URL || '').trim().replace(/\/+$/, '');
181
266
  this.oidcClientId = (process.env.OIDC_CLIENT_ID || '').trim();
182
267
  this.oidcClientSecret = (process.env.OIDC_CLIENT_SECRET || '').trim();
@@ -190,15 +275,14 @@ export class CoreConfig {
190
275
  throw new Error(`OIDC_ISSUER_URL is not a valid URL: "${this.oidcIssuerUrl}"`);
191
276
  }
192
277
  }
193
- // No default: the KB repo is deployment-specific, and a wrong fallback
194
- // would surface much later as a confusing clone failure. Fail at boot.
278
+ // No default, and no longer fatal when absent: an unconfigured deployment
279
+ // boots into the setup screen, where an admin supplies it (and where the
280
+ // value can be verified against the real remote before it is saved). What
281
+ // stays fatal is a value that is present and WRONG — see
282
+ // `validateKbIdentity` — because that surfaces later as a confusing clone
283
+ // failure instead of a clear one now.
195
284
  this.kbRepoUrl = (process.env.KB_REPO_URL || '').trim();
196
- if (this.kbRepoUrl === '') {
197
- throw new Error(
198
- 'KB_REPO_URL is required — the https clone/push URL of the git repository that stores your knowledge base.',
199
- );
200
- }
201
- this.kbDirName = process.env.KB_DIR_NAME || 'knowledge-base';
285
+ this.kbDirName = (process.env.KB_DIR_NAME || '').trim();
202
286
  // Provider-neutral git token: operators can set GIT_TOKEN (or the legacy
203
287
  // GITHUB_TOKEN / GH_TOKEN). Normalize onto GITHUB_TOKEN — the name the
204
288
  // credential helper and every `$GITHUB_TOKEN` read + redaction use — so all
@@ -218,14 +302,8 @@ export class CoreConfig {
218
302
  // Default: the `kb-template/` folder shipped inside this package (works
219
303
  // both from src/ and compiled dist/ — see assets.ts).
220
304
  this.kbTemplateDir = process.env.KB_TEMPLATE_DIR || defaultKbTemplateDir();
221
- this.seedAdminEmails = (process.env.SEED_ADMIN_EMAILS || '')
222
- .split(/[\s,]+/)
223
- .map((e) => e.trim().toLowerCase())
224
- .filter((e) => e.length > 0);
225
305
  this.ontologySessionBlock =
226
306
  (process.env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false';
227
- this.loginPasswordEnabled =
228
- (process.env.LOGIN_PASSWORD ?? 'true').trim().toLowerCase() !== 'false';
229
307
  this.allowedEmailDomains = (process.env.ALLOWED_EMAIL_DOMAINS || '')
230
308
  .split(/[\s,]+/)
231
309
  .map((d) => d.trim().toLowerCase().replace(/^[@.]+/, ''))
@@ -236,6 +314,13 @@ export class CoreConfig {
236
314
  process.env.SHAREPOINT_TOKEN_ENC_KEY ||
237
315
  ''
238
316
  ).trim();
317
+ if (!this.secretsEncKey) {
318
+ throw new Error(
319
+ 'SECRETS_ENC_KEY is required — it encrypts the secrets vault, the MCP OAuth ' +
320
+ 'tokens and the git credential saved from the setup screen. Generate one ' +
321
+ `with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`,
322
+ );
323
+ }
239
324
  this.internalTokenSecret = (process.env.INTERNAL_TOKEN_SECRET || '').trim();
240
325
  this.trustProxy = (process.env.TRUST_PROXY || '').trim();
241
326
  this.publicBackendUrl = (process.env.PUBLIC_BACKEND_URL || `http://localhost:${this.port}`)
@@ -258,14 +343,6 @@ export class CoreConfig {
258
343
  }
259
344
  }
260
345
 
261
- // Validate format now so a typo surfaces at boot, not at first-seed time.
262
- // Emptiness is NOT an error here: an already-seeded remote never needs it.
263
- for (const email of this.seedAdminEmails) {
264
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
265
- throw new Error(`SEED_ADMIN_EMAILS contains an invalid email: "${email}"`);
266
- }
267
- }
268
-
269
346
  this.validateKbIdentity();
270
347
  }
271
348
 
@@ -289,25 +366,28 @@ export class CoreConfig {
289
366
  return `${this.tenantId}-mcp_`;
290
367
  }
291
368
 
369
+ /**
370
+ * Validate the KB-identity inputs THAT WERE SUPPLIED. Absent is fine — the
371
+ * setup screen collects them — but a present-and-malformed value fails at
372
+ * boot rather than later, as a clone error nobody can read.
373
+ */
292
374
  private validateKbIdentity(): void {
293
- // Validate the two KB-identity inputs up front: `kbRepoUrl` (the clone/push
294
- // URL) must be a valid https URL, and `kbDirName` (the dir the clone lives
295
- // under in each workspace) must be a single safe path segment. A bad value
296
- // in either otherwise surfaces much later as a confusing clone/path failure.
297
- let parsed: URL;
298
- try {
299
- parsed = new URL(this.kbRepoUrl);
300
- } catch {
301
- throw new Error(`KB_REPO_URL is not a valid URL: ${this.kbRepoUrl}`);
302
- }
303
- if (parsed.protocol !== 'https:') {
304
- throw new Error(`KB_REPO_URL must use https://: ${this.kbRepoUrl}`);
375
+ if (this.kbRepoUrl) {
376
+ let parsed: URL;
377
+ try {
378
+ parsed = new URL(this.kbRepoUrl);
379
+ } catch {
380
+ throw new Error(`KB_REPO_URL is not a valid URL: ${this.kbRepoUrl}`);
381
+ }
382
+ if (parsed.protocol !== 'https:') {
383
+ throw new Error(`KB_REPO_URL must use https://: ${this.kbRepoUrl}`);
384
+ }
305
385
  }
306
386
 
307
387
  // Joined with workspace paths via path.join — separators or ".." would let it
308
388
  // escape the workspace dir.
389
+ if (!this.kbDirName) return;
309
390
  if (
310
- !this.kbDirName ||
311
391
  this.kbDirName === '.' ||
312
392
  this.kbDirName === '..' ||
313
393
  this.kbDirName.includes('/') ||
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { FileTreeEntry, IWorkspaceService } from '@bevel-software/platform-shared';
3
+ import {
4
+ listAccessDeclarationsUnder,
5
+ AccessDeclarationsError,
6
+ DECLARATION_SCAN_FILE_CAP,
7
+ } from '../access-declarations.js';
8
+
9
+ /**
10
+ * The declaration scan: WHICH files under a folder declare access rules, and
11
+ * what those rules literally say. Everything here is about honesty of report —
12
+ * the folder's own summary must not be echoed back as an override, a broken
13
+ * rule file must be visible, and a typo'd node must not take the page down.
14
+ */
15
+
16
+ const WS = 'main';
17
+ const KB = 'knowledge-base';
18
+
19
+ /** Build the nested tree `listFiles` returns from a flat ws-path → text map. */
20
+ function buildTree(files: Record<string, string>): FileTreeEntry {
21
+ const root: FileTreeEntry = { name: '', relativePath: '', type: 'directory', children: [] };
22
+ for (const wsPath of Object.keys(files)) {
23
+ const segments = wsPath.split('/');
24
+ let node = root;
25
+ segments.forEach((segment, i) => {
26
+ const isLeaf = i === segments.length - 1;
27
+ const relativePath = segments.slice(0, i + 1).join('/');
28
+ let child = node.children?.find((c) => c.name === segment);
29
+ if (!child) {
30
+ child = {
31
+ name: segment,
32
+ relativePath,
33
+ type: isLeaf ? 'file' : 'directory',
34
+ ...(isLeaf ? {} : { children: [] }),
35
+ };
36
+ node.children?.push(child);
37
+ }
38
+ node = child;
39
+ });
40
+ }
41
+ return root;
42
+ }
43
+
44
+ function makeWorkspace(files: Record<string, string>): IWorkspaceService {
45
+ return {
46
+ listFiles: async () => buildTree(files),
47
+ readFile: async (_id: string, relativePath: string) => {
48
+ const text = files[relativePath];
49
+ if (text === undefined) throw new Error(`ENOENT ${relativePath}`);
50
+ return text;
51
+ },
52
+ } as unknown as IWorkspaceService;
53
+ }
54
+
55
+ const scan = (files: Record<string, string>, folder = 'Groups/GTM') =>
56
+ listAccessDeclarationsUnder(makeWorkspace(files), WS, KB, folder);
57
+
58
+ const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
59
+
60
+ describe('listAccessDeclarationsUnder', () => {
61
+ it('reports a descendant access.md against the directory it governs', async () => {
62
+ const { overrides, truncated } = await scan({
63
+ [`${KB}/Groups/GTM/battlecards/access.md`]: fm('write:\n - GTM Team'),
64
+ });
65
+
66
+ expect(truncated).toBe(false);
67
+ expect(overrides).toEqual([
68
+ {
69
+ path: 'Groups/GTM/battlecards/access.md',
70
+ governs: 'Groups/GTM/battlecards',
71
+ source: 'access-md',
72
+ entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'GTM Team' } }],
73
+ },
74
+ ]);
75
+ });
76
+
77
+ it("excludes the target folder's own access.md — that is the summary, not an override", async () => {
78
+ const { overrides } = await scan({
79
+ [`${KB}/Groups/GTM/access.md`]: fm('read:\n - GTM Team'),
80
+ [`${KB}/Groups/GTM/battlecards/access.md`]: fm('read:\n - Developer'),
81
+ });
82
+
83
+ expect(overrides.map((o) => o.path)).toEqual(['Groups/GTM/battlecards/access.md']);
84
+ });
85
+
86
+ it('reads verbs out of SKILL.md and .tool frontmatter, governing the file itself', async () => {
87
+ const { overrides } = await scan({
88
+ [`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\nowner: Ali <ali@bevel.software>'),
89
+ [`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer'),
90
+ });
91
+
92
+ expect(overrides).toEqual([
93
+ {
94
+ path: 'Groups/GTM/outreach/SKILL.md',
95
+ governs: 'Groups/GTM/outreach/SKILL.md',
96
+ source: 'frontmatter',
97
+ entries: [
98
+ {
99
+ verb: 'owner',
100
+ deny: false,
101
+ principal: { kind: 'user', email: 'ali@bevel.software', name: 'Ali' },
102
+ },
103
+ ],
104
+ },
105
+ {
106
+ path: 'Groups/GTM/slack.tool',
107
+ governs: 'Groups/GTM/slack.tool',
108
+ source: 'frontmatter',
109
+ entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
110
+ },
111
+ ]);
112
+ });
113
+
114
+ it('ignores nodes whose frontmatter declares no access verb', async () => {
115
+ const { overrides } = await scan({
116
+ [`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
117
+ });
118
+
119
+ expect(overrides).toEqual([]);
120
+ });
121
+
122
+ it('preserves the deny prefix, collapses everyone, and carries user name + email', async () => {
123
+ const { overrides } = await scan({
124
+ [`${KB}/Groups/GTM/battlecards/access.md`]: fm(
125
+ 'read:\n - everyone\n - deny Bob Ruiz <bob@bevel.software>\nwrite:\n - deny GTM Team',
126
+ ),
127
+ });
128
+
129
+ expect(overrides[0].entries).toEqual([
130
+ { verb: 'read', deny: false, principal: { kind: 'everyone' } },
131
+ {
132
+ verb: 'read',
133
+ deny: true,
134
+ principal: { kind: 'user', email: 'bob@bevel.software', name: 'Bob Ruiz' },
135
+ },
136
+ { verb: 'write', deny: true, principal: { kind: 'role', role: 'GTM Team' } },
137
+ ]);
138
+ });
139
+
140
+ it('surfaces an unparseable access.md and silently skips an unparseable node', async () => {
141
+ const { overrides } = await scan({
142
+ // `read:` must be a LIST in an access.md — this file controls nothing.
143
+ [`${KB}/Groups/GTM/battlecards/access.md`]: fm('read: GTM Team'),
144
+ // Unterminated frontmatter on a node — forgiven, exactly like the resolver.
145
+ [`${KB}/Groups/GTM/outreach/SKILL.md`]: '---\nowner: Ali <ali@bevel.software>\n\n# Body\n',
146
+ });
147
+
148
+ expect(overrides).toHaveLength(1);
149
+ expect(overrides[0].path).toBe('Groups/GTM/battlecards/access.md');
150
+ expect(overrides[0].entries).toEqual([]);
151
+ expect(overrides[0].parseError).toContain("'read:' must be a list");
152
+ });
153
+
154
+ it('stops at the file cap and says so', async () => {
155
+ const files: Record<string, string> = {};
156
+ for (let i = 0; i < DECLARATION_SCAN_FILE_CAP + 5; i++) {
157
+ const n = String(i).padStart(4, '0');
158
+ files[`${KB}/Groups/GTM/n${n}/SKILL.md`] = fm('read:\n - Developer');
159
+ }
160
+
161
+ const { overrides, truncated } = await scan(files);
162
+
163
+ expect(truncated).toBe(true);
164
+ expect(overrides).toHaveLength(DECLARATION_SCAN_FILE_CAP);
165
+ });
166
+
167
+ it('returns an empty result for a folder with nothing in it', async () => {
168
+ expect(await scan({ [`${KB}/Groups/Product/roadmap/SKILL.md`]: fm('read:\n - Developer') }))
169
+ .toEqual({ overrides: [], truncated: false });
170
+ });
171
+
172
+ it('returns an empty result for a folder that is not in the clone', async () => {
173
+ expect(await scan({}, 'Groups/Nope')).toEqual({ overrides: [], truncated: false });
174
+ });
175
+
176
+ it('refuses a target that is a file', async () => {
177
+ await expect(
178
+ scan({ [`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer') }, 'Groups/GTM/slack.tool'),
179
+ ).rejects.toBeInstanceOf(AccessDeclarationsError);
180
+ });
181
+
182
+ it('sorts rows by declaring path', async () => {
183
+ const { overrides } = await scan({
184
+ [`${KB}/Groups/GTM/zeta.tool`]: fm('write:\n - Developer'),
185
+ [`${KB}/Groups/GTM/alpha/access.md`]: fm('read:\n - Developer'),
186
+ [`${KB}/Groups/GTM/mid/SKILL.md`]: fm('owner:\n - Developer'),
187
+ });
188
+
189
+ expect(overrides.map((o) => o.path)).toEqual([
190
+ 'Groups/GTM/alpha/access.md',
191
+ 'Groups/GTM/mid/SKILL.md',
192
+ 'Groups/GTM/zeta.tool',
193
+ ]);
194
+ });
195
+ });