@bevel-software/platform-core-backend 0.13.6 → 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 (456) 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 +101 -9
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +20 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +105 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +12 -5
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +18 -6
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +26 -1
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +213 -71
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access.routes.d.ts.map +1 -1
  23. package/dist/modules/access/access.routes.js +59 -7
  24. package/dist/modules/access/access.routes.js.map +1 -1
  25. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  26. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  27. package/dist/modules/access-model/access-grammar.js +95 -1
  28. package/dist/modules/access-model/access-grammar.js.map +1 -1
  29. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  30. package/dist/modules/access-model/group-files.js +4 -1
  31. package/dist/modules/access-model/group-files.js.map +1 -1
  32. package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
  33. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
  34. package/dist/modules/access-model/kb-read-filter.js +15 -0
  35. package/dist/modules/access-model/kb-read-filter.js.map +1 -1
  36. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  37. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  38. package/dist/modules/access-model/plugin-principals.js +125 -0
  39. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  40. package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
  41. package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
  42. package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
  43. package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
  44. package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
  45. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
  46. package/dist/modules/connection-probe/connection-probe.service.js +684 -0
  47. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
  48. package/dist/modules/connection-probe/index.d.ts +3 -0
  49. package/dist/modules/connection-probe/index.d.ts.map +1 -0
  50. package/dist/modules/connection-probe/index.js +3 -0
  51. package/dist/modules/connection-probe/index.js.map +1 -0
  52. package/dist/modules/database/core-schema.d.ts +326 -0
  53. package/dist/modules/database/core-schema.d.ts.map +1 -1
  54. package/dist/modules/database/core-schema.js +52 -0
  55. package/dist/modules/database/core-schema.js.map +1 -1
  56. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  57. package/dist/modules/diff/diff.routes.js +3 -5
  58. package/dist/modules/diff/diff.routes.js.map +1 -1
  59. package/dist/modules/kb-fs/mutex.d.ts +37 -0
  60. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/mutex.js +48 -5
  62. package/dist/modules/kb-fs/mutex.js.map +1 -1
  63. package/dist/modules/kb-sync/index.d.ts +6 -0
  64. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  65. package/dist/modules/kb-sync/index.js +5 -0
  66. package/dist/modules/kb-sync/index.js.map +1 -0
  67. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  68. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  69. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  70. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  71. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  72. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  73. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  74. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  75. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  76. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  77. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  78. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  79. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  80. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  81. package/dist/modules/kb-sync/sync-auth.js +62 -0
  82. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  83. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  84. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  85. package/dist/modules/kb-sync/sync-payload.js +105 -0
  86. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  87. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  88. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  89. package/dist/modules/marketplace/git-http.routes.js +190 -0
  90. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  91. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  92. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  93. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  94. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  95. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  96. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  97. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  98. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  99. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  100. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  101. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  102. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  103. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  104. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  105. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  106. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  107. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  108. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  109. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  110. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  111. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  112. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  113. package/dist/modules/marketplace/github-facade/index.js +6 -0
  114. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  115. package/dist/modules/marketplace/index.d.ts +4 -0
  116. package/dist/modules/marketplace/index.d.ts.map +1 -0
  117. package/dist/modules/marketplace/index.js +4 -0
  118. package/dist/modules/marketplace/index.js.map +1 -0
  119. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  120. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  121. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  122. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  123. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  124. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  125. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  126. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  127. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  128. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  129. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  130. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  131. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  132. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  133. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  134. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  135. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  136. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  137. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  138. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  139. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  140. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  141. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  142. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  143. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  144. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  145. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  146. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  147. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  148. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  149. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  150. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  151. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  152. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  153. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  154. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  155. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  156. package/dist/modules/plugins/discovery/native.source.js +102 -0
  157. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  158. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  159. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  160. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  161. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  162. package/dist/modules/plugins/index.d.ts +10 -1
  163. package/dist/modules/plugins/index.d.ts.map +1 -1
  164. package/dist/modules/plugins/index.js +8 -0
  165. package/dist/modules/plugins/index.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  167. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  168. package/dist/modules/plugins/plugin-links.js +160 -0
  169. package/dist/modules/plugins/plugin-links.js.map +1 -0
  170. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  171. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  172. package/dist/modules/plugins/plugin-links.service.js +304 -0
  173. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  174. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  175. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  177. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  178. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  179. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  180. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  181. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  182. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  183. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  185. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  186. package/dist/modules/plugins/plugins.routes.js +120 -12
  187. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  188. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  189. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  190. package/dist/modules/plugins/plugins.service.js +108 -47
  191. package/dist/modules/plugins/plugins.service.js.map +1 -1
  192. package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
  193. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
  195. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
  197. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  198. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  199. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  200. package/dist/modules/settings/deployment-settings.service.js +82 -2
  201. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  202. package/dist/modules/settings/setup.routes.d.ts +26 -0
  203. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  204. package/dist/modules/settings/setup.routes.js +9 -1
  205. package/dist/modules/settings/setup.routes.js.map +1 -1
  206. package/dist/modules/skills/index.d.ts +1 -0
  207. package/dist/modules/skills/index.d.ts.map +1 -1
  208. package/dist/modules/skills/index.js +1 -0
  209. package/dist/modules/skills/index.js.map +1 -1
  210. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  211. package/dist/modules/skills/pending-skills.service.js +9 -4
  212. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  213. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  214. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  215. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  216. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  217. package/dist/modules/skills/skills.contract.d.ts +31 -0
  218. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  219. package/dist/modules/skills/skills.routes.d.ts +28 -3
  220. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  221. package/dist/modules/skills/skills.routes.js +42 -3
  222. package/dist/modules/skills/skills.routes.js.map +1 -1
  223. package/dist/modules/skills/skills.service.d.ts +4 -0
  224. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  225. package/dist/modules/skills/skills.service.js +83 -22
  226. package/dist/modules/skills/skills.service.js.map +1 -1
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  228. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  229. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  230. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  232. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  233. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  234. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  235. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  236. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
  238. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  239. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  240. package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
  241. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  242. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
  243. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
  245. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  246. package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
  247. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  248. package/dist/modules/workflow/git/git.service.d.ts +58 -3
  249. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  250. package/dist/modules/workflow/git/git.service.js +325 -60
  251. package/dist/modules/workflow/git/git.service.js.map +1 -1
  252. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  253. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  254. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  255. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  256. package/dist/modules/workflow/workflow.routes.d.ts +2 -1
  257. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  258. package/dist/modules/workflow/workflow.routes.js +98 -9
  259. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +133 -9
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +462 -52
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  268. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  270. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  271. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  272. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  273. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  274. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspace.routes.js +11 -14
  276. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  277. package/dist/modules/workspace/workspace.service.d.ts +38 -1
  278. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.service.js +131 -20
  280. package/dist/modules/workspace/workspace.service.js.map +1 -1
  281. package/dist/shared/domain-errors.d.ts +20 -0
  282. package/dist/shared/domain-errors.d.ts.map +1 -1
  283. package/dist/shared/domain-errors.js +29 -0
  284. package/dist/shared/domain-errors.js.map +1 -1
  285. package/dist/shared/fs-errors.d.ts +8 -0
  286. package/dist/shared/fs-errors.d.ts.map +1 -0
  287. package/dist/shared/fs-errors.js +11 -0
  288. package/dist/shared/fs-errors.js.map +1 -0
  289. package/dist/shared/fs-walk.d.ts +20 -6
  290. package/dist/shared/fs-walk.d.ts.map +1 -1
  291. package/dist/shared/fs-walk.js +35 -27
  292. package/dist/shared/fs-walk.js.map +1 -1
  293. package/dist/shared/kb-walk.d.ts +52 -0
  294. package/dist/shared/kb-walk.d.ts.map +1 -0
  295. package/dist/shared/kb-walk.js +76 -0
  296. package/dist/shared/kb-walk.js.map +1 -0
  297. package/dist/shared/path-order.d.ts +18 -0
  298. package/dist/shared/path-order.d.ts.map +1 -0
  299. package/dist/shared/path-order.js +35 -0
  300. package/dist/shared/path-order.js.map +1 -0
  301. package/dist/shared/token-crypto.d.ts +17 -2
  302. package/dist/shared/token-crypto.d.ts.map +1 -1
  303. package/dist/shared/token-crypto.js +25 -8
  304. package/dist/shared/token-crypto.js.map +1 -1
  305. package/dist/shared/ttl-cache.d.ts +28 -1
  306. package/dist/shared/ttl-cache.d.ts.map +1 -1
  307. package/dist/shared/ttl-cache.js +33 -1
  308. package/dist/shared/ttl-cache.js.map +1 -1
  309. package/kb-template/.bevelignore +1 -1
  310. package/kb-template/AGENTS.md +95 -30
  311. package/kb-template/Skills/.gitkeep +0 -0
  312. package/migrations/0005_github_facade.sql +25 -0
  313. package/migrations/meta/0005_snapshot.json +1717 -0
  314. package/migrations/meta/_journal.json +7 -0
  315. package/package.json +6 -6
  316. package/src/__tests__/core-config.admin.test.ts +21 -0
  317. package/src/__tests__/kb-layout-config.test.ts +110 -0
  318. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  319. package/src/core/catalog-cache-invalidation.ts +92 -0
  320. package/src/core/create-core-server.ts +126 -7
  321. package/src/core/create-core-services.ts +173 -41
  322. package/src/core-config.ts +14 -7
  323. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  324. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  325. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  326. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  327. package/src/modules/access/access-control.interface.ts +20 -9
  328. package/src/modules/access/access-control.service.ts +240 -78
  329. package/src/modules/access/access.routes.ts +96 -14
  330. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  331. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  332. package/src/modules/access-model/access-grammar.ts +114 -2
  333. package/src/modules/access-model/group-files.ts +4 -0
  334. package/src/modules/access-model/kb-read-filter.ts +22 -0
  335. package/src/modules/access-model/plugin-principals.ts +137 -0
  336. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
  337. package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
  338. package/src/modules/connection-probe/connection-probe.service.ts +734 -0
  339. package/src/modules/connection-probe/index.ts +2 -0
  340. package/src/modules/database/core-schema.ts +54 -0
  341. package/src/modules/diff/diff.routes.ts +9 -5
  342. package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
  343. package/src/modules/kb-fs/mutex.ts +50 -5
  344. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  345. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  346. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  347. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  348. package/src/modules/kb-sync/index.ts +19 -0
  349. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  350. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  351. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  352. package/src/modules/kb-sync/sync-auth.ts +118 -0
  353. package/src/modules/kb-sync/sync-payload.ts +143 -0
  354. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  355. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  356. package/src/modules/marketplace/git-http.routes.ts +193 -0
  357. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  358. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  359. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  360. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  361. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  362. package/src/modules/marketplace/github-facade/index.ts +25 -0
  363. package/src/modules/marketplace/index.ts +3 -0
  364. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  365. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  366. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  367. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  368. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  369. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  370. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  371. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  372. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  373. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  374. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  375. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  376. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  377. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  378. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  379. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  380. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  381. package/src/modules/plugins/discovery/native.source.ts +122 -0
  382. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  383. package/src/modules/plugins/index.ts +10 -0
  384. package/src/modules/plugins/plugin-links.service.ts +363 -0
  385. package/src/modules/plugins/plugin-links.ts +184 -0
  386. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  387. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  388. package/src/modules/plugins/plugins.contract.ts +28 -1
  389. package/src/modules/plugins/plugins.routes.ts +128 -11
  390. package/src/modules/plugins/plugins.service.ts +106 -53
  391. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
  392. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
  393. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
  394. package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
  395. package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
  396. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  397. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  398. package/src/modules/settings/deployment-settings.service.ts +95 -2
  399. package/src/modules/settings/setup.routes.ts +24 -0
  400. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  401. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  402. package/src/modules/skills/index.ts +1 -0
  403. package/src/modules/skills/pending-skills.service.ts +7 -5
  404. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  405. package/src/modules/skills/skills.contract.ts +32 -0
  406. package/src/modules/skills/skills.routes.ts +59 -3
  407. package/src/modules/skills/skills.service.ts +84 -22
  408. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  409. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  410. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
  411. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  412. package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
  413. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
  414. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
  415. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
  416. package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
  417. package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
  418. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  419. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
  420. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
  421. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  422. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
  423. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
  424. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  425. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  426. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
  427. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  428. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  429. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  430. package/src/modules/workflow/git/git.service.ts +354 -63
  431. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  432. package/src/modules/workflow/workflow.routes.ts +104 -4
  433. package/src/modules/workflow/workflow.service.ts +505 -64
  434. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  435. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  436. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  437. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  438. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  439. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  440. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  441. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  442. package/src/modules/workspace/workspace.routes.ts +17 -21
  443. package/src/modules/workspace/workspace.service.ts +128 -19
  444. package/src/shared/__tests__/filename.test.ts +0 -0
  445. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  446. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  447. package/src/shared/__tests__/path-order.test.ts +24 -0
  448. package/src/shared/__tests__/token-crypto.test.ts +34 -0
  449. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  450. package/src/shared/domain-errors.ts +29 -0
  451. package/src/shared/fs-errors.ts +10 -0
  452. package/src/shared/fs-walk.ts +38 -23
  453. package/src/shared/kb-walk.ts +95 -0
  454. package/src/shared/path-order.ts +33 -0
  455. package/src/shared/token-crypto.ts +28 -10
  456. package/src/shared/ttl-cache.ts +33 -1
@@ -15,10 +15,10 @@ import {
15
15
  import {
16
16
  DEFAULT_BRANCH,
17
17
  PLUGINS_DIR,
18
- PLUGIN_MANIFEST_FILE,
19
- PLUGIN_MCP_FILE,
20
18
  } from '@bevel-software/platform-shared';
21
19
  import { descriptorsFromMcpJson } from './mcp-json-discovery.js';
20
+ import type { PluginSource } from '../plugins/discovery/plugin-source.js';
21
+ import { KbPluginSource } from '../plugins/discovery/kb-plugin-source.js';
22
22
  import type { WorkspaceService } from '../workspace/workspace.service.js';
23
23
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
24
24
  import type { IAccessControl } from '../access/access-control.interface.js';
@@ -45,6 +45,8 @@ import {
45
45
  type ToolManualPreview,
46
46
  type ToolManualType,
47
47
  type ToolVariable,
48
+ type ToolHealthCheck,
49
+ type ToolProbeTarget,
48
50
  type ToolVariableScope,
49
51
  type ToolVariableOAuth,
50
52
  } from './tool-manuals.contract.js';
@@ -148,6 +150,8 @@ export class ToolManualService implements IToolManualService {
148
150
  private readonly accessControl: IAccessControl,
149
151
  private readonly kbDirName: string,
150
152
  now: () => number = Date.now,
153
+ /** Where plugins (and their MCP servers) come from — native manifests unless a dialect is configured. */
154
+ private readonly source: PluginSource = new KbPluginSource(),
151
155
  ) {
152
156
  this.cache = new TtlCache(CACHE_TTL_MS, now);
153
157
  }
@@ -235,6 +239,41 @@ export class ToolManualService implements IToolManualService {
235
239
  return declared?.scope ?? 'admin';
236
240
  }
237
241
 
242
+ /**
243
+ * The probe config for a manual this caller can read — the internal
244
+ * counterpart to everything on `ToolManualSummary`.
245
+ *
246
+ * Access-gated through `accessibleManuals` like every other read, so this
247
+ * cannot become a way to learn what a `.tool` you can't see declares.
248
+ */
249
+ async probeTargetFor(userEmail: string, slug: string): Promise<ToolProbeTarget | null> {
250
+ const manuals = await this.accessibleManuals(userEmail);
251
+ const m = manuals.find((x) => x.slug === slug);
252
+ if (!m) return null;
253
+ // Built here, from the manual already in hand. `toManualCallTemplates`
254
+ // would validate every manual in the workspace to hand back the one we
255
+ // want; an invalid template for THIS manual is not an error, just a probe
256
+ // that has nothing to dial (the caller reports it as unverifiable).
257
+ //
258
+ // Only for the ONE path that dials it — an `mcp` manual, reachable from
259
+ // this process, that declared no health check of its own. Everywhere else
260
+ // the template is built, validated and thrown away, and an http tool with
261
+ // an unvalidatable template warn-logs on every probe about a value nothing
262
+ // was ever going to use.
263
+ const dialsTheTemplate = m.type === 'mcp' && !m.healthCheck && m.remote !== false;
264
+ let callTemplate: CallTemplate | null = null;
265
+ if (dialsTheTemplate) {
266
+ try {
267
+ callTemplate = callTemplateSerializer.validateDict(this.buildCallTemplateDict(m));
268
+ } catch (err) {
269
+ console.warn(
270
+ `[tool-manuals] no valid call template for "${m.path}": ${err instanceof Error ? err.message : String(err)}`,
271
+ );
272
+ }
273
+ }
274
+ return { name: m.name, type: m.type, remote: m.remote, healthCheck: m.healthCheck, callTemplate };
275
+ }
276
+
238
277
  async toManualCallTemplates(userEmail: string, opts?: { remoteOnly?: boolean }): Promise<CallTemplate[]> {
239
278
  const manuals = await this.accessibleManuals(userEmail);
240
279
  const out: CallTemplate[] = [];
@@ -388,12 +427,15 @@ export class ToolManualService implements IToolManualService {
388
427
  private async scan(): Promise<ToolManualDescriptor[]> {
389
428
  const cached = this.cache.get();
390
429
  if (cached) return cached;
430
+ // See `TtlCache.begin`: taken before the read so an `invalidate()` that
431
+ // lands mid-scan discards this result instead of being overwritten by it.
432
+ const token = this.cache.begin();
391
433
  const manuals = await this.scanDisk();
392
434
  await this.decorateMcpOAuth(manuals);
393
435
  // AFTER the oauth decoration, so an injected `${MCP_OAUTH}` header ref is
394
436
  // already declared and isn't re-surfaced as a bare admin key.
395
437
  for (const m of manuals) this.surfaceReferencedVariables(m);
396
- this.cache.set(manuals);
438
+ this.cache.set(manuals, token);
397
439
  return manuals;
398
440
  }
399
441
 
@@ -415,6 +457,19 @@ export class ToolManualService implements IToolManualService {
415
457
  if (m.type === 'inline' && m.tools) {
416
458
  refs.push(...variableSubstitutor.findRequiredVariables(m.tools, m.name));
417
459
  }
460
+ // A probe-only credential still has to reach the secrets UI. The call
461
+ // template is built from `url`/`headers` and never carries `healthCheck`,
462
+ // so a `${VAR}` used solely by the probe would never be surfaced for
463
+ // provisioning — and `probeDeclared` would then report `unverifiable`
464
+ // forever on a variable no screen ever offered anyone to fill in.
465
+ if (m.healthCheck) {
466
+ refs.push(
467
+ ...variableSubstitutor.findRequiredVariables(
468
+ { url: m.healthCheck.url, headers: m.healthCheck.headers ?? {} },
469
+ m.name,
470
+ ),
471
+ );
472
+ }
418
473
  } catch {
419
474
  return; // a malformed template is reported elsewhere; never break the scan
420
475
  }
@@ -482,6 +537,20 @@ export class ToolManualService implements IToolManualService {
482
537
  }
483
538
  m.setup = { kind: 'oauth-auto' };
484
539
  m.headers = { ...(m.headers ?? {}), Authorization: `Bearer \${${MCP_OAUTH_VAR}}` };
540
+ // A declared healthCheck froze its headers during normalize —
541
+ // BEFORE this decoration — so it would dial without the bearer
542
+ // every real call now carries and read its own 401 as a rejected
543
+ // credential. The injected sign-in reaches the check too, unless
544
+ // the check declares its own Authorization.
545
+ if (
546
+ m.healthCheck &&
547
+ !Object.keys(m.healthCheck.headers ?? {}).some((h) => h.toLowerCase() === 'authorization')
548
+ ) {
549
+ m.healthCheck = {
550
+ ...m.healthCheck,
551
+ headers: { ...(m.healthCheck.headers ?? {}), Authorization: `Bearer \${${MCP_OAUTH_VAR}}` },
552
+ };
553
+ }
485
554
  m.variables = [
486
555
  ...(m.variables ?? []),
487
556
  {
@@ -590,27 +659,13 @@ export class ToolManualService implements IToolManualService {
590
659
  // shape. Listed BEFORE the `.tool` files: on a name collision (a legacy
591
660
  // mcp `.tool` the migration has not converted yet), the shared dedup keeps
592
661
  // the first occurrence, and the authoritative source must be the one kept.
662
+ // The plugins (and their mcp.json text) come from the configured source, so
663
+ // a dialect that expands servers from a registry lands here unchanged.
593
664
  const parsed: ToolManualDescriptor[] = [];
594
- let pluginFolders: string[] = [];
595
- try {
596
- pluginFolders = (await fs.readdir(root, { withFileTypes: true }))
597
- .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
598
- .map((e) => e.name)
599
- .sort();
600
- } catch {
601
- /* no Plugins/ root — nothing to scan */
602
- }
603
- for (const folder of pluginFolders) {
604
- let mcpJson: string;
605
- try {
606
- mcpJson = await fs.readFile(path.join(root, folder, PLUGIN_MCP_FILE), 'utf-8');
607
- } catch {
608
- continue; // no mcp.json is the common case, not an error
609
- }
610
- const pluginJson = await fs
611
- .readFile(path.join(root, folder, PLUGIN_MANIFEST_FILE), 'utf-8')
612
- .catch(() => null);
613
- parsed.push(...descriptorsFromMcpJson(folder, mcpJson, pluginJson));
665
+ const discovered = await this.source.discover(kbRoot);
666
+ for (const plugin of discovered.plugins) {
667
+ if (plugin.mcpJsonText === null) continue; // no servers is the common case, not an error
668
+ parsed.push(...descriptorsFromMcpJson(plugin.relFolder, plugin.mcpJsonText, plugin.manifestText));
614
669
  }
615
670
 
616
671
  for (const f of files) {
@@ -848,20 +903,8 @@ export function normalizeToolManual(
848
903
  // literal internal host slips past as a templated scheme or userinfo.
849
904
  // Local-only (`remote: false`) `.tool`s are never fetched server-side, so
850
905
  // are exempt.
851
- const literalScheme = /^([a-zA-Z][a-zA-Z0-9+.-]*:)[\\/]{2}/.exec(url)?.[1];
852
- const sepMatch = /:[\\/]{2}/.exec(url);
853
- const sepIdx = sepMatch ? sepMatch.index : -1;
854
- const authority = sepIdx >= 0 ? url.slice(sepIdx + 3).split(/[/\\?#]/, 1)[0] : url;
855
- const hostPort = authority.slice(authority.lastIndexOf('@') + 1);
856
- const host = hostPort.startsWith('[') ? hostPort.slice(0, hostPort.indexOf(']') + 1) : hostPort.split(':')[0];
857
- if (descriptor.remote !== false && !host.includes('${')) {
858
- const checkUrl =
859
- literalScheme && !authority.includes('${')
860
- ? url // fully literal scheme + authority → validate the URL as-is
861
- : sepIdx >= 0
862
- ? `${literalScheme ?? 'http:'}//${host}` // templated scheme/userinfo/port, literal host
863
- : url; // no authority shape at all → parse raw (refuses malformed)
864
- assertSafeFetchUrl(checkUrl, { label: `\`.tool\` "${name}" url` });
906
+ if (descriptor.remote !== false) {
907
+ assertSafeManualFetchUrl(url, `\`.tool\` "${name}" url`);
865
908
  }
866
909
  if (obj.headers && typeof obj.headers === 'object' && !Array.isArray(obj.headers)) {
867
910
  descriptor.headers = obj.headers as Record<string, string>;
@@ -871,9 +914,139 @@ export function normalizeToolManual(
871
914
  descriptor.httpMethod = m === 'POST' ? 'POST' : 'GET';
872
915
  }
873
916
  }
917
+
918
+ // LAST, because it inherits the manual's own `headers` when it declares none
919
+ // — which is the common case, and the reason a one-line `healthCheck: {url}`
920
+ // authenticates exactly like a real call. Resolving that default here, where
921
+ // the whole descriptor is in hand, keeps the probe self-contained: nothing
922
+ // downstream has to re-derive which headers a manual would have sent.
923
+ //
924
+ // For `http`/`mcp` only. An `inline` manual's execution never sends its
925
+ // top-level `headers` — each embedded tool carries its own call template —
926
+ // so inheriting them would have the probe prove a request no real call
927
+ // makes. An inline probe declares its own headers on the healthCheck, or
928
+ // sends none.
929
+ const declaredHeaders =
930
+ type !== 'inline' && obj.headers && typeof obj.headers === 'object' && !Array.isArray(obj.headers)
931
+ ? (obj.headers as Record<string, string>)
932
+ : undefined;
933
+ const healthCheck = normalizeHealthCheck(obj.healthCheck, name, descriptor.remote, declaredHeaders);
934
+ if (healthCheck) descriptor.healthCheck = healthCheck;
935
+
874
936
  return descriptor;
875
937
  }
876
938
 
939
+ /**
940
+ * SSRF guard for a URL a `.tool` will make the SERVER fetch — its manual `url`
941
+ * and its declared `healthCheck.url` alike, which is why this is a function
942
+ * rather than an inline block: two fetch targets policed by one rule cannot
943
+ * drift apart.
944
+ *
945
+ * A remote-capable `.tool` triggers a server-side fetch (the MCP proxy, the
946
+ * in-process agent, headless routines, and now the credential probe), so a
947
+ * literal private/loopback/metadata host is refused at the producing boundary.
948
+ * Only a TEMPLATED HOSTNAME (resolved at call time) is uncheckable here — a
949
+ * `${...}` in the scheme, userinfo, port, path, or query still leaves a
950
+ * concrete network target (`${S}://169.254.169.254/x` and
951
+ * `http://${U}@169.254.169.254/x` target the metadata IP no matter what
952
+ * resolves), so the guard must still run against the literal host. The
953
+ * authority is therefore taken from `://` INDEPENDENT of the scheme being
954
+ * literal, and when the raw url can't parse (templated scheme or port) the
955
+ * check runs on a synthetic `<scheme-or-http>//host`. A BACKSLASH behaves as a
956
+ * slash for http(s) in WHATWG `new URL` — BOTH as the scheme separator
957
+ * (`${S}:\\169.254.169.254\\p` → the IP is the host) and inside the authority
958
+ * (`http://169.254.169.254\\@${HOST}/x` fetches the IP, the `\\@…` becoming
959
+ * path). So the authority separator is `:` + two `[\/]` (not just `://`), and
960
+ * `\` also terminates the authority alongside `/?#`; otherwise a literal
961
+ * internal host slips past as a templated scheme or userinfo.
962
+ *
963
+ * Callers gate on `remote !== false`: a local-only `.tool` is never fetched
964
+ * server-side, so it is exempt.
965
+ */
966
+ function assertSafeManualFetchUrl(url: string, label: string): void {
967
+ const literalScheme = /^([a-zA-Z][a-zA-Z0-9+.-]*:)[\\/]{2}/.exec(url)?.[1];
968
+ const sepMatch = /:[\\/]{2}/.exec(url);
969
+ const sepIdx = sepMatch ? sepMatch.index : -1;
970
+ const authority = sepIdx >= 0 ? url.slice(sepIdx + 3).split(/[/\\?#]/, 1)[0] : url;
971
+ const hostPort = authority.slice(authority.lastIndexOf('@') + 1);
972
+ const host = hostPort.startsWith('[') ? hostPort.slice(0, hostPort.indexOf(']') + 1) : hostPort.split(':')[0];
973
+ // A templated host resolves to something we can't know yet — nothing to check.
974
+ if (host.includes('${')) return;
975
+ const checkUrl =
976
+ literalScheme && !authority.includes('${')
977
+ ? url // fully literal scheme + authority → validate the URL as-is
978
+ : sepIdx >= 0
979
+ ? `${literalScheme ?? 'http:'}//${host}` // templated scheme/userinfo/port, literal host
980
+ : url; // no authority shape at all → parse raw (refuses malformed)
981
+ assertSafeFetchUrl(checkUrl, { label });
982
+ }
983
+
984
+ /**
985
+ * Parse the optional `healthCheck:` block — the read-only call that proves this
986
+ * tool's credential works (see {@link ToolHealthCheck}).
987
+ *
988
+ * Throws on a malformed block rather than dropping it, matching `variables`
989
+ * and for the same reason inverted: a silently-ignored probe would leave the
990
+ * tool reporting "can't verify" forever while its author believes they wired
991
+ * one up, and the whole point of the field is to stop the UI overclaiming.
992
+ *
993
+ * `method` accepts only `GET`. A probe that can mutate is not a probe — it runs
994
+ * unattended on every save and re-check, so `POST` is refused outright instead
995
+ * of being quietly downgraded.
996
+ */
997
+ function normalizeHealthCheck(
998
+ raw: unknown,
999
+ manualName: string,
1000
+ remote: boolean | undefined,
1001
+ manualHeaders: Record<string, string> | undefined,
1002
+ ): ToolHealthCheck | undefined {
1003
+ if (raw === undefined || raw === null) return undefined;
1004
+ if (typeof raw !== 'object' || Array.isArray(raw)) throw new Error('`healthCheck` must be an object');
1005
+ const e = raw as Record<string, unknown>;
1006
+ const url = typeof e.url === 'string' ? e.url.trim() : '';
1007
+ if (!url) throw new Error('`healthCheck` must have a `url`');
1008
+ if (remote !== false) {
1009
+ assertSafeManualFetchUrl(url, `\`.tool\` "${manualName}" healthCheck.url`);
1010
+ }
1011
+ const check: ToolHealthCheck = { url };
1012
+ if (e.method !== undefined) {
1013
+ const m = typeof e.method === 'string' ? e.method.toUpperCase() : '';
1014
+ if (m !== 'GET') throw new Error('`healthCheck.method` must be `GET` — a health check may not mutate');
1015
+ check.method = 'GET';
1016
+ }
1017
+ // Whichever headers the probe ends up carrying get checked — declared OR
1018
+ // inherited. Validating only the declared branch left the common case
1019
+ // unguarded: `healthCheck: { url }` alone inherits the manual's headers, so a
1020
+ // YAML `Authorization: 1234` there still reached the probe untouched.
1021
+ if (e.headers !== undefined) {
1022
+ if (!e.headers || typeof e.headers !== 'object' || Array.isArray(e.headers)) {
1023
+ throw new Error('`healthCheck.headers` must be an object');
1024
+ }
1025
+ check.headers = assertStringHeaders(e.headers as Record<string, unknown>, 'healthCheck.headers');
1026
+ } else if (manualHeaders) {
1027
+ check.headers = assertStringHeaders(manualHeaders as Record<string, unknown>, 'headers');
1028
+ }
1029
+ return check;
1030
+ }
1031
+
1032
+ /**
1033
+ * The VALUES of a header map, not just its container.
1034
+ *
1035
+ * YAML types `Authorization: 1234` as a number, and a cast alone let it through
1036
+ * to the probe, where substitution calls `.matchAll` on it — surfacing
1037
+ * `text.matchAll is not a function` to the user as the reason their credential
1038
+ * is unhealthy. Caught at parse time, next to `url` and `method`, it reads as
1039
+ * what it is: a mistake in the `.tool` file.
1040
+ */
1041
+ function assertStringHeaders(headers: Record<string, unknown>, label: string): Record<string, string> {
1042
+ for (const [k, v] of Object.entries(headers)) {
1043
+ if (typeof v !== 'string') {
1044
+ throw new Error(`\`${label}.${k}\` must be a string (quote it if it looks like a number)`);
1045
+ }
1046
+ }
1047
+ return headers as Record<string, string>;
1048
+ }
1049
+
877
1050
  /**
878
1051
  * Parse the optional `variables:` block of a `.tool` file. Each entry names a
879
1052
  * `${VAR}` and who provisions it (`admin` default | `user`). Throws on a
@@ -80,7 +80,7 @@ describe('mergeChangeRequest — roles.yaml preservation guard', () => {
80
80
 
81
81
  const git = {
82
82
  fetch: vi.fn().mockResolvedValue(undefined),
83
- pull: vi.fn().mockResolvedValue(undefined),
83
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
84
84
  // Resolve roles.yaml at origin/<ref> from the per-test content.
85
85
  readFileAtRef: vi.fn(async (_ws: string, ref: string) =>
86
86
  ref === `origin/${BASE}` ? opts.baseRoles : ref === `origin/${HEAD}` ? opts.headRoles : null,
@@ -0,0 +1,139 @@
1
+ import type { Server } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import express from 'express';
4
+ import { describe, it, expect, afterEach, vi } from 'vitest';
5
+ import type { IWorkflowService } from '@bevel-software/platform-shared';
6
+ import type { IAccessControl } from '../../access/access-control.interface.js';
7
+ import type { AuthService } from '../../auth/auth.service.js';
8
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
+ import type { WorkflowEventBus } from '../event-bus.js';
10
+ import { createWorkflowRoutes } from '../workflow.routes.js';
11
+
12
+ // A file's history is its content with a time axis: the same default-deny
13
+ // read model that hides a file must hide its commit list, its diffs, and its
14
+ // content at any commit. These routes used to require only "is authenticated".
15
+
16
+ const USER = { id: 'u1', email: 'alice@example.com', name: 'Alice' };
17
+ const WS = 'main';
18
+ const KB = 'knowledge-base';
19
+ const allow = (p: string) => !p.includes('Secret');
20
+
21
+ interface Harness {
22
+ server: Server;
23
+ canRead: ReturnType<typeof vi.fn>;
24
+ canReadAtRef: ReturnType<typeof vi.fn>;
25
+ workflow: {
26
+ listChangesForFile: ReturnType<typeof vi.fn>;
27
+ compareFile: ReturnType<typeof vi.fn>;
28
+ showFileAtChange: ReturnType<typeof vi.fn>;
29
+ fileAtChange: ReturnType<typeof vi.fn>;
30
+ };
31
+ baseUrl: string;
32
+ }
33
+
34
+ async function makeHarness(): Promise<Harness> {
35
+ const canRead = vi.fn(async (_w: string, _e: string, p: string) => allow(p));
36
+ const canReadAtRef = vi.fn(async (_w: string, _r: string, _e: string, p: string) => allow(p));
37
+ const accessControl = { canRead, canReadAtRef } as unknown as IAccessControl;
38
+ const workflow = {
39
+ listChangesForFile: vi.fn(async () => []),
40
+ compareFile: vi.fn(async () => ''),
41
+ showFileAtChange: vi.fn(async () => ''),
42
+ fileAtChange: vi.fn(async () => ({ baseline: null, current: 'x' })),
43
+ };
44
+ const authService = { getUserById: vi.fn(async () => USER) } as unknown as AuthService;
45
+
46
+ const app = express();
47
+ app.use(express.json());
48
+ app.use('/api', (req, _res, next) => {
49
+ (req as unknown as { userId: string }).userId = USER.id;
50
+ next();
51
+ });
52
+ app.use(
53
+ '/api',
54
+ createWorkflowRoutes(
55
+ workflow as unknown as IWorkflowService,
56
+ {} as unknown as WorkspaceService,
57
+ authService,
58
+ { subscribe: vi.fn(), publish: vi.fn() } as unknown as WorkflowEventBus,
59
+ accessControl,
60
+ KB,
61
+ ),
62
+ );
63
+ const server = await new Promise<Server>((resolve) => {
64
+ const s = app.listen(0, () => resolve(s));
65
+ });
66
+ const addr = server.address() as AddressInfo;
67
+ return { server, canRead, canReadAtRef, workflow, baseUrl: `http://127.0.0.1:${addr.port}` };
68
+ }
69
+
70
+ function close(s: Server): Promise<void> {
71
+ return new Promise((resolve, reject) => s.close((e) => (e ? reject(e) : resolve())));
72
+ }
73
+
74
+ const OPEN = encodeURIComponent(`${KB}/Open/a.md`);
75
+ const SECRET = encodeURIComponent(`${KB}/Secret/x.md`);
76
+
77
+ describe('history routes enforce the read model', () => {
78
+ let h: Harness | null = null;
79
+ afterEach(async () => {
80
+ if (h) await close(h.server);
81
+ h = null;
82
+ });
83
+ const get = (p: string) => fetch(`${h!.baseUrl}/api/workspace/${WS}/workflow${p}`);
84
+
85
+ it.each([
86
+ ['/changes?path=%s', 'listChangesForFile'],
87
+ ['/show-file?sha=abc&path=%s', 'showFileAtChange'],
88
+ ['/file-at-change?sha=abc&path=%s', 'fileAtChange'],
89
+ ['/compare-file?from=a&to=b&path=%s', 'compareFile'],
90
+ ] as const)('%s: readable → 200, denied → 403 and git is never consulted', async (route, method) => {
91
+ h = await makeHarness();
92
+ expect((await get(route.replace('%s', OPEN))).status).toBe(200);
93
+ expect((await get(route.replace('%s', SECRET))).status).toBe(403);
94
+ // The denial happened BEFORE the service — nothing read the repository.
95
+ const calls = h.workflow[method].mock.calls as unknown[][];
96
+ expect(calls.length).toBe(1);
97
+ expect(String(calls[0][1])).toContain('Open');
98
+ });
99
+
100
+ it('a path without the repo prefix is refused outright — the git layer would read it as repo-relative', async () => {
101
+ // `stripRepoPrefix` makes `Secret/x.md` and `knowledge-base/Secret/x.md`
102
+ // name the same object, so an ungated "non-KB" branch would be a gate
103
+ // bypass by spelling. History is for tracked files only; no prefix, 400.
104
+ h = await makeHarness();
105
+ for (const p of ['Secret/x.md', 'scratch/notes.txt']) {
106
+ const res = await get(`/changes?path=${encodeURIComponent(p)}`);
107
+ expect(res.status).toBe(400);
108
+ }
109
+ expect(h.workflow.listChangesForFile).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('compare-file authorizes at every ref the comparison could serve', async () => {
113
+ // The comparison prefers a local ref (and the working tree) over
114
+ // `origin/<branch>`, so BOTH candidates are authorized per branch.
115
+ h = await makeHarness();
116
+ const res = await get(`/compare-file?from=a&to=b&path=${OPEN}`);
117
+ expect(res.status).toBe(200);
118
+ const refs = h.canReadAtRef.mock.calls.map((c) => c[1]).sort();
119
+ expect(refs).toEqual(['a', 'b', 'origin/a', 'origin/b']);
120
+
121
+ // Any RESOLVABLE ref denying the read refuses the diff.
122
+ h.canReadAtRef.mockImplementation(async (_w: string, ref: string) => (ref === 'b' ? false : true));
123
+ expect((await get(`/compare-file?from=a&to=b&path=${OPEN}`)).status).toBe(403);
124
+
125
+ // A branch none of whose refs resolve cannot be authorized: refused.
126
+ h.canReadAtRef.mockImplementation(async (_w: string, ref: string) =>
127
+ ref.endsWith('b') ? null : true,
128
+ );
129
+ expect((await get(`/compare-file?from=a&to=b&path=${OPEN}`)).status).toBe(403);
130
+ });
131
+
132
+ it('an access-model error fails closed, not open', async () => {
133
+ h = await makeHarness();
134
+ h.canRead.mockRejectedValueOnce(new Error('access tree unreadable'));
135
+ const res = await get(`/changes?path=${OPEN}`);
136
+ expect(res.status).toBeGreaterThanOrEqual(500);
137
+ expect(h.workflow.listChangesForFile).not.toHaveBeenCalled();
138
+ });
139
+ });