@bevel-software/platform-core-backend 0.22.0 → 0.24.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 (466) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  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 +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +46 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +12 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +137 -24
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +46 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +99 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +16 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +28 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +17 -16
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  34. package/dist/modules/access/directory-sync-bot.js +7 -3
  35. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  36. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  37. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  38. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  39. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  40. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  41. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  42. package/dist/modules/agent-instructions/compose.js +39 -6
  43. package/dist/modules/agent-instructions/compose.js.map +1 -1
  44. package/dist/modules/agent-instructions/index.d.ts +2 -1
  45. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  46. package/dist/modules/agent-instructions/index.js +2 -1
  47. package/dist/modules/agent-instructions/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  49. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  50. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  51. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  52. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  53. package/dist/modules/audit/agent-audit.service.js +4 -2
  54. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  55. package/dist/modules/auth/account-admission.d.ts +58 -7
  56. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  57. package/dist/modules/auth/account-admission.js +44 -1
  58. package/dist/modules/auth/account-admission.js.map +1 -1
  59. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  60. package/dist/modules/auth/account-erasure.service.js +57 -16
  61. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  62. package/dist/modules/auth/account.routes.d.ts +1 -1
  63. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  64. package/dist/modules/auth/account.routes.js +61 -5
  65. package/dist/modules/auth/account.routes.js.map +1 -1
  66. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  67. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  68. package/dist/modules/auth/auth.middleware.js +18 -7
  69. package/dist/modules/auth/auth.middleware.js.map +1 -1
  70. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  71. package/dist/modules/auth/auth.routes.js +8 -0
  72. package/dist/modules/auth/auth.routes.js.map +1 -1
  73. package/dist/modules/auth/auth.service.d.ts +70 -2
  74. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  75. package/dist/modules/auth/auth.service.js +197 -18
  76. package/dist/modules/auth/auth.service.js.map +1 -1
  77. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  78. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  79. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  80. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  81. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  82. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  83. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  84. package/dist/modules/database/connection.d.ts +16 -0
  85. package/dist/modules/database/connection.d.ts.map +1 -1
  86. package/dist/modules/database/connection.js +117 -0
  87. package/dist/modules/database/connection.js.map +1 -1
  88. package/dist/modules/database/core-schema.d.ts +337 -120
  89. package/dist/modules/database/core-schema.d.ts.map +1 -1
  90. package/dist/modules/database/core-schema.js +116 -58
  91. package/dist/modules/database/core-schema.js.map +1 -1
  92. package/dist/modules/database/migrate.d.ts +8 -0
  93. package/dist/modules/database/migrate.d.ts.map +1 -1
  94. package/dist/modules/database/migrate.js +271 -1
  95. package/dist/modules/database/migrate.js.map +1 -1
  96. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  97. package/dist/modules/kb-fs/branch-name.js +12 -2
  98. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  99. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  100. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  101. package/dist/modules/kb-fs/repo-path.js +79 -0
  102. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  104. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  105. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  106. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  108. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js +1 -1
  110. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  112. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  113. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  114. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  115. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  116. package/dist/modules/mcp/mcp.service.js +38 -8
  117. package/dist/modules/mcp/mcp.service.js.map +1 -1
  118. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  119. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  120. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  121. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-request-records.store.js +7 -4
  123. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  124. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  125. package/dist/modules/settings/deployment-settings.service.js +8 -3
  126. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  127. package/dist/modules/settings/setup.routes.d.ts +52 -1
  128. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  129. package/dist/modules/settings/setup.routes.js +207 -19
  130. package/dist/modules/settings/setup.routes.js.map +1 -1
  131. package/dist/modules/skills/skills.contract.d.ts +90 -18
  132. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  133. package/dist/modules/skills/skills.contract.js +4 -1
  134. package/dist/modules/skills/skills.contract.js.map +1 -1
  135. package/dist/modules/skills/skills.service.d.ts +98 -9
  136. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  137. package/dist/modules/skills/skills.service.js +239 -37
  138. package/dist/modules/skills/skills.service.js.map +1 -1
  139. package/dist/modules/skills/skills.tools.d.ts +7 -0
  140. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  141. package/dist/modules/skills/skills.tools.js +71 -7
  142. package/dist/modules/skills/skills.tools.js.map +1 -1
  143. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  144. package/dist/modules/tool-auth/external-api-key.service.js +12 -4
  145. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  146. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  147. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  148. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  149. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  150. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  151. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  152. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  153. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  154. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  155. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  156. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  157. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  158. package/dist/modules/tool-helpers/validate-token.js +1 -1
  159. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  160. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  161. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  162. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  163. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  164. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  165. package/dist/modules/tool-registry/description-length.js +108 -0
  166. package/dist/modules/tool-registry/description-length.js.map +1 -0
  167. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  168. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  169. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  170. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  171. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  172. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  173. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  174. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  175. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  176. package/dist/modules/workflow/file-lock.service.js +30 -0
  177. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  178. package/dist/modules/workflow/git/git.service.d.ts +94 -0
  179. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  180. package/dist/modules/workflow/git/git.service.js +193 -3
  181. package/dist/modules/workflow/git/git.service.js.map +1 -1
  182. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  183. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  184. package/dist/modules/workflow/pending-commits.service.js +60 -1
  185. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  186. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  187. package/dist/modules/workflow/recovery-bot.js +7 -3
  188. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  189. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  190. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  191. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  192. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  193. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  194. package/dist/modules/workflow/workflow-hooks.js +16 -1
  195. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  196. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  197. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  198. package/dist/modules/workflow/workflow.service.js +115 -5
  199. package/dist/modules/workflow/workflow.service.js.map +1 -1
  200. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  201. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  202. package/dist/modules/workspace/agent-access.gate.js +123 -0
  203. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  204. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  205. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  206. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  207. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  208. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  209. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  210. package/dist/modules/workspace/agent-upload.store.js +553 -0
  211. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  212. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  213. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  214. package/dist/modules/workspace/routine-write-policy.js +5 -6
  215. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  216. package/dist/modules/workspace/session-sink.d.ts +5 -5
  217. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  218. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  219. package/dist/modules/workspace/set-aside-clone.js +92 -0
  220. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  221. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  222. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  223. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  224. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  225. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  226. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  227. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  228. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  229. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  231. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  232. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  233. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  234. package/dist/modules/workspace/upload-limits.js +13 -0
  235. package/dist/modules/workspace/upload-limits.js.map +1 -0
  236. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  237. package/dist/modules/workspace/workspace.routes.js +93 -4
  238. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  239. package/dist/modules/workspace/workspace.service.d.ts +128 -5
  240. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  241. package/dist/modules/workspace/workspace.service.js +316 -58
  242. package/dist/modules/workspace/workspace.service.js.map +1 -1
  243. package/dist/modules/workspace/workspace.tools.d.ts +13 -11
  244. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  245. package/dist/modules/workspace/workspace.tools.js +791 -198
  246. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  247. package/dist/modules/workspace/write-denial.d.ts +0 -6
  248. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  249. package/dist/modules/workspace/write-denial.js +0 -6
  250. package/dist/modules/workspace/write-denial.js.map +1 -1
  251. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  252. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  253. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  254. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  255. package/dist/modules/write-access/write-access.d.ts +60 -0
  256. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  257. package/dist/modules/write-access/write-access.js +129 -0
  258. package/dist/modules/write-access/write-access.js.map +1 -0
  259. package/dist/shared/column-crypto.d.ts +194 -0
  260. package/dist/shared/column-crypto.d.ts.map +1 -0
  261. package/dist/shared/column-crypto.js +144 -0
  262. package/dist/shared/column-crypto.js.map +1 -0
  263. package/dist/shared/domain-errors.d.ts +25 -0
  264. package/dist/shared/domain-errors.d.ts.map +1 -1
  265. package/dist/shared/domain-errors.js +28 -0
  266. package/dist/shared/domain-errors.js.map +1 -1
  267. package/dist/shared/git.contract.d.ts +20 -0
  268. package/dist/shared/git.contract.d.ts.map +1 -1
  269. package/dist/shared/git.contract.js +26 -0
  270. package/dist/shared/git.contract.js.map +1 -1
  271. package/dist/shared/token-crypto.d.ts.map +1 -1
  272. package/dist/shared/token-crypto.js +25 -1
  273. package/dist/shared/token-crypto.js.map +1 -1
  274. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  275. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  276. package/dist/tenancy/static-tenant-source.js +1 -2
  277. package/dist/tenancy/static-tenant-source.js.map +1 -1
  278. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  279. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  280. package/dist/tenancy/tenant-secrets.js +4 -0
  281. package/dist/tenancy/tenant-secrets.js.map +1 -1
  282. package/kb-template/AGENTS.md +2 -0
  283. package/migrations/0014_change_request_closed_reason.sql +1 -0
  284. package/migrations/0015_account_deactivation.sql +3 -0
  285. package/migrations/0016_pii_encryption.sql +20 -0
  286. package/migrations/meta/0014_snapshot.json +2265 -0
  287. package/migrations/meta/0015_snapshot.json +2271 -0
  288. package/migrations/meta/0016_snapshot.json +2327 -0
  289. package/migrations/meta/_journal.json +21 -0
  290. package/package.json +3 -3
  291. package/src/__tests__/kb-layout-config.test.ts +0 -2
  292. package/src/__tests__/retired-settings.test.ts +96 -0
  293. package/src/core/__tests__/gated-boot.test.ts +132 -0
  294. package/src/core/__tests__/lifecycle.test.ts +241 -5
  295. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  296. package/src/core/core-ports.ts +11 -1
  297. package/src/core/create-core-server.ts +52 -16
  298. package/src/core/create-core-services.ts +156 -24
  299. package/src/core/lifecycle.ts +117 -13
  300. package/src/core-config.ts +31 -14
  301. package/src/index.ts +30 -1
  302. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  303. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  304. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  305. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  306. package/src/modules/access/access-control.interface.ts +43 -12
  307. package/src/modules/access/access-control.service.ts +234 -36
  308. package/src/modules/access/access.routes.ts +17 -16
  309. package/src/modules/access/directory-sync-bot.ts +7 -3
  310. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
  311. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  312. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  313. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  314. package/src/modules/agent-instructions/compose.ts +50 -7
  315. package/src/modules/agent-instructions/index.ts +12 -0
  316. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  317. package/src/modules/audit/agent-audit.service.ts +4 -2
  318. package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
  319. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  320. package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
  321. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  322. package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
  323. package/src/modules/auth/account-admission.ts +78 -9
  324. package/src/modules/auth/account-erasure.service.ts +69 -18
  325. package/src/modules/auth/account.routes.ts +63 -7
  326. package/src/modules/auth/auth.middleware.ts +19 -8
  327. package/src/modules/auth/auth.routes.ts +8 -0
  328. package/src/modules/auth/auth.service.ts +206 -23
  329. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  330. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  331. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  332. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  333. package/src/modules/database/__tests__/connection.test.ts +12 -0
  334. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
  335. package/src/modules/database/connection.ts +117 -0
  336. package/src/modules/database/core-schema.ts +116 -58
  337. package/src/modules/database/migrate.ts +353 -1
  338. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  339. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  340. package/src/modules/kb-fs/branch-name.ts +14 -1
  341. package/src/modules/kb-fs/repo-path.ts +85 -0
  342. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  343. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  344. package/src/modules/kb-sync/sync-auth.ts +2 -2
  345. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  346. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  347. package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
  348. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  349. package/src/modules/mcp/mcp.service.ts +46 -7
  350. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  351. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  352. package/src/modules/plugins/join-request-records.store.ts +7 -4
  353. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  354. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  355. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  356. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  357. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  358. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  359. package/src/modules/settings/deployment-settings.service.ts +8 -3
  360. package/src/modules/settings/setup.routes.ts +263 -19
  361. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  362. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  363. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  364. package/src/modules/skills/skills.contract.ts +91 -18
  365. package/src/modules/skills/skills.service.ts +278 -41
  366. package/src/modules/skills/skills.tools.ts +80 -8
  367. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  368. package/src/modules/tool-auth/external-api-key.service.ts +12 -4
  369. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  370. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  371. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  372. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  373. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  374. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  375. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  376. package/src/modules/tool-helpers/validate-token.ts +1 -1
  377. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  378. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  379. package/src/modules/tool-registry/description-length.ts +111 -0
  380. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  381. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  382. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  383. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  384. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  385. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  386. package/src/modules/workflow/file-lock.service.ts +31 -0
  387. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  388. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  389. package/src/modules/workflow/git/git.service.ts +215 -1
  390. package/src/modules/workflow/pending-commits.service.ts +64 -2
  391. package/src/modules/workflow/recovery-bot.ts +7 -3
  392. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  393. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  394. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  395. package/src/modules/workflow/workflow-hooks.ts +64 -26
  396. package/src/modules/workflow/workflow.service.ts +125 -5
  397. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  398. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  399. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
  400. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  401. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  402. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  403. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  404. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  405. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  406. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  407. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
  408. package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
  409. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
  410. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  411. package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
  412. package/src/modules/workspace/agent-access.gate.ts +164 -0
  413. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  414. package/src/modules/workspace/agent-upload.store.ts +668 -0
  415. package/src/modules/workspace/routine-write-policy.ts +5 -6
  416. package/src/modules/workspace/session-sink.ts +5 -5
  417. package/src/modules/workspace/set-aside-clone.ts +96 -0
  418. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  419. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  420. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  421. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  422. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  423. package/src/modules/workspace/upload-limits.ts +12 -0
  424. package/src/modules/workspace/workspace.routes.ts +96 -5
  425. package/src/modules/workspace/workspace.service.ts +319 -61
  426. package/src/modules/workspace/workspace.tools.ts +894 -214
  427. package/src/modules/workspace/write-denial.ts +0 -8
  428. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  429. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  430. package/src/modules/write-access/write-access.ts +153 -0
  431. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  432. package/src/shared/column-crypto.ts +218 -0
  433. package/src/shared/domain-errors.ts +31 -0
  434. package/src/shared/git.contract.ts +27 -0
  435. package/src/shared/token-crypto.ts +28 -1
  436. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
  437. package/src/tenancy/static-tenant-source.ts +1 -3
  438. package/src/tenancy/tenant-secrets.ts +5 -1
  439. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  440. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  441. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  442. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  443. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  444. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  445. package/dist/modules/workflow/session-ontology.service.js +0 -147
  446. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  447. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  448. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  449. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  450. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  451. package/dist/shared/kb-layout.d.ts +0 -39
  452. package/dist/shared/kb-layout.d.ts.map +0 -1
  453. package/dist/shared/kb-layout.js +0 -103
  454. package/dist/shared/kb-layout.js.map +0 -1
  455. package/dist/shared/kb-layout.test.d.ts +0 -2
  456. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  457. package/dist/shared/kb-layout.test.js +0 -75
  458. package/dist/shared/kb-layout.test.js.map +0 -1
  459. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  460. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  461. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  462. package/src/modules/workflow/session-ontology.service.ts +0 -183
  463. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  464. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  465. package/src/shared/kb-layout.test.ts +0 -98
  466. package/src/shared/kb-layout.ts +0 -102
@@ -3,7 +3,7 @@ import express from 'express';
3
3
  import type { Server } from 'node:http';
4
4
  import { createAccountRoutes } from '../account.routes.js';
5
5
  import { AuthService } from '../auth.service.js';
6
- import { AccountAdmissionRefusedError } from '../account-admission.js';
6
+ import { AccountAdmissionRefusedError, AccountChangeRefusedError } from '../account-admission.js';
7
7
  import { hashPassword } from '../password-hash.js';
8
8
  import type { Database } from '../../database/connection.js';
9
9
  import type { IAdminAccessService } from '../../admin/admin.interface.js';
@@ -13,6 +13,8 @@ const authService = {
13
13
  { id: 'u1', email: 'a@example.com', name: 'A', hasPassword: true, createdAt: new Date() },
14
14
  ]),
15
15
  createAccount: vi.fn(async (email: string) => ({ id: 'u2', email, name: 'B' })),
16
+ deactivate: vi.fn(async (userId: string) => userId !== 'missing'),
17
+ reactivate: vi.fn(async (userId: string) => userId !== 'missing'),
16
18
  } as unknown as AuthService;
17
19
 
18
20
  // Satisfies the route's narrow Pick<IAccountErasureService, 'eraseUser'>
@@ -91,9 +93,11 @@ describe('account routes — admin gate', () => {
91
93
  { id: 'u2', email: 'b@example.com', name: 'B', passwordHash, createdAt: new Date() },
92
94
  ];
93
95
  // A real AuthService over a stub db — the route's body is what the
94
- // service produces from full `users` rows, hash column included.
96
+ // service produces from full `users` rows, hash column included. The
97
+ // listing is sorted in-process (the email column is ciphertext in the
98
+ // database), so the read is a bare `select().from()`.
95
99
  const db = {
96
- select: () => ({ from: () => ({ orderBy: async () => rows }) }),
100
+ select: () => ({ from: async () => rows }),
97
101
  } as unknown as Database;
98
102
  const realAuth = new AuthService(db, {
99
103
  jwtSecret: 'test-jwt-secret',
@@ -116,9 +120,10 @@ describe('account routes — admin gate', () => {
116
120
  const body = JSON.parse(text) as {
117
121
  accounts: Array<{ email: string; hasPassword: boolean; isEnvAdmin: boolean }>;
118
122
  };
123
+ // Sorted by email in-process, since the column is ciphertext in the database.
119
124
  expect(body.accounts.map((a) => [a.email, a.hasPassword, a.isEnvAdmin])).toEqual([
120
- ['root@example.com', false, true],
121
125
  ['b@example.com', true, false],
126
+ ['root@example.com', false, true],
122
127
  ]);
123
128
  expect(text).not.toContain('scrypt:');
124
129
  expect(text).not.toContain('passwordHash');
@@ -172,12 +177,12 @@ describe('account routes — admin gate', () => {
172
177
  expect(JSON.stringify(body)).not.toContain('relation');
173
178
  });
174
179
 
175
- it('400s on missing fields and surfaces service validation errors', async () => {
180
+ it('400s without an email (a password is optional) and surfaces service validation errors', async () => {
176
181
  const base = await listen(makeApp({ admin: true }));
177
182
  const missing = await fetch(`${base}/api/admin/accounts`, {
178
183
  method: 'POST',
179
184
  headers: { 'Content-Type': 'application/json' },
180
- body: JSON.stringify({ email: 'b@example.com' }),
185
+ body: JSON.stringify({ name: 'B' }),
181
186
  });
182
187
  expect(missing.status).toBe(400);
183
188
 
@@ -326,3 +331,79 @@ describe('account routes — removing the erased address from access files', ()
326
331
  expect(accessRemoval.remove).not.toHaveBeenCalled();
327
332
  });
328
333
  });
334
+
335
+ describe('account routes — switching an account off and on', () => {
336
+ const post = (base: string, path: string) => fetch(`${base}/api/admin/accounts/${path}`, { method: 'POST' });
337
+
338
+ it('switches another account off and back on', async () => {
339
+ const base = await listen(makeApp({ admin: true }));
340
+ expect((await post(base, 'u2/deactivate')).status).toBe(204);
341
+ expect(authService.deactivate).toHaveBeenCalledWith('u2');
342
+ expect((await post(base, 'u2/reactivate')).status).toBe(204);
343
+ expect(authService.reactivate).toHaveBeenCalledWith('u2');
344
+ });
345
+
346
+ it('refuses an admin switching off their own account, so an admin who can sign in always remains', async () => {
347
+ vi.mocked(authService.deactivate).mockClear();
348
+ const base = await listen(makeApp({ admin: true }));
349
+ const res = await post(base, 'u1/deactivate');
350
+ expect(res.status).toBe(400);
351
+ expect(authService.deactivate).not.toHaveBeenCalled();
352
+ });
353
+
354
+ it('refuses non-admins', async () => {
355
+ vi.mocked(authService.deactivate).mockClear();
356
+ const base = await listen(makeApp({ admin: false }));
357
+ expect((await post(base, 'u2/deactivate')).status).toBe(403);
358
+ expect((await post(base, 'u2/reactivate')).status).toBe(403);
359
+ expect(authService.deactivate).not.toHaveBeenCalled();
360
+ });
361
+
362
+ it('404s for an account that does not exist', async () => {
363
+ const base = await listen(makeApp({ admin: true }));
364
+ expect((await post(base, 'missing/deactivate')).status).toBe(404);
365
+ expect((await post(base, 'missing/reactivate')).status).toBe(404);
366
+ });
367
+
368
+ it("answers a reactivation the deployment has no room for with 403 and the port's words", async () => {
369
+ vi.mocked(authService.reactivate).mockRejectedValueOnce(new AccountAdmissionRefusedError('All 3 seats are taken'));
370
+ const base = await listen(makeApp({ admin: true }));
371
+ const res = await post(base, 'u2/reactivate');
372
+ expect(res.status).toBe(403);
373
+ expect(((await res.json()) as { error: string }).error).toBe('All 3 seats are taken');
374
+ });
375
+
376
+ it('answers a refused deactivation (the deployment admin, say) with 400 and the reason', async () => {
377
+ vi.mocked(authService.deactivate).mockRejectedValueOnce(new AccountChangeRefusedError('The deployment admin cannot be switched off'));
378
+ const base = await listen(makeApp({ admin: true }));
379
+ const res = await post(base, 'u2/deactivate');
380
+ expect(res.status).toBe(400);
381
+ expect(((await res.json()) as { error: string }).error).toContain('deployment admin');
382
+ });
383
+
384
+ it('answers an unexpected failure with a 500 that names nothing of it', async () => {
385
+ vi.mocked(authService.deactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
386
+ vi.mocked(authService.reactivate).mockRejectedValueOnce(new Error('relation "users" does not exist'));
387
+ const base = await listen(makeApp({ admin: true }));
388
+ const off = await post(base, 'u2/deactivate');
389
+ expect(off.status).toBe(500);
390
+ expect(JSON.stringify(await off.json())).not.toContain('relation');
391
+ const on = await post(base, 'u2/reactivate');
392
+ expect(on.status).toBe(500);
393
+ expect(JSON.stringify(await on.json())).not.toContain('relation');
394
+ });
395
+
396
+ it('refuses a password that is not a string, rather than making an account for single sign-on', async () => {
397
+ vi.mocked(authService.createAccount).mockClear();
398
+ const base = await listen(makeApp({ admin: true }));
399
+ for (const password of [false, null, 0]) {
400
+ const res = await fetch(`${base}/api/admin/accounts`, {
401
+ method: 'POST',
402
+ headers: { 'Content-Type': 'application/json' },
403
+ body: JSON.stringify({ email: 'b@example.com', password }),
404
+ });
405
+ expect(res.status).toBe(400);
406
+ }
407
+ expect(authService.createAccount).not.toHaveBeenCalled();
408
+ });
409
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import type { Request, Response, NextFunction } from 'express';
3
3
  import { createAuthMiddleware, AUTH_COOKIE_NAME } from '../auth.middleware.js';
4
+ import { AccountDeactivatedError, AuthBackendError } from '../account-admission.js';
4
5
  import type { AuthService } from '../auth.service.js';
5
6
 
6
7
  /**
@@ -11,68 +12,90 @@ import type { AuthService } from '../auth.service.js';
11
12
  */
12
13
 
13
14
  const GOOD_TOKEN = 'good-token';
15
+ const OFF_TOKEN = 'switched-off-token';
16
+ const DOWN_TOKEN = 'database-down-token';
14
17
  const IDENTITY = { userId: 'user-1', email: 'alice@example.com' };
15
18
 
16
- function run(headers: Record<string, string>) {
17
- const verifyToken = vi.fn((token: string) => {
19
+ async function run(headers: Record<string, string>) {
20
+ const resolveSession = vi.fn(async (token: string) => {
21
+ if (token === OFF_TOKEN) throw new AccountDeactivatedError();
22
+ if (token === DOWN_TOKEN) throw new AuthBackendError(new Error('db down'));
18
23
  if (token !== GOOD_TOKEN) throw new Error('bad token');
19
24
  return IDENTITY;
20
25
  });
21
- const authService = { verifyToken } as unknown as AuthService;
26
+ const authService = { resolveSession } as unknown as AuthService;
22
27
  const req = { headers } as unknown as Request;
23
28
  const json = vi.fn();
24
29
  const status = vi.fn(() => ({ json }));
25
30
  const res = { status } as unknown as Response;
26
31
  const next = vi.fn() as unknown as NextFunction;
27
- createAuthMiddleware(authService)(req, res, next);
28
- return { req, next, status, json, verifyToken };
32
+ await createAuthMiddleware(authService)(req, res, next);
33
+ return { req, next, status, json, resolveSession };
29
34
  }
30
35
 
31
- describe('createAuthMiddleware', () => {
32
- it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', () => {
33
- const { req, next, status } = run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
36
+ describe('createAuthMiddleware', async () => {
37
+ it('authenticates a request that carries only the auth cookie (an <img>, an EventSource)', async () => {
38
+ const { req, next, status } = await run({ cookie: `${AUTH_COOKIE_NAME}=${GOOD_TOKEN}` });
34
39
  expect(next).toHaveBeenCalledTimes(1);
35
40
  expect(status).not.toHaveBeenCalled();
36
41
  expect(req.userId).toBe(IDENTITY.userId);
37
42
  expect(req.userEmail).toBe(IDENTITY.email);
38
43
  });
39
44
 
40
- it('finds the cookie among others, and decodes a percent-encoded value', () => {
41
- const { next, verifyToken } = run({
45
+ it('finds the cookie among others, and decodes a percent-encoded value', async () => {
46
+ const { next, resolveSession } = await run({
42
47
  cookie: `theme=dark; ${AUTH_COOKIE_NAME}=${encodeURIComponent(GOOD_TOKEN)}; seen=1`,
43
48
  });
44
- expect(verifyToken).toHaveBeenCalledWith(GOOD_TOKEN);
49
+ expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
45
50
  expect(next).toHaveBeenCalledTimes(1);
46
51
  });
47
52
 
48
- it('prefers the Bearer header when both are present', () => {
49
- const { next, verifyToken } = run({
53
+ it('prefers the Bearer header when both are present', async () => {
54
+ const { next, resolveSession } = await run({
50
55
  authorization: `Bearer ${GOOD_TOKEN}`,
51
56
  cookie: `${AUTH_COOKIE_NAME}=stale-cookie`,
52
57
  });
53
- expect(verifyToken).toHaveBeenCalledTimes(1);
54
- expect(verifyToken).toHaveBeenCalledWith(GOOD_TOKEN);
58
+ expect(resolveSession).toHaveBeenCalledTimes(1);
59
+ expect(resolveSession).toHaveBeenCalledWith(GOOD_TOKEN);
55
60
  expect(next).toHaveBeenCalledTimes(1);
56
61
  });
57
62
 
58
- it('refuses a request with neither', () => {
59
- const { next, status, json } = run({});
63
+ it('refuses a request with neither', async () => {
64
+ const { next, status, json } = await run({});
60
65
  expect(next).not.toHaveBeenCalled();
61
66
  expect(status).toHaveBeenCalledWith(401);
62
67
  expect(json.mock.calls[0][0].error).toContain('auth cookie');
63
68
  });
64
69
 
65
- it('refuses an empty cookie value as missing, not as a token to verify', () => {
66
- const { next, status, verifyToken } = run({ cookie: `${AUTH_COOKIE_NAME}=` });
67
- expect(verifyToken).not.toHaveBeenCalled();
70
+ it('refuses an empty cookie value as missing, not as a token to verify', async () => {
71
+ const { next, status, resolveSession } = await run({ cookie: `${AUTH_COOKIE_NAME}=` });
72
+ expect(resolveSession).not.toHaveBeenCalled();
68
73
  expect(next).not.toHaveBeenCalled();
69
74
  expect(status).toHaveBeenCalledWith(401);
70
75
  });
71
76
 
72
- it('refuses a cookie the auth service rejects', () => {
73
- const { next, status, json } = run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
77
+ it('refuses a cookie the auth service rejects', async () => {
78
+ const { next, status, json } = await run({ cookie: `${AUTH_COOKIE_NAME}=forged` });
74
79
  expect(next).not.toHaveBeenCalled();
75
80
  expect(status).toHaveBeenCalledWith(401);
76
81
  expect(json.mock.calls[0][0].error).toBe('Invalid or expired token');
77
82
  });
78
83
  });
84
+
85
+ describe('createAuthMiddleware — a switched-off account', () => {
86
+ it('refuses a valid session whose account an admin switched off, and says why', async () => {
87
+ const { next, status, json } = await run({ authorization: `Bearer ${OFF_TOKEN}` });
88
+ expect(next).not.toHaveBeenCalled();
89
+ expect(status).toHaveBeenCalledWith(401);
90
+ expect(json.mock.calls[0][0].error).toContain('switched off');
91
+ });
92
+ });
93
+
94
+ describe('createAuthMiddleware — the account cannot be looked up', () => {
95
+ it('500s rather than signing a valid session out during an outage', async () => {
96
+ const { next, status, json } = await run({ authorization: `Bearer ${DOWN_TOKEN}` });
97
+ expect(next).not.toHaveBeenCalled();
98
+ expect(status).toHaveBeenCalledWith(500);
99
+ expect(json.mock.calls[0][0].error).toBe('Authentication backend unavailable');
100
+ });
101
+ });
@@ -521,9 +521,11 @@ describe('AuthService.listAccounts', () => {
521
521
  const config = makeConfig({ adminEmail: 'root@example.com', adminPassword: 'sup3r-secret' });
522
522
 
523
523
  const withHash = await new AuthService(makeFakeDb([rows]).db, config).listAccounts();
524
+ // Sorted by email in-process: the email column is ciphertext in the
525
+ // database, so the listing orders itself after decrypting.
524
526
  expect(withHash.map((a) => [a.email, a.hasPassword, a.isEnvAdmin])).toEqual([
525
- ['root@example.com', true, true],
526
527
  ['bob@example.com', true, false],
528
+ ['root@example.com', true, true],
527
529
  ['sso@example.com', false, false],
528
530
  ]);
529
531
 
@@ -537,7 +539,7 @@ describe('AuthService.listAccounts', () => {
537
539
  expect(json).not.toContain('sup3r-secret');
538
540
  for (const account of [...withHash, ...withoutHash]) {
539
541
  expect(Object.keys(account).sort()).toEqual(
540
- ['createdAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'name'],
542
+ ['createdAt', 'deactivatedAt', 'email', 'hasPassword', 'id', 'isEnvAdmin', 'isSystem', 'name'],
541
543
  );
542
544
  }
543
545
  });
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Whether a NEW account may be provisioned on this deployment, asked before
3
- * the row is inserted. The one seam a host that sells seats needs: core
4
- * admits everyone, and a cloud overlay answers from its plan without
5
- * forking any sign-in path.
3
+ * the row is inserted — and whether a deactivated one may be switched back
4
+ * on, which takes up the same place. The one seam a host that sells seats
5
+ * needs: core admits everyone, and a cloud overlay answers from its plan
6
+ * without forking any sign-in path.
6
7
  *
7
- * Asked only for an address with no account yet. An existing user's sign-in
8
- * is never a provisioning, so a refusal can never lock out someone who is
9
- * already in.
8
+ * Never asked for an account that is already on. An existing user's sign-in
9
+ * is not a provisioning, so a refusal can never lock out someone who is
10
+ * already in; taking someone out is the admin's act (deactivation), not the
11
+ * port's.
10
12
  */
11
13
  export type AccountProvisionReason =
12
14
  /** A first sign-in through single sign-on, which provisions on its own. */
@@ -16,9 +18,29 @@ export type AccountProvisionReason =
16
18
  /** The deployment owner's first password sign-in, which creates their row. */
17
19
  | 'bootstrap'
18
20
  /** A verified identity from an embedding surface that needs a row without a session. */
19
- | 'embed';
21
+ | 'embed'
22
+ /** An admin switching a deactivated account back on. */
23
+ | 'reactivate';
20
24
 
21
- export type AccountAdmissionVerdict = { ok: true } | { ok: false; message: string };
25
+ export type AccountAdmissionVerdict =
26
+ | { ok: true }
27
+ | {
28
+ ok: false;
29
+ message: string;
30
+ /**
31
+ * Keep the person on file instead of turning them away: the account is
32
+ * created DEACTIVATED, the sign-in is still refused with `message`, and
33
+ * an admin finds them on the accounts list, ready to be switched on.
34
+ * For a host whose rule is "whoever is on our domain may join while
35
+ * there is room" — the one past the room waits instead of bouncing.
36
+ *
37
+ * Honoured only for a sign-in through single sign-on, the one
38
+ * provisioning nobody asked for. Everywhere else a verdict carrying it
39
+ * is a plain refusal: an admin creating an account would be told no,
40
+ * and a deactivated account is never reactivated into one.
41
+ */
42
+ waitForAdmin?: boolean;
43
+ };
22
44
 
23
45
  export interface IAccountAdmission {
24
46
  canProvision(email: string, reason: AccountProvisionReason): Promise<AccountAdmissionVerdict>;
@@ -36,9 +58,56 @@ export const admitEveryone: IAccountAdmission = {
36
58
  */
37
59
  export class AccountAdmissionRefusedError extends Error {
38
60
  readonly status = 403;
61
+ /** The person was put on file, switched off, for an admin to switch on (see `waitForAdmin`). */
62
+ readonly waitingForAdmin: boolean;
39
63
 
40
- constructor(message: string) {
64
+ constructor(message: string, opts: { waitingForAdmin?: boolean } = {}) {
41
65
  super(message);
42
66
  this.name = 'AccountAdmissionRefusedError';
67
+ this.waitingForAdmin = opts.waitingForAdmin ?? false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * The account exists and an admin switched it off — at sign-in, or for a
73
+ * credential it still holds. A 403 like a refusal: the identity is fine,
74
+ * the deployment no longer lets it in.
75
+ */
76
+ export class AccountDeactivatedError extends Error {
77
+ readonly status = 403;
78
+
79
+ constructor(message = ACCOUNT_DEACTIVATED_MESSAGE) {
80
+ super(message);
81
+ this.name = 'AccountDeactivatedError';
43
82
  }
44
83
  }
84
+
85
+ /**
86
+ * Whether the account behind a credential is on could not be asked — the
87
+ * database failed. Not the caller's fault and not a verdict on them: a
88
+ * route answers it with a 500, never with the 401 that would sign a valid
89
+ * caller out during an outage.
90
+ */
91
+ export class AuthBackendError extends Error {
92
+ constructor(readonly cause: unknown) {
93
+ super('The account could not be looked up');
94
+ this.name = 'AuthBackendError';
95
+ }
96
+ }
97
+
98
+ /**
99
+ * An admin's change to an account that the deployment refuses on purpose
100
+ * (switching off the deployment admin, or an account the platform runs its
101
+ * own work as). Its message is for the admin; a route answers it with 400.
102
+ */
103
+ export class AccountChangeRefusedError extends Error {
104
+ readonly status = 400;
105
+
106
+ constructor(message: string) {
107
+ super(message);
108
+ this.name = 'AccountChangeRefusedError';
109
+ }
110
+ }
111
+
112
+ /** What a person with a deactivated account is told, wherever they knock. */
113
+ export const ACCOUNT_DEACTIVATED_MESSAGE = 'Your account on this workspace is switched off. Ask its admin to turn it back on.';
@@ -3,6 +3,7 @@ import { logger } from '../../shared/logging.js';
3
3
 
4
4
  const log = logger('account-erasure');
5
5
  import { and, eq, notExists } from 'drizzle-orm';
6
+ import { isEncryptedBlob } from '../../shared/column-crypto.js';
6
7
  import type { Database } from '../database/connection.js';
7
8
  import type { IReviewWorkflowService } from '../workflow/review-workflow/review-workflow.interface.js';
8
9
  import {
@@ -123,17 +124,34 @@ export class AccountErasureService implements IAccountErasureService {
123
124
  ) {}
124
125
 
125
126
  async listUsers(): Promise<AdminUserView[]> {
127
+ // Sorted in-process: `email` is ciphertext in the database, so ORDER BY
128
+ // would sort by IV noise. One row per team member.
126
129
  const rows = await this.db
127
130
  .select({ id: users.id, email: users.email, name: users.name, createdAt: users.createdAt })
128
- .from(users)
129
- .orderBy(users.email);
130
- return rows.map((r) => ({ ...r, createdAt: r.createdAt.getTime() }));
131
+ .from(users);
132
+ return rows
133
+ .map((r) => ({ ...r, createdAt: r.createdAt.getTime() }))
134
+ .sort((a, b) => a.email.localeCompare(b.email));
131
135
  }
132
136
 
133
137
  async eraseUser(userId: string, opts: { erasureId?: string } = {}): Promise<boolean> {
134
138
  const [user] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
135
139
  if (!user) return false;
136
140
 
141
+ // Fail CLOSED on an undecryptable email. The encrypted column's read
142
+ // fallback returns the raw ciphertext when the configured key cannot open
143
+ // it (a rotated or wrong SECRETS_ENC_KEY); a blind index of that blob
144
+ // would match zero audit rows while the user delete still committed — an
145
+ // erasure that silently left the personal data behind. Nothing is
146
+ // deleted until the key is fixed.
147
+ if (isEncryptedBlob(user.email)) {
148
+ throw new Error(
149
+ `account-erasure: cannot decrypt the stored email for user id=${userId} with the ` +
150
+ 'configured SECRETS_ENC_KEY — refusing to erase, because the email-keyed audit rows ' +
151
+ 'could not be anonymised. Restore the correct key, then retry.',
152
+ );
153
+ }
154
+
137
155
  const target: ErasureTarget = {
138
156
  userId,
139
157
  email: user.email.toLowerCase(),
@@ -149,6 +167,14 @@ export class AccountErasureService implements IAccountErasureService {
149
167
  if (p.before) await p.before(target);
150
168
  }
151
169
 
170
+ // Every email-keyed row below is matched through its blind index — the
171
+ // email columns are randomized ciphertext — and the index is rewritten to
172
+ // the placeholder's too, so no value keyed to the erased address survives.
173
+ // An index column is compared and written with the address it is the
174
+ // index of; the handle the statement runs on makes the index.
175
+ const emailBidx = target.email;
176
+ const erasedBidx = target.erasedEmail;
177
+
152
178
  const postCommit: Array<() => Promise<void>> = [];
153
179
  await this.db.transaction(async (tx) => {
154
180
  // Token-shaped rows (all hashed, but they key to the user). Dependents
@@ -180,7 +206,7 @@ export class AccountErasureService implements IAccountErasureService {
180
206
  // writing anything back — so no row is resurrected here either.
181
207
  await tx
182
208
  .delete(pluginJoinRequests)
183
- .where(eq(pluginJoinRequests.requesterEmail, target.email));
209
+ .where(eq(pluginJoinRequests.requesterEmailBidx, emailBidx));
184
210
 
185
211
  // Audit rows: anonymize in place (no user FK on these; they key by email).
186
212
  //
@@ -197,23 +223,34 @@ export class AccountErasureService implements IAccountErasureService {
197
223
  });
198
224
  await tx
199
225
  .update(prMergeLog)
200
- .set({ triggeredByEmail: target.erasedEmail, triggeredByName: target.erasedName })
201
- .where(eq(prMergeLog.triggeredByEmail, target.email));
226
+ .set({ triggeredByEmail: target.erasedEmail, triggeredByEmailBidx: erasedBidx, triggeredByName: target.erasedName })
227
+ .where(eq(prMergeLog.triggeredByEmailBidx, emailBidx));
202
228
  await tx
203
229
  .update(prComments)
204
- .set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
205
- .where(eq(prComments.authorEmail, target.email));
230
+ .set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
231
+ .where(eq(prComments.authorEmailBidx, emailBidx));
206
232
  await tx
207
233
  .update(changeRequests)
208
- .set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
209
- .where(eq(changeRequests.authorEmail, target.email));
234
+ .set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
235
+ .where(eq(changeRequests.authorEmailBidx, emailBidx));
236
+ // The refusal a still-open request shows ("<name> could not apply
237
+ // this"), on requests of any author — found by the index of the address
238
+ // it was recorded with, like every other row. One recorded before the
239
+ // encryption release kept no address, and has no name to find either:
240
+ // the first start on this release took it off (`migrate.ts`,
241
+ // `clearUnindexedRefusalNames`), because matching such a row by display
242
+ // name missed a person who had been renamed and hit their namesake.
243
+ await tx
244
+ .update(changeRequests)
245
+ .set({ applyFailedByName: target.erasedName, applyFailedByEmailBidx: erasedBidx })
246
+ .where(eq(changeRequests.applyFailedByEmailBidx, emailBidx));
210
247
  // Queued-but-uncommitted saves: the eventual git commit is authored with
211
248
  // the placeholder instead of the erased identity. The file content still
212
249
  // lands — erasing an account must not lose other people's KB state.
213
250
  await tx
214
251
  .update(pendingCommits)
215
- .set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
216
- .where(eq(pendingCommits.authorEmail, target.email));
252
+ .set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
253
+ .where(eq(pendingCommits.authorEmailBidx, emailBidx));
217
254
 
218
255
  // Module-owned rows, before the users delete so FKs onto users are
219
256
  // still satisfiable — and so a participant that MISSES rows makes the
@@ -251,11 +288,11 @@ export class AccountErasureService implements IAccountErasureService {
251
288
  // approvals they make are their own.
252
289
  await this.db
253
290
  .update(prFileApprovals)
254
- .set({ approverEmail: target.erasedEmail, approverName: target.erasedName })
291
+ .set({ approverEmail: target.erasedEmail, approverEmailBidx: erasedBidx, approverName: target.erasedName })
255
292
  .where(
256
293
  and(
257
- eq(prFileApprovals.approverEmail, target.email),
258
- notExists(this.db.select({ id: users.id }).from(users).where(eq(users.email, target.email))),
294
+ eq(prFileApprovals.approverEmailBidx, emailBidx),
295
+ notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
259
296
  ),
260
297
  );
261
298
 
@@ -272,11 +309,25 @@ export class AccountErasureService implements IAccountErasureService {
272
309
  // person, and their requests are their own.
273
310
  await this.db
274
311
  .update(changeRequests)
275
- .set({ authorEmail: target.erasedEmail, authorName: target.erasedName })
312
+ .set({ authorEmail: target.erasedEmail, authorEmailBidx: erasedBidx, authorName: target.erasedName })
313
+ .where(
314
+ and(
315
+ eq(changeRequests.authorEmailBidx, emailBidx),
316
+ notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
317
+ ),
318
+ );
319
+
320
+ // And the refusal note, for the apply this person was inside when the
321
+ // commit landed: recording it takes no lock and names no user row, so it
322
+ // can write their name after the rewrite above. Same statement, same
323
+ // guard, same reason.
324
+ await this.db
325
+ .update(changeRequests)
326
+ .set({ applyFailedByName: target.erasedName, applyFailedByEmailBidx: erasedBidx })
276
327
  .where(
277
328
  and(
278
- eq(changeRequests.authorEmail, target.email),
279
- notExists(this.db.select({ id: users.id }).from(users).where(eq(users.email, target.email))),
329
+ eq(changeRequests.applyFailedByEmailBidx, emailBidx),
330
+ notExists(this.db.select({ id: users.id }).from(users).where(eq(users.emailBidx, emailBidx))),
280
331
  ),
281
332
  );
282
333
 
@@ -6,7 +6,7 @@ import { printable } from '../../shared/printable.js';
6
6
  const log = logger('accounts');
7
7
  import type { AuthUser } from '@bevel-software/platform-shared';
8
8
  import type { AuthService } from './auth.service.js';
9
- import { AccountAdmissionRefusedError } from './account-admission.js';
9
+ import { AccountAdmissionRefusedError, AccountChangeRefusedError } from './account-admission.js';
10
10
  import { erasedAccountId, type IAccountErasureService } from './account-erasure.service.js';
11
11
  import type { IAdminAccessService } from '../admin/admin.interface.js';
12
12
  import type { UserAccessRemovalService } from '../access/user-access-removal.service.js';
@@ -26,7 +26,7 @@ import './auth.middleware.js'; // Express Request augmentation
26
26
  * bootstrap admin (always-admin, see AdminAccessService).
27
27
  */
28
28
  export function createAccountRoutes(
29
- authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById'>,
29
+ authService: Pick<AuthService, 'listAccounts' | 'createAccount' | 'getUserById' | 'deactivate' | 'reactivate'>,
30
30
  adminAccess: IAdminAccessService,
31
31
  accountErasure: Pick<IAccountErasureService, 'eraseUser'>,
32
32
  accessRemoval?: Pick<UserAccessRemovalService, 'report' | 'assertRemovable' | 'remove' | 'filesNaming'>,
@@ -47,17 +47,23 @@ export function createAccountRoutes(
47
47
  res.json({ accounts: await authService.listAccounts() });
48
48
  });
49
49
 
50
- // POST /api/admin/accounts { email, name?, password } — create an account
50
+ // POST /api/admin/accounts { email, name?, password? } — create an account
51
51
  // (or reset the password of an existing one; upsert-by-email is deliberate,
52
- // see AuthService.createAccount).
52
+ // see AuthService.createAccount). Without a password the account is for
53
+ // single sign-on: the person finds it waiting when they first sign in.
53
54
  router.post('/admin/accounts', requireAdmin, async (req, res) => {
54
55
  const { email, name, password } = req.body as {
55
56
  email?: string;
56
57
  name?: string;
57
- password?: string;
58
+ password?: unknown;
58
59
  };
59
- if (!email || !password) {
60
- res.status(400).json({ error: 'email and password are required' });
60
+ if (!email) {
61
+ res.status(400).json({ error: 'email is required' });
62
+ return;
63
+ }
64
+ // Absent means single sign-on; anything else that is not text is a mistake, not that.
65
+ if (password !== undefined && typeof password !== 'string') {
66
+ res.status(400).json({ error: 'password must be a string' });
61
67
  return;
62
68
  }
63
69
  try {
@@ -71,6 +77,56 @@ export function createAccountRoutes(
71
77
  }
72
78
  });
73
79
 
80
+ // POST /api/admin/accounts/:userId/deactivate — switch an account off. It
81
+ // keeps its row, history and place in roles and groups; nothing it holds
82
+ // signs in any more (see AuthService.isActive). An admin's own account is
83
+ // refused, so whoever does this is still an admin who is on afterwards —
84
+ // the deployment is never left with no admin able to sign in.
85
+ router.post('/admin/accounts/:userId/deactivate', requireAdmin, async (req, res) => {
86
+ const userId = String(req.params.userId);
87
+ if (userId === req.userId) {
88
+ res.status(400).json({ error: 'You cannot switch off your own account. Ask another admin.' });
89
+ return;
90
+ }
91
+ try {
92
+ if (!(await authService.deactivate(userId))) {
93
+ res.status(404).json({ error: 'No such user' });
94
+ return;
95
+ }
96
+ log.info('account audit:', { action: 'deactivate', actorUserId: req.userId, targetUserId: userId });
97
+ res.status(204).end();
98
+ } catch (error) {
99
+ if (error instanceof AccountChangeRefusedError) {
100
+ res.status(400).json({ error: error.message });
101
+ return;
102
+ }
103
+ log.error('switching an account off failed:', { err: error });
104
+ res.status(500).json({ error: 'Could not switch this account off' });
105
+ }
106
+ });
107
+
108
+ // POST /api/admin/accounts/:userId/reactivate — switch it back on, if the
109
+ // deployment has room: the admission port is asked, as for a new account
110
+ // (a host that sells seats answers from its plan; 403 with its words).
111
+ router.post('/admin/accounts/:userId/reactivate', requireAdmin, async (req, res) => {
112
+ const userId = String(req.params.userId);
113
+ try {
114
+ if (!(await authService.reactivate(userId))) {
115
+ res.status(404).json({ error: 'No such user' });
116
+ return;
117
+ }
118
+ log.info('account audit:', { action: 'reactivate', actorUserId: req.userId, targetUserId: userId });
119
+ res.status(204).end();
120
+ } catch (error) {
121
+ if (error instanceof AccountAdmissionRefusedError) {
122
+ res.status(403).json({ error: error.message });
123
+ return;
124
+ }
125
+ log.error('switching an account on failed:', { err: error });
126
+ res.status(500).json({ error: 'Could not switch this account on' });
127
+ }
128
+ });
129
+
74
130
  // GET /api/admin/accounts/:userId/references — how many places in the
75
131
  // knowledge base name this account's address (roles, groups, access rules,
76
132
  // file grants) and whether removing them is allowed. Feeds the delete