@bevel-software/platform-core-backend 0.21.0 → 0.23.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 (434) 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 +62 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +22 -3
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +158 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  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-requests.contract.d.ts +75 -0
  31. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  32. package/dist/modules/access/access-requests.contract.js +20 -0
  33. package/dist/modules/access/access-requests.contract.js.map +1 -0
  34. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  35. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  36. package/dist/modules/access/access-requests.routes.js +237 -0
  37. package/dist/modules/access/access-requests.routes.js.map +1 -0
  38. package/dist/modules/access/access-requests.service.d.ts +123 -0
  39. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-requests.service.js +337 -0
  41. package/dist/modules/access/access-requests.service.js.map +1 -0
  42. package/dist/modules/access/access.routes.d.ts.map +1 -1
  43. package/dist/modules/access/access.routes.js +13 -15
  44. package/dist/modules/access/access.routes.js.map +1 -1
  45. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  46. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  47. package/dist/modules/access-model/access-grammar.js +24 -0
  48. package/dist/modules/access-model/access-grammar.js.map +1 -1
  49. package/dist/modules/auth/account-admission.d.ts +58 -7
  50. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  51. package/dist/modules/auth/account-admission.js +44 -1
  52. package/dist/modules/auth/account-admission.js.map +1 -1
  53. package/dist/modules/auth/account.routes.d.ts +1 -1
  54. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  55. package/dist/modules/auth/account.routes.js +61 -5
  56. package/dist/modules/auth/account.routes.js.map +1 -1
  57. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  58. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  59. package/dist/modules/auth/auth.middleware.js +18 -7
  60. package/dist/modules/auth/auth.middleware.js.map +1 -1
  61. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  62. package/dist/modules/auth/auth.routes.js +8 -0
  63. package/dist/modules/auth/auth.routes.js.map +1 -1
  64. package/dist/modules/auth/auth.service.d.ts +96 -3
  65. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  66. package/dist/modules/auth/auth.service.js +187 -9
  67. package/dist/modules/auth/auth.service.js.map +1 -1
  68. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  69. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  70. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  71. package/dist/modules/database/core-schema.d.ts +41 -24
  72. package/dist/modules/database/core-schema.d.ts.map +1 -1
  73. package/dist/modules/database/core-schema.js +35 -27
  74. package/dist/modules/database/core-schema.js.map +1 -1
  75. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  76. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  77. package/dist/modules/github-app/github-app.client.js +222 -0
  78. package/dist/modules/github-app/github-app.client.js.map +1 -0
  79. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  80. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  81. package/dist/modules/github-app/github-app.connection.js +185 -0
  82. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  83. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  84. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  85. package/dist/modules/github-app/github-app.routes.js +323 -0
  86. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  87. package/dist/modules/github-app/index.d.ts +4 -0
  88. package/dist/modules/github-app/index.d.ts.map +1 -0
  89. package/dist/modules/github-app/index.js +4 -0
  90. package/dist/modules/github-app/index.js.map +1 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  92. package/dist/modules/kb-fs/branch-name.js +12 -2
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  94. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  95. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/remote-url.js +35 -0
  97. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  98. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  99. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/repo-path.js +79 -0
  101. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  102. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  104. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  105. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  106. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  108. package/dist/modules/kb-sync/sync-auth.js +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  110. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  112. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  113. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  114. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  115. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  116. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  117. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  118. package/dist/modules/plugins/join-proposals.js +76 -22
  119. package/dist/modules/plugins/join-proposals.js.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.d.ts +111 -18
  121. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-requests.service.js +173 -29
  123. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  124. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  125. package/dist/modules/plugins/plugins.routes.js +3 -2
  126. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  127. package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
  128. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  129. package/dist/modules/settings/deployment-settings.service.js +101 -7
  130. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  131. package/dist/modules/settings/managed-repository.d.ts +43 -0
  132. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  133. package/dist/modules/settings/managed-repository.js +60 -0
  134. package/dist/modules/settings/managed-repository.js.map +1 -0
  135. package/dist/modules/settings/repository-source.d.ts +128 -0
  136. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  137. package/dist/modules/settings/repository-source.js +150 -0
  138. package/dist/modules/settings/repository-source.js.map +1 -0
  139. package/dist/modules/settings/setup.routes.d.ts +78 -4
  140. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  141. package/dist/modules/settings/setup.routes.js +379 -17
  142. package/dist/modules/settings/setup.routes.js.map +1 -1
  143. package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
  144. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  145. package/dist/modules/skills/skill-access-requests.routes.js +24 -63
  146. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  147. package/dist/modules/skills/skills.contract.d.ts +90 -18
  148. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  149. package/dist/modules/skills/skills.contract.js +4 -1
  150. package/dist/modules/skills/skills.contract.js.map +1 -1
  151. package/dist/modules/skills/skills.service.d.ts +98 -9
  152. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  153. package/dist/modules/skills/skills.service.js +239 -37
  154. package/dist/modules/skills/skills.service.js.map +1 -1
  155. package/dist/modules/skills/skills.tools.d.ts +7 -0
  156. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  157. package/dist/modules/skills/skills.tools.js +71 -7
  158. package/dist/modules/skills/skills.tools.js.map +1 -1
  159. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  160. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  161. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  162. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  163. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  164. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  165. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  166. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  167. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  168. package/dist/modules/tool-helpers/tool-context.js +15 -0
  169. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  170. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  171. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  172. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  173. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  174. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  175. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  176. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  177. package/dist/modules/tool-helpers/validate-token.js +1 -1
  178. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  179. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  180. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  181. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  182. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  183. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  184. package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
  185. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  186. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  187. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  188. package/dist/modules/workflow/file-lock.service.js +30 -0
  189. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  190. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  191. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  192. package/dist/modules/workflow/git/git.service.js +170 -6
  193. package/dist/modules/workflow/git/git.service.js.map +1 -1
  194. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  195. package/dist/modules/workflow/git/node-git-runner.js +5 -0
  196. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  197. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  198. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  199. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  200. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  201. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  202. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  203. package/dist/modules/workflow/pending-commits.service.js +55 -0
  204. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  205. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  206. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  207. package/dist/modules/workflow/workflow-hooks.js +16 -1
  208. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  209. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  210. package/dist/modules/workflow/workflow.routes.js +3 -1
  211. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  212. package/dist/modules/workflow/workflow.service.d.ts +94 -13
  213. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  214. package/dist/modules/workflow/workflow.service.js +258 -51
  215. package/dist/modules/workflow/workflow.service.js.map +1 -1
  216. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  217. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  218. package/dist/modules/workspace/agent-access.gate.js +123 -0
  219. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  220. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  221. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  222. package/dist/modules/workspace/routine-write-policy.js +5 -6
  223. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  224. package/dist/modules/workspace/session-sink.d.ts +5 -5
  225. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  226. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  227. package/dist/modules/workspace/set-aside-clone.js +92 -0
  228. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  229. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
  230. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
  232. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +92 -3
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +272 -26
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  241. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  242. package/dist/modules/workspace/workspace.tools.js +337 -117
  243. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  244. package/dist/modules/write-access/write-access.d.ts +60 -0
  245. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  246. package/dist/modules/write-access/write-access.js +129 -0
  247. package/dist/modules/write-access/write-access.js.map +1 -0
  248. package/dist/shared/domain-errors.d.ts +64 -0
  249. package/dist/shared/domain-errors.d.ts.map +1 -1
  250. package/dist/shared/domain-errors.js +83 -0
  251. package/dist/shared/domain-errors.js.map +1 -1
  252. package/dist/shared/git.contract.d.ts +30 -0
  253. package/dist/shared/git.contract.d.ts.map +1 -1
  254. package/dist/shared/git.contract.js +26 -0
  255. package/dist/shared/git.contract.js.map +1 -1
  256. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  257. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  258. package/dist/tenancy/static-tenant-source.js +0 -2
  259. package/dist/tenancy/static-tenant-source.js.map +1 -1
  260. package/migrations/0014_change_request_closed_reason.sql +1 -0
  261. package/migrations/0015_account_deactivation.sql +3 -0
  262. package/migrations/meta/0014_snapshot.json +2265 -0
  263. package/migrations/meta/0015_snapshot.json +2271 -0
  264. package/migrations/meta/_journal.json +14 -0
  265. package/package.json +3 -3
  266. package/src/__tests__/kb-layout-config.test.ts +0 -2
  267. package/src/__tests__/retired-settings.test.ts +96 -0
  268. package/src/core/__tests__/gated-boot.test.ts +132 -0
  269. package/src/core/__tests__/lifecycle.test.ts +170 -1
  270. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  271. package/src/core/core-ports.ts +11 -1
  272. package/src/core/create-core-server.ts +69 -15
  273. package/src/core/create-core-services.ts +190 -27
  274. package/src/core/lifecycle.ts +98 -13
  275. package/src/core-config.ts +13 -14
  276. package/src/index.ts +12 -1
  277. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  278. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  279. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  280. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  281. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  282. package/src/modules/access/access-control.interface.ts +43 -12
  283. package/src/modules/access/access-control.service.ts +234 -36
  284. package/src/modules/access/access-requests.contract.ts +93 -0
  285. package/src/modules/access/access-requests.routes.ts +286 -0
  286. package/src/modules/access/access-requests.service.ts +420 -0
  287. package/src/modules/access/access.routes.ts +13 -15
  288. package/src/modules/access-model/access-grammar.ts +22 -0
  289. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  290. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  291. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  292. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  293. package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
  294. package/src/modules/auth/account-admission.ts +78 -9
  295. package/src/modules/auth/account.routes.ts +63 -7
  296. package/src/modules/auth/auth.middleware.ts +19 -8
  297. package/src/modules/auth/auth.routes.ts +8 -0
  298. package/src/modules/auth/auth.service.ts +207 -7
  299. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  300. package/src/modules/database/core-schema.ts +35 -27
  301. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  302. package/src/modules/github-app/github-app.client.ts +268 -0
  303. package/src/modules/github-app/github-app.connection.ts +204 -0
  304. package/src/modules/github-app/github-app.routes.ts +359 -0
  305. package/src/modules/github-app/index.ts +19 -0
  306. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  307. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  308. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  309. package/src/modules/kb-fs/branch-name.ts +14 -1
  310. package/src/modules/kb-fs/remote-url.ts +35 -0
  311. package/src/modules/kb-fs/repo-path.ts +85 -0
  312. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  313. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  314. package/src/modules/kb-sync/sync-auth.ts +2 -2
  315. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  316. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  317. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  318. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  319. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  320. package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
  321. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  322. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  323. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  324. package/src/modules/plugins/join-proposals.ts +87 -19
  325. package/src/modules/plugins/join-requests.service.ts +199 -39
  326. package/src/modules/plugins/plugins.routes.ts +3 -2
  327. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  328. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  329. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
  330. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
  331. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
  332. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  333. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  334. package/src/modules/settings/deployment-settings.service.ts +117 -6
  335. package/src/modules/settings/managed-repository.ts +68 -0
  336. package/src/modules/settings/repository-source.ts +197 -0
  337. package/src/modules/settings/setup.routes.ts +463 -15
  338. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  339. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
  341. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  342. package/src/modules/skills/skill-access-requests.routes.ts +25 -75
  343. package/src/modules/skills/skills.contract.ts +91 -18
  344. package/src/modules/skills/skills.service.ts +278 -41
  345. package/src/modules/skills/skills.tools.ts +80 -8
  346. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  347. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  348. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  349. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  350. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  351. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  352. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  353. package/src/modules/tool-helpers/tool-context.ts +15 -0
  354. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  355. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  356. package/src/modules/tool-helpers/validate-token.ts +1 -1
  357. package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
  358. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  359. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  360. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
  361. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  362. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
  363. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
  364. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  365. package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
  366. package/src/modules/workflow/file-lock.service.ts +31 -0
  367. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  368. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
  369. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  370. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  371. package/src/modules/workflow/git/git.service.ts +193 -4
  372. package/src/modules/workflow/git/node-git-runner.ts +6 -0
  373. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  374. package/src/modules/workflow/pending-commits.service.ts +59 -1
  375. package/src/modules/workflow/workflow-hooks.ts +64 -26
  376. package/src/modules/workflow/workflow.routes.ts +3 -1
  377. package/src/modules/workflow/workflow.service.ts +290 -55
  378. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  379. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  380. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  381. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  382. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  383. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  384. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  385. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  386. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  387. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  388. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
  389. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  390. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
  391. package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
  392. package/src/modules/workspace/agent-access.gate.ts +164 -0
  393. package/src/modules/workspace/routine-write-policy.ts +5 -6
  394. package/src/modules/workspace/session-sink.ts +5 -5
  395. package/src/modules/workspace/set-aside-clone.ts +96 -0
  396. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
  397. package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
  398. package/src/modules/workspace/workspace.routes.ts +95 -3
  399. package/src/modules/workspace/workspace.service.ts +273 -25
  400. package/src/modules/workspace/workspace.tools.ts +373 -119
  401. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  402. package/src/modules/write-access/write-access.ts +153 -0
  403. package/src/shared/domain-errors.ts +89 -0
  404. package/src/shared/git.contract.ts +37 -0
  405. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  406. package/src/tenancy/static-tenant-source.ts +0 -3
  407. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  408. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  409. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  410. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  411. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  412. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  413. package/dist/modules/workflow/session-ontology.service.js +0 -147
  414. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  415. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  416. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  417. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  418. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  419. package/dist/shared/kb-layout.d.ts +0 -39
  420. package/dist/shared/kb-layout.d.ts.map +0 -1
  421. package/dist/shared/kb-layout.js +0 -103
  422. package/dist/shared/kb-layout.js.map +0 -1
  423. package/dist/shared/kb-layout.test.d.ts +0 -2
  424. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  425. package/dist/shared/kb-layout.test.js +0 -75
  426. package/dist/shared/kb-layout.test.js.map +0 -1
  427. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  428. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  429. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  430. package/src/modules/workflow/session-ontology.service.ts +0 -183
  431. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  432. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  433. package/src/shared/kb-layout.test.ts +0 -98
  434. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,191 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { NodeGitRunner } from '../../workflow/git/node-git-runner.js';
6
+ import { ManagedRepository, MANAGED_REPOSITORY_DIR } from '../managed-repository.js';
7
+ import { RepositorySource, type GitHubAppRepository } from '../repository-source.js';
8
+
9
+ let root: string;
10
+
11
+ beforeEach(async () => {
12
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'hexis-managed-'));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(root, { recursive: true, force: true });
17
+ });
18
+
19
+ const reading = (values: Record<string, string>) => (key: string) => values[key] ?? '';
20
+
21
+ function source(values: Record<string, string>, extra: { fallbackToken?: string; githubApp?: GitHubAppRepository } = {}) {
22
+ const managed = new ManagedRepository(path.join(root, 'managed-repository'));
23
+ return {
24
+ managed,
25
+ source: new RepositorySource({
26
+ read: reading(values),
27
+ fallback: { username: 'x-access-token', token: extra.fallbackToken ?? '' },
28
+ managed,
29
+ ...(extra.githubApp ? { githubApp: extra.githubApp } : {}),
30
+ }),
31
+ };
32
+ }
33
+
34
+ const HOSTED = { kbRepoUrl: 'https://git.example.com/acme/kb.git', gitToken: 'the-token', gitUsername: 'oauth2' };
35
+
36
+ describe('RepositorySource: which way a deployment has its repository', () => {
37
+ it('has none while nothing is configured', () => {
38
+ const { source: s } = source({});
39
+ expect(s.mode()).toBeNull();
40
+ expect(s.url()).toBe('');
41
+ expect(s.answered()).toBe(false);
42
+ expect(s.credentials.token()).toBeNull();
43
+ });
44
+
45
+ it('reads a deployment configured before there was a choice as what it has: an address and a token', () => {
46
+ const { source: s } = source(HOSTED);
47
+ expect(s.mode()).toBe('token');
48
+ expect(s.url()).toBe(HOSTED.kbRepoUrl);
49
+ expect(s.credentials.token()).toBe('the-token');
50
+ expect(s.credentials.username()).toBe('oauth2');
51
+ expect(s.answered()).toBe(true);
52
+ });
53
+
54
+ it('reads a token the environment spells the old way', () => {
55
+ const { source: s } = source({ kbRepoUrl: HOSTED.kbRepoUrl }, { fallbackToken: 'from-GITHUB_TOKEN' });
56
+ expect(s.mode()).toBe('token');
57
+ expect(s.credentials.token()).toBe('from-GITHUB_TOKEN');
58
+ expect(s.answered()).toBe(true);
59
+ });
60
+
61
+ it('is not answered by half of an address and a token', () => {
62
+ expect(source({ kbRepoUrl: HOSTED.kbRepoUrl }).source.answered()).toBe(false);
63
+ expect(source({ gitToken: 'the-token' }).source.answered()).toBe(false);
64
+ });
65
+
66
+ it('ignores a mode it does not know, and reads what is configured', () => {
67
+ expect(source({ ...HOSTED, gitMode: 'carrier-pigeon' }).source.mode()).toBe('token');
68
+ });
69
+
70
+ it('answers for the values a save would put in effect, without the save', () => {
71
+ const { source: s, managed } = source(HOSTED);
72
+ const after = reading({ ...HOSTED, gitMode: 'managed' });
73
+ expect(s.mode(after)).toBe('managed');
74
+ expect(s.url(after)).toBe(managed.path);
75
+ // What is in effect has not moved.
76
+ expect(s.mode()).toBe('token');
77
+ });
78
+ });
79
+
80
+ describe('RepositorySource: a repository the deployment keeps', () => {
81
+ it('is reached by its path, and presented nothing', () => {
82
+ const { source: s, managed } = source({ gitMode: 'managed' });
83
+ expect(s.url()).toBe(managed.path);
84
+ expect(s.url()).toBe(path.join(root, 'managed-repository', MANAGED_REPOSITORY_DIR));
85
+ expect(s.answered()).toBe(true);
86
+ expect(s.credentials.token()).toBeNull();
87
+ });
88
+
89
+ /**
90
+ * One mode's credential is never presented to another mode's repository.
91
+ * A deployment that moved still has its old token stored, and it must not
92
+ * ride along in the environment of every git call made against a folder.
93
+ */
94
+ it('is never presented the token of the repository the deployment had before', () => {
95
+ const { source: s } = source({ ...HOSTED, gitMode: 'managed' }, { fallbackToken: 'from-the-environment' });
96
+ expect(s.credentials.token()).toBeNull();
97
+ expect(s.credentials.username()).toBe('x-access-token');
98
+ });
99
+ });
100
+
101
+ describe('RepositorySource: a repository reached through a GitHub App', () => {
102
+ const app = (overrides: Partial<GitHubAppRepository> = {}): GitHubAppRepository => ({
103
+ url: () => 'https://github.com/acme/kb.git',
104
+ token: () => 'an-installation-token',
105
+ prepare: async () => undefined,
106
+ answered: () => true,
107
+ ...overrides,
108
+ });
109
+
110
+ it('asks the app for the address and the token', () => {
111
+ const { source: s } = source({ gitMode: 'github-app', gitToken: 'a-leftover' }, { githubApp: app() });
112
+ expect(s.url()).toBe('https://github.com/acme/kb.git');
113
+ expect(s.credentials.token()).toBe('an-installation-token');
114
+ expect(s.credentials.username()).toBe('x-access-token');
115
+ expect(s.answered()).toBe(true);
116
+ });
117
+
118
+ it('renews the token before git is run, and only in this mode', async () => {
119
+ let renewed = 0;
120
+ const githubApp = app({ prepare: async () => void (renewed += 1) });
121
+ await source({ gitMode: 'github-app' }, { githubApp }).source.credentials.prepare?.();
122
+ await source(HOSTED, { githubApp }).source.credentials.prepare?.();
123
+ await source({ gitMode: 'managed' }, { githubApp }).source.credentials.prepare?.();
124
+ expect(renewed).toBe(1);
125
+ });
126
+
127
+ it('is unanswered on a deployment that has no such app', () => {
128
+ const { source: s } = source({ gitMode: 'github-app' });
129
+ expect(s.answered()).toBe(false);
130
+ expect(s.url()).toBe('');
131
+ expect(s.credentials.token()).toBeNull();
132
+ });
133
+ });
134
+
135
+ describe('ManagedRepository', () => {
136
+ const runner = new NodeGitRunner(30_000);
137
+ const git = async (cwd: string, ...args: string[]) => (await runner.run(cwd, args)).stdout.trim();
138
+
139
+ it('is created empty, bare, on the branch it is given', async () => {
140
+ const managed = new ManagedRepository(path.join(root, 'managed-repository'));
141
+ expect(await managed.exists()).toBe(false);
142
+ await managed.ensure(runner, 'trunk');
143
+ expect(await managed.exists()).toBe(true);
144
+ expect(await git(managed.path, 'rev-parse', '--is-bare-repository')).toBe('true');
145
+ expect(await git(managed.path, 'symbolic-ref', 'HEAD')).toBe('refs/heads/trunk');
146
+ expect(await git(managed.path, 'for-each-ref')).toBe('');
147
+ });
148
+
149
+ it('is cloned from, pushed to and fetched from like any remote', async () => {
150
+ const managed = new ManagedRepository(path.join(root, 'managed-repository'));
151
+ await managed.ensure(runner);
152
+ const one = path.join(root, 'one');
153
+ await fs.mkdir(one);
154
+ await git(one, 'init', '--initial-branch=main');
155
+ await fs.writeFile(path.join(one, 'README.md'), 'hello\n');
156
+ await git(one, 'add', '.');
157
+ await git(one, '-c', 'user.name=t', '-c', 'user.email=t@example.test', 'commit', '-m', 'first');
158
+ await git(one, 'push', managed.path, 'main');
159
+
160
+ const two = path.join(root, 'two');
161
+ await git(root, 'clone', '-b', 'main', managed.path, two);
162
+ // Whatever this machine's git makes of line endings on checkout.
163
+ expect((await fs.readFile(path.join(two, 'README.md'), 'utf8')).replace(/\r\n/g, '\n')).toBe('hello\n');
164
+ expect(await git(two, 'config', '--get', 'remote.origin.url')).toBe(managed.path);
165
+ });
166
+
167
+ it('is left exactly as it is when it already exists', async () => {
168
+ const managed = new ManagedRepository(path.join(root, 'managed-repository'));
169
+ await managed.ensure(runner);
170
+ const work = path.join(root, 'work');
171
+ await fs.mkdir(work);
172
+ await git(work, 'init', '--initial-branch=main');
173
+ await fs.writeFile(path.join(work, 'a.md'), 'a\n');
174
+ await git(work, 'add', '.');
175
+ await git(work, '-c', 'user.name=t', '-c', 'user.email=t@example.test', 'commit', '-m', 'first');
176
+ await git(work, 'push', managed.path, 'main');
177
+ const before = await git(managed.path, 'for-each-ref');
178
+
179
+ await managed.ensure(runner, 'another-branch');
180
+ expect(await git(managed.path, 'for-each-ref')).toBe(before);
181
+ expect(await git(managed.path, 'symbolic-ref', 'HEAD')).toBe('refs/heads/main');
182
+ });
183
+
184
+ it('refuses a folder that holds something that is not a repository', async () => {
185
+ const managed = new ManagedRepository(path.join(root, 'managed-repository'));
186
+ await fs.mkdir(managed.path, { recursive: true });
187
+ await fs.writeFile(path.join(managed.path, 'somebody.txt'), 'theirs');
188
+ await expect(managed.ensure(runner)).rejects.toThrow(/is not a git repository/);
189
+ expect(await fs.readFile(path.join(managed.path, 'somebody.txt'), 'utf8')).toBe('theirs');
190
+ });
191
+ });
@@ -0,0 +1,386 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import express from 'express';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { testKbContext } from '../../../__tests__/kb-context.js';
6
+ import type { KbContext } from '../../../shared/kb-context.js';
7
+ import { createSetupRoutes } from '../setup.routes.js';
8
+ import { DeploymentSettingsService } from '../deployment-settings.service.js';
9
+ import { ManagedRepository } from '../managed-repository.js';
10
+ import { RepositorySource } from '../repository-source.js';
11
+ import type { Database } from '../../database/connection.js';
12
+ import type { IAdminAccessService } from '../../admin/admin.interface.js';
13
+ import type { ConnectionCheck, RepositoryConnection } from '../connection-check.js';
14
+
15
+ const ENC_KEY = 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=';
16
+ const KB_ENV = ['KB_REPO_URL', 'GIT_TOKEN', 'GIT_USERNAME', 'GIT_MODE', 'GITHUB_TOKEN', 'DEFAULT_BRANCH', 'PROTECTED_BRANCHES'] as const;
17
+
18
+ let server: HttpServer | null = null;
19
+ let savedEnv: Partial<Record<(typeof KB_ENV)[number], string | undefined>> = {};
20
+
21
+ beforeEach(() => {
22
+ savedEnv = {};
23
+ for (const k of KB_ENV) {
24
+ savedEnv[k] = process.env[k];
25
+ delete process.env[k];
26
+ }
27
+ });
28
+
29
+ afterEach(() => {
30
+ server?.close();
31
+ server = null;
32
+ for (const k of KB_ENV) {
33
+ const original = savedEnv[k];
34
+ if (original === undefined) delete process.env[k];
35
+ else process.env[k] = original;
36
+ }
37
+ });
38
+
39
+ /**
40
+ * The setup router with the choice of repository, over settings that are
41
+ * stored in memory. The managed repository is a stand-in that records being
42
+ * asked for; the startup phase and the remote are stand-ins that count.
43
+ */
44
+ function listen(
45
+ opts: {
46
+ kb?: KbContext;
47
+ ensureFails?: boolean;
48
+ stored?: Record<string, string>;
49
+ /** The addresses the startup phase cannot reach. */
50
+ unreachable?: (url: string) => boolean;
51
+ /** The deployment booted onto a repository it could not reach, and came up gated. */
52
+ bootFailed?: boolean;
53
+ } = {},
54
+ ) {
55
+ const db = {
56
+ select: () => ({ from: () => Promise.resolve([]) }),
57
+ insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
58
+ delete: () => ({ where: () => Promise.resolve() }),
59
+ } as unknown as Database;
60
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
61
+ const kb = opts.kb ?? testKbContext({ branchModel: null });
62
+ const managed = new ManagedRepository('/data/backups/managed-repository');
63
+ const source = new RepositorySource({ read: (key) => settings.resolve(key), managed });
64
+ const ensured: string[] = [];
65
+ const probed: RepositoryConnection[] = [];
66
+ let phaseRuns = 0;
67
+ /** The address the startup phase found configured when it ran. */
68
+ const phaseSaw: string[] = [];
69
+ /** Stands until the deployment is pointed somewhere the phase can reach. */
70
+ let bootFailed = opts.bootFailed ?? false;
71
+ const app = express();
72
+ app.use(express.json());
73
+ app.use((req, _res, next) => {
74
+ req.userEmail = 'root@example.com';
75
+ req.userId = 'user-1';
76
+ next();
77
+ });
78
+ app.use(
79
+ '/api',
80
+ createSetupRoutes(
81
+ settings,
82
+ { isAdmin: async () => true } as IAdminAccessService,
83
+ {
84
+ runAll: async () => {
85
+ phaseRuns += 1;
86
+ phaseSaw.push(source.url());
87
+ if (opts.unreachable?.(source.url())) throw new Error(`fatal: repository '${source.url()}' not found`);
88
+ bootFailed = false;
89
+ },
90
+ // A boot that survived an unreachable repository, as the runner reports it.
91
+ ...(opts.bootFailed ? { lastFailure: () => (bootFailed ? 'fatal: unable to access the repository' : null) } : {}),
92
+ },
93
+ kb,
94
+ undefined,
95
+ async (connection): Promise<ConnectionCheck> => {
96
+ probed.push(connection);
97
+ return { outcome: 'connected', branches: ['main'], defaultBranch: 'main', empty: false };
98
+ },
99
+ undefined,
100
+ undefined,
101
+ undefined,
102
+ undefined,
103
+ {
104
+ source,
105
+ ensureManaged: async (branch) => {
106
+ if (opts.ensureFails) throw new Error('EACCES: permission denied, mkdir /data/backups');
107
+ ensured.push(branch);
108
+ },
109
+ },
110
+ ),
111
+ );
112
+ server = app.listen(0);
113
+ const { port } = server.address() as AddressInfo;
114
+ const base = `http://127.0.0.1:${port}`;
115
+ /** `confirm` is the admin's answer to the question a move is asked, when they have given one. */
116
+ const save = (entries: Record<string, string>, confirm?: 'keep' | 'close') =>
117
+ fetch(`${base}/api/setup/settings`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ settings: entries, ...(confirm ? { confirmRepositoryChange: confirm } : {}) }),
121
+ });
122
+ const status = async () => (await fetch(`${base}/api/setup/status`)).json() as Promise<Record<string, unknown>>;
123
+ return { save, status, settings, source, managed, ensured, probed, kb, phase: { runs: () => phaseRuns, saw: phaseSaw } };
124
+ }
125
+
126
+ const HOSTED = { kbRepoUrl: 'https://git.example.com/acme/kb.git', gitToken: 'the-token' };
127
+ const BRANCHES = { defaultBranch: 'main', protectedBranches: 'main' };
128
+
129
+ describe('setup status: the choice of repository', () => {
130
+ it('offers the ways there are, and says none is chosen on a new deployment', async () => {
131
+ const { status } = listen();
132
+ expect(await status()).toMatchObject({ complete: false, repository: { mode: null, modes: ['managed', 'token'] } });
133
+ });
134
+
135
+ it('says a deployment configured before there was a choice is on the way it has', async () => {
136
+ const { status, save } = listen({ kb: testKbContext() });
137
+ await save({ ...HOSTED, ...BRANCHES });
138
+ expect(await status()).toMatchObject({ repository: { mode: 'token' } });
139
+ });
140
+ });
141
+
142
+ describe('choosing a repository the deployment keeps', () => {
143
+ it('finishes setup on that choice alone', async () => {
144
+ const { save, ensured, probed, phase, managed, kb } = listen();
145
+ const res = await save({ gitMode: 'managed' });
146
+ expect(res.status).toBe(200);
147
+ const body = (await res.json()) as Record<string, unknown>;
148
+ expect(body).toMatchObject({ ok: true, complete: true, restartRequired: false, repository: { mode: 'managed' } });
149
+ // The repository was there before the save pointed everything at it,
150
+ expect(ensured).toEqual(['main']);
151
+ // no host was asked anything,
152
+ expect(probed).toEqual([]);
153
+ // and the startup phase ran against it, on the branches named for it.
154
+ expect(phase.runs()).toBe(1);
155
+ expect(phase.saw).toEqual([managed.path]);
156
+ expect(kb.defaultBranch).toBe('main');
157
+ expect([...kb.protectedBranches]).toEqual(['main']);
158
+ });
159
+
160
+ it('keeps the branches an admin named over its own', async () => {
161
+ const { save, ensured, settings } = listen();
162
+ const res = await save({ gitMode: 'managed', defaultBranch: 'live', protectedBranches: 'live, draft' });
163
+ expect(res.status).toBe(200);
164
+ expect(ensured).toEqual(['live']);
165
+ expect(settings.resolve('defaultBranch')).toBe('live');
166
+ expect(settings.resolve('protectedBranches')).toBe('live, draft');
167
+ });
168
+
169
+ it('names no branches for any other way: a repository that exists is asked for its own', async () => {
170
+ const { save, settings } = listen();
171
+ await save({ ...HOSTED, gitMode: 'token' });
172
+ expect(settings.resolve('defaultBranch')).toBe('');
173
+ expect(settings.resolve('protectedBranches')).toBe('');
174
+ });
175
+
176
+ it('refuses the choice when the repository cannot be created, and stores nothing', async () => {
177
+ const { save, settings, phase } = listen({ ensureFails: true });
178
+ const res = await save({ gitMode: 'managed' });
179
+ expect(res.status).toBe(400);
180
+ const body = (await res.json()) as { error: string; problems: Record<string, string> };
181
+ expect(body.problems.gitMode).toMatch(/could not be created/);
182
+ // What the disk said stays in the log.
183
+ expect(JSON.stringify(body)).not.toMatch(/EACCES|\/data\/backups/);
184
+ expect(settings.resolve('gitMode')).toBe('');
185
+ expect(phase.runs()).toBe(0);
186
+ });
187
+
188
+ it('refuses a way it does not know', async () => {
189
+ const { save } = listen();
190
+ const res = await save({ gitMode: 'carrier-pigeon' });
191
+ expect(res.status).toBe(400);
192
+ expect(((await res.json()) as { problems: Record<string, string> }).problems.gitMode).toMatch(/managed, github-app, token/);
193
+ });
194
+ });
195
+
196
+ describe('a deployment that is serving moves to another repository', () => {
197
+ async function serving() {
198
+ const mounted = listen({ kb: testKbContext() });
199
+ await mounted.save({ ...HOSTED, ...BRANCHES });
200
+ return mounted;
201
+ }
202
+
203
+ /**
204
+ * A move is asked about before anything is stored: the save is refused
205
+ * until the answer comes back, naming the way left and the way moved to.
206
+ * Refused, it has changed nothing — the deployment is where it was.
207
+ */
208
+ it('is asked first, and until it answers nothing has moved', async () => {
209
+ const { save, status, phase, source, settings } = await serving();
210
+ const runsBefore = phase.runs();
211
+ const res = await save({ gitMode: 'managed' });
212
+ expect(res.status).toBe(409);
213
+ expect(await res.json()).toMatchObject({ repositoryChange: { openChangeRequests: 0, from: 'token', to: 'managed' } });
214
+ expect(settings.resolve('gitMode')).toBe('');
215
+ expect(source.mode()).toBe('token');
216
+ expect(source.url()).toBe(HOSTED.kbRepoUrl);
217
+ expect(phase.runs()).toBe(runsBefore);
218
+ expect(await status()).toMatchObject({ complete: true, repository: { mode: 'token', chosen: 'token' } });
219
+ });
220
+
221
+ /**
222
+ * Confirmed, the move takes effect ON THE SAVE: the way chosen is the way
223
+ * in effect, the startup phase runs against the repository moved to, and
224
+ * no restart is owed. It once waited for the next start, which an admin of
225
+ * a hosted workspace has no way to give.
226
+ */
227
+ it('moves on the save once confirmed, and owes no restart', async () => {
228
+ const { save, status, phase, ensured, managed, source } = await serving();
229
+ const runsBefore = phase.runs();
230
+ const res = await save({ gitMode: 'managed' }, 'keep');
231
+ const body = (await res.json()) as Record<string, unknown>;
232
+ expect(res.status, JSON.stringify(body)).toBe(200);
233
+ expect(body).toMatchObject({
234
+ ok: true,
235
+ complete: true,
236
+ restartRequired: false,
237
+ repository: { mode: 'managed', chosen: 'managed' },
238
+ repositoryChange: { choice: 'keep' },
239
+ });
240
+ expect(ensured).toHaveLength(1);
241
+ // The phase that sets the old working copies aside ran, against the repository moved to.
242
+ expect(phase.runs()).toBe(runsBefore + 1);
243
+ expect(phase.saw.at(-1)).toBe(managed.path);
244
+ expect(source.url()).toBe(managed.path);
245
+ expect(await status()).toMatchObject({ complete: true, repository: { mode: 'managed', chosen: 'managed' } });
246
+ });
247
+
248
+ /** One way's credential is never presented to another way's repository. */
249
+ it('presents the repository moved to nothing of the one that was left', async () => {
250
+ const { save, source } = await serving();
251
+ expect(source.credentials.token()).toBe('the-token');
252
+ await save({ gitMode: 'managed' }, 'keep');
253
+ expect(source.credentials.token()).toBeNull();
254
+ });
255
+
256
+ it('owes nothing through later saves about other things, and is not asked again', async () => {
257
+ const { save } = await serving();
258
+ await save({ gitMode: 'managed' }, 'keep');
259
+ const res = await save({ kbSyncSecret: 'a-secret-of-sixteen-or-more' });
260
+ expect(res.status).toBe(200);
261
+ expect(await res.json()).toMatchObject({ ok: true, restartRequired: false, repository: { mode: 'managed', chosen: 'managed' } });
262
+ });
263
+
264
+ it('is asked again to move back: that is a move too', async () => {
265
+ const { save, source } = await serving();
266
+ await save({ gitMode: 'managed' }, 'keep');
267
+ const back = await save({ gitMode: 'token' });
268
+ expect(back.status).toBe(409);
269
+ expect(await back.json()).toMatchObject({ repositoryChange: { from: 'managed', to: 'token' } });
270
+ const done = await save({ gitMode: 'token' }, 'keep');
271
+ expect(await done.json()).toMatchObject({ ok: true, restartRequired: false, repository: { mode: 'token', chosen: 'token' } });
272
+ expect(source.url()).toBe(HOSTED.kbRepoUrl);
273
+ });
274
+
275
+ /** Another address under the same way is the same question, with the same answer. */
276
+ it('asks the same of another address under the way it is on', async () => {
277
+ const { save, source, phase } = await serving();
278
+ const moved = { kbRepoUrl: 'https://git.example.com/acme/another.git', gitToken: 'another-token' };
279
+ const asked = await save(moved);
280
+ expect(asked.status).toBe(409);
281
+ expect(await asked.json()).toMatchObject({ repositoryChange: { from: 'token', to: 'token' } });
282
+ const runsBefore = phase.runs();
283
+ const done = await save(moved, 'close');
284
+ expect(await done.json()).toMatchObject({ ok: true, restartRequired: false, repositoryChange: { choice: 'close' } });
285
+ expect(phase.runs()).toBe(runsBefore + 1);
286
+ expect(source.url()).toBe(moved.kbRepoUrl);
287
+ });
288
+
289
+ it('asks nothing of a save that only spells the same address another way', async () => {
290
+ const { save } = await serving();
291
+ const res = await save({ kbRepoUrl: 'https://GIT.example.com/acme/kb/' });
292
+ expect(res.status).toBe(200);
293
+ expect(await res.json()).not.toHaveProperty('repositoryChange');
294
+ });
295
+
296
+ it('owes none for naming the way it was already on', async () => {
297
+ const { save, probed } = await serving();
298
+ const asked = probed.length;
299
+ const body = (await (await save({ gitMode: 'token' })).json()) as Record<string, unknown>;
300
+ expect(body).toMatchObject({ ok: true, restartRequired: false, repository: { mode: 'token', chosen: 'token' } });
301
+ expect(probed).toHaveLength(asked);
302
+ });
303
+ });
304
+
305
+ /**
306
+ * Behind a shut gate nothing is running on the way in effect, however
307
+ * completely the settings were answered, so a deployment there is got out of
308
+ * a repository that does not work by choosing one that does. One that never
309
+ * served is not even asked: there is no repository anyone worked on to
310
+ * leave. One that served before its boot failed is asked, as any move is.
311
+ */
312
+ describe('a deployment that answered everything and serves nobody', () => {
313
+ const isHosted = (url: string) => url === HOSTED.kbRepoUrl;
314
+
315
+ it('is got going by choosing another way, when its first initialisation failed', async () => {
316
+ const { save, status, phase, managed, source } = listen({ kb: testKbContext(), unreachable: isHosted });
317
+ // First run: an address and a token, accepted, and a first clone that fails.
318
+ const first = await save({ ...HOSTED, ...BRANCHES });
319
+ expect(first.status).toBe(500);
320
+ expect(await status()).toMatchObject({ complete: false, kbInit: expect.any(Object), repository: { mode: 'token' } });
321
+
322
+ const second = await save({ gitMode: 'managed' });
323
+ const body = (await second.json()) as Record<string, unknown>;
324
+ expect(second.status, JSON.stringify(body)).toBe(200);
325
+ // The retry ran against the repository that was chosen, not the one that had just failed,
326
+ expect(phase.saw).toEqual([HOSTED.kbRepoUrl, managed.path]);
327
+ // nothing is owed, and the gate is open.
328
+ expect(body).toMatchObject({ complete: true, restartRequired: false, repository: { mode: 'managed', chosen: 'managed' } });
329
+ expect(source.credentials.token()).toBeNull();
330
+ expect(await status()).toMatchObject({ complete: true });
331
+ expect(await status()).not.toHaveProperty('kbInit');
332
+ });
333
+
334
+ it('is got going the same way when it booted onto a repository it could not reach', async () => {
335
+ Object.assign(process.env, { KB_REPO_URL: HOSTED.kbRepoUrl, GIT_TOKEN: HOSTED.gitToken, DEFAULT_BRANCH: 'main', PROTECTED_BRANCHES: 'main' });
336
+ const { save, status, phase, managed } = listen({ kb: testKbContext(), bootFailed: true, unreachable: isHosted });
337
+ expect(await status()).toMatchObject({ complete: false, repository: { mode: 'token' } });
338
+
339
+ // It served before this boot, so there may be work on the repository it
340
+ // leaves: it is asked, and nothing moves until it answers.
341
+ const asked = await save({ gitMode: 'managed' });
342
+ expect(asked.status).toBe(409);
343
+ expect(phase.saw).toEqual([]);
344
+
345
+ const body = (await (await save({ gitMode: 'managed' }, 'keep')).json()) as Record<string, unknown>;
346
+ expect(phase.saw).toEqual([managed.path]);
347
+ expect(body).toMatchObject({ complete: true, restartRequired: false, repository: { mode: 'managed', chosen: 'managed' } });
348
+ });
349
+ });
350
+
351
+ describe('a deployment whose way is chosen by the environment', () => {
352
+ it('says which variable chose', async () => {
353
+ process.env.GIT_MODE = 'managed';
354
+ const { status } = listen();
355
+ expect(await status()).toMatchObject({ repository: { mode: 'managed', chosen: 'managed', pinned: 'GIT_MODE' } });
356
+ });
357
+
358
+ it('says nothing of the kind when the admin chose', async () => {
359
+ const { save, status } = listen();
360
+ await save({ gitMode: 'managed' });
361
+ expect(((await status()) as { repository: object }).repository).not.toHaveProperty('pinned');
362
+ });
363
+ });
364
+
365
+ describe('a mount without the choice', () => {
366
+ it('is the one way there was: an address and a token', async () => {
367
+ const db = {
368
+ select: () => ({ from: () => Promise.resolve([]) }),
369
+ insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
370
+ delete: () => ({ where: () => Promise.resolve() }),
371
+ } as unknown as Database;
372
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
373
+ const app = express();
374
+ app.use(express.json());
375
+ app.use((req, _res, next) => {
376
+ req.userEmail = 'root@example.com';
377
+ next();
378
+ });
379
+ app.use('/api', createSetupRoutes(settings, { isAdmin: async () => true } as IAdminAccessService, { runAll: async () => {} }, testKbContext()));
380
+ server = app.listen(0);
381
+ const { port } = server.address() as AddressInfo;
382
+ const status = (await (await fetch(`http://127.0.0.1:${port}/api/setup/status`)).json()) as Record<string, unknown>;
383
+ expect(status).not.toHaveProperty('repository');
384
+ expect(status.complete).toBe(false);
385
+ });
386
+ });