@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
@@ -0,0 +1,684 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import '@utcp/http'; // side effect: registers the 'http' UTCP communication protocol
3
+ import '@utcp/mcp'; // side effect: registers the 'mcp' protocol (remote MCP `.tool` sources)
4
+ import { CommunicationProtocol, UtcpClientConfigSerializer } from '@utcp/sdk';
5
+ import { CodeModeUtcpClient } from '@utcp/code-mode';
6
+ import { registerManual } from '@bevel-software/platform-mcp-core';
7
+ // The concrete module, NOT the `secrets-vault` barrel: that barrel re-exports
8
+ // the routes, and the routes import this module's contract — so going through
9
+ // it would close an import cycle between the two modules.
10
+ import { bevelSecretsLoaderConfig } from '../secrets-vault/secrets-variable-loader.js';
11
+ import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
12
+ import { assertSafeFetchUrl } from '../../shared/ssrf.js';
13
+ /** How long a probe may take before we call it inconclusive. */
14
+ const PROBE_TIMEOUT_MS = 10_000;
15
+ const OK = { status: 'ok', detail: null };
16
+ const NO_PROBE = {
17
+ status: 'unverifiable',
18
+ detail: "This tool doesn't offer a way to test its connection.",
19
+ };
20
+ /**
21
+ * Variable references, in the SAME grammar UTCP itself substitutes.
22
+ *
23
+ * `@utcp/sdk`'s `DefaultVariableSubstitutor` accepts BOTH `${VAR}` and bare
24
+ * `$VAR`, and so does the scanner that decides which variables the UI asks the
25
+ * user to fill in. Matching only the braced form meant a manual written as
26
+ * `Authorization: Bearer $API_KEY` — which works perfectly in real calls, and
27
+ * whose `API_KEY` the tool page duly asks for — had the LITERAL string sent by
28
+ * the probe, drawing a 401 and reporting a correct credential as "Not working".
29
+ *
30
+ * A fresh regex per call: `lastIndex` is mutable state on a `/g` pattern, and
31
+ * this one is used for both scanning and replacing.
32
+ */
33
+ function varRefPattern() {
34
+ return /\$\{([a-zA-Z0-9_]+)\}|\$([a-zA-Z0-9_]+)/g;
35
+ }
36
+ /**
37
+ * Does this failure ACCUSE the credential, or merely fail to exonerate it?
38
+ *
39
+ * Only a definitive rejection may set `failed`, because that is the status that
40
+ * tells a user their key is wrong. A provider that is down, rate-limiting, or
41
+ * unreachable says nothing about the key, and reporting those as "not working"
42
+ * would make the badge cry wolf during someone else's outage — which teaches
43
+ * people to ignore it, landing us back where we started by a different road.
44
+ *
45
+ * Matching on message text is unavoidable for the MCP path: registration
46
+ * failures surface as prose from the transport, not as a status code. The list
47
+ * is deliberately narrow and the default is "we don't know", so the failure
48
+ * mode of a miss is an over-cautious `unverifiable` rather than a false
49
+ * accusation.
50
+ */
51
+ function looksLikeAuthRejection(message) {
52
+ const m = message.toLowerCase();
53
+ // A status number proves nothing from inside a URL — `/v1/401/stream`, a
54
+ // port, an id in a path all contain the digits without any rejection having
55
+ // happened. Scrub URLs before reading digits; the word list below still
56
+ // runs against the full message, because words like "unauthorized" accuse
57
+ // wherever they appear.
58
+ const scrubbed = m.replace(/\bhttps?:\/\/\S+/g, ' ');
59
+ return (/\b(401|403)\b/.test(scrubbed) ||
60
+ m.includes('unauthorized') ||
61
+ m.includes('unauthenticated') ||
62
+ m.includes('forbidden') ||
63
+ m.includes('invalid_token') ||
64
+ m.includes('invalid token') ||
65
+ m.includes('invalid api key') ||
66
+ m.includes('invalid_api_key') ||
67
+ m.includes('authentication failed') ||
68
+ m.includes('invalid_grant'));
69
+ }
70
+ /** What {@link withProbeTimeout} returns for work that outlived the deadline. */
71
+ const TIMED_OUT = Symbol('probe-timed-out');
72
+ /**
73
+ * Resolve `work`, or {@link TIMED_OUT} if it outlives `PROBE_TIMEOUT_MS`.
74
+ *
75
+ * The declared-probe path gets its deadline from `AbortSignal.timeout`, but the
76
+ * MCP path cannot: neither `CodeModeUtcpClient.create` nor `registerManual`
77
+ * accepts a signal. Without this, an unresponsive server leaves a probe pending
78
+ * forever and the caller never gets a verdict at all.
79
+ *
80
+ * Abandoning work is not the same as stopping it, and on the MCP path the
81
+ * difference is a live socket. `@utcp/mcp` caches a session only AFTER its
82
+ * `connect()` resolves, so a handshake still in flight at the deadline has
83
+ * nothing for the caller's `finally` to find — and then caches its session (and
84
+ * its standalone SSE stream) moments later with nobody left holding it. That is
85
+ * one leaked session per probe against a slow server, on every credential save.
86
+ * `onLateSettle` is the owner for exactly that window: it runs when abandoned
87
+ * work eventually settles, and it is the only thing that can close a session
88
+ * opened after the verdict was returned.
89
+ */
90
+ async function withProbeTimeout(work, onLateSettle) {
91
+ let timer;
92
+ let abandoned = false;
93
+ // Attached before the race, so the continuation exists no matter when `work`
94
+ // settles. Both branches: a rejection can also have opened something — a
95
+ // successful `connect()` followed by a failing `listTools` leaves a session
96
+ // cached and the promise rejected.
97
+ void work
98
+ .then(() => (abandoned ? onLateSettle?.() : undefined), () => (abandoned ? onLateSettle?.() : undefined))
99
+ .catch((err) => {
100
+ console.warn(`[probe] late cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
101
+ });
102
+ const deadline = new Promise((resolve) => {
103
+ timer = setTimeout(() => {
104
+ abandoned = true;
105
+ resolve(TIMED_OUT);
106
+ }, PROBE_TIMEOUT_MS);
107
+ });
108
+ try {
109
+ return await Promise.race([work, deadline]);
110
+ }
111
+ finally {
112
+ if (timer)
113
+ clearTimeout(timer);
114
+ }
115
+ }
116
+ /** Below this, a "secret" is too short to blank out without eating the message. */
117
+ const MIN_SECRET_CHARS = 6;
118
+ /** The shortest leading run of a secret we will treat as an echo of it. */
119
+ const MIN_ECHOED_PREFIX_CHARS = 8;
120
+ const REDACTED = '[redacted]';
121
+ /**
122
+ * Every secret this probe resolved, so that none of them can come back out in
123
+ * something we quote.
124
+ *
125
+ * A rejection is quoted verbatim because the provider's own words are the
126
+ * actionable part — but providers routinely echo the credential they refused
127
+ * ("Incorrect API key provided: sk-…"). Anyone who can READ a tool can probe
128
+ * it, so without this a plain reader could recover part of a shared ADMIN key
129
+ * they were never allowed to see by saving a probe's failure text. The 200-char
130
+ * cap on the quote bounds it; it does not redact it.
131
+ *
132
+ * Leading PREFIXES count as well as whole values, because that echo is usually
133
+ * masked in the middle ("sk-live-abcd…wxyz"): a whole-value search finds
134
+ * nothing there and prints the head of the key regardless. Short values are
135
+ * left alone — blanking a five-character string would hit unrelated text and
136
+ * destroy the message that makes the verdict useful.
137
+ */
138
+ class SecretRedactor {
139
+ values = new Set();
140
+ remember(value) {
141
+ if (value.length >= MIN_SECRET_CHARS)
142
+ this.values.add(value);
143
+ }
144
+ redact(text) {
145
+ let out = text;
146
+ // Longest SECRET first, and not merely the longest prefix of each: when one
147
+ // resolved value is a prefix of another (a key and that same key with a
148
+ // suffix), redacting the short one first eats the head of the long one and
149
+ // leaves its tail printed — the exact half-redaction this class exists to
150
+ // avoid, arrived at from the other direction.
151
+ for (const secret of [...this.values].sort((a, b) => b.length - a.length)) {
152
+ // Longest match first: replacing the whole value when it is present beats
153
+ // replacing a prefix of it and leaving the tail on screen.
154
+ const shortest = Math.min(secret.length, MIN_ECHOED_PREFIX_CHARS);
155
+ for (let len = secret.length; len >= shortest; len--) {
156
+ const candidate = secret.slice(0, len);
157
+ if (!out.includes(candidate))
158
+ continue;
159
+ out = out.split(candidate).join(REDACTED);
160
+ break;
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+ }
166
+ /**
167
+ * Names — of a header or of a query parameter — whose value IS a credential
168
+ * rather than a description of the request. Matched as substrings and on
169
+ * purpose generously: a name wrongly treated as secret costs one blanked word
170
+ * in an error message, while one wrongly treated as public costs the
171
+ * credential. `signature` and `hmac` earn their place because a signed url
172
+ * (`X-Amz-Signature`, an Azure SAS) grants access exactly as a bearer token
173
+ * does — the secret is what derived it, and the derivation is what is sent.
174
+ */
175
+ const CREDENTIAL_MARKERS = [
176
+ 'auth',
177
+ 'key',
178
+ 'token',
179
+ 'secret',
180
+ 'password',
181
+ 'cookie',
182
+ 'credential',
183
+ 'signature',
184
+ 'hmac',
185
+ ];
186
+ /**
187
+ * The same, for names too short to match as substrings: `sig` inside a word
188
+ * would swallow `design`, `assign` and `signal`, and blanking those would eat
189
+ * the message rather than protect anything in it.
190
+ */
191
+ const CREDENTIAL_WORDS = /(?:^|[-_])(sig|hash)(?:$|[-_])/;
192
+ function carriesCredential(fieldName) {
193
+ const name = fieldName.toLowerCase();
194
+ return CREDENTIAL_MARKERS.some((marker) => name.includes(marker)) || CREDENTIAL_WORDS.test(name);
195
+ }
196
+ /**
197
+ * Remember a credential the manual spelled out in full.
198
+ *
199
+ * {@link ConnectionProbeService.substitute} only ever learns values it resolved
200
+ * from the vault, so a `.tool` whose header reads `Authorization: Bearer sk-…`
201
+ * rather than `Bearer ${TOKEN}` left the redactor with nothing to look for. That
202
+ * is the one credential a reader has no other route to: `ToolManualSummary`
203
+ * withholds `headers` from the browser for precisely this reason, and a
204
+ * provider quoting it in a 401 would have handed it back regardless.
205
+ *
206
+ * What follows the auth scheme is remembered as well as the whole value,
207
+ * because `Bearer sk-…` is echoed as the bare token far more often than in
208
+ * full, and `Bearer` by itself is not worth blanking out of a message.
209
+ */
210
+ function rememberCredentialHeaders(headers, redactor) {
211
+ for (const [name, value] of Object.entries(headers)) {
212
+ if (typeof value !== 'string' || !carriesCredential(name))
213
+ continue;
214
+ redactor.remember(value);
215
+ const afterScheme = /^\S+\s+(\S.*)$/.exec(value.trim());
216
+ if (afterScheme)
217
+ redactor.remember(afterScheme[1]);
218
+ }
219
+ }
220
+ /**
221
+ * The same, for a credential written into a query string (`?api_key=…`) rather
222
+ * than a header. Only the values of credential-NAMED parameters: a rejection
223
+ * usually names the endpoint it rejected, and blanking the url wholesale would
224
+ * cost more of the message than it protects.
225
+ */
226
+ function rememberCredentialQuery(url, redactor) {
227
+ let params;
228
+ try {
229
+ params = new URL(url).searchParams;
230
+ }
231
+ catch {
232
+ return;
233
+ }
234
+ for (const [name, value] of params) {
235
+ if (carriesCredential(name))
236
+ redactor.remember(value);
237
+ }
238
+ }
239
+ /**
240
+ * The same, for the headers an `mcp` manual carries on its server entry. This
241
+ * path never substitutes them — UTCP's own loader does that inside the SDK — so
242
+ * a templated value belongs to the vault and `rememberTemplateSecrets` already
243
+ * holds it; only what the file spells out in full is new here, and skipping the
244
+ * rest keeps a half-literal value like `example.com/${TENANT}` from lending its
245
+ * public prefix to the redactor.
246
+ */
247
+ function rememberLiteralServerHeaders(template, redactor) {
248
+ const servers = template.config?.mcpServers;
249
+ for (const server of Object.values(servers ?? {})) {
250
+ const headers = server.headers;
251
+ if (!headers || typeof headers !== 'object')
252
+ continue;
253
+ const literal = Object.fromEntries(Object.entries(headers).filter(([, value]) => typeof value === 'string' && !varRefPattern().test(value)));
254
+ rememberCredentialHeaders(literal, redactor);
255
+ }
256
+ }
257
+ /**
258
+ * A copy of `template` whose MCP server keys are unique to one probe.
259
+ *
260
+ * This is what makes an MCP probe test the credential rather than the cache.
261
+ * `@utcp/mcp` registers a MODULE-LEVEL singleton protocol whose session map is
262
+ * keyed only `<serverName>:<transport>`, and `_getOrCreateSession` returns a
263
+ * cached hit WITHOUT consulting the auth it was handed — so a throwaway client
264
+ * is emphatically not a throwaway session. Probing under the manual's own name
265
+ * would hand back whatever session the MCP proxy opened earlier, `listTools`
266
+ * would succeed on that older token, and a freshly mistyped key would be
267
+ * reported **Connected**: the exact bug this module exists to remove, rebuilt
268
+ * inside its own fix. It would also let one user's probe answer from another
269
+ * user's session, and let a failing probe evict the session everyone is using.
270
+ *
271
+ * The key is only a cache key and a tool-name prefix (`<manual>.<server>.<tool>`),
272
+ * and variables resolve against the MANUAL's name, not this one — so renaming
273
+ * it changes nothing about what gets dialled or which secrets it carries.
274
+ */
275
+ function withIsolatedServerKeys(template) {
276
+ const clone = structuredClone(template);
277
+ const servers = clone.config?.mcpServers;
278
+ if (!servers)
279
+ return clone;
280
+ const nonce = randomUUID().replace(/-/g, '');
281
+ clone.config.mcpServers = Object.fromEntries(Object.entries(servers).map(([key, value]) => [`${key}__probe_${nonce}`, value]));
282
+ return clone;
283
+ }
284
+ /**
285
+ * Close whatever MCP sessions this probe opened.
286
+ *
287
+ * Through the PROTOCOL rather than `client.deregisterManual(name)`, which only
288
+ * works for a manual the client managed to register: on a failed handshake the
289
+ * client never saves the manual, so the client-level call finds nothing and
290
+ * returns `false` — while the session, created before `listTools` was ever
291
+ * attempted, stays cached and open. A failed probe is exactly when that
292
+ * happens, so cleanup has to be reachable without the registry. The protocol's
293
+ * own `deregisterManual` takes the template directly and closes each
294
+ * `<server>:<transport>` session it names.
295
+ *
296
+ * Best-effort: the probe has already produced its verdict, and failing to tidy
297
+ * up must not turn a successful check into an error.
298
+ */
299
+ async function closeProbeSessions(client, template) {
300
+ try {
301
+ const protocol = CommunicationProtocol.communicationProtocols[template.call_template_type];
302
+ await protocol?.deregisterManual(client, template);
303
+ }
304
+ catch (err) {
305
+ console.warn(`[probe] closing session failed: ${err instanceof Error ? err.message : String(err)}`);
306
+ }
307
+ }
308
+ /**
309
+ * Runs one credential probe and hands back the answer. Holds no state and
310
+ * writes nothing: see {@link ProbeVerdict} for why the verdict isn't stored.
311
+ */
312
+ export class ConnectionProbeService {
313
+ toolManualService;
314
+ secretsVault;
315
+ constructor(toolManualService, secretsVault) {
316
+ this.toolManualService = toolManualService;
317
+ this.secretsVault = secretsVault;
318
+ }
319
+ async probe(userId, userEmail, slug) {
320
+ // ONE catalog + ACL pass for every fact this probe needs: whether the tool
321
+ // is local-only, what it declares as a health check, and the call template
322
+ // an `mcp` handshake would dial.
323
+ const target = await this.toolManualService.probeTargetFor(userEmail, slug);
324
+ if (!target)
325
+ return null;
326
+ const outcome = await this.runProbe(userId, target);
327
+ return { status: outcome.status, detail: outcome.detail, checkedAt: new Date() };
328
+ }
329
+ // --- internal --------------------------------------------------------------
330
+ /**
331
+ * Pick the probe for this manual.
332
+ *
333
+ * A DECLARED health check wins for every type, including `mcp`. The author
334
+ * naming an endpoint is a stronger statement about how to test their tool
335
+ * than any inference of ours, and honouring it means a declaration is never
336
+ * silently ignored — which is the whole contract of the field.
337
+ *
338
+ * Failing that, an `mcp` manual tests itself: its handshake is authenticated,
339
+ * so connecting at all proves the token. `http` and `inline` have no such
340
+ * moment (an `http` manual's registration fetches its public DESCRIPTION; an
341
+ * `inline` manual's points at Bevel's own API), so with nothing declared
342
+ * there is genuinely nothing to call.
343
+ */
344
+ async runProbe(userId, target) {
345
+ // A local-only tool runs in someone's own agent; this server cannot reach it
346
+ // (that is what `remote: false` MEANS), so a failure here would say nothing
347
+ // about the credential.
348
+ if (target.remote === false) {
349
+ return { status: 'unverifiable', detail: 'This tool runs only in a local agent, so the server cannot test it.' };
350
+ }
351
+ if (target.healthCheck)
352
+ return this.probeDeclared(userId, target, target.healthCheck);
353
+ if (target.type === 'mcp')
354
+ return this.probeMcpHandshake(userId, target);
355
+ return NO_PROBE;
356
+ }
357
+ /**
358
+ * Call the endpoint the manual declared, carrying the caller's credential.
359
+ *
360
+ * Headers default to the manual's own — that is where the credential normally
361
+ * lives, so the common case is a one-line `healthCheck: { url }` and the probe
362
+ * authenticates exactly like a real call.
363
+ */
364
+ async probeDeclared(userId, target, check) {
365
+ // `headers` is already defaulted to the manual's own at parse time, so a
366
+ // one-line `healthCheck: { url }` still carries the credential.
367
+ const headerTemplate = check.headers ?? {};
368
+ // Collects what the substitutions resolve, so the provider cannot quote any
369
+ // of it back at a reader who is not allowed to see it.
370
+ const redactor = new SecretRedactor();
371
+ let url;
372
+ let headers;
373
+ try {
374
+ url = await this.substitute(userId, target.name, check.url, redactor);
375
+ headers = Object.fromEntries(await Promise.all(Object.entries(headerTemplate).map(async ([k, v]) => [k, await this.substitute(userId, target.name, v, redactor)])));
376
+ }
377
+ catch (err) {
378
+ // An unset variable is not a broken credential — the vault's own status
379
+ // already says "needs a key from you", and firing a request with an empty
380
+ // Bearer would turn that into a spurious rejection.
381
+ return { status: 'unverifiable', detail: err instanceof Error ? err.message : String(err) };
382
+ }
383
+ // A `.tool` may write its token straight into `headers` rather than through
384
+ // a `${VAR}`, and substitution never saw it. Record what is actually about
385
+ // to be sent, so the provider cannot quote it back at a reader.
386
+ rememberCredentialHeaders(headers, redactor);
387
+ rememberCredentialQuery(url, redactor);
388
+ // Re-check at fetch time even though the declaration was checked at parse
389
+ // time: a TEMPLATED host is unknowable until now, so this is the first
390
+ // moment the real target exists (`${HOST}` could resolve to the metadata IP).
391
+ try {
392
+ assertSafeFetchUrl(url, { label: 'healthCheck.url' });
393
+ }
394
+ catch (err) {
395
+ return { status: 'unverifiable', detail: err instanceof Error ? err.message : String(err) };
396
+ }
397
+ let res;
398
+ try {
399
+ res = await fetch(url, {
400
+ method: check.method ?? 'GET',
401
+ headers,
402
+ // Don't follow redirects: a validated host could 302 to an internal
403
+ // target, and a 3xx tells us nothing about the credential anyway.
404
+ redirect: 'manual',
405
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
406
+ });
407
+ }
408
+ catch (err) {
409
+ // Redacted like everything else quoted from outside: a transport error
410
+ // names the url it failed on, and a templated url can carry a token.
411
+ return {
412
+ status: 'unverifiable',
413
+ detail: `Couldn't reach the provider to check: ${redactor.redact(err instanceof Error ? err.message : String(err))}`,
414
+ };
415
+ }
416
+ if (res.ok)
417
+ return OK;
418
+ if (res.status === 401 || res.status === 403) {
419
+ return { status: 'failed', detail: await describeRejection(res, redactor) };
420
+ }
421
+ return {
422
+ status: 'unverifiable',
423
+ detail: `The provider answered ${res.status}, which doesn't say whether the credential is valid.`,
424
+ };
425
+ }
426
+ /**
427
+ * Register the manual on a throwaway client and a session of its own: for a
428
+ * remote MCP server the handshake is authenticated, so a successful
429
+ * registration IS proof the token works, and a rejection is the provider's own
430
+ * verdict.
431
+ */
432
+ async probeMcpHandshake(userId, target) {
433
+ if (!target.callTemplate) {
434
+ return { status: 'unverifiable', detail: "This tool's definition couldn't be resolved, so it wasn't tested." };
435
+ }
436
+ // See `withIsolatedServerKeys`: without this the probe can be answered by a
437
+ // session opened with an older credential.
438
+ const template = withIsolatedServerKeys(target.callTemplate);
439
+ const redactor = new SecretRedactor();
440
+ // The SAME fetch-time SSRF re-check the declared probe gets, for the same
441
+ // reason: a templated host does not exist at parse time, so the guard there
442
+ // could not see it. Without this, an `mcp` manual whose url is
443
+ // `https://${HOST}/mcp` reaches the metadata endpoint the moment someone
444
+ // clicks Test connection. `@utcp/mcp`'s own `ensureSecureMcpUrl` is no help
445
+ // here — it checks the SCHEME, and allows https to any host at all.
446
+ const unsafe = await this.resolveServerUrls(userId, target.name, template, redactor);
447
+ if (unsafe)
448
+ return unsafe;
449
+ // The rest of the template keeps its `${VAR}`s for UTCP's own loader; these
450
+ // values are read only so that nothing quoted below can echo one back.
451
+ await this.rememberTemplateSecrets(userId, target.name, target.callTemplate, redactor);
452
+ rememberLiteralServerHeaders(target.callTemplate, redactor);
453
+ const timedOut = {
454
+ status: 'unverifiable',
455
+ detail: `The server didn't answer within ${PROBE_TIMEOUT_MS / 1000}s, so the credential wasn't tested.`,
456
+ };
457
+ let client = null;
458
+ try {
459
+ // No loopback seeding (`API_URL`/`CONNECTION_KEY`): those belong to
460
+ // Bevel-hosted manuals, and this path only ever registers a third-party
461
+ // MCP server. Its `${VAR}`s resolve lazily through the caller's own vault
462
+ // loader, which is exactly the credential under test.
463
+ const config = new UtcpClientConfigSerializer().validateDict({
464
+ variables: {},
465
+ load_variables_from: [bevelSecretsLoaderConfig(userId)],
466
+ });
467
+ // No late-settle owner: a client that arrives after the deadline has no
468
+ // manual registered and therefore no session, and neither
469
+ // `CodeModeUtcpClient` nor the SDK client it wraps exposes any disposal.
470
+ const created = await withProbeTimeout(CodeModeUtcpClient.create(process.cwd(), config));
471
+ if (created === TIMED_OUT)
472
+ return timedOut;
473
+ client = created;
474
+ // The handshake IS the connection-opener, so its abandoned form gets a
475
+ // closing owner: past the deadline the `finally` below has already run
476
+ // and found nothing to close.
477
+ const dialled = client;
478
+ const result = await withProbeTimeout(registerManual(dialled, template), () => closeProbeSessions(dialled, template));
479
+ if (result === TIMED_OUT)
480
+ return timedOut;
481
+ if (result.ok)
482
+ return OK;
483
+ // Classified on the raw error, shown redacted: the provider's rejection
484
+ // text may quote the very credential under test back at us.
485
+ const detail = redactor.redact(result.error);
486
+ return looksLikeAuthRejection(result.error)
487
+ ? { status: 'failed', detail }
488
+ : { status: 'unverifiable', detail: `Couldn't reach the provider to check: ${detail}` };
489
+ }
490
+ catch (err) {
491
+ return {
492
+ status: 'unverifiable',
493
+ detail: `Couldn't build the connection to test: ${redactor.redact(err instanceof Error ? err.message : String(err))}`,
494
+ };
495
+ }
496
+ finally {
497
+ // Runs on every path that got as far as a client: the session is keyed to
498
+ // this probe alone, so leaving it open leaks one Streamable-HTTP session
499
+ // per credential save. Past the deadline this finds nothing — the session
500
+ // does not exist yet — which is what the late-settle owner above is for.
501
+ if (client)
502
+ await closeProbeSessions(client, template);
503
+ }
504
+ }
505
+ /**
506
+ * Resolve every MCP server url in `template` IN PLACE, or say why one of them
507
+ * may not be dialled.
508
+ *
509
+ * The substitution and the check are the same act, deliberately: checking a
510
+ * resolved url and then handing UTCP the `${HOST}` template back would leave
511
+ * the vault free to answer differently the second time. A concurrent
512
+ * `PUT …/vars/HOST` landing in that gap aims the handshake at whatever the
513
+ * new value says — the metadata endpoint, say — with the guard's approval
514
+ * behind it. Writing the resolved url back means the bytes that passed the
515
+ * check are the bytes dialled, which is how the declared probe already works.
516
+ */
517
+ async resolveServerUrls(userId, manualName, template, redactor) {
518
+ const servers = template.config?.mcpServers;
519
+ for (const server of Object.values(servers ?? {})) {
520
+ // `stdio` servers carry a command, not a url, and are never reached from
521
+ // this process anyway (they imply `remote: false`, refused above).
522
+ const entry = server;
523
+ if (typeof entry.url !== 'string')
524
+ continue;
525
+ let resolved;
526
+ try {
527
+ resolved = await this.substitute(userId, manualName, entry.url, redactor);
528
+ }
529
+ catch (err) {
530
+ // An unset variable, exactly as on the declared path: nothing to test,
531
+ // and nothing said about the credential.
532
+ return { status: 'unverifiable', detail: err instanceof Error ? err.message : String(err) };
533
+ }
534
+ try {
535
+ assertSafeFetchUrl(resolved, { label: 'MCP server url' });
536
+ }
537
+ catch (err) {
538
+ return { status: 'unverifiable', detail: err instanceof Error ? err.message : String(err) };
539
+ }
540
+ entry.url = resolved;
541
+ rememberCredentialQuery(resolved, redactor);
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Read the values behind every variable `template` references, purely so
547
+ * {@link SecretRedactor} can keep them out of anything quoted.
548
+ *
549
+ * The MCP path leaves headers and auth as `${VAR}` for UTCP's loader, so
550
+ * unlike the declared probe it never learns those values in passing — and a
551
+ * rejected handshake surfaces as prose from the transport, which on many
552
+ * servers includes the response body and therefore the key. Best-effort by
553
+ * design: redaction is a safety net over the quote, and failing to build it
554
+ * must not fail the probe.
555
+ */
556
+ async rememberTemplateSecrets(userId, manualName, template, redactor) {
557
+ const names = new Set([...JSON.stringify(template).matchAll(varRefPattern())].map((m) => m[1] ?? m[2]));
558
+ for (const name of names) {
559
+ try {
560
+ const value = await this.secretsVault.resolve(userId, utcpNamespacedKey(manualName, name));
561
+ if (value)
562
+ redactor.remember(value);
563
+ }
564
+ catch {
565
+ // A variable we cannot read is one the provider cannot echo either.
566
+ }
567
+ }
568
+ }
569
+ /**
570
+ * Replace every variable reference with the caller's resolved secret. Throws
571
+ * when one has no value, so the caller can report "not set yet" rather than
572
+ * sending a request with an empty credential and reading the inevitable 401
573
+ * as proof the key is wrong.
574
+ *
575
+ * One left-to-right pass, like the SDK's own substitutor, rather than a
576
+ * replace per name: with bare `$VAR` accepted, substituting `$API` before
577
+ * `$API_KEY` would corrupt the longer reference.
578
+ */
579
+ async substitute(userId, manualName, template, redactor) {
580
+ // UTCP leaves any string containing `$ref` alone (JSON-Schema references),
581
+ // so a probe that substituted one would stop testing what a real call sends.
582
+ if (template.includes('$ref'))
583
+ return template;
584
+ const names = new Set([...template.matchAll(varRefPattern())].map((m) => m[1] ?? m[2]));
585
+ if (names.size === 0)
586
+ return template;
587
+ const values = new Map();
588
+ for (const name of names) {
589
+ let value = await this.secretsVault.resolve(userId, utcpNamespacedKey(manualName, name));
590
+ if (value === null) {
591
+ // UTCP keeps `process.env` as its LAST resolution tier, so a real
592
+ // call can resolve what the vault does not hold (a deployment-level
593
+ // variable). The probe must test the request the call would send —
594
+ // and the SDK's `_getVariable` builds ONE `effectiveKey` (the
595
+ // namespace with doubled underscores plus the variable, exactly
596
+ // `utcpNamespacedKey`) and consults every tier, env included, under
597
+ // that single spelling. No bare-name lookup here: an env var under a
598
+ // spelling the call never reads would have the probe testing a
599
+ // request no call sends.
600
+ value = process.env[utcpNamespacedKey(manualName, name)] ?? null;
601
+ }
602
+ if (value === null)
603
+ throw new Error(`\${${name}} isn't set yet, so there was nothing to test.`);
604
+ values.set(name, value);
605
+ redactor?.remember(value);
606
+ }
607
+ return template.replace(varRefPattern(), (_full, braced, bare) => {
608
+ const name = braced ?? bare;
609
+ return name === undefined ? _full : (values.get(name) ?? _full);
610
+ });
611
+ }
612
+ }
613
+ /** How much of a rejecting body we are willing to read to quote it. */
614
+ const MAX_REJECTION_BYTES = 8 * 1024;
615
+ /** How much of what we read we are willing to show. */
616
+ const REJECTION_SNIPPET_CHARS = 200;
617
+ /**
618
+ * A short, human-readable reason from a rejecting response — the provider's own
619
+ * words are far more actionable than "401" — with every secret this probe
620
+ * resolved taken back out of it (see {@link SecretRedactor}). Bounded and
621
+ * best-effort: a body that is huge, binary, or unreadable falls back to the
622
+ * status line.
623
+ *
624
+ * Read from the STREAM with a byte cap rather than `res.text()`. The body here
625
+ * is written by whatever server the credential points at, so buffering all of
626
+ * it to keep 200 characters lets that server decide how much memory this
627
+ * process spends and how long an unattended probe takes. The reader is
628
+ * cancelled as soon as we have enough, which closes the connection rather than
629
+ * politely draining a response we already stopped caring about.
630
+ */
631
+ async function describeRejection(res, redactor) {
632
+ const fallback = `The provider rejected this credential (${res.status}).`;
633
+ try {
634
+ // Redact BEFORE the snippet is cut, so a secret straddling the 200-character
635
+ // boundary loses its head rather than surviving as one.
636
+ const body = redactor.redact((await readCapped(res, MAX_REJECTION_BYTES)).trim());
637
+ if (!body)
638
+ return fallback;
639
+ const snippet = body.length > REJECTION_SNIPPET_CHARS ? `${body.slice(0, REJECTION_SNIPPET_CHARS)}…` : body;
640
+ return `${fallback} ${snippet}`;
641
+ }
642
+ catch {
643
+ return fallback;
644
+ }
645
+ }
646
+ /**
647
+ * At most `limit` bytes of a response body, decoded as text.
648
+ *
649
+ * Each CHUNK is sliced to the remaining budget before being decoded, not just
650
+ * counted against it: a provider that answers in one 5MB chunk would otherwise
651
+ * put the whole thing through the decoder before the loop noticed it was over,
652
+ * which is the bound bypassed by the very party it is meant to bound.
653
+ *
654
+ * Exported for its test — the property here is how much is held in memory,
655
+ * which the caller's 200-character snippet hides completely.
656
+ */
657
+ export async function readCapped(res, limit) {
658
+ const reader = res.body?.getReader();
659
+ // No stream (a mocked or already-consumed response): fall back to the whole
660
+ // body, which for those cases is ours and small.
661
+ if (!reader)
662
+ return (await res.text()).slice(0, limit);
663
+ const decoder = new TextDecoder();
664
+ let out = '';
665
+ let read = 0;
666
+ try {
667
+ while (read < limit) {
668
+ const { done, value } = await reader.read();
669
+ if (done)
670
+ break;
671
+ const remaining = limit - read;
672
+ const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
673
+ read += chunk.byteLength;
674
+ out += decoder.decode(chunk, { stream: true });
675
+ if (chunk.byteLength < value.byteLength)
676
+ break;
677
+ }
678
+ }
679
+ finally {
680
+ await reader.cancel().catch(() => { });
681
+ }
682
+ return out;
683
+ }
684
+ //# sourceMappingURL=connection-probe.service.js.map