@bevel-software/platform-core-backend 0.20.0 → 0.22.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 (666) hide show
  1. package/dist/core/catalog-cache-invalidation.d.ts +7 -2
  2. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  3. package/dist/core/catalog-cache-invalidation.js +8 -9
  4. package/dist/core/catalog-cache-invalidation.js.map +1 -1
  5. package/dist/core/core-ports.d.ts +19 -0
  6. package/dist/core/core-ports.d.ts.map +1 -1
  7. package/dist/core/core-ports.js.map +1 -1
  8. package/dist/core/create-core-server.d.ts +2 -0
  9. package/dist/core/create-core-server.d.ts.map +1 -1
  10. package/dist/core/create-core-server.js +55 -107
  11. package/dist/core/create-core-server.js.map +1 -1
  12. package/dist/core/create-core-services.d.ts +41 -4
  13. package/dist/core/create-core-services.d.ts.map +1 -1
  14. package/dist/core/create-core-services.js +183 -82
  15. package/dist/core/create-core-services.js.map +1 -1
  16. package/dist/core/lifecycle.d.ts +74 -28
  17. package/dist/core/lifecycle.d.ts.map +1 -1
  18. package/dist/core/lifecycle.js +157 -20
  19. package/dist/core/lifecycle.js.map +1 -1
  20. package/dist/core/public-config.d.ts +5 -1
  21. package/dist/core/public-config.d.ts.map +1 -1
  22. package/dist/core/public-config.js +7 -5
  23. package/dist/core/public-config.js.map +1 -1
  24. package/dist/core-config.d.ts +95 -1
  25. package/dist/core-config.d.ts.map +1 -1
  26. package/dist/core-config.js +24 -8
  27. package/dist/core-config.js.map +1 -1
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/modules/access/access-control.service.d.ts +18 -1
  33. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  34. package/dist/modules/access/access-control.service.js +19 -8
  35. package/dist/modules/access/access-control.service.js.map +1 -1
  36. package/dist/modules/access/access-requests.contract.d.ts +75 -0
  37. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  38. package/dist/modules/access/access-requests.contract.js +20 -0
  39. package/dist/modules/access/access-requests.contract.js.map +1 -0
  40. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  41. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  42. package/dist/modules/access/access-requests.routes.js +237 -0
  43. package/dist/modules/access/access-requests.routes.js.map +1 -0
  44. package/dist/modules/access/access-requests.service.d.ts +123 -0
  45. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  46. package/dist/modules/access/access-requests.service.js +337 -0
  47. package/dist/modules/access/access-requests.service.js.map +1 -0
  48. package/dist/modules/access/access.routes.d.ts +2 -1
  49. package/dist/modules/access/access.routes.d.ts.map +1 -1
  50. package/dist/modules/access/access.routes.js +10 -10
  51. package/dist/modules/access/access.routes.js.map +1 -1
  52. package/dist/modules/access/change-read-gate.d.ts +6 -3
  53. package/dist/modules/access/change-read-gate.d.ts.map +1 -1
  54. package/dist/modules/access/change-read-gate.js +9 -6
  55. package/dist/modules/access/change-read-gate.js.map +1 -1
  56. package/dist/modules/access/creator-access.d.ts +4 -2
  57. package/dist/modules/access/creator-access.d.ts.map +1 -1
  58. package/dist/modules/access/creator-access.js +7 -4
  59. package/dist/modules/access/creator-access.js.map +1 -1
  60. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  61. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  62. package/dist/modules/access-model/access-grammar.js +24 -0
  63. package/dist/modules/access-model/access-grammar.js.map +1 -1
  64. package/dist/modules/agent-instructions/read-preamble.d.ts +2 -1
  65. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -1
  66. package/dist/modules/agent-instructions/read-preamble.js +6 -9
  67. package/dist/modules/agent-instructions/read-preamble.js.map +1 -1
  68. package/dist/modules/auth/account-admission.d.ts +40 -0
  69. package/dist/modules/auth/account-admission.d.ts.map +1 -0
  70. package/dist/modules/auth/account-admission.js +17 -0
  71. package/dist/modules/auth/account-admission.js.map +1 -0
  72. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  73. package/dist/modules/auth/account.routes.js +4 -1
  74. package/dist/modules/auth/account.routes.js.map +1 -1
  75. package/dist/modules/auth/auth.service.d.ts +45 -2
  76. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  77. package/dist/modules/auth/auth.service.js +44 -3
  78. package/dist/modules/auth/auth.service.js.map +1 -1
  79. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  80. package/dist/modules/auth/oidc-auth-provider.js +9 -0
  81. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  82. package/dist/modules/connection-probe/connection-probe.service.d.ts +13 -1
  83. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  84. package/dist/modules/connection-probe/connection-probe.service.js +10 -2
  85. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  86. package/dist/modules/database/advisory-lock.d.ts +17 -0
  87. package/dist/modules/database/advisory-lock.d.ts.map +1 -1
  88. package/dist/modules/database/advisory-lock.js +24 -3
  89. package/dist/modules/database/advisory-lock.js.map +1 -1
  90. package/dist/modules/database/connection.d.ts +67 -2
  91. package/dist/modules/database/connection.d.ts.map +1 -1
  92. package/dist/modules/database/connection.js +118 -12
  93. package/dist/modules/database/connection.js.map +1 -1
  94. package/dist/modules/database/migrate.d.ts +1 -1
  95. package/dist/modules/database/migrate.d.ts.map +1 -1
  96. package/dist/modules/database/migrate.js +27 -3
  97. package/dist/modules/database/migrate.js.map +1 -1
  98. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  99. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  100. package/dist/modules/github-app/github-app.client.js +222 -0
  101. package/dist/modules/github-app/github-app.client.js.map +1 -0
  102. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  103. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  104. package/dist/modules/github-app/github-app.connection.js +185 -0
  105. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  106. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  107. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  108. package/dist/modules/github-app/github-app.routes.js +323 -0
  109. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  110. package/dist/modules/github-app/index.d.ts +4 -0
  111. package/dist/modules/github-app/index.d.ts.map +1 -0
  112. package/dist/modules/github-app/index.js +4 -0
  113. package/dist/modules/github-app/index.js.map +1 -0
  114. package/dist/modules/kb-fs/branch-name.d.ts +0 -1
  115. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  116. package/dist/modules/kb-fs/branch-name.js +3 -3
  117. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  118. package/dist/modules/kb-fs/clone-config.d.ts +9 -8
  119. package/dist/modules/kb-fs/clone-config.d.ts.map +1 -1
  120. package/dist/modules/kb-fs/clone-config.js +14 -12
  121. package/dist/modules/kb-fs/clone-config.js.map +1 -1
  122. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  123. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  124. package/dist/modules/kb-fs/remote-url.js +35 -0
  125. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  126. package/dist/modules/mcp/mcp.service.d.ts +7 -0
  127. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  128. package/dist/modules/mcp/mcp.service.js +4 -4
  129. package/dist/modules/mcp/mcp.service.js.map +1 -1
  130. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +4 -4
  131. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
  132. package/dist/modules/plugins/compile/marketplace-compiler.service.js +7 -10
  133. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
  134. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +4 -1
  135. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
  136. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +5 -3
  137. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
  138. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +6 -1
  139. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
  140. package/dist/modules/plugins/discovery/kb-plugin-source.js +14 -9
  141. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
  142. package/dist/modules/plugins/discovery/native.source.d.ts +4 -1
  143. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
  144. package/dist/modules/plugins/discovery/native.source.js +4 -2
  145. package/dist/modules/plugins/discovery/native.source.js.map +1 -1
  146. package/dist/modules/plugins/index.d.ts +1 -1
  147. package/dist/modules/plugins/index.d.ts.map +1 -1
  148. package/dist/modules/plugins/index.js +1 -1
  149. package/dist/modules/plugins/index.js.map +1 -1
  150. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  151. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  152. package/dist/modules/plugins/join-proposals.js +76 -22
  153. package/dist/modules/plugins/join-proposals.js.map +1 -1
  154. package/dist/modules/plugins/join-request-jobs.service.d.ts +3 -1
  155. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  156. package/dist/modules/plugins/join-request-jobs.service.js +6 -6
  157. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  158. package/dist/modules/plugins/join-requests.service.d.ts +113 -18
  159. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  160. package/dist/modules/plugins/join-requests.service.js +178 -33
  161. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  162. package/dist/modules/plugins/plugin-links.d.ts +65 -10
  163. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugin-links.js +136 -31
  165. package/dist/modules/plugins/plugin-links.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.service.d.ts +107 -5
  167. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
  168. package/dist/modules/plugins/plugin-links.service.js +151 -21
  169. package/dist/modules/plugins/plugin-links.service.js.map +1 -1
  170. package/dist/modules/plugins/plugin-provision.service.d.ts +10 -3
  171. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  172. package/dist/modules/plugins/plugin-provision.service.js +33 -26
  173. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  174. package/dist/modules/plugins/plugin-rename.service.d.ts +4 -2
  175. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-rename.service.js +10 -12
  177. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  178. package/dist/modules/plugins/plugins.routes.d.ts +3 -0
  179. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  180. package/dist/modules/plugins/plugins.routes.js +48 -17
  181. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  182. package/dist/modules/plugins/plugins.service.d.ts +4 -4
  183. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.service.js +7 -10
  185. package/dist/modules/plugins/plugins.service.js.map +1 -1
  186. package/dist/modules/plugins/teams.routes.d.ts +4 -1
  187. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  188. package/dist/modules/plugins/teams.routes.js +11 -9
  189. package/dist/modules/plugins/teams.routes.js.map +1 -1
  190. package/dist/modules/secrets-vault/index.d.ts +1 -1
  191. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  192. package/dist/modules/secrets-vault/index.js +1 -1
  193. package/dist/modules/secrets-vault/index.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts +29 -11
  195. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-variable-loader.js +46 -21
  197. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  198. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +3 -0
  199. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  200. package/dist/modules/secrets-vault/secrets-vault.routes.js +4 -6
  201. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  202. package/dist/modules/settings/deployment-settings.service.d.ts +34 -9
  203. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  204. package/dist/modules/settings/deployment-settings.service.js +113 -26
  205. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  206. package/dist/modules/settings/managed-repository.d.ts +43 -0
  207. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  208. package/dist/modules/settings/managed-repository.js +60 -0
  209. package/dist/modules/settings/managed-repository.js.map +1 -0
  210. package/dist/modules/settings/repository-source.d.ts +128 -0
  211. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  212. package/dist/modules/settings/repository-source.js +150 -0
  213. package/dist/modules/settings/repository-source.js.map +1 -0
  214. package/dist/modules/settings/setup.routes.d.ts +34 -4
  215. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  216. package/dist/modules/settings/setup.routes.js +198 -18
  217. package/dist/modules/settings/setup.routes.js.map +1 -1
  218. package/dist/modules/skills/allowed-tools-check.d.ts +4 -3
  219. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -1
  220. package/dist/modules/skills/allowed-tools-check.js +8 -8
  221. package/dist/modules/skills/allowed-tools-check.js.map +1 -1
  222. package/dist/modules/skills/pending-skills.service.d.ts +3 -1
  223. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  224. package/dist/modules/skills/pending-skills.service.js +9 -6
  225. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  226. package/dist/modules/skills/skill-access-requests.routes.d.ts +8 -9
  227. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  228. package/dist/modules/skills/skill-access-requests.routes.js +26 -65
  229. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  230. package/dist/modules/skills/skills.routes.d.ts +4 -1
  231. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  232. package/dist/modules/skills/skills.routes.js +4 -4
  233. package/dist/modules/skills/skills.routes.js.map +1 -1
  234. package/dist/modules/skills/skills.service.d.ts +4 -2
  235. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  236. package/dist/modules/skills/skills.service.js +13 -11
  237. package/dist/modules/skills/skills.service.js.map +1 -1
  238. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  239. package/dist/modules/tool-helpers/tool-context.js +15 -0
  240. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  241. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +2 -1
  242. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  243. package/dist/modules/tool-manuals/mcp-json-discovery.js +6 -5
  244. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  245. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +4 -2
  246. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  247. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -10
  248. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  249. package/dist/modules/tool-manuals/pending-tools.service.d.ts +3 -1
  250. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -1
  251. package/dist/modules/tool-manuals/pending-tools.service.js +19 -16
  252. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -1
  253. package/dist/modules/tool-manuals/tool-delete.service.d.ts +4 -2
  254. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -1
  255. package/dist/modules/tool-manuals/tool-delete.service.js +14 -12
  256. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -1
  257. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +3 -1
  258. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  259. package/dist/modules/tool-manuals/tool-manuals.routes.js +7 -7
  260. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  261. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -2
  262. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  263. package/dist/modules/tool-manuals/tool-manuals.service.js +15 -12
  264. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  265. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +3 -0
  266. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  267. package/dist/modules/tool-manuals/tool-manuals.tools.js +4 -6
  268. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  269. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +3 -2
  270. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  271. package/dist/modules/workflow/agent-tools/workflow.tools.js +47 -11
  272. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  273. package/dist/modules/workflow/git/git.service.d.ts +14 -6
  274. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  275. package/dist/modules/workflow/git/git.service.js +40 -18
  276. package/dist/modules/workflow/git/git.service.js.map +1 -1
  277. package/dist/modules/workflow/git/node-git-runner.d.ts +26 -2
  278. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  279. package/dist/modules/workflow/git/node-git-runner.js +40 -5
  280. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  281. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  282. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  283. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  284. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  285. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  286. package/dist/modules/workflow/review-workflow/review-workflow.service.js +9 -4
  287. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  288. package/dist/modules/workflow/session-ontology.service.d.ts +6 -1
  289. package/dist/modules/workflow/session-ontology.service.d.ts.map +1 -1
  290. package/dist/modules/workflow/session-ontology.service.js +6 -2
  291. package/dist/modules/workflow/session-ontology.service.js.map +1 -1
  292. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  293. package/dist/modules/workflow/workflow.routes.js +3 -1
  294. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  295. package/dist/modules/workflow/workflow.service.d.ts +42 -10
  296. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  297. package/dist/modules/workflow/workflow.service.js +181 -71
  298. package/dist/modules/workflow/workflow.service.js.map +1 -1
  299. package/dist/modules/workspace/session-ontology.gate.d.ts +3 -1
  300. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  301. package/dist/modules/workspace/session-ontology.gate.js +3 -3
  302. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  303. package/dist/modules/workspace/startup/kb-git.d.ts +4 -3
  304. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/kb-git.js +15 -13
  306. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  307. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +70 -7
  308. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  309. package/dist/modules/workspace/startup/kb-startup-runner.js +183 -38
  310. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  311. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +7 -1
  312. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  313. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +23 -16
  314. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  315. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +7 -1
  316. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -1
  317. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +12 -5
  318. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -1
  319. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +7 -1
  320. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -1
  321. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +12 -5
  322. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -1
  323. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
  324. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
  325. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +12 -5
  326. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
  327. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +4 -1
  328. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  329. package/dist/modules/workspace/startup/steps/seed-tree.js +14 -11
  330. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  331. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +17 -3
  332. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  333. package/dist/modules/workspace/startup/steps/template-files.step.js +36 -30
  334. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  335. package/dist/modules/workspace/startup/steps/template-source.d.ts +6 -1
  336. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  337. package/dist/modules/workspace/startup/steps/template-source.js +10 -6
  338. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  339. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  340. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  341. package/dist/modules/workspace/workspace.routes.js +8 -7
  342. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  343. package/dist/modules/workspace/workspace.service.d.ts +18 -12
  344. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  345. package/dist/modules/workspace/workspace.service.js +49 -29
  346. package/dist/modules/workspace/workspace.service.js.map +1 -1
  347. package/dist/modules/workspace/workspace.tools.d.ts +2 -1
  348. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  349. package/dist/modules/workspace/workspace.tools.js +48 -20
  350. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  351. package/dist/modules/workspace/write-denial.d.ts +3 -2
  352. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  353. package/dist/modules/workspace/write-denial.js +6 -5
  354. package/dist/modules/workspace/write-denial.js.map +1 -1
  355. package/dist/shared/domain-errors.d.ts +39 -0
  356. package/dist/shared/domain-errors.d.ts.map +1 -1
  357. package/dist/shared/domain-errors.js +55 -0
  358. package/dist/shared/domain-errors.js.map +1 -1
  359. package/dist/shared/git.contract.d.ts +62 -7
  360. package/dist/shared/git.contract.d.ts.map +1 -1
  361. package/dist/shared/git.contract.js +30 -8
  362. package/dist/shared/git.contract.js.map +1 -1
  363. package/dist/shared/kb-context.d.ts +76 -0
  364. package/dist/shared/kb-context.d.ts.map +1 -0
  365. package/dist/shared/kb-context.js +108 -0
  366. package/dist/shared/kb-context.js.map +1 -0
  367. package/dist/shared/kb-layout.d.ts +3 -2
  368. package/dist/shared/kb-layout.d.ts.map +1 -1
  369. package/dist/shared/kb-layout.js +4 -3
  370. package/dist/shared/kb-layout.js.map +1 -1
  371. package/dist/shared/kb-layout.test.js +35 -27
  372. package/dist/shared/kb-layout.test.js.map +1 -1
  373. package/dist/shared/pending-proposals.d.ts +4 -1
  374. package/dist/shared/pending-proposals.d.ts.map +1 -1
  375. package/dist/shared/pending-proposals.js +3 -4
  376. package/dist/shared/pending-proposals.js.map +1 -1
  377. package/dist/shared/redact-secret.d.ts +5 -5
  378. package/dist/shared/redact-secret.js +5 -5
  379. package/dist/tenancy/index.d.ts +13 -0
  380. package/dist/tenancy/index.d.ts.map +1 -0
  381. package/dist/tenancy/index.js +13 -0
  382. package/dist/tenancy/index.js.map +1 -0
  383. package/dist/tenancy/static-tenant-source.d.ts +98 -0
  384. package/dist/tenancy/static-tenant-source.d.ts.map +1 -0
  385. package/dist/tenancy/static-tenant-source.js +220 -0
  386. package/dist/tenancy/static-tenant-source.js.map +1 -0
  387. package/dist/tenancy/tenant-host.d.ts +80 -0
  388. package/dist/tenancy/tenant-host.d.ts.map +1 -0
  389. package/dist/tenancy/tenant-host.js +217 -0
  390. package/dist/tenancy/tenant-host.js.map +1 -0
  391. package/dist/tenancy/tenant-runtime.d.ts +91 -0
  392. package/dist/tenancy/tenant-runtime.d.ts.map +1 -0
  393. package/dist/tenancy/tenant-runtime.js +174 -0
  394. package/dist/tenancy/tenant-runtime.js.map +1 -0
  395. package/dist/tenancy/tenant-secrets.d.ts +24 -0
  396. package/dist/tenancy/tenant-secrets.d.ts.map +1 -0
  397. package/dist/tenancy/tenant-secrets.js +30 -0
  398. package/dist/tenancy/tenant-secrets.js.map +1 -0
  399. package/dist/tenancy/tenant-source.contract.d.ts +45 -0
  400. package/dist/tenancy/tenant-source.contract.d.ts.map +1 -0
  401. package/dist/tenancy/tenant-source.contract.js +21 -0
  402. package/dist/tenancy/tenant-source.contract.js.map +1 -0
  403. package/migrations/0000_core_init.sql +14 -14
  404. package/migrations/0003_deployment_settings.sql +1 -1
  405. package/migrations/0005_github_facade.sql +1 -1
  406. package/migrations/0012_agent_audit.sql +6 -6
  407. package/package.json +3 -3
  408. package/src/__tests__/branch-model.test.ts +7 -6
  409. package/src/__tests__/kb-context.ts +30 -0
  410. package/src/__tests__/kb-layout-config.test.ts +35 -30
  411. package/src/core/__tests__/catalog-cache-invalidation.test.ts +2 -1
  412. package/src/core/__tests__/lifecycle.test.ts +78 -1
  413. package/src/core/__tests__/public-config.test.ts +5 -1
  414. package/src/core/catalog-cache-invalidation.ts +16 -11
  415. package/src/core/core-ports.ts +19 -0
  416. package/src/core/create-core-server.ts +64 -107
  417. package/src/core/create-core-services.ts +238 -96
  418. package/src/core/lifecycle.ts +214 -29
  419. package/src/core/public-config.ts +8 -5
  420. package/src/core-config.ts +107 -8
  421. package/src/index.ts +25 -2
  422. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +2 -1
  423. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  424. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  425. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -1
  426. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -1
  427. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +2 -1
  428. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  429. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +2 -1
  430. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -1
  431. package/src/modules/access/__tests__/access.routes.revoke.test.ts +2 -1
  432. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -1
  433. package/src/modules/access/__tests__/change-read-gate.test.ts +13 -11
  434. package/src/modules/access/__tests__/creator-access.test.ts +3 -2
  435. package/src/modules/access/__tests__/roles.routes.test.ts +2 -1
  436. package/src/modules/access/access-control.service.ts +20 -6
  437. package/src/modules/access/access-requests.contract.ts +93 -0
  438. package/src/modules/access/access-requests.routes.ts +286 -0
  439. package/src/modules/access/access-requests.service.ts +420 -0
  440. package/src/modules/access/access.routes.ts +10 -13
  441. package/src/modules/access/change-read-gate.ts +10 -4
  442. package/src/modules/access/creator-access.ts +7 -2
  443. package/src/modules/access-model/access-grammar.ts +22 -0
  444. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +13 -12
  445. package/src/modules/agent-instructions/read-preamble.ts +10 -12
  446. package/src/modules/auth/__tests__/account.routes.test.ts +15 -0
  447. package/src/modules/auth/__tests__/auth.service.test.ts +98 -0
  448. package/src/modules/auth/account-admission.ts +44 -0
  449. package/src/modules/auth/account.routes.ts +4 -1
  450. package/src/modules/auth/auth.service.ts +61 -2
  451. package/src/modules/auth/oidc-auth-provider.ts +9 -0
  452. package/src/modules/connection-probe/connection-probe.service.ts +7 -1
  453. package/src/modules/database/__tests__/advisory-lock.test.ts +23 -1
  454. package/src/modules/database/__tests__/connection.test.ts +98 -0
  455. package/src/modules/database/__tests__/migrations-unqualified.test.ts +32 -0
  456. package/src/modules/database/advisory-lock.ts +28 -4
  457. package/src/modules/database/connection.ts +143 -13
  458. package/src/modules/database/migrate.ts +48 -14
  459. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  460. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  461. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  462. package/src/modules/github-app/github-app.client.ts +268 -0
  463. package/src/modules/github-app/github-app.connection.ts +204 -0
  464. package/src/modules/github-app/github-app.routes.ts +359 -0
  465. package/src/modules/github-app/index.ts +19 -0
  466. package/src/modules/kb-fs/__tests__/branch-name.test.ts +6 -6
  467. package/src/modules/kb-fs/__tests__/clone-config.test.ts +24 -20
  468. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  469. package/src/modules/kb-fs/branch-name.ts +3 -3
  470. package/src/modules/kb-fs/clone-config.ts +15 -12
  471. package/src/modules/kb-fs/remote-url.ts +35 -0
  472. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +2 -1
  473. package/src/modules/mcp/mcp.service.ts +11 -4
  474. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +12 -11
  475. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +2 -1
  476. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +6 -4
  477. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  478. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +2 -1
  479. package/src/modules/plugins/__tests__/join-requests.service.test.ts +28 -11
  480. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  481. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +4 -3
  482. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +6 -4
  483. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +220 -5
  484. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +3 -1
  485. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +7 -6
  486. package/src/modules/plugins/__tests__/plugins.routes.test.ts +12 -3
  487. package/src/modules/plugins/__tests__/teams.routes.test.ts +2 -1
  488. package/src/modules/plugins/compile/marketplace-compiler.service.ts +7 -9
  489. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +20 -16
  490. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +9 -2
  491. package/src/modules/plugins/discovery/kb-plugin-source.ts +15 -8
  492. package/src/modules/plugins/discovery/native.source.ts +4 -1
  493. package/src/modules/plugins/index.ts +1 -1
  494. package/src/modules/plugins/join-proposals.ts +87 -19
  495. package/src/modules/plugins/join-request-jobs.service.ts +8 -7
  496. package/src/modules/plugins/join-requests.service.ts +203 -43
  497. package/src/modules/plugins/plugin-links.service.ts +192 -19
  498. package/src/modules/plugins/plugin-links.ts +147 -27
  499. package/src/modules/plugins/plugin-provision.service.ts +54 -27
  500. package/src/modules/plugins/plugin-rename.service.ts +10 -10
  501. package/src/modules/plugins/plugins.routes.ts +48 -17
  502. package/src/modules/plugins/plugins.service.ts +7 -11
  503. package/src/modules/plugins/teams.routes.ts +16 -8
  504. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +7 -0
  505. package/src/modules/secrets-vault/__tests__/secrets-variable-loader.test.ts +40 -0
  506. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +2 -0
  507. package/src/modules/secrets-vault/index.ts +2 -0
  508. package/src/modules/secrets-vault/secrets-variable-loader.ts +51 -21
  509. package/src/modules/secrets-vault/secrets-vault.routes.ts +7 -6
  510. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +24 -11
  511. package/src/modules/settings/__tests__/git-root-folders.test.ts +2 -1
  512. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  513. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +2 -1
  514. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +353 -0
  515. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +282 -0
  516. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +7 -4
  517. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +233 -0
  518. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +2 -0
  519. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +2 -0
  520. package/src/modules/settings/__tests__/setup.routes.test.ts +33 -52
  521. package/src/modules/settings/deployment-settings.service.ts +130 -23
  522. package/src/modules/settings/managed-repository.ts +68 -0
  523. package/src/modules/settings/repository-source.ts +197 -0
  524. package/src/modules/settings/setup.routes.ts +233 -19
  525. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +11 -10
  526. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +4 -3
  527. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -1
  528. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +7 -2
  529. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +2 -1
  530. package/src/modules/skills/__tests__/skills.service.test.ts +3 -2
  531. package/src/modules/skills/allowed-tools-check.ts +7 -6
  532. package/src/modules/skills/pending-skills.service.ts +9 -8
  533. package/src/modules/skills/skill-access-requests.routes.ts +29 -79
  534. package/src/modules/skills/skills.routes.ts +4 -3
  535. package/src/modules/skills/skills.service.ts +13 -9
  536. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +3 -2
  537. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -2
  538. package/src/modules/tool-helpers/tool-context.ts +15 -0
  539. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +17 -12
  540. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  541. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +4 -1
  542. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +2 -1
  543. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +2 -1
  544. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +5 -4
  545. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +2 -1
  546. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +4 -2
  547. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +13 -5
  548. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -1
  549. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +9 -1
  550. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +3 -2
  551. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +2 -0
  552. package/src/modules/tool-manuals/mcp-json-discovery.ts +6 -4
  553. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -11
  554. package/src/modules/tool-manuals/pending-tools.service.ts +19 -16
  555. package/src/modules/tool-manuals/tool-delete.service.ts +13 -10
  556. package/src/modules/tool-manuals/tool-manuals.routes.ts +10 -8
  557. package/src/modules/tool-manuals/tool-manuals.service.ts +15 -13
  558. package/src/modules/tool-manuals/tool-manuals.tools.ts +7 -6
  559. package/src/modules/workflow/__tests__/apply-failure.test.ts +11 -2
  560. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +14 -26
  561. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -1
  562. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +3 -2
  563. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +2 -1
  564. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +3 -10
  565. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -1
  566. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +3 -10
  567. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +187 -35
  568. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +2 -1
  569. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +2 -1
  570. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +3 -2
  571. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +2 -1
  572. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -4
  573. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +148 -41
  574. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +143 -2
  575. package/src/modules/workflow/agent-tools/workflow.tools.ts +60 -12
  576. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +5 -4
  577. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +12 -11
  578. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +9 -8
  579. package/src/modules/workflow/git/__tests__/git.service.commitFile.strayPath.test.ts +8 -7
  580. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +6 -5
  581. package/src/modules/workflow/git/__tests__/git.service.createBranch.test.ts +3 -2
  582. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +3 -2
  583. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +7 -6
  584. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +16 -15
  585. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +3 -2
  586. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +2 -1
  587. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +2 -1
  588. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +10 -9
  589. package/src/modules/workflow/git/__tests__/git.service.listTrackedFiles.test.ts +2 -1
  590. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +6 -5
  591. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +9 -8
  592. package/src/modules/workflow/git/__tests__/git.service.pendingChanges.test.ts +2 -1
  593. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +4 -3
  594. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +130 -8
  595. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +2 -1
  596. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +2 -1
  597. package/src/modules/workflow/git/__tests__/git.service.status.test.ts +4 -3
  598. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +8 -7
  599. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +41 -3
  600. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  601. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  602. package/src/modules/workflow/git/git.service.ts +49 -17
  603. package/src/modules/workflow/git/node-git-runner.ts +43 -4
  604. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  605. package/src/modules/workflow/review-workflow/review-workflow.service.ts +9 -6
  606. package/src/modules/workflow/session-ontology.service.ts +7 -2
  607. package/src/modules/workflow/workflow.routes.ts +3 -1
  608. package/src/modules/workflow/workflow.service.ts +201 -76
  609. package/src/modules/workspace/__tests__/file-stat-access.test.ts +3 -2
  610. package/src/modules/workspace/__tests__/git-internals.security.test.ts +10 -9
  611. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +4 -2
  612. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -2
  613. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +2 -1
  614. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +2 -1
  615. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +2 -1
  616. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +2 -1
  617. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -1
  618. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +2 -1
  619. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +5 -4
  620. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +2 -1
  621. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +2 -1
  622. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  623. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
  624. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  625. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  626. package/src/modules/workspace/__tests__/workspace.service.test.ts +84 -50
  627. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +45 -1
  628. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -7
  629. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +237 -10
  630. package/src/modules/workspace/__tests__/workspace.tools.test.ts +44 -11
  631. package/src/modules/workspace/__tests__/write-denial.test.ts +5 -4
  632. package/src/modules/workspace/session-ontology.gate.ts +6 -4
  633. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +6 -2
  634. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +240 -21
  635. package/src/modules/workspace/startup/kb-git.ts +15 -22
  636. package/src/modules/workspace/startup/kb-startup-runner.ts +198 -45
  637. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +104 -98
  638. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +24 -16
  639. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +14 -5
  640. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +13 -5
  641. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +14 -5
  642. package/src/modules/workspace/startup/steps/seed-tree.ts +11 -9
  643. package/src/modules/workspace/startup/steps/template-files.step.ts +43 -34
  644. package/src/modules/workspace/startup/steps/template-source.ts +8 -5
  645. package/src/modules/workspace/workspace.routes.ts +8 -7
  646. package/src/modules/workspace/workspace.service.ts +47 -28
  647. package/src/modules/workspace/workspace.tools.ts +56 -20
  648. package/src/modules/workspace/write-denial.ts +7 -5
  649. package/src/shared/__tests__/kb-layout.plugin.test.ts +19 -16
  650. package/src/shared/domain-errors.ts +58 -0
  651. package/src/shared/git.contract.ts +80 -8
  652. package/src/shared/kb-context.ts +130 -0
  653. package/src/shared/kb-layout.test.ts +37 -27
  654. package/src/shared/kb-layout.ts +4 -3
  655. package/src/shared/pending-proposals.ts +7 -4
  656. package/src/shared/redact-secret.ts +5 -5
  657. package/src/tenancy/__tests__/static-tenant-source.test.ts +177 -0
  658. package/src/tenancy/__tests__/tenant-host.test.ts +372 -0
  659. package/src/tenancy/__tests__/tenant-runtime.test.ts +221 -0
  660. package/src/tenancy/__tests__/tenant-secrets.test.ts +35 -0
  661. package/src/tenancy/index.ts +35 -0
  662. package/src/tenancy/static-tenant-source.ts +296 -0
  663. package/src/tenancy/tenant-host.ts +261 -0
  664. package/src/tenancy/tenant-runtime.ts +208 -0
  665. package/src/tenancy/tenant-secrets.ts +41 -0
  666. package/src/tenancy/tenant-source.contract.ts +59 -0
@@ -0,0 +1,848 @@
1
+ import { createVerify, generateKeyPairSync } from 'node:crypto';
2
+ import type { Server as HttpServer } from 'node:http';
3
+ import type { AddressInfo } from 'node:net';
4
+ import express from 'express';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import type { IAdminAccessService } from '../../admin/admin.interface.js';
7
+ import type { Database } from '../../database/connection.js';
8
+ import { DeploymentSettingsService, SettingsValidationError } from '../../settings/deployment-settings.service.js';
9
+ import { GitHubAppClient, GitHubAppError, appJwt, normalizePrivateKey } from '../github-app.client.js';
10
+ import { GitHubAppConnection } from '../github-app.connection.js';
11
+ import { createGitHubAppRoutes, githubAppManifest } from '../github-app.routes.js';
12
+
13
+ const ENC_KEY = 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=';
14
+ const { privateKey: PEM, publicKey: PUBLIC } = generateKeyPairSync('rsa', {
15
+ modulusLength: 2048,
16
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
17
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
18
+ });
19
+
20
+ const APP = { appId: '4242', slug: 'hexis-acme', privateKey: PEM, clientId: 'Iv1.abc', clientSecret: 'a-client-secret' };
21
+ const APP_ENV = {
22
+ GITHUB_APP_ID: APP.appId,
23
+ GITHUB_APP_SLUG: APP.slug,
24
+ GITHUB_APP_PRIVATE_KEY: PEM.replace(/\n/g, '\\n'),
25
+ GITHUB_APP_CLIENT_ID: APP.clientId,
26
+ GITHUB_APP_CLIENT_SECRET: APP.clientSecret,
27
+ };
28
+
29
+ let server: HttpServer | null = null;
30
+
31
+ afterEach(() => {
32
+ server?.close();
33
+ server = null;
34
+ });
35
+
36
+ /** GitHub, as far as a suite says: what each person can reach, and every call that was made. */
37
+ function github(
38
+ world: {
39
+ /** The installations each person reaches. */
40
+ installations?: Record<string, string[]>;
41
+ /** What the INSTALLATION covers. */
42
+ repositories?: string[];
43
+ /** What each PERSON may do in the repositories the installation covers. Default: push, to all of them. */
44
+ theirs?: Record<string, Record<string, 'push' | 'pull'>>;
45
+ down?: boolean;
46
+ /** Accepts the connection and never answers. */
47
+ silent?: boolean;
48
+ tokenLifeMs?: number;
49
+ /** The time GitHub goes by, for a suite that moves the clock. */
50
+ now?: () => number;
51
+ } = {},
52
+ ) {
53
+ const calls: { method: string; url: string; authorization: string }[] = [];
54
+ let issued = 0;
55
+ const json = (status: number, body: unknown) => new Response(JSON.stringify(body), { status });
56
+ const covered = () => world.repositories ?? ['acme/kb', 'acme/another'];
57
+ const impl = (async (input: string | URL | Request, init?: RequestInit) => {
58
+ const url = String(input);
59
+ const headers = (init?.headers ?? {}) as Record<string, string>;
60
+ calls.push({ method: init?.method ?? 'GET', url, authorization: headers.Authorization ?? '' });
61
+ if (world.down) throw new Error('getaddrinfo ENOTFOUND api.github.com');
62
+ if (world.silent) {
63
+ return new Promise<Response>((_resolve, reject) => {
64
+ init?.signal?.addEventListener('abort', () => reject(init.signal!.reason as Error));
65
+ });
66
+ }
67
+ const theirRepositories = /\/user\/installations\/(\d+)\/repositories/.exec(url);
68
+ if (theirRepositories) {
69
+ const person = headers.Authorization.replace('Bearer user-token-of-', '');
70
+ if (!(world.installations?.[person] ?? []).includes(theirRepositories[1]!)) return json(404, { message: 'Not Found' });
71
+ const may = world.theirs?.[person] ?? Object.fromEntries(covered().map((name) => [name, 'push' as const]));
72
+ const names = Object.keys(may);
73
+ return json(200, {
74
+ total_count: names.length,
75
+ repositories: names.map((full_name) => ({
76
+ full_name,
77
+ private: true,
78
+ default_branch: 'main',
79
+ permissions: { pull: true, push: may[full_name] === 'push' },
80
+ })),
81
+ });
82
+ }
83
+ if (url.includes('/app-manifests/')) {
84
+ return url.includes('/good-code/')
85
+ ? json(201, { id: 4242, slug: APP.slug, pem: PEM, client_id: APP.clientId, client_secret: APP.clientSecret, owner: { login: 'acme' }, html_url: 'https://github.com/apps/hexis-acme' })
86
+ : json(404, { message: 'Not Found' });
87
+ }
88
+ if (url.endsWith('/login/oauth/access_token')) {
89
+ const { code } = JSON.parse(String(init?.body)) as { code: string };
90
+ return code.startsWith('code-of-') ? json(200, { access_token: `user-token-of-${code.slice(8)}` }) : json(200, { error: 'bad_verification_code' });
91
+ }
92
+ if (url.includes('/user/installations')) {
93
+ const person = headers.Authorization.replace('Bearer user-token-of-', '');
94
+ const ids = world.installations?.[person] ?? [];
95
+ return json(200, { installations: ids.map((id) => ({ id: Number(id), account: { login: `org-of-${id}` }, repository_selection: 'selected' })) });
96
+ }
97
+ if (/\/app\/installations\/\d+\/access_tokens$/.test(url)) {
98
+ issued += 1;
99
+ return json(201, {
100
+ token: `installation-token-${issued}`,
101
+ expires_at: new Date((world.now?.() ?? Date.now()) + (world.tokenLifeMs ?? 3_600_000)).toISOString(),
102
+ });
103
+ }
104
+ if (url.includes('/installation/repositories')) {
105
+ const names = covered();
106
+ return json(200, {
107
+ total_count: names.length,
108
+ repositories: names.map((full_name) => ({ full_name, private: true, default_branch: 'main', permissions: { push: true } })),
109
+ });
110
+ }
111
+ return json(404, { message: 'Not Found' });
112
+ }) as typeof fetch;
113
+ return { client: new GitHubAppClient(impl, Date.now, 150), calls, tokensIssued: () => issued };
114
+ }
115
+
116
+ function settingsWith(env: Record<string, string> = {}) {
117
+ const rows = new Map<string, { value: string; encrypted: boolean }>();
118
+ const db = {
119
+ select: () => ({ from: () => Promise.resolve([]) }),
120
+ insert: () => ({
121
+ values: (row: { key: string; value: string; encrypted: boolean }) => ({
122
+ onConflictDoUpdate: () => {
123
+ rows.set(row.key, { value: row.value, encrypted: row.encrypted });
124
+ return Promise.resolve();
125
+ },
126
+ }),
127
+ }),
128
+ delete: () => ({ where: () => Promise.resolve() }),
129
+ } as unknown as Database;
130
+ return { settings: new DeploymentSettingsService(db, ENC_KEY, undefined, { env }), rows };
131
+ }
132
+
133
+ /** A deployment's GitHub routes, and a browser that keeps its cookies and does not follow redirects. */
134
+ function deployment(opts: { env?: Record<string, string>; world?: Parameters<typeof github>[0]; admin?: boolean; complete?: boolean } = {}) {
135
+ const { settings, rows } = settingsWith(opts.env);
136
+ const hub = github(opts.world);
137
+ const connection = new GitHubAppConnection({
138
+ read: (key) => settings.resolve(key),
139
+ sourceOf: (key) => settings.sourceOf(key),
140
+ client: hub.client,
141
+ });
142
+ const app = express();
143
+ app.use(express.json());
144
+ app.use((req, _res, next) => {
145
+ req.userEmail = 'root@example.com';
146
+ req.userId = 'user-1';
147
+ next();
148
+ });
149
+ app.use(
150
+ '/api',
151
+ createGitHubAppRoutes({
152
+ settings,
153
+ connection,
154
+ adminAccess: { isAdmin: async () => opts.admin ?? true } as IAdminAccessService,
155
+ publicBackendUrl: 'https://kb.acme.test',
156
+ publicFrontendUrl: 'https://kb.acme.test',
157
+ isComplete: () => opts.complete ?? false,
158
+ client: hub.client,
159
+ }),
160
+ );
161
+ server = app.listen(0);
162
+ const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
163
+ const jar = new Map<string, string>();
164
+ const go = async (path: string, init: RequestInit = {}) => {
165
+ const res = await fetch(`${base}${path}`, {
166
+ ...init,
167
+ redirect: 'manual',
168
+ headers: {
169
+ ...(init.body ? { 'content-type': 'application/json' } : {}),
170
+ ...(jar.size ? { cookie: [...jar].map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
171
+ },
172
+ });
173
+ for (const line of res.headers.getSetCookie()) {
174
+ const [pair] = line.split(';');
175
+ const [name, value = ''] = pair!.split('=');
176
+ if (!value || /Expires=Thu, 01 Jan 1970/i.test(line)) jar.delete(name!);
177
+ else jar.set(name!, value);
178
+ }
179
+ return res;
180
+ };
181
+ const stateOf = (location: string | null) => new URL(location!).searchParams.get('state')!;
182
+ return { go, settings, rows, connection, hub, stateOf, jar };
183
+ }
184
+
185
+ describe('the JWT an app presents as itself', () => {
186
+ it('is signed with the app key, names the app, and is good for minutes', () => {
187
+ const now = Date.parse('2026-09-29T10:00:00Z');
188
+ const [header, claims, signature] = appJwt(APP, now).split('.');
189
+ expect(createVerify('RSA-SHA256').update(`${header}.${claims}`).end().verify(PUBLIC, signature!, 'base64url')).toBe(true);
190
+ const read = JSON.parse(Buffer.from(claims!, 'base64url').toString('utf8')) as { iss: string; iat: number; exp: number };
191
+ expect(read.iss).toBe('4242');
192
+ // Issued a minute back, for a clock that runs ahead of GitHub's.
193
+ expect(read.iat).toBe(now / 1000 - 60);
194
+ expect(read.exp - now / 1000).toBeLessThanOrEqual(600);
195
+ });
196
+
197
+ it.each([
198
+ ['as issued', PEM],
199
+ ['with its line breaks written out, as an environment file holds it', PEM.replace(/\n/g, '\\n')],
200
+ ['in base64', Buffer.from(PEM).toString('base64')],
201
+ ])('reads the key %s', (_how, key) => {
202
+ expect(normalizePrivateKey(key).trim()).toBe(PEM.trim());
203
+ expect(() => appJwt({ appId: '1', privateKey: key })).not.toThrow();
204
+ });
205
+
206
+ it('says the key could not be read, without quoting it', () => {
207
+ const err = (() => {
208
+ try {
209
+ appJwt({ appId: '1', privateKey: 'not-a-key-but-a-secret' });
210
+ } catch (e) {
211
+ return e;
212
+ }
213
+ })();
214
+ expect(err).toBeInstanceOf(GitHubAppError);
215
+ expect((err as Error).message).not.toContain('not-a-key-but-a-secret');
216
+ });
217
+ });
218
+
219
+ describe('what the settings keep for the deployment itself', () => {
220
+ it('refuses them from a save, the way it refuses a setting that does not exist', async () => {
221
+ const { settings } = settingsWith();
222
+ for (const key of ['githubInstallationId', 'githubAppId', 'githubAppPrivateKey', 'githubAppStateTag']) {
223
+ const err = await settings.save({ [key]: '42' }, 'user-1').catch((e: unknown) => e);
224
+ expect(err).toBeInstanceOf(SettingsValidationError);
225
+ expect((err as SettingsValidationError).problems).toEqual({ [key]: 'Unknown setting.' });
226
+ expect(settings.resolve(key)).toBe('');
227
+ }
228
+ });
229
+
230
+ it('never tells the setup screen about them', () => {
231
+ const { settings } = settingsWith(APP_ENV);
232
+ const told = settings.describe().map((s) => s.key);
233
+ expect(told).toContain('githubRepository');
234
+ expect(told.filter((key) => key.startsWith('githubApp') || key.startsWith('githubInstallation'))).toEqual([]);
235
+ });
236
+
237
+ it('stores the key it was issued encrypted', async () => {
238
+ const { settings, rows } = settingsWith();
239
+ await settings.record({ githubAppPrivateKey: PEM, githubAppId: '4242' }, null);
240
+ expect(rows.get('githubAppPrivateKey')).toMatchObject({ encrypted: true });
241
+ expect(rows.get('githubAppPrivateKey')!.value).not.toContain('PRIVATE KEY');
242
+ expect(settings.resolve('githubAppPrivateKey')).toBe(PEM.trim());
243
+ });
244
+ });
245
+
246
+ describe('GitHubAppConnection: the token git presents', () => {
247
+ function connected(world: Parameters<typeof github>[0] = {}, clock = { now: Date.now() }) {
248
+ const values: Record<string, string> = {
249
+ ...Object.fromEntries(Object.entries(APP).map(([k, v]) => [`githubApp${k[0]!.toUpperCase()}${k.slice(1)}`, v])),
250
+ githubAppId: APP.appId,
251
+ githubInstallationId: '77',
252
+ githubRepository: 'acme/kb',
253
+ githubRepositoriesPermitted: 'acme/kb',
254
+ };
255
+ world.now = () => clock.now;
256
+ const hub = github(world);
257
+ const connection = new GitHubAppConnection({
258
+ read: (key) => values[key] ?? '',
259
+ sourceOf: () => 'stored',
260
+ client: hub.client,
261
+ now: () => clock.now,
262
+ });
263
+ return { connection, hub, values, clock };
264
+ }
265
+
266
+ it('has none until it is asked for, then the one GitHub issued for the installation', async () => {
267
+ const { connection, hub } = connected();
268
+ expect(connection.token()).toBeNull();
269
+ await connection.prepare();
270
+ expect(connection.token()).toBe('installation-token-1');
271
+ expect(hub.calls.at(-1)).toMatchObject({ method: 'POST', url: 'https://api.github.com/app/installations/77/access_tokens' });
272
+ expect(connection.url()).toBe('https://github.com/acme/kb.git');
273
+ expect(connection.answered()).toBe(true);
274
+ });
275
+
276
+ /** What a renewal started in the background has come to. */
277
+ const settled = () => new Promise<void>((resolve) => setTimeout(resolve, 20));
278
+
279
+ it('asks nothing while the one in hand has time to spare', async () => {
280
+ const { connection, hub, clock } = connected();
281
+ await connection.prepare();
282
+ await connection.prepare();
283
+ clock.now += 45 * 60_000;
284
+ await connection.prepare();
285
+ expect(hub.tokensIssued()).toBe(1);
286
+ });
287
+
288
+ /**
289
+ * This runs before every git call, the ones that never leave the disk
290
+ * included. A token that is still good is not a reason to wait.
291
+ */
292
+ it('renews one that is close to running out without making the call wait', async () => {
293
+ const world: Parameters<typeof github>[0] = {};
294
+ const { connection, hub, clock } = connected(world);
295
+ await connection.prepare();
296
+ clock.now += 51 * 60_000;
297
+ // GitHub stops answering: a call that waited would wait for the deadline.
298
+ world.silent = true;
299
+ const started = Date.now();
300
+ await connection.prepare();
301
+ expect(Date.now() - started).toBeLessThan(100);
302
+ expect(connection.token()).toBe('installation-token-1');
303
+ world.silent = false;
304
+ await new Promise((resolve) => setTimeout(resolve, 200));
305
+
306
+ // And once GitHub answers, the next call carries the new one.
307
+ clock.now += 10_000;
308
+ await connection.prepare();
309
+ await settled();
310
+ expect(hub.tokensIssued()).toBe(2);
311
+ expect(connection.token()).toBe('installation-token-2');
312
+ });
313
+
314
+ it('gives a request to GitHub a deadline of its own', async () => {
315
+ const { connection } = connected({ silent: true });
316
+ const started = Date.now();
317
+ await expect(connection.prepare()).rejects.toBeInstanceOf(GitHubAppError);
318
+ // The suite's client is given 150 ms; left to the platform it is minutes.
319
+ expect(Date.now() - started).toBeLessThan(1_000);
320
+ expect(connection.token()).toBeNull();
321
+ });
322
+
323
+ it('remembers that GitHub gave none, for a while that grows, and asks nothing inside it', async () => {
324
+ const world: Parameters<typeof github>[0] = { down: true };
325
+ const { connection, hub, clock } = connected(world);
326
+ await expect(connection.prepare()).rejects.toBeInstanceOf(GitHubAppError);
327
+ expect(hub.calls).toHaveLength(1);
328
+
329
+ // Every git call in the meantime goes ahead at once, with no token.
330
+ for (let i = 0; i < 20; i += 1) await connection.prepare();
331
+ expect(hub.calls).toHaveLength(1);
332
+ expect(connection.token()).toBeNull();
333
+
334
+ // Five seconds the first time, ten the second.
335
+ clock.now += 5_001;
336
+ await expect(connection.prepare()).rejects.toBeInstanceOf(GitHubAppError);
337
+ expect(hub.calls).toHaveLength(2);
338
+ clock.now += 5_001;
339
+ await connection.prepare();
340
+ expect(hub.calls).toHaveLength(2);
341
+ clock.now += 5_000;
342
+ await expect(connection.prepare()).rejects.toBeInstanceOf(GitHubAppError);
343
+ expect(hub.calls).toHaveLength(3);
344
+
345
+ // GitHub is back: the next ask gets a token, and nothing is remembered.
346
+ world.down = false;
347
+ clock.now += 20_001;
348
+ await connection.prepare();
349
+ expect(connection.token()).toBe('installation-token-1');
350
+ });
351
+
352
+ it('asks whatever it remembers when an admin is waiting on the answer', async () => {
353
+ const world: Parameters<typeof github>[0] = { down: true };
354
+ const { connection, hub } = connected(world);
355
+ await expect(connection.prepare()).rejects.toBeInstanceOf(GitHubAppError);
356
+ await expect(connection.prepare({ asked: true })).rejects.toBeInstanceOf(GitHubAppError);
357
+ expect(hub.calls).toHaveLength(2);
358
+ world.down = false;
359
+ await connection.prepare({ asked: true });
360
+ expect(connection.token()).toBe('installation-token-1');
361
+ });
362
+
363
+ it('asks once for every git call that arrives while it is renewing', async () => {
364
+ const { connection, hub } = connected();
365
+ await Promise.all([connection.prepare(), connection.prepare(), connection.prepare()]);
366
+ expect(hub.tokensIssued()).toBe(1);
367
+ });
368
+
369
+ it('never presents a token that has run out', async () => {
370
+ const { connection, clock } = connected({ tokenLifeMs: 60_000 });
371
+ await connection.prepare();
372
+ clock.now += 61_000;
373
+ expect(connection.token()).toBeNull();
374
+ });
375
+
376
+ it('never presents one installation its token to another', async () => {
377
+ const { connection, values, hub } = connected();
378
+ await connection.prepare();
379
+ values.githubInstallationId = '88';
380
+ expect(connection.token()).toBeNull();
381
+ await connection.prepare();
382
+ expect(hub.calls.at(-1)!.url).toContain('/installations/88/');
383
+ expect(connection.token()).toBe('installation-token-2');
384
+ });
385
+
386
+ it('keeps the token it has when GitHub will not renew it', async () => {
387
+ const world: Parameters<typeof github>[0] = {};
388
+ const { connection, clock } = connected(world);
389
+ await connection.prepare();
390
+ world.down = true;
391
+ clock.now += 55 * 60_000;
392
+ await connection.prepare();
393
+ await settled();
394
+ expect(connection.token()).toBe('installation-token-1');
395
+ });
396
+ });
397
+
398
+ describe('GitHubAppConnection: what the deployment may be pointed at', () => {
399
+ function connected(permitted: string, installationFrom: 'env' | 'stored' = 'stored', world: Parameters<typeof github>[0] = {}) {
400
+ const values: Record<string, string> = {
401
+ githubAppId: APP.appId,
402
+ githubAppSlug: APP.slug,
403
+ githubAppPrivateKey: APP.privateKey,
404
+ githubAppClientId: APP.clientId,
405
+ githubAppClientSecret: APP.clientSecret,
406
+ githubInstallationId: '77',
407
+ githubRepositoriesPermitted: permitted,
408
+ };
409
+ const hub = github(world);
410
+ return new GitHubAppConnection({
411
+ read: (key) => values[key] ?? '',
412
+ sourceOf: (key) => (key === 'githubInstallationId' ? installationFrom : 'stored'),
413
+ client: hub.client,
414
+ });
415
+ }
416
+
417
+ it('is what the person who connected it could push to, whatever case it is written in', () => {
418
+ const connection = connected('acme/kb\nAcme/Website');
419
+ expect(connection.permits('acme/kb')).toBe(true);
420
+ expect(connection.permits('ACME/website')).toBe(true);
421
+ expect(connection.permits('acme/secrets')).toBe(false);
422
+ expect(connection.permits('acme/kb-private')).toBe(false);
423
+ expect(connection.permits('')).toBe(false);
424
+ });
425
+
426
+ it('is nothing when nobody was shown to be able to push to anything', () => {
427
+ expect(connected('').permits('acme/kb')).toBe(false);
428
+ });
429
+
430
+ it('is everything the installation reaches when the operator supplied the installation', () => {
431
+ expect(connected('', 'env').permits('acme/anything')).toBe(true);
432
+ });
433
+
434
+ it('lists what the installation covers AND the person could push to, never the rest', async () => {
435
+ const world = { repositories: ['acme/kb', 'acme/payroll', 'acme/secrets'] };
436
+ expect((await connected('acme/kb\nacme/gone-since', 'stored', world).repositories()).repositories.map((r) => r.fullName)).toEqual([
437
+ 'acme/kb',
438
+ ]);
439
+ expect((await connected('', 'env', world).repositories()).repositories.map((r) => r.fullName)).toEqual([
440
+ 'acme/kb',
441
+ 'acme/payroll',
442
+ 'acme/secrets',
443
+ ]);
444
+ });
445
+
446
+ it('asks GitHub nothing while there is no app or no installation', async () => {
447
+ const hub = github();
448
+ const connection = new GitHubAppConnection({ read: () => '', sourceOf: () => 'unset', client: hub.client });
449
+ await connection.prepare();
450
+ expect(hub.calls).toEqual([]);
451
+ expect(connection.answered()).toBe(false);
452
+ expect(connection.registeredBy()).toBeNull();
453
+ });
454
+ });
455
+
456
+ describe('registering a deployment its own GitHub App', () => {
457
+ it('asks for the contents of the repositories it is installed on, and nothing else', () => {
458
+ const manifest = githubAppManifest('https://kb.acme.test', 'https://kb.acme.test');
459
+ expect(manifest).toMatchObject({
460
+ name: 'Hexis kb.acme.test',
461
+ public: false,
462
+ default_permissions: { contents: 'write', metadata: 'read' },
463
+ default_events: [],
464
+ redirect_url: 'https://kb.acme.test/api/setup/github-app/registered',
465
+ callback_urls: ['https://kb.acme.test/api/setup/github-app/callback'],
466
+ request_oauth_on_install: true,
467
+ });
468
+ expect(manifest).not.toHaveProperty('hook_attributes');
469
+ const long = githubAppManifest('https://x.test', 'https://a-very-long-host-name.knowledge.example.test');
470
+ expect(String(long.name).length).toBeLessThanOrEqual(34);
471
+ });
472
+
473
+ it('hands the browser the manifest and where to post it, for a person or an organisation', async () => {
474
+ const { go, stateOf, jar } = deployment();
475
+ const personal = (await (await go('/api/setup/github-app/manifest', { method: 'POST', body: '{}' })).json()) as { action: string; manifest: unknown };
476
+ expect(personal.action).toMatch(/^https:\/\/github\.com\/settings\/apps\/new\?state=/);
477
+ expect(stateOf(personal.action)).toBe(decodeURIComponent(jar.get('hexis_github_state')!));
478
+ const org = (await (await go('/api/setup/github-app/manifest', { method: 'POST', body: JSON.stringify({ organization: 'acme-inc' }) })).json()) as { action: string };
479
+ expect(org.action).toMatch(/^https:\/\/github\.com\/organizations\/acme-inc\/settings\/apps\/new\?state=/);
480
+ const bad = await go('/api/setup/github-app/manifest', { method: 'POST', body: JSON.stringify({ organization: 'acme/../evil' }) });
481
+ expect(bad.status).toBe(400);
482
+ });
483
+
484
+ it('keeps what GitHub issued and goes straight on to installing the app', async () => {
485
+ const { go, stateOf, settings, connection, rows } = deployment();
486
+ const { action } = (await (await go('/api/setup/github-app/manifest', { method: 'POST', body: '{}' })).json()) as { action: string };
487
+ const back = await go(`/api/setup/github-app/registered?code=good-code&state=${encodeURIComponent(stateOf(action))}`);
488
+ expect(back.status).toBe(302);
489
+ expect(back.headers.get('location')).toMatch(/^https:\/\/github\.com\/apps\/hexis-acme\/installations\/new\?state=/);
490
+ expect(connection.credentials()).toMatchObject({ appId: '4242', slug: 'hexis-acme', clientId: 'Iv1.abc' });
491
+ expect(connection.registeredBy()).toBe('setup');
492
+ expect(rows.get('githubAppPrivateKey')?.encrypted).toBe(true);
493
+ expect(rows.get('githubAppClientSecret')?.encrypted).toBe(true);
494
+ expect(settings.resolve('githubAppSlug')).toBe('hexis-acme');
495
+ });
496
+
497
+ it('spends the state on every return, however the return ends', async () => {
498
+ for (const query of ['code=good-code', 'code=stale', '']) {
499
+ const { go, stateOf, jar } = deployment();
500
+ const { action } = (await (await go('/api/setup/github-app/manifest', { method: 'POST', body: '{}' })).json()) as { action: string };
501
+ const state = stateOf(action);
502
+ const back = await go(`/api/setup/github-app/registered?${query}&state=${encodeURIComponent(state)}`);
503
+ // Going on to install is a round trip of its own, with a state of its own.
504
+ const kept = jar.get('hexis_github_state');
505
+ expect(kept === undefined || decodeURIComponent(kept) !== state, query).toBe(true);
506
+ if (!back.headers.get('location')!.startsWith('https://github.com/')) expect(kept, query).toBeUndefined();
507
+ server?.close();
508
+ server = null;
509
+ }
510
+ });
511
+
512
+ it('starts nothing from a link: what starts a round trip is a POST', async () => {
513
+ const { go, jar } = deployment({ env: APP_ENV });
514
+ const followed = await go('/api/setup/github-app/install');
515
+ expect(followed.status).toBe(404);
516
+ expect(jar.size).toBe(0);
517
+ });
518
+
519
+ it('registers nothing for a browser that was not sent, or a code GitHub does not know', async () => {
520
+ const { go, stateOf, connection } = deployment();
521
+ const stranger = await go('/api/setup/github-app/registered?code=good-code&state=made-up');
522
+ expect(stranger.headers.get('location')).toBe('https://kb.acme.test/?github=state');
523
+ const { action } = (await (await go('/api/setup/github-app/manifest', { method: 'POST', body: '{}' })).json()) as { action: string };
524
+ const unknown = await go(`/api/setup/github-app/registered?code=stale&state=${encodeURIComponent(stateOf(action))}`);
525
+ expect(unknown.headers.get('location')).toBe('https://kb.acme.test/?github=refused');
526
+ expect(connection.credentials()).toBeNull();
527
+ });
528
+
529
+ it('offers no registration to a deployment whose operator supplied the app', async () => {
530
+ const { go, connection } = deployment({ env: APP_ENV });
531
+ expect(connection.registeredBy()).toBe('environment');
532
+ expect((await go('/api/setup/github-app/manifest', { method: 'POST', body: '{}' })).status).toBe(409);
533
+ const status = (await (await go('/api/setup/github-app')).json()) as Record<string, unknown>;
534
+ expect(status).toEqual({
535
+ registeredBy: 'environment',
536
+ app: { slug: 'hexis-acme', url: 'https://github.com/apps/hexis-acme' },
537
+ installation: null,
538
+ repository: null,
539
+ });
540
+ });
541
+ });
542
+
543
+ describe('installing the app, and whose installation it is', () => {
544
+ const world = { installations: { ada: ['77'], mallory: ['99'] } };
545
+
546
+ async function sentToInstall(d: ReturnType<typeof deployment>) {
547
+ const out = await d.go('/api/setup/github-app/install', { method: 'POST', body: '{}' });
548
+ const { url } = (await out.json()) as { url: string };
549
+ expect(url).toMatch(/^https:\/\/github\.com\/apps\/hexis-acme\/installations\/new\?state=/);
550
+ return d.stateOf(url);
551
+ }
552
+ const cameBackAs = (d: ReturnType<typeof deployment>, person: string, state: string, installation = '77') =>
553
+ d.go(`/api/setup/github-app/callback?code=code-of-${person}&installation_id=${installation}&setup_action=install&state=${encodeURIComponent(state)}`);
554
+
555
+ it('records the installation, and what the person who connected it can push to', async () => {
556
+ const d = deployment({ env: APP_ENV, world });
557
+ const state = await sentToInstall(d);
558
+ const back = await cameBackAs(d, 'ada', state);
559
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=connected');
560
+ expect(d.settings.resolve('githubInstallationId')).toBe('77');
561
+ expect(d.settings.resolve('githubInstallationAccount')).toBe('org-of-77');
562
+ expect(d.settings.resolve('githubRepositoriesPermitted').split('\n')).toEqual(['acme/another', 'acme/kb']);
563
+ // The state is spent with the round trip.
564
+ expect(d.jar.has('hexis_github_state')).toBe(false);
565
+ });
566
+
567
+ /**
568
+ * GitHub counts an installation among a person's when they can READ one
569
+ * repository it covers. Organisation X installed the app for its own
570
+ * workspace; Eve, who may read X's handbook, opens a workspace of her own
571
+ * and comes back naming X's installation. The installation's token reads
572
+ * and writes everything X gave the app. What Eve may point her workspace
573
+ * at is what her own account could push to: nothing.
574
+ */
575
+ it('gives someone who can only read what the installation covers nothing at all', async () => {
576
+ const d = deployment({
577
+ env: APP_ENV,
578
+ world: {
579
+ installations: { eve: ['77'] },
580
+ repositories: ['x/handbook', 'x/payroll', 'x/secrets'],
581
+ theirs: { eve: { 'x/handbook': 'pull' } },
582
+ },
583
+ });
584
+ const state = await sentToInstall(d);
585
+ const back = await cameBackAs(d, 'eve', state);
586
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=nothing-to-write');
587
+ expect(d.settings.resolve('githubInstallationId')).toBe('');
588
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe('');
589
+ // No token for the installation was ever asked for, so nothing of X's was listed.
590
+ expect(d.hub.tokensIssued()).toBe(0);
591
+ expect((await d.go('/api/setup/github-app/repositories')).status).toBe(409);
592
+ });
593
+
594
+ it('offers someone who can push to one repository that one, and nothing else the installation covers', async () => {
595
+ const d = deployment({
596
+ env: APP_ENV,
597
+ world: {
598
+ installations: { bo: ['77'] },
599
+ repositories: ['x/docs', 'x/handbook', 'x/payroll'],
600
+ theirs: { bo: { 'x/docs': 'push', 'x/handbook': 'pull' } },
601
+ },
602
+ });
603
+ const state = await sentToInstall(d);
604
+ expect((await cameBackAs(d, 'bo', state)).headers.get('location')).toBe('https://kb.acme.test/?github=connected');
605
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe('x/docs');
606
+ const listed = (await (await d.go('/api/setup/github-app/repositories')).json()) as { repositories: { fullName: string }[] };
607
+ expect(listed.repositories.map((r) => r.fullName)).toEqual(['x/docs']);
608
+ expect(d.connection.permits('x/payroll')).toBe(false);
609
+ expect(d.connection.permits('x/handbook')).toBe(false);
610
+ });
611
+
612
+ it('brings what may be connected up to date when GitHub is connected again', async () => {
613
+ const theirs: Record<string, Record<string, 'push' | 'pull'>> = { ada: { 'acme/kb': 'push' } };
614
+ const d = deployment({ env: APP_ENV, world: { installations: { ada: ['77'] }, theirs } });
615
+ await cameBackAs(d, 'ada', await sentToInstall(d));
616
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe('acme/kb');
617
+ theirs.ada = { 'acme/another': 'push' };
618
+ await cameBackAs(d, 'ada', await sentToInstall(d));
619
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe('acme/another');
620
+ });
621
+
622
+ /**
623
+ * With one app serving many deployments, the number on the address is the
624
+ * only thing between a deployment and another organisation's repositories.
625
+ * Mallory installs the app on her own account, then comes back naming Ada's.
626
+ */
627
+ it('refuses an installation the person cannot reach, whatever the address says', async () => {
628
+ const d = deployment({ env: APP_ENV, world });
629
+ const state = await sentToInstall(d);
630
+ const back = await d.go(`/api/setup/github-app/callback?code=code-of-mallory&installation_id=77&setup_action=install&state=${encodeURIComponent(state)}`);
631
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=not-yours');
632
+ expect(d.settings.resolve('githubInstallationId')).toBe('');
633
+ expect(d.hub.tokensIssued()).toBe(0);
634
+ });
635
+
636
+ it.each([
637
+ ['no sign-in came back with it', 'installation_id=77&setup_action=install', 'refused'],
638
+ ['GitHub does not know the sign-in', 'code=made-up&installation_id=77&setup_action=install', 'refused'],
639
+ ['the installation is not a number', 'code=code-of-ada&installation_id=77%2F..%2F99&setup_action=install', 'refused'],
640
+ ['it was only requested of an owner', 'code=code-of-ada&installation_id=77&setup_action=request', 'requested'],
641
+ ])('records nothing when %s', async (_why, query, outcome) => {
642
+ const d = deployment({ env: APP_ENV, world });
643
+ const state = await sentToInstall(d);
644
+ const back = await d.go(`/api/setup/github-app/callback?${query}&state=${encodeURIComponent(state)}`);
645
+ expect(back.headers.get('location')).toBe(`https://kb.acme.test/?github=${outcome}`);
646
+ expect(d.settings.resolve('githubInstallationId')).toBe('');
647
+ });
648
+
649
+ it('records nothing for a browser that was not sent', async () => {
650
+ const d = deployment({ env: APP_ENV, world });
651
+ const back = await d.go('/api/setup/github-app/callback?code=code-of-ada&installation_id=77&setup_action=install&state=made-up');
652
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=state');
653
+ expect(d.settings.resolve('githubInstallationId')).toBe('');
654
+ expect(d.hub.calls).toEqual([]);
655
+ });
656
+
657
+ it('says GitHub could not be reached, when that is what happened', async () => {
658
+ const d = deployment({ env: APP_ENV, world: { ...world, down: true } });
659
+ const state = await sentToInstall(d);
660
+ const back = await d.go(`/api/setup/github-app/callback?code=code-of-ada&installation_id=77&setup_action=install&state=${encodeURIComponent(state)}`);
661
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=unreachable');
662
+ });
663
+
664
+ it('comes back to the Deployment page on a deployment that is set up', async () => {
665
+ const d = deployment({ env: APP_ENV, world, complete: true });
666
+ const state = await sentToInstall(d);
667
+ const back = await d.go(`/api/setup/github-app/callback?code=code-of-ada&installation_id=77&setup_action=install&state=${encodeURIComponent(state)}`);
668
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/deployment?github=connected');
669
+ });
670
+
671
+ it('puts the tag of a host that serves several deployments in front of the state', async () => {
672
+ const d = deployment({ env: { ...APP_ENV, GITHUB_APP_STATE_TAG: 'acme' }, world });
673
+ const state = await sentToInstall(d);
674
+ expect(state).toMatch(/^acme\.[A-Za-z0-9_-]{20,}$/);
675
+ const back = await d.go(`/api/setup/github-app/callback?code=code-of-ada&installation_id=77&setup_action=install&state=${encodeURIComponent(state)}`);
676
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=connected');
677
+ });
678
+ });
679
+
680
+ describe('the repositories an installation reaches', () => {
681
+ it('lists them by name, with the installation token', async () => {
682
+ const d = deployment({ env: { ...APP_ENV, GITHUB_APP_INSTALLATION_ID: '77' }, world: { repositories: ['acme/zeta', 'acme/kb'] } });
683
+ const res = await d.go('/api/setup/github-app/repositories');
684
+ expect(await res.json()).toEqual({
685
+ repositories: [
686
+ { fullName: 'acme/kb', private: true, defaultBranch: 'main', writable: true },
687
+ { fullName: 'acme/zeta', private: true, defaultBranch: 'main', writable: true },
688
+ ],
689
+ more: false,
690
+ });
691
+ expect(d.hub.calls.at(-1)!.authorization).toBe('Bearer installation-token-1');
692
+ });
693
+
694
+ it('asks for the connection first on a deployment that has none', async () => {
695
+ const d = deployment({ env: APP_ENV });
696
+ expect((await d.go('/api/setup/github-app/repositories')).status).toBe(409);
697
+ expect(d.hub.calls).toEqual([]);
698
+ });
699
+
700
+ it('says so when GitHub cannot be reached', async () => {
701
+ const d = deployment({ env: { ...APP_ENV, GITHUB_APP_INSTALLATION_ID: '77' }, world: { down: true } });
702
+ const res = await d.go('/api/setup/github-app/repositories');
703
+ expect(res.status).toBe(502);
704
+ expect(((await res.json()) as { error: string }).error).toMatch(/could not be reached/);
705
+ });
706
+ });
707
+
708
+ /**
709
+ * Installing comes back once. With the app installed, the address that
710
+ * installs it opens the installation's settings on GitHub, and GitHub sends
711
+ * nobody back from there: a repository added on that page was reached by the
712
+ * app and never offered. So what may be connected is read again by a sign-in
713
+ * alone, which comes back with a code and names no installation.
714
+ */
715
+ describe('reading again what may be connected', () => {
716
+ async function sentTo(d: ReturnType<typeof deployment>, path: string) {
717
+ const out = await d.go(path, { method: 'POST', body: '{}' });
718
+ const { url } = (await out.json()) as { url: string };
719
+ return { url, state: d.stateOf(url) };
720
+ }
721
+ const installedAs = async (d: ReturnType<typeof deployment>, person: string) => {
722
+ const { state } = await sentTo(d, '/api/setup/github-app/install');
723
+ await d.go(`/api/setup/github-app/callback?code=code-of-${person}&installation_id=77&setup_action=install&state=${encodeURIComponent(state)}`);
724
+ };
725
+ const signedInAs = (d: ReturnType<typeof deployment>, person: string, state: string) =>
726
+ d.go(`/api/setup/github-app/callback?code=code-of-${person}&state=${encodeURIComponent(state)}`);
727
+
728
+ it('sends the browser to sign in with the app, and nowhere that installs anything', async () => {
729
+ const d = deployment({ env: APP_ENV });
730
+ const { url } = await sentTo(d, '/api/setup/github-app/refresh');
731
+ const address = new URL(url);
732
+ expect(`${address.origin}${address.pathname}`).toBe('https://github.com/login/oauth/authorize');
733
+ expect(address.searchParams.get('client_id')).toBe(APP.clientId);
734
+ expect(address.searchParams.get('state')).toBeTruthy();
735
+ expect(d.hub.calls).toEqual([]);
736
+ });
737
+
738
+ it('offers a repository that was added to the installation on GitHub since', async () => {
739
+ const world = {
740
+ installations: { ada: ['77'] },
741
+ repositories: ['acme/kb'],
742
+ theirs: { ada: { 'acme/kb': 'push' as const } } as Record<string, Record<string, 'push' | 'pull'>>,
743
+ };
744
+ const d = deployment({ env: APP_ENV, world });
745
+ await installedAs(d, 'ada');
746
+ const listed = async () =>
747
+ ((await (await d.go('/api/setup/github-app/repositories')).json()) as { repositories: { fullName: string }[] }).repositories.map((r) => r.fullName);
748
+ expect(await listed()).toEqual(['acme/kb']);
749
+
750
+ // Added on the installation's settings page, from which nobody is sent back.
751
+ world.repositories = ['acme/kb', 'acme/handbook'];
752
+ world.theirs = { ada: { 'acme/kb': 'push', 'acme/handbook': 'push' } };
753
+ // The app reaches it, and it is not offered: it was not there when Ada connected.
754
+ expect(await listed()).toEqual(['acme/kb']);
755
+
756
+ const { state } = await sentTo(d, '/api/setup/github-app/refresh');
757
+ const back = await signedInAs(d, 'ada', state);
758
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=refreshed');
759
+ expect(await listed()).toEqual(['acme/handbook', 'acme/kb']);
760
+ expect(d.settings.resolve('githubInstallationId')).toBe('77');
761
+ expect(d.jar.has('hexis_github_state')).toBe(false);
762
+ });
763
+
764
+ it('is asked of the person who came back, about the installation the deployment has', async () => {
765
+ const d = deployment({ env: APP_ENV, world: { installations: { ada: ['77'], mallory: ['99'] } } });
766
+ await installedAs(d, 'ada');
767
+ const before = d.settings.resolve('githubRepositoriesPermitted');
768
+ const { state } = await sentTo(d, '/api/setup/github-app/refresh');
769
+ // Mallory reaches another installation of the app, and not this one.
770
+ const back = await signedInAs(d, 'mallory', state);
771
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=not-yours');
772
+ expect(d.settings.resolve('githubInstallationId')).toBe('77');
773
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe(before);
774
+ });
775
+
776
+ it('keeps what was permitted when the person who came back can push to nothing', async () => {
777
+ const world = {
778
+ installations: { ada: ['77'], rita: ['77'] },
779
+ theirs: { rita: { 'acme/kb': 'pull' as const } } as Record<string, Record<string, 'push' | 'pull'>>,
780
+ };
781
+ const d = deployment({ env: APP_ENV, world });
782
+ await installedAs(d, 'ada');
783
+ const before = d.settings.resolve('githubRepositoriesPermitted');
784
+ const { state } = await sentTo(d, '/api/setup/github-app/refresh');
785
+ const back = await signedInAs(d, 'rita', state);
786
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=nothing-to-write');
787
+ expect(d.settings.resolve('githubRepositoriesPermitted')).toBe(before);
788
+ });
789
+
790
+ /**
791
+ * Someone without the right to install asked an owner, who approved it
792
+ * later in a browser of their own: the app is installed, and this
793
+ * deployment was never told. A sign-in finds it.
794
+ */
795
+ it('finds an installation this browser never came back from, when there is one', async () => {
796
+ const d = deployment({ env: APP_ENV, world: { installations: { ada: ['77'] } } });
797
+ const { state } = await sentTo(d, '/api/setup/github-app/refresh');
798
+ const back = await signedInAs(d, 'ada', state);
799
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=refreshed');
800
+ expect(d.settings.resolve('githubInstallationId')).toBe('77');
801
+ expect(d.settings.resolve('githubRepositoriesPermitted').split('\n')).toEqual(['acme/another', 'acme/kb']);
802
+ });
803
+
804
+ it('chooses none of several, and says there is none when there is none', async () => {
805
+ const several = deployment({ env: APP_ENV, world: { installations: { ada: ['77', '78'] } } });
806
+ let trip = await sentTo(several, '/api/setup/github-app/refresh');
807
+ expect((await signedInAs(several, 'ada', trip.state)).headers.get('location')).toBe('https://kb.acme.test/?github=several');
808
+ expect(several.settings.resolve('githubInstallationId')).toBe('');
809
+
810
+ const none = deployment({ env: APP_ENV, world: { installations: {} } });
811
+ trip = await sentTo(none, '/api/setup/github-app/refresh');
812
+ expect((await signedInAs(none, 'ada', trip.state)).headers.get('location')).toBe('https://kb.acme.test/?github=not-installed');
813
+ expect(none.settings.resolve('githubInstallationId')).toBe('');
814
+ });
815
+
816
+ it('reads nothing for a browser that was not sent, and refuses an installation that is not a number', async () => {
817
+ const d = deployment({ env: APP_ENV, world: { installations: { ada: ['77'] } } });
818
+ expect((await signedInAs(d, 'ada', 'made-up')).headers.get('location')).toBe('https://kb.acme.test/?github=state');
819
+ const { state } = await sentTo(d, '/api/setup/github-app/refresh');
820
+ const back = await d.go(`/api/setup/github-app/callback?code=code-of-ada&installation_id=7x7&state=${encodeURIComponent(state)}`);
821
+ expect(back.headers.get('location')).toBe('https://kb.acme.test/?github=refused');
822
+ expect(d.hub.calls).toEqual([]);
823
+ });
824
+
825
+ it('is not offered to a deployment that has no app', async () => {
826
+ const d = deployment();
827
+ expect((await d.go('/api/setup/github-app/refresh', { method: 'POST', body: '{}' })).status).toBe(409);
828
+ expect(d.jar.size).toBe(0);
829
+ });
830
+ });
831
+
832
+ describe('who these routes answer', () => {
833
+ it('answers admins only, and starts nothing for anyone else', async () => {
834
+ const d = deployment({ env: APP_ENV, admin: false });
835
+ for (const [method, path] of [
836
+ ['GET', '/api/setup/github-app'],
837
+ ['POST', '/api/setup/github-app/manifest'],
838
+ ['POST', '/api/setup/github-app/install'],
839
+ ['POST', '/api/setup/github-app/refresh'],
840
+ ['GET', '/api/setup/github-app/callback?code=code-of-ada&installation_id=77&state=x'],
841
+ ['GET', '/api/setup/github-app/repositories'],
842
+ ] as const) {
843
+ expect((await d.go(path, { method, ...(method === 'POST' ? { body: '{}' } : {}) })).status, path).toBe(403);
844
+ }
845
+ expect(d.hub.calls).toEqual([]);
846
+ expect(d.jar.size).toBe(0);
847
+ });
848
+ });