@bevel-software/platform-core-backend 0.4.1 → 0.5.1

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/THIRD-PARTY-NOTICES.md +6118 -0
  2. package/dist/core/core-ports.d.ts +15 -0
  3. package/dist/core/core-ports.d.ts.map +1 -1
  4. package/dist/core/core-ports.js.map +1 -1
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +40 -6
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +17 -1
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +153 -29
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core-config.d.ts +70 -21
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +134 -58
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +6 -0
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +74 -5
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +116 -15
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access-declarations.d.ts +93 -0
  23. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  24. package/dist/modules/access/access-declarations.js +168 -0
  25. package/dist/modules/access/access-declarations.js.map +1 -0
  26. package/dist/modules/access/access-mutation.service.js +4 -4
  27. package/dist/modules/access/access-mutation.service.js.map +1 -1
  28. package/dist/modules/access/access-splice.d.ts +16 -1
  29. package/dist/modules/access/access-splice.d.ts.map +1 -1
  30. package/dist/modules/access/access-splice.js +40 -21
  31. package/dist/modules/access/access-splice.js.map +1 -1
  32. package/dist/modules/access/access.routes.d.ts.map +1 -1
  33. package/dist/modules/access/access.routes.js +51 -0
  34. package/dist/modules/access/access.routes.js.map +1 -1
  35. package/dist/modules/access/creator-access.d.ts +8 -0
  36. package/dist/modules/access/creator-access.d.ts.map +1 -1
  37. package/dist/modules/access/creator-access.js +21 -2
  38. package/dist/modules/access/creator-access.js.map +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  40. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  41. package/dist/modules/admin/admin-access.service.js +3 -2
  42. package/dist/modules/admin/admin-access.service.js.map +1 -1
  43. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  44. package/dist/modules/auth/auth.routes.js +50 -1
  45. package/dist/modules/auth/auth.routes.js.map +1 -1
  46. package/dist/modules/auth/auth.service.d.ts +39 -10
  47. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  48. package/dist/modules/auth/auth.service.js +38 -16
  49. package/dist/modules/auth/auth.service.js.map +1 -1
  50. package/dist/modules/database/core-schema.d.ts +124 -0
  51. package/dist/modules/database/core-schema.d.ts.map +1 -1
  52. package/dist/modules/database/core-schema.js +45 -0
  53. package/dist/modules/database/core-schema.js.map +1 -1
  54. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  55. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  56. package/dist/modules/groups/group-provision.service.js +288 -0
  57. package/dist/modules/groups/group-provision.service.js.map +1 -0
  58. package/dist/modules/groups/groups.contract.d.ts +106 -0
  59. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  60. package/dist/modules/groups/groups.contract.js +36 -0
  61. package/dist/modules/groups/groups.contract.js.map +1 -0
  62. package/dist/modules/groups/groups.routes.d.ts +42 -0
  63. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  64. package/dist/modules/groups/groups.routes.js +379 -0
  65. package/dist/modules/groups/groups.routes.js.map +1 -0
  66. package/dist/modules/groups/groups.service.d.ts +60 -0
  67. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  68. package/dist/modules/groups/groups.service.js +172 -0
  69. package/dist/modules/groups/groups.service.js.map +1 -0
  70. package/dist/modules/groups/index.d.ts +7 -0
  71. package/dist/modules/groups/index.d.ts.map +1 -0
  72. package/dist/modules/groups/index.js +6 -0
  73. package/dist/modules/groups/index.js.map +1 -0
  74. package/dist/modules/groups/join-proposals.d.ts +53 -0
  75. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  76. package/dist/modules/groups/join-proposals.js +67 -0
  77. package/dist/modules/groups/join-proposals.js.map +1 -0
  78. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  79. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  80. package/dist/modules/groups/join-requests.service.js +135 -0
  81. package/dist/modules/groups/join-requests.service.js.map +1 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  83. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  85. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  86. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  87. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  88. package/dist/modules/settings/deployment-settings.service.js +368 -0
  89. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  90. package/dist/modules/settings/setup.routes.d.ts +49 -0
  91. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  92. package/dist/modules/settings/setup.routes.js +292 -0
  93. package/dist/modules/settings/setup.routes.js.map +1 -0
  94. package/dist/modules/skills/index.d.ts +2 -1
  95. package/dist/modules/skills/index.d.ts.map +1 -1
  96. package/dist/modules/skills/index.js +1 -0
  97. package/dist/modules/skills/index.js.map +1 -1
  98. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  99. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  100. package/dist/modules/skills/pending-skills.service.js +147 -0
  101. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  102. package/dist/modules/skills/skills.contract.d.ts +32 -3
  103. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  104. package/dist/modules/skills/skills.contract.js +1 -1
  105. package/dist/modules/skills/skills.routes.d.ts +3 -2
  106. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  107. package/dist/modules/skills/skills.routes.js +18 -1
  108. package/dist/modules/skills/skills.routes.js.map +1 -1
  109. package/dist/modules/skills/skills.service.d.ts +22 -0
  110. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  111. package/dist/modules/skills/skills.service.js +24 -12
  112. package/dist/modules/skills/skills.service.js.map +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts +1 -1
  114. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  115. package/dist/modules/tool-manuals/index.js.map +1 -1
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  117. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  120. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  122. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  125. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  126. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  129. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  132. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  133. package/dist/modules/workflow/event-bus.d.ts +11 -0
  134. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  135. package/dist/modules/workflow/event-bus.js +22 -0
  136. package/dist/modules/workflow/event-bus.js.map +1 -1
  137. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  138. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  139. package/dist/modules/workflow/file-lock.service.js +16 -0
  140. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  142. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  143. package/dist/modules/workflow/git/branch-name.js +1 -1
  144. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  145. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  146. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  147. package/dist/modules/workflow/git/git.service.js +144 -52
  148. package/dist/modules/workflow/git/git.service.js.map +1 -1
  149. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  150. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  151. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  152. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  155. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  157. package/dist/modules/workflow/workflow.routes.js +95 -14
  158. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  159. package/dist/modules/workflow/workflow.service.d.ts +92 -3
  160. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  161. package/dist/modules/workflow/workflow.service.js +381 -108
  162. package/dist/modules/workflow/workflow.service.js.map +1 -1
  163. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  164. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  165. package/dist/modules/workspace/bevel-ignore.js +5 -1
  166. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  167. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  168. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  169. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  170. package/dist/modules/workspace/kb-seed.service.js +228 -51
  171. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  173. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  174. package/dist/modules/workspace/workspace.routes.js +51 -18
  175. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  176. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  177. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  178. package/dist/modules/workspace/workspace.service.js +36 -9
  179. package/dist/modules/workspace/workspace.service.js.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  181. package/dist/modules/workspace/workspace.tools.js +8 -3
  182. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  183. package/dist/shared/kb-layout.test.js +2 -2
  184. package/dist/test-setup.d.ts +2 -0
  185. package/dist/test-setup.d.ts.map +1 -0
  186. package/dist/test-setup.js +18 -0
  187. package/dist/test-setup.js.map +1 -0
  188. package/kb-template/.bevelignore +6 -2
  189. package/kb-template/AGENTS.md +272 -0
  190. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  191. package/kb-template/access.md +36 -36
  192. package/migrations/0002_user_onboarding_done.sql +1 -0
  193. package/migrations/0003_deployment_settings.sql +9 -0
  194. package/migrations/meta/0001_snapshot.json +1486 -0
  195. package/migrations/meta/0002_snapshot.json +1493 -0
  196. package/migrations/meta/0003_snapshot.json +1552 -0
  197. package/migrations/meta/_journal.json +15 -1
  198. package/package.json +5 -4
  199. package/src/__tests__/branch-model.test.ts +79 -0
  200. package/src/__tests__/core-config.admin.test.ts +114 -0
  201. package/src/__tests__/database-url.test.ts +70 -0
  202. package/src/core/core-ports.ts +15 -0
  203. package/src/core/create-core-server.ts +55 -6
  204. package/src/core/create-core-services.ts +201 -36
  205. package/src/core-config.ts +139 -59
  206. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  207. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  208. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  209. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  210. package/src/modules/access/access-control.interface.ts +11 -0
  211. package/src/modules/access/access-control.service.ts +113 -15
  212. package/src/modules/access/access-declarations.ts +251 -0
  213. package/src/modules/access/access-mutation.service.ts +4 -4
  214. package/src/modules/access/access-splice.ts +64 -21
  215. package/src/modules/access/access.routes.ts +61 -0
  216. package/src/modules/access/creator-access.ts +22 -2
  217. package/src/modules/admin/admin-access.service.ts +4 -3
  218. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  219. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  220. package/src/modules/auth/auth.routes.ts +50 -1
  221. package/src/modules/auth/auth.service.ts +55 -17
  222. package/src/modules/database/core-schema.ts +46 -0
  223. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  224. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  225. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  226. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  227. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  228. package/src/modules/groups/group-provision.service.ts +328 -0
  229. package/src/modules/groups/groups.contract.ts +107 -0
  230. package/src/modules/groups/groups.routes.ts +433 -0
  231. package/src/modules/groups/groups.service.ts +193 -0
  232. package/src/modules/groups/index.ts +17 -0
  233. package/src/modules/groups/join-proposals.ts +114 -0
  234. package/src/modules/groups/join-requests.service.ts +185 -0
  235. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  236. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  237. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  238. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  239. package/src/modules/settings/deployment-settings.service.ts +419 -0
  240. package/src/modules/settings/setup.routes.ts +317 -0
  241. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  242. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  243. package/src/modules/skills/index.ts +3 -0
  244. package/src/modules/skills/pending-skills.service.ts +168 -0
  245. package/src/modules/skills/skills.contract.ts +34 -3
  246. package/src/modules/skills/skills.routes.ts +23 -2
  247. package/src/modules/skills/skills.service.ts +24 -16
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  251. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  252. package/src/modules/tool-manuals/index.ts +2 -0
  253. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  254. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  255. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  256. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  257. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -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 +5 -0
  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 +159 -62
  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 +353 -13
  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
@@ -0,0 +1,419 @@
1
+ import { eq, inArray } from 'drizzle-orm';
2
+ import type { Database } from '../database/connection.js';
3
+ import { deploymentSettings } from '../database/core-schema.js';
4
+ import { validateBranchModel } from '@bevel-software/platform-shared';
5
+ import { TokenCrypto } from '../../shared/token-crypto.js';
6
+
7
+ /**
8
+ * A setting an admin may set from the setup screen instead of the environment.
9
+ *
10
+ * `envVar` is the name that still wins if it is set — see
11
+ * {@link DeploymentSettingsService}. `secret` values are sealed at rest and
12
+ * never read back out to a client.
13
+ */
14
+ export interface SettingDef {
15
+ key: string;
16
+ envVar: string;
17
+ /** Which block of the setup screen it belongs to. */
18
+ section: 'knowledge-base' | 'sign-in';
19
+ secret?: boolean;
20
+ /** Applied on save; the message is shown against the field. */
21
+ validate?(value: string): string | null;
22
+ /**
23
+ * True when a running server cannot pick the new value up. Everything the
24
+ * KB remote needs is read per-operation and applies at once; anything that
25
+ * was copied into a service at construction is not.
26
+ */
27
+ restartToApply?: boolean;
28
+ }
29
+
30
+ /**
31
+ * The one rule for what a KB remote may look like, shared by the setting below
32
+ * and the connection test — which must apply it BEFORE handing the value to
33
+ * git. An unvalidated string reaching `git ls-remote` is argument injection:
34
+ * `--upload-pack=…` runs a command of the caller's choosing, and git's `ext::`
35
+ * transport is a shell escape by design.
36
+ */
37
+ export const validateHttpsRemote = (value: string): string | null => {
38
+ let parsed: URL;
39
+ try {
40
+ parsed = new URL(value);
41
+ } catch {
42
+ return 'Enter a full URL, e.g. https://github.com/acme/knowledge-base.git';
43
+ }
44
+ return parsed.protocol === 'https:' ? null : 'The URL must start with https://';
45
+ };
46
+
47
+ /**
48
+ * The core catalogue. Order is the order the setup screen renders them in.
49
+ */
50
+ export const CORE_SETTINGS: SettingDef[] = [
51
+ {
52
+ key: 'kbRepoUrl',
53
+ envVar: 'KB_REPO_URL',
54
+ section: 'knowledge-base',
55
+ validate: validateHttpsRemote,
56
+ },
57
+ {
58
+ key: 'gitToken',
59
+ envVar: 'GIT_TOKEN',
60
+ section: 'knowledge-base',
61
+ secret: true,
62
+ validate: (v) => (v.trim() ? null : 'A token is required to read and write the repository.'),
63
+ },
64
+ {
65
+ key: 'gitUsername',
66
+ envVar: 'GIT_USERNAME',
67
+ section: 'knowledge-base',
68
+ // Interpolated into the credential-helper shell snippet, so anything that
69
+ // is not a plain token is rejected rather than escaped.
70
+ validate: (v) =>
71
+ /^[A-Za-z0-9._-]+$/.test(v) ? null : 'Use only letters, digits, dot, underscore or hyphen.',
72
+ },
73
+ {
74
+ key: 'kbDirName',
75
+ envVar: 'KB_DIR_NAME',
76
+ section: 'knowledge-base',
77
+ // Joined with workspace paths, so a separator or `..` would let it escape
78
+ // the workspace directory.
79
+ validate: (v) =>
80
+ v && v !== '.' && v !== '..' && !v.includes('/') && !v.includes('\\')
81
+ ? null
82
+ : 'Use a single folder name — no slashes.',
83
+ // Copied into a dozen services when they are constructed. A running server
84
+ // keeps the name it started with.
85
+ restartToApply: true,
86
+ },
87
+
88
+ /**
89
+ * The branch model. Both are restart-to-apply and could not be otherwise:
90
+ * the backend hands `DEFAULT_BRANCH` to services at construction, and the
91
+ * browser is served the pair once at boot — a live swap would leave half the
92
+ * app on the old model and half on the new one.
93
+ *
94
+ * The pair is ALSO checked together in `save`, because neither field is
95
+ * valid or invalid on its own: the default branch must appear in the
96
+ * protected list, and a per-field rule cannot see the other side.
97
+ */
98
+ {
99
+ key: 'defaultBranch',
100
+ envVar: 'DEFAULT_BRANCH',
101
+ section: 'knowledge-base',
102
+ validate: (v) => (v.includes(',') ? 'One branch name, not a list.' : null),
103
+ restartToApply: true,
104
+ },
105
+ {
106
+ key: 'protectedBranches',
107
+ envVar: 'PROTECTED_BRANCHES',
108
+ section: 'knowledge-base',
109
+ restartToApply: true,
110
+ },
111
+
112
+ /**
113
+ * Single sign-on. Restart-to-apply because the provider is built once at boot
114
+ * and pushed into the auth plugin array the server mounts from.
115
+ */
116
+ {
117
+ key: 'oidcIssuerUrl',
118
+ envVar: 'OIDC_ISSUER_URL',
119
+ section: 'sign-in',
120
+ validate: (v) => {
121
+ try {
122
+ return new URL(v).protocol === 'https:' ? null : 'The issuer URL must start with https://';
123
+ } catch {
124
+ return 'Enter the issuer URL, e.g. https://login.microsoftonline.com/<tenant>/v2.0';
125
+ }
126
+ },
127
+ restartToApply: true,
128
+ },
129
+ {
130
+ key: 'oidcClientId',
131
+ envVar: 'OIDC_CLIENT_ID',
132
+ section: 'sign-in',
133
+ restartToApply: true,
134
+ },
135
+ {
136
+ key: 'oidcClientSecret',
137
+ envVar: 'OIDC_CLIENT_SECRET',
138
+ section: 'sign-in',
139
+ secret: true,
140
+ restartToApply: true,
141
+ },
142
+ {
143
+ key: 'oidcScopes',
144
+ envVar: 'OIDC_SCOPES',
145
+ section: 'sign-in',
146
+ restartToApply: true,
147
+ },
148
+ {
149
+ key: 'oidcProviderLabel',
150
+ envVar: 'OIDC_PROVIDER_LABEL',
151
+ section: 'sign-in',
152
+ restartToApply: true,
153
+ },
154
+ {
155
+ // Belongs with SSO because SSO is what makes it load-bearing: sign-in
156
+ // auto-provisions, so against a multi-tenant issuer this list is the only
157
+ // thing between "has an account somewhere" and "has an account here".
158
+ key: 'allowedEmailDomains',
159
+ envVar: 'ALLOWED_EMAIL_DOMAINS',
160
+ section: 'sign-in',
161
+ restartToApply: true,
162
+ },
163
+ ];
164
+
165
+ /** Where a resolved value came from, which is what the UI renders as its status. */
166
+ export type SettingSource = 'env' | 'stored' | 'unset';
167
+
168
+ export interface ResolvedSetting {
169
+ key: string;
170
+ envVar: string;
171
+ section: SettingDef['section'];
172
+ source: SettingSource;
173
+ /** Omitted entirely for secrets — `configured` is all a client ever learns. */
174
+ value?: string;
175
+ configured: boolean;
176
+ secret: boolean;
177
+ restartToApply: boolean;
178
+ }
179
+
180
+ /**
181
+ * Deployment settings, resolved ENVIRONMENT-FIRST.
182
+ *
183
+ * The precedence is the whole design. A stored row is a FALLBACK for a variable
184
+ * nobody set, never an override of one they did — so:
185
+ *
186
+ * - an existing deployment adopts this table with no behaviour change at all;
187
+ * - a value typed once into a browser cannot quietly outrank the
188
+ * infrastructure config that is under review in someone's repo;
189
+ * - and "why is it not using my env var" never becomes a question, because
190
+ * the answer is always "it is".
191
+ *
192
+ * Values are cached in memory after {@link load}. Reads happen on every clone
193
+ * and every git call, and a database round-trip there would be a tax on the
194
+ * hot path for data that changes about twice in a deployment's life.
195
+ *
196
+ * WHICH MAKES THE CACHE PER-PROCESS, and that is a real constraint rather than
197
+ * an oversight: a second replica keeps serving what it read at boot until it
198
+ * restarts. It is acceptable here only because of what these settings are —
199
+ * the knowledge-base connection, written once during first-run setup, on a
200
+ * deployment that has nothing to serve until it is. Nobody is mid-session on a
201
+ * second replica at that moment.
202
+ *
203
+ * It stops being acceptable the moment a setting is something an operator
204
+ * changes on a live multi-replica deployment. Adding one means adding
205
+ * invalidation with it — the event bus already carries user-scoped and
206
+ * broadcast messages, so a `settings-changed` event that triggers `load()` is
207
+ * the natural shape.
208
+ */
209
+ export class DeploymentSettingsService {
210
+ private readonly defs = new Map<string, SettingDef>();
211
+ private stored = new Map<string, string>();
212
+ private readonly crypto: TokenCrypto | null;
213
+
214
+ constructor(
215
+ private readonly db: Database,
216
+ secretsEncKey: string,
217
+ defs: SettingDef[] = CORE_SETTINGS,
218
+ ) {
219
+ for (const def of defs) this.defs.set(def.key, def);
220
+ // No key configured means secrets cannot be stored — surfaced when someone
221
+ // tries, rather than pretended away by writing plaintext.
222
+ this.crypto = secretsEncKey ? new TokenCrypto(secretsEncKey) : null;
223
+ }
224
+
225
+ get definitions(): SettingDef[] {
226
+ return [...this.defs.values()];
227
+ }
228
+
229
+ /** Read every stored row into memory. Call once, before services are built. */
230
+ async load(): Promise<void> {
231
+ const rows = await this.db.select().from(deploymentSettings);
232
+ const next = new Map<string, string>();
233
+ for (const row of rows) {
234
+ if (!this.defs.has(row.key)) continue; // a setting this build no longer has
235
+ if (row.encrypted) {
236
+ if (!this.crypto) {
237
+ console.warn(
238
+ `[settings] "${row.key}" is stored encrypted but SECRETS_ENC_KEY is unset — ignoring it.`,
239
+ );
240
+ continue;
241
+ }
242
+ try {
243
+ next.set(row.key, this.crypto.decrypt(row.value));
244
+ } catch {
245
+ // A rotated or mistyped key. Loud, and skipped rather than fatal:
246
+ // one unreadable setting must not stop the server from booting into
247
+ // the screen where it can be fixed.
248
+ console.error(`[settings] could not decrypt "${row.key}" — is SECRETS_ENC_KEY correct?`);
249
+ }
250
+ continue;
251
+ }
252
+ next.set(row.key, row.value);
253
+ }
254
+ this.stored = next;
255
+ }
256
+
257
+ /**
258
+ * The value in effect: the environment variable if set, else the stored row,
259
+ * else empty. Trimmed, because both sources arrive from a human.
260
+ */
261
+ resolve(key: string): string {
262
+ const def = this.defs.get(key);
263
+ if (!def) return '';
264
+ const fromEnv = (process.env[def.envVar] ?? '').trim();
265
+ if (fromEnv) return fromEnv;
266
+ return (this.stored.get(key) ?? '').trim();
267
+ }
268
+
269
+ /** Where {@link resolve} got its answer — what the setup screen labels the field with. */
270
+ sourceOf(key: string): SettingSource {
271
+ const def = this.defs.get(key);
272
+ if (!def) return 'unset';
273
+ if ((process.env[def.envVar] ?? '').trim()) return 'env';
274
+ return (this.stored.get(key) ?? '').trim() ? 'stored' : 'unset';
275
+ }
276
+
277
+ /**
278
+ * Every setting's status, for the setup screen. A secret's VALUE is never
279
+ * included — the screen shows whether one is configured and offers to
280
+ * replace it, which is all anyone needs to finish setup.
281
+ */
282
+ describe(): ResolvedSetting[] {
283
+ return this.definitions.map((def) => {
284
+ const source = this.sourceOf(def.key);
285
+ const base = {
286
+ key: def.key,
287
+ envVar: def.envVar,
288
+ section: def.section,
289
+ source,
290
+ configured: source !== 'unset',
291
+ secret: def.secret === true,
292
+ restartToApply: def.restartToApply === true,
293
+ };
294
+ return def.secret ? base : { ...base, value: this.resolve(def.key) };
295
+ });
296
+ }
297
+
298
+ /**
299
+ * Validate and persist. Rejects the whole batch if any field fails, so a
300
+ * half-applied configuration is never written — a KB URL saved without the
301
+ * token that reads it is a deployment that fails at first clone.
302
+ *
303
+ * A setting whose environment variable is set is REFUSED rather than silently
304
+ * stored, because storing it would write a row that can never take effect and
305
+ * leave the screen implying otherwise.
306
+ */
307
+ async save(
308
+ entries: Record<string, string>,
309
+ updatedBy: string | null,
310
+ ): Promise<{ restartRequired: boolean }> {
311
+ const problems: Record<string, string> = {};
312
+ const toWrite: { key: string; value: string; def: SettingDef }[] = [];
313
+
314
+ for (const [key, raw] of Object.entries(entries)) {
315
+ const def = this.defs.get(key);
316
+ if (!def) {
317
+ problems[key] = 'Unknown setting.';
318
+ continue;
319
+ }
320
+ if (this.sourceOf(key) === 'env') {
321
+ problems[key] = `Set by the ${def.envVar} environment variable — change it there.`;
322
+ continue;
323
+ }
324
+ const value = raw.trim();
325
+ // An empty field means "leave it alone", not "erase it". Clearing a
326
+ // setting is not something the setup screen offers, and treating a blank
327
+ // input as a delete would let a stray Enter unconfigure a deployment.
328
+ if (!value) continue;
329
+ const problem = def.validate?.(value);
330
+ if (problem) {
331
+ problems[key] = problem;
332
+ continue;
333
+ }
334
+ if (def.secret && !this.crypto) {
335
+ problems[key] = 'SECRETS_ENC_KEY is not set, so secrets cannot be stored.';
336
+ continue;
337
+ }
338
+ toWrite.push({ key, value, def });
339
+ }
340
+
341
+ // The branch pair is the one cross-field rule: neither name is valid or
342
+ // invalid alone, since the default must appear in the protected list. Check
343
+ // the model this save WOULD produce — the written value where there is one,
344
+ // the value already in effect where there is not — so setting one half
345
+ // against an existing other half is judged on the result, not the input.
346
+ const branchKeys = ['defaultBranch', 'protectedBranches'];
347
+ if (toWrite.some((w) => branchKeys.includes(w.key))) {
348
+ const effective = (key: string) =>
349
+ toWrite.find((w) => w.key === key)?.value ?? this.resolve(key);
350
+ const problem = validateBranchModel({
351
+ defaultBranch: effective('defaultBranch'),
352
+ protectedBranches: effective('protectedBranches'),
353
+ });
354
+ // Reported against the protected list: it is the field with room to hold
355
+ // the answer, and "add it to this list" is the usual fix.
356
+ if (problem) problems.protectedBranches = problem;
357
+ }
358
+
359
+ if (Object.keys(problems).length > 0) throw new SettingsValidationError(problems);
360
+
361
+ let restartRequired = false;
362
+ for (const { key, value, def } of toWrite) {
363
+ if (def.restartToApply && this.resolve(key) !== value) restartRequired = true;
364
+ const encrypted = def.secret === true;
365
+ const stored = encrypted ? this.crypto!.encrypt(value) : value;
366
+ await this.db
367
+ .insert(deploymentSettings)
368
+ .values({ key, value: stored, encrypted, updatedBy })
369
+ .onConflictDoUpdate({
370
+ target: deploymentSettings.key,
371
+ set: { value: stored, encrypted, updatedBy, updatedAt: new Date() },
372
+ });
373
+ this.stored.set(key, value);
374
+ }
375
+
376
+ // The git token is consumed through the environment (the credential helper
377
+ // reads `$GITHUB_TOKEN` at call time, so it never appears in argv). Putting
378
+ // it there is what makes a token saved here work without a restart.
379
+ this.syncGitTokenEnv();
380
+ return { restartRequired };
381
+ }
382
+
383
+ /** Drop stored rows for settings this build no longer defines. */
384
+ async prune(): Promise<void> {
385
+ const known = [...this.defs.keys()];
386
+ if (known.length === 0) return;
387
+ const rows = await this.db.select({ key: deploymentSettings.key }).from(deploymentSettings);
388
+ const orphans = rows.map((r) => r.key).filter((k) => !known.includes(k));
389
+ if (orphans.length > 0) {
390
+ await this.db.delete(deploymentSettings).where(inArray(deploymentSettings.key, orphans));
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Publish the resolved git token as `GITHUB_TOKEN`, the name the credential
396
+ * helper and every redaction path already read. Only when the environment did
397
+ * not supply one — otherwise this would overwrite the operator's value with
398
+ * a stored fallback, inverting the precedence everything else here obeys.
399
+ */
400
+ syncGitTokenEnv(): void {
401
+ if (this.sourceOf('gitToken') !== 'stored') return;
402
+ const token = this.resolve('gitToken');
403
+ if (token) process.env.GITHUB_TOKEN = token;
404
+ }
405
+
406
+ /** Remove one stored row (used by tests and by `prune`). */
407
+ async clear(key: string): Promise<void> {
408
+ await this.db.delete(deploymentSettings).where(eq(deploymentSettings.key, key));
409
+ this.stored.delete(key);
410
+ }
411
+ }
412
+
413
+ /** Field-keyed validation failures, so the screen can mark the offending input. */
414
+ export class SettingsValidationError extends Error {
415
+ constructor(readonly problems: Record<string, string>) {
416
+ super(`Invalid settings: ${Object.keys(problems).join(', ')}`);
417
+ this.name = 'SettingsValidationError';
418
+ }
419
+ }