@bevel-software/platform-core-backend 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -0,0 +1,118 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { verifySyncCredential, type SyncSessionVerifier } from '../sync-auth.js';
4
+
5
+ const SECRET = 'a-very-long-and-random-sync-secret';
6
+
7
+ function session(opts: { email?: string; admin?: boolean } = {}): SyncSessionVerifier {
8
+ return {
9
+ verifyJwt: vi.fn((token: string) =>
10
+ token === 'jwt-ok' && opts.email ? { email: opts.email } : null,
11
+ ),
12
+ isAdmin: vi.fn(async () => opts.admin === true),
13
+ };
14
+ }
15
+
16
+ describe('verifySyncCredential', () => {
17
+ it('accepts the secret as a bearer', async () => {
18
+ const r = await verifySyncCredential(
19
+ { secret: SECRET, authorization: `Bearer ${SECRET}` },
20
+ session(),
21
+ );
22
+ expect(r).toEqual({ ok: true, credential: { kind: 'bearer' } });
23
+ });
24
+
25
+ it('refuses a wrong bearer with 401', async () => {
26
+ const r = await verifySyncCredential(
27
+ { secret: SECRET, authorization: 'Bearer nope' },
28
+ session(),
29
+ );
30
+ expect(r).toMatchObject({ ok: false, status: 401 });
31
+ });
32
+
33
+ it('refuses a missing credential with 401', async () => {
34
+ const r = await verifySyncCredential({ secret: SECRET }, session());
35
+ expect(r).toMatchObject({ ok: false, status: 401 });
36
+ });
37
+
38
+ it('accepts the GitLab token header', async () => {
39
+ const r = await verifySyncCredential({ secret: SECRET, gitlabToken: SECRET }, session());
40
+ expect(r).toEqual({ ok: true, credential: { kind: 'gitlab-token' } });
41
+ });
42
+
43
+ it('accepts a GitHub signature over the raw body, and only over those bytes', async () => {
44
+ const body = Buffer.from('{"ref":"refs/heads/main"}');
45
+ const sig = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex');
46
+ expect(
47
+ await verifySyncCredential({ secret: SECRET, hubSignature: sig, rawBody: body }, session()),
48
+ ).toEqual({ ok: true, credential: { kind: 'github-signature' } });
49
+ expect(
50
+ await verifySyncCredential(
51
+ { secret: SECRET, hubSignature: sig, rawBody: Buffer.from('{"ref":"refs/heads/other"}') },
52
+ session(),
53
+ ),
54
+ ).toMatchObject({ ok: false, status: 401 });
55
+ expect(
56
+ await verifySyncCredential(
57
+ { secret: SECRET, hubSignature: 'sha1=abc', rawBody: body },
58
+ session(),
59
+ ),
60
+ ).toMatchObject({ ok: false, status: 401 });
61
+ });
62
+
63
+ it("accepts an admin's own session as the bearer", async () => {
64
+ const r = await verifySyncCredential(
65
+ { secret: SECRET, authorization: 'Bearer jwt-ok' },
66
+ session({ email: 'admin@example.com', admin: true }),
67
+ );
68
+ expect(r).toEqual({
69
+ ok: true,
70
+ credential: { kind: 'admin-session', email: 'admin@example.com' },
71
+ });
72
+ });
73
+
74
+ it("refuses a non-admin's session", async () => {
75
+ const r = await verifySyncCredential(
76
+ { secret: SECRET, authorization: 'Bearer jwt-ok' },
77
+ session({ email: 'someone@example.com', admin: false }),
78
+ );
79
+ expect(r).toMatchObject({ ok: false, status: 401 });
80
+ });
81
+
82
+ it('with no secret configured, a header credential gets 503 that says so', async () => {
83
+ const r = await verifySyncCredential({ secret: '', authorization: 'Bearer anything' }, session());
84
+ expect(r).toMatchObject({ ok: false, status: 503 });
85
+ expect((r as { message: string }).message).toContain('KB_SYNC_SECRET');
86
+ });
87
+
88
+ it('with no secret configured, an admin session still works', async () => {
89
+ const r = await verifySyncCredential(
90
+ { secret: '', authorization: 'Bearer jwt-ok' },
91
+ session({ email: 'admin@example.com', admin: true }),
92
+ );
93
+ expect(r).toMatchObject({ ok: true });
94
+ });
95
+
96
+ it('with no secret configured and no credential, 401 not 503', async () => {
97
+ expect(await verifySyncCredential({ secret: '' }, session())).toMatchObject({
98
+ ok: false,
99
+ status: 401,
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('verifySyncCredential — a secret set too short', () => {
105
+ it('is refused with 503 that says so, even when the caller presents it exactly', async () => {
106
+ const r = await verifySyncCredential({ secret: 'short', authorization: 'Bearer short' }, session());
107
+ expect(r).toMatchObject({ ok: false, status: 503 });
108
+ expect((r as { message: string }).message).toContain('shorter than 16');
109
+ });
110
+
111
+ it('an admin session still works past a too-short secret', async () => {
112
+ const r = await verifySyncCredential(
113
+ { secret: 'short', authorization: 'Bearer jwt-ok' },
114
+ session({ email: 'admin@example.com', admin: true }),
115
+ );
116
+ expect(r).toMatchObject({ ok: true });
117
+ });
118
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseSyncPayload } from '../sync-payload.js';
3
+
4
+ describe('parseSyncPayload', () => {
5
+ it('an empty or foreign body syncs everything', () => {
6
+ expect(parseSyncPayload(undefined)).toEqual({ source: 'none', branches: 'all', invalid: [] });
7
+ expect(parseSyncPayload({ hello: 'world' })).toEqual({ source: 'none', branches: 'all', invalid: [] });
8
+ expect(parseSyncPayload('text')).toEqual({ source: 'none', branches: 'all', invalid: [] });
9
+ });
10
+
11
+ it('explicit branches are kept in order, trimmed and de-duplicated', () => {
12
+ expect(parseSyncPayload({ branches: [' main ', 'ali/x', 'main'] })).toEqual({
13
+ source: 'explicit',
14
+ branches: ['main', 'ali/x'],
15
+ invalid: [],
16
+ });
17
+ });
18
+
19
+ it('an explicit name git would refuse is reported, not silently dropped', () => {
20
+ const parsed = parseSyncPayload({ branches: ['main', '--upload-pack=x', 'a..b', 7] });
21
+ expect(parsed.branches).toEqual(['main']);
22
+ expect(parsed.invalid).toEqual(['--upload-pack=x', 'a..b', '7']);
23
+ });
24
+
25
+ it('an explicit `branches` that is not a list means everything', () => {
26
+ expect(parseSyncPayload({ branches: 'main' }).branches).toBe('all');
27
+ });
28
+
29
+ it('Azure DevOps push: every updated head ref, tags ignored', () => {
30
+ const parsed = parseSyncPayload({
31
+ eventType: 'git.push',
32
+ resource: {
33
+ refUpdates: [
34
+ { name: 'refs/heads/main', oldObjectId: 'a', newObjectId: 'b' },
35
+ { name: 'refs/tags/v1', oldObjectId: '0', newObjectId: 'c' },
36
+ { name: 'refs/heads/ali/new-skill' },
37
+ ],
38
+ },
39
+ });
40
+ expect(parsed).toEqual({
41
+ source: 'azure-devops',
42
+ branches: ['main', 'ali/new-skill'],
43
+ invalid: [],
44
+ });
45
+ });
46
+
47
+ it('Azure DevOps pull request: target then source', () => {
48
+ for (const eventType of ['git.pullrequest.created', 'git.pullrequest.updated', 'git.pullrequest.merged']) {
49
+ const parsed = parseSyncPayload({
50
+ eventType,
51
+ resource: {
52
+ sourceRefName: 'refs/heads/ali/new-skill',
53
+ targetRefName: 'refs/heads/main',
54
+ status: 'completed',
55
+ },
56
+ });
57
+ expect(parsed).toEqual({
58
+ source: 'azure-devops',
59
+ branches: ['main', 'ali/new-skill'],
60
+ invalid: [],
61
+ });
62
+ }
63
+ });
64
+
65
+ it('an Azure DevOps event we do not know syncs everything', () => {
66
+ expect(parseSyncPayload({ eventType: 'build.complete', resource: {} }).branches).toBe('all');
67
+ });
68
+
69
+ it('GitHub push: the pushed branch', () => {
70
+ expect(parseSyncPayload({ ref: 'refs/heads/main', before: 'a', after: 'b' })).toEqual({
71
+ source: 'github',
72
+ branches: ['main'],
73
+ invalid: [],
74
+ });
75
+ });
76
+
77
+ it('GitHub tag push names nothing to sync', () => {
78
+ expect(parseSyncPayload({ ref: 'refs/tags/v1' })).toEqual({
79
+ source: 'github',
80
+ branches: [],
81
+ invalid: [],
82
+ });
83
+ });
84
+
85
+ it('GitLab push: the pushed branch', () => {
86
+ expect(parseSyncPayload({ object_kind: 'push', ref: 'refs/heads/main' })).toEqual({
87
+ source: 'gitlab',
88
+ branches: ['main'],
89
+ invalid: [],
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('parseSyncPayload — hostile shapes', () => {
95
+ it('an explicit entry that cannot be stringified is reported, not thrown', () => {
96
+ const parsed = parseSyncPayload({ branches: ['main', { toString: 1 }, [1], null] });
97
+ expect(parsed.branches).toEqual(['main']);
98
+ expect(parsed.invalid).toEqual(['(object)', '(array)', 'null']);
99
+ });
100
+
101
+ it('a GitLab event that is not a push falls back to everything, even with a stray ref', () => {
102
+ expect(parseSyncPayload({ object_kind: 'merge_request', ref: 'refs/heads/main' })).toEqual({
103
+ source: 'none',
104
+ branches: 'all',
105
+ invalid: [],
106
+ });
107
+ });
108
+
109
+ it('a GitLab tag push names nothing to sync', () => {
110
+ expect(parseSyncPayload({ object_kind: 'tag_push', ref: 'refs/tags/v1' })).toEqual({
111
+ source: 'gitlab',
112
+ branches: [],
113
+ invalid: [],
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('parseSyncPayload — deletions', () => {
119
+ it('a GitHub branch deletion names its branch, so the sync finds it gone and retires the clone', () => {
120
+ expect(parseSyncPayload({ ref: 'refs/heads/ali/x', deleted: true, after: '0'.repeat(40) })).toEqual({
121
+ source: 'github',
122
+ branches: ['ali/x'],
123
+ invalid: [],
124
+ });
125
+ });
126
+
127
+ it('an Azure DevOps deletion (new object id all zeros) names its branch beside the real update', () => {
128
+ expect(
129
+ parseSyncPayload({
130
+ eventType: 'git.push',
131
+ resource: {
132
+ refUpdates: [
133
+ { name: 'refs/heads/ali/x', oldObjectId: 'a'.repeat(40), newObjectId: '0'.repeat(40) },
134
+ { name: 'refs/heads/main', oldObjectId: 'b'.repeat(40), newObjectId: 'c'.repeat(40) },
135
+ ],
136
+ },
137
+ }),
138
+ ).toEqual({ source: 'azure-devops', branches: ['ali/x', 'main'], invalid: [] });
139
+ });
140
+
141
+ it('a GitLab branch deletion (after all zeros) names its branch', () => {
142
+ expect(parseSyncPayload({ object_kind: 'push', ref: 'refs/heads/ali/x', after: '0'.repeat(40) })).toEqual({
143
+ source: 'gitlab',
144
+ branches: ['ali/x'],
145
+ invalid: [],
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,19 @@
1
+ export type {
2
+ IKbSyncService,
3
+ LastSync,
4
+ SyncRequest,
5
+ SyncResult,
6
+ SyncWorkflowPort,
7
+ SyncWorkspacePort,
8
+ } from './kb-sync.interface.js';
9
+ export { KbSyncService } from './kb-sync.service.js';
10
+ export {
11
+ createKbSyncRoutes,
12
+ httpStatusFor,
13
+ isSyncRawBodyPath,
14
+ SYNC_RESPONSE_HEADER,
15
+ SYNC_RESPONSE_MARKER,
16
+ type KbSyncRouteDeps,
17
+ } from './kb-sync.routes.js';
18
+ export { parseSyncPayload, type ParsedSyncPayload, type SyncPayloadSource } from './sync-payload.js';
19
+ export { verifySyncCredential, type SyncAuthInput, type SyncAuthResult, type SyncCredential } from './sync-auth.js';
@@ -0,0 +1,79 @@
1
+ import type { BranchSyncOutcome } from '@bevel-software/platform-shared';
2
+
3
+ /**
4
+ * Remote sync: a git host (Azure DevOps, GitHub, GitLab, a pipeline) tells
5
+ * Hexis "the repository changed" and Hexis brings the clones it holds up to
6
+ * date. The module owns the HTTP surface, the credential check, the payload
7
+ * parsing and the coalescing; every git operation happens behind the
8
+ * workflow port below, never here.
9
+ */
10
+
11
+ /** Which branches a caller asked to sync. `'all'` = every clone Hexis holds. */
12
+ export interface SyncRequest {
13
+ branches: string[] | 'all';
14
+ /** Who asked — a credential kind or an admin's email — for the last-sync record. */
15
+ by?: string;
16
+ }
17
+
18
+ /**
19
+ * What the most recent sync did, for the admin's Deployment page. Kept in
20
+ * memory only: it answers "is the hook wired up and working?", and a restart
21
+ * resetting it to "none yet" is the honest answer for a fresh process.
22
+ */
23
+ export interface LastSync {
24
+ /** Epoch ms when the sync finished. */
25
+ at: number;
26
+ by: string;
27
+ status: SyncResult['status'];
28
+ /** One entry per branch, as the response carried them. */
29
+ results: BranchSyncOutcome[];
30
+ }
31
+
32
+ export interface SyncResult {
33
+ /** `synced` when no branch needs a person or a retry; `partial` otherwise. */
34
+ status: 'synced' | 'partial';
35
+ results: BranchSyncOutcome[];
36
+ changeRequests: {
37
+ /** Open change requests closed because one of their branches is gone from the host. */
38
+ closedDeletedBranch: number;
39
+ };
40
+ }
41
+
42
+ export interface IKbSyncService {
43
+ /**
44
+ * Sync the requested branches. One sync runs at a time; a request arriving
45
+ * mid-run is folded into a single queued follow-up (the union of every
46
+ * request that arrived meanwhile) and resolves with that follow-up's result.
47
+ * Never rejects for a per-branch failure — those are outcomes.
48
+ */
49
+ sync(request: SyncRequest): Promise<SyncResult>;
50
+ /** The most recent completed sync in this process, or null before the first. */
51
+ lastSync(): LastSync | null;
52
+ }
53
+
54
+ /** The slice of the workflow module a sync drives. */
55
+ export interface SyncWorkflowPort {
56
+ syncWorkspaceFromRemote(workspaceId: string): Promise<BranchSyncOutcome>;
57
+ /**
58
+ * Retire the clone of a branch the host deleted, under the branch-lifecycle
59
+ * lock and only if origin still lacks it. Returns whether it was removed.
60
+ */
61
+ retireRemoteGoneClone(workspaceId: string): Promise<boolean>;
62
+ /** The existing sweep; returns how many requests it closed. */
63
+ closeChangeRequestsWithDeletedBranches(): Promise<number>;
64
+ }
65
+
66
+ /**
67
+ * The slice of the workspace module a sync reads: which clones exist ON
68
+ * DISK. Not the in-memory "seen this process" cache — clones survive a
69
+ * restart and a hook that fires before anyone has opened a branch must still
70
+ * find them.
71
+ */
72
+ export interface SyncWorkspacePort {
73
+ /**
74
+ * `unreadable` names a clone the listing could see but not probe (an ACL,
75
+ * a half-deleted directory); the sync reports it as that branch's error
76
+ * rather than letting one bad directory fail every other branch.
77
+ */
78
+ listClonedWorkspaces(): Promise<Array<{ id: string; branch: string; unreadable?: string }>>;
79
+ }
@@ -0,0 +1,197 @@
1
+ import express from 'express';
2
+ import type { IAdminAccessService } from '../admin/admin.interface.js';
3
+ import { assertValidBranchName } from '../kb-fs/branch-name.js';
4
+ import type { IKbSyncService, SyncResult } from './kb-sync.interface.js';
5
+ import { parseSyncPayload, type SyncPayloadSource } from './sync-payload.js';
6
+ import { verifySyncCredential } from './sync-auth.js';
7
+
8
+ /**
9
+ * Remote sync — a git host or a pipeline tells Hexis the repository changed.
10
+ *
11
+ * POST /api/sync/<branch> sync that one branch — the branch whose change
12
+ * made the call. What a GitHub Action or a
13
+ * pipeline step uses: the branch name goes in the
14
+ * URL, the body is ignored for branch selection.
15
+ * POST /api/sync the branches named by the body — an Azure
16
+ * DevOps service hook or a GitHub/GitLab webhook
17
+ * payload (they cannot template the URL), an
18
+ * explicit `{ "branches": [...] }`, or nothing,
19
+ * which syncs every clone Hexis holds.
20
+ *
21
+ * Mounted BEFORE every JWT-protected `/api` mount: the caller is a machine
22
+ * holding the deployment's sync secret, not a browser session, and Express 5
23
+ * would otherwise 401 it in the first protected mount. The route parses its
24
+ * own body as raw bytes (the global JSON parser skips this path) because the
25
+ * GitHub-style credential is an HMAC over exactly the bytes that arrived.
26
+ *
27
+ * Status codes are chosen for the two kinds of caller. A pipeline's
28
+ * `curl --fail` must fail exactly when Hexis is not in sync; a webhook host
29
+ * retries 5xx and gives up on 4xx:
30
+ *
31
+ * 200 every branch updated, already current, or not cloned here
32
+ * 409 a branch has a CONFLICT: Hexis-side commits clash with the host.
33
+ * Automatic recovery is queued, but the branch is not in sync at the
34
+ * time of the answer; the body's `error` names the files. A retry
35
+ * answers 200 once recovery (or a person) has cleared it.
36
+ * 503 a branch could not be pulled (origin unreachable, credential
37
+ * refused) — retriable. Also: no sync secret configured.
38
+ * 401 missing or wrong credential
39
+ * 400 a branch name git would refuse, or a body that is not JSON
40
+ */
41
+ export interface KbSyncRouteDeps {
42
+ kbSync: IKbSyncService;
43
+ /** The configured secret, read per request so a setup-screen save applies at once. */
44
+ syncSecret: () => string;
45
+ authService: { verifyToken(token: string): { userId: string; email: string } };
46
+ adminAccess: IAdminAccessService;
47
+ }
48
+
49
+ /** Cap on a webhook body. ADO's PR payloads run to a few hundred KB. */
50
+ const BODY_LIMIT = '2mb';
51
+
52
+ /**
53
+ * Stamped on EVERY response this router writes. The endpoint answers 503 for
54
+ * an ordinary result (a branch could not be pulled), and a reverse proxy
55
+ * answers 503 when the backend is down; by status alone the browser cannot
56
+ * tell them apart, and it must — one is a line on the page, the other the
57
+ * maintenance overlay. A proxy standing in for a dead upstream never sets
58
+ * this header. Exposed to cross-origin callers in `create-core-server.ts`.
59
+ */
60
+ export const SYNC_RESPONSE_HEADER = 'X-Hexis-Sync';
61
+ export const SYNC_RESPONSE_MARKER = 'result';
62
+
63
+ /**
64
+ * Whether a request path belongs to this router and must reach it with its
65
+ * body UNPARSED. The server's global JSON parser consults this: once that
66
+ * parser has read the stream, `express.raw` below sees nothing and the
67
+ * signature credential has no bytes to verify. Both routes share the check,
68
+ * so the branch form (`/api/sync/<branch>`) is covered as well as the bare
69
+ * one — an exact-path exemption used to protect only the latter.
70
+ */
71
+ export function isSyncRawBodyPath(path: string): boolean {
72
+ // Case-insensitive, because Express routing is: `/api/Sync` reaches this
73
+ // router, and if this check said no the global parser would eat the body
74
+ // first. On either form a signed call would then 401 (no bytes to verify).
75
+ // On the bare form a bearer call would silently sync every clone, since a
76
+ // body that is no longer a Buffer parses as "no body"; the branch form is
77
+ // unaffected there, its branch comes from the URL.
78
+ const lower = path.toLowerCase();
79
+ return lower === '/api/sync' || lower.startsWith('/api/sync/');
80
+ }
81
+
82
+ export function httpStatusFor(result: SyncResult): 200 | 409 | 503 {
83
+ if (result.results.some((r) => r.outcome === 'conflict')) return 409;
84
+ if (result.results.some((r) => r.outcome === 'error')) return 503;
85
+ return 200;
86
+ }
87
+
88
+ type Selection = { source: SyncPayloadSource | 'path'; branches: string[] | 'all' };
89
+
90
+ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
91
+ const router = express.Router();
92
+
93
+ const rawBody = express.raw({ type: () => true, limit: BODY_LIMIT });
94
+
95
+ /**
96
+ * Everything both routes share: the credential check, then the sync and
97
+ * its answer. `select` turns the request into the branches to sync, or
98
+ * sends its own 400 and returns null.
99
+ */
100
+ async function handle(
101
+ req: express.Request,
102
+ res: express.Response,
103
+ select: () => Selection | null,
104
+ ): Promise<void> {
105
+ res.setHeader(SYNC_RESPONSE_HEADER, SYNC_RESPONSE_MARKER);
106
+ const raw: Buffer | undefined = Buffer.isBuffer(req.body) ? req.body : undefined;
107
+
108
+ const auth = await verifySyncCredential(
109
+ {
110
+ secret: deps.syncSecret(),
111
+ authorization: header(req, 'authorization'),
112
+ gitlabToken: header(req, 'x-gitlab-token'),
113
+ hubSignature: header(req, 'x-hub-signature-256'),
114
+ rawBody: raw,
115
+ },
116
+ {
117
+ verifyJwt: (token) => {
118
+ try {
119
+ return { email: deps.authService.verifyToken(token).email };
120
+ } catch {
121
+ return null;
122
+ }
123
+ },
124
+ isAdmin: (email) => deps.adminAccess.isAdmin(email),
125
+ },
126
+ );
127
+ if (!auth.ok) {
128
+ if (auth.status === 401) res.setHeader('WWW-Authenticate', 'Bearer realm="hexis-sync"');
129
+ res.status(auth.status).json({ error: auth.message });
130
+ return;
131
+ }
132
+
133
+ const selection = select();
134
+ if (!selection) return;
135
+
136
+ const who = auth.credential.kind === 'admin-session' ? auth.credential.email : auth.credential.kind;
137
+ const asked =
138
+ selection.branches === 'all' ? 'all cloned branches' : selection.branches.join(', ') || '(none)';
139
+ try {
140
+ const result = await deps.kbSync.sync({ branches: selection.branches, by: who });
141
+ const summary = result.results.map((r) => `${r.branch}=${r.outcome}`).join(' ');
142
+ console.log(`[sync] by ${who} via ${selection.source} for ${asked}: ${result.status} ${summary}`);
143
+ res.status(httpStatusFor(result)).json(result);
144
+ } catch (err) {
145
+ console.error('[sync] unhandled error:', err);
146
+ res.status(500).json({ error: 'Internal server error' });
147
+ }
148
+ }
149
+
150
+ // The branch in the URL. A wildcard rather than `:branch` so a branch with
151
+ // slashes (`ali/new-skill`) works spelled either way — as segments or
152
+ // percent-encoded as one.
153
+ router.post('/sync/*branch', rawBody, (req, res) =>
154
+ handle(req, res, () => {
155
+ const param = req.params.branch as unknown;
156
+ const branch = (Array.isArray(param) ? param.join('/') : String(param ?? '')).trim();
157
+ try {
158
+ assertValidBranchName(branch);
159
+ } catch {
160
+ res.status(400).json({ error: `Not a valid branch name: ${branch}` });
161
+ return null;
162
+ }
163
+ return { source: 'path', branches: [branch] };
164
+ }),
165
+ );
166
+
167
+ router.post('/sync', rawBody, (req, res) =>
168
+ handle(req, res, () => {
169
+ const raw: Buffer | undefined = Buffer.isBuffer(req.body) ? req.body : undefined;
170
+ let body: unknown = undefined;
171
+ if (raw && raw.length > 0) {
172
+ try {
173
+ body = JSON.parse(raw.toString('utf8'));
174
+ } catch {
175
+ res.status(400).json({ error: 'The request body is not valid JSON.' });
176
+ return null;
177
+ }
178
+ }
179
+ const parsed = parseSyncPayload(body);
180
+ if (parsed.invalid.length > 0) {
181
+ res.status(400).json({
182
+ error: `Not a valid branch name: ${parsed.invalid.join(', ')}`,
183
+ invalid: parsed.invalid,
184
+ });
185
+ return null;
186
+ }
187
+ return { source: parsed.source, branches: parsed.branches };
188
+ }),
189
+ );
190
+
191
+ return router;
192
+ }
193
+
194
+ function header(req: express.Request, name: string): string | undefined {
195
+ const v = req.headers[name];
196
+ return typeof v === 'string' ? v : undefined;
197
+ }