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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -7,13 +7,45 @@
7
7
  * bundled file's content (level 3).
8
8
  */
9
9
 
10
+ /**
11
+ * One plugin a skill belongs to — by sitting INSIDE the plugin folder
12
+ * (`linked: false`) or by being LINKED from the plugin's manifest
13
+ * (`linked: true`). For a link, `granted` is the link's HEALTH: whether the
14
+ * plugin's members can read the skill through it. For a link this platform
15
+ * manages that is whether the skill's own access rules name the plugin's
16
+ * `plugin/<Name>/read` principal — the link service writes that grant with
17
+ * the link, so `false` means a hand edit took it away. A link read from an
18
+ * external plugin format needs no grant (the skill's own scope decides who
19
+ * reads it) and is always `true`; the flag never proves a principal exists.
20
+ */
21
+ export interface PluginMembership {
22
+ /** The plugin folder name. */
23
+ name: string;
24
+ linked: boolean;
25
+ granted: boolean;
26
+ }
27
+
10
28
  export interface SkillSummary {
11
29
  /** Canonical id = the skill's folder name (e.g. `rfi`). */
12
30
  name: string;
13
31
  description: string;
14
32
  version?: string;
33
+ /** The governance owner from `metadata.owner`, when the skill declares one. */
34
+ owner?: string;
35
+ /**
36
+ * The governance lifecycle from `metadata.lifecycle`, lowercased — `active`
37
+ * (or absent), `deprecated` (still served, flagged), `retired` (served to the
38
+ * catalog for its owners, never compiled into a distribution).
39
+ */
40
+ lifecycle?: string;
15
41
  /** Repo-root-relative skill folder, e.g. `Plugins/Everyone/rfi`. */
16
42
  path: string;
43
+ /**
44
+ * The plugins this skill belongs to, decorated onto the catalog by the
45
+ * browser route (the catalog itself is plugin-unaware). Absent on the agent
46
+ * surfaces, which load skills by name and never by plugin.
47
+ */
48
+ plugins?: PluginMembership[];
17
49
  }
18
50
 
19
51
  export interface Skill extends SkillSummary {
@@ -1,6 +1,21 @@
1
1
  import express from 'express';
2
+ import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
2
3
  import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
3
- import type { IPendingSkillService, ISkillService } from './skills.contract.js';
4
+ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
5
+ import type { IPendingSkillService, ISkillService, PluginMembership } from './skills.contract.js';
6
+
7
+ /** The slice of the plugin link index this surface reads — see `PluginLinkIndex`. */
8
+ export interface SkillMembershipSource {
9
+ membership(): Promise<{
10
+ bySkill: Map<string, PluginMembership[]>;
11
+ byPlugin: Map<string, { folder: string }>;
12
+ }>;
13
+ }
14
+
15
+ /** The one access verdict this surface needs: may the caller read a path on the default branch? */
16
+ export interface SkillMembershipGate {
17
+ canReadBatch(workspaceId: string, userEmail: string, paths: string[]): Promise<Map<string, boolean>>;
18
+ }
4
19
 
5
20
  /**
6
21
  * Browser-facing (JWT) skill routes for the `/`-command menu, mounted behind
@@ -8,14 +23,27 @@ import type { IPendingSkillService, ISkillService } from './skills.contract.js';
8
23
  * so discovery, default-branch pinning, and per-user `canRead` filtering live
9
24
  * ONCE — the frontend never re-implements skill discovery or bypasses access.
10
25
  *
11
- * GET /api/skills → { skills: SkillSummary[] } (filtered to this user)
26
+ * GET /api/skills → { skills: SkillSummary[] } (filtered to this user,
27
+ * each with its `plugins`)
12
28
  * GET /api/skills/pending → { skills: PendingSkill[] } (proposed, not released)
13
29
  * GET /api/skills/:name → GetSkillResult (body + files)
14
30
  * GET /api/skills/:name?file=… → GetSkillResult (a bundled file's content)
31
+ *
32
+ * `plugins` is decorated HERE, not in the catalog: the catalog is what agents
33
+ * load by name and it stays plugin-unaware; the browser is the surface that
34
+ * groups skills by plugin. Optional dependency, so a host without the link
35
+ * index gets undecorated summaries rather than a 500.
15
36
  */
16
37
  export function createSkillsRoutes(
17
38
  skillService: ISkillService,
18
39
  pendingSkills?: IPendingSkillService,
40
+ links?: SkillMembershipSource,
41
+ /**
42
+ * With `links`: the memberships a caller sees name only plugins they may
43
+ * DISCOVER (read the plugin's access.md — the plugin index's own listing
44
+ * verdict), so this surface never reveals a plugin `/api/plugins` omits.
45
+ */
46
+ gate?: SkillMembershipGate,
19
47
  ): express.Router {
20
48
  const router = express.Router();
21
49
 
@@ -25,7 +53,35 @@ export function createSkillsRoutes(
25
53
  res.status(401).json({ error: 'Unauthenticated' });
26
54
  return;
27
55
  }
28
- res.json({ skills: await skillService.listSkills(email) });
56
+ const skills = await skillService.listSkills(email);
57
+ if (!links) {
58
+ res.json({ skills });
59
+ return;
60
+ }
61
+ let bySkill: Map<string, PluginMembership[]>;
62
+ // Fail closed: memberships without a gate to judge them by name nothing.
63
+ let discoverable: (plugin: string) => boolean = () => false;
64
+ try {
65
+ const membership = await links.membership();
66
+ bySkill = membership.bySkill;
67
+ if (gate) {
68
+ const folders = [...membership.byPlugin.entries()];
69
+ const verdicts = folders.length
70
+ ? await gate.canReadBatch(
71
+ workspaceIdForBranch(DEFAULT_BRANCH),
72
+ email,
73
+ folders.map(([, p]) => `${p.folder}/access.md`),
74
+ )
75
+ : new Map<string, boolean>();
76
+ const visible = new Set(folders.filter(([, p]) => verdicts.get(`${p.folder}/access.md`) === true).map(([name]) => name));
77
+ discoverable = (plugin) => visible.has(plugin);
78
+ }
79
+ } catch {
80
+ bySkill = new Map(); // membership is a decoration; the shelf must not fall with it
81
+ }
82
+ res.json({
83
+ skills: skills.map((s) => ({ ...s, plugins: (bySkill.get(s.path) ?? []).filter((m) => discoverable(m.name)) })),
84
+ });
29
85
  });
30
86
 
31
87
  /**
@@ -1,13 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { parseDocument } from 'yaml';
4
- import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
4
+ import { DEFAULT_BRANCH, PLUGINS_DIR, SKILLS_DIR } from '@bevel-software/platform-shared';
5
5
  import type { WorkspaceService } from '../workspace/workspace.service.js';
6
6
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
7
7
  import type { IAccessControl } from '../access/access-control.interface.js';
8
8
  import { extractFrontmatter, resolveDeclaredId, dedupeById } from '../../shared/frontmatter-id.js';
9
9
  import { walkFiles } from '../../shared/fs-walk.js';
10
10
  import { TtlCache } from '../../shared/ttl-cache.js';
11
+ import { BevelIgnoreStack } from '../workspace/bevel-ignore.js';
11
12
  import type {
12
13
  ISkillService,
13
14
  GetSkillResult,
@@ -75,6 +76,9 @@ export class SkillService implements ISkillService {
75
76
  if (file !== undefined) {
76
77
  if (!isSafeRelFile(file)) return { ok: false, error: 'invalid_file' };
77
78
  const repoPath = `${found.summary.path}/${file}`;
79
+ // Only a file the catalog LISTS is served: the listing already applied
80
+ // the ignore rules, so a path that is not in it is one they hid.
81
+ if (!found.files.includes(repoPath)) return { ok: false, error: 'not_found' };
78
82
  try {
79
83
  const content = await this.workspaceService.readFile(wsId, `${this.kbDirName}/${repoPath}`);
80
84
  return { ok: true, kind: 'file', file: { name, file, path: repoPath, content } };
@@ -97,15 +101,20 @@ export class SkillService implements ISkillService {
97
101
  private async scan(): Promise<ParsedSkill[]> {
98
102
  const cached = this.cache.get();
99
103
  if (cached) return cached;
104
+ // Token first: a merge's `invalidate()` can land while `scanDisk` is still
105
+ // reading the pre-merge tree, and storing that read afterwards would undo
106
+ // the drop for a full TTL.
107
+ const token = this.cache.begin();
100
108
  const skills = await this.scanDisk();
101
- this.cache.set(skills);
109
+ this.cache.set(skills, token);
102
110
  return skills;
103
111
  }
104
112
 
105
113
  private async scanDisk(): Promise<ParsedSkill[]> {
106
- // Ensure the default-branch clone exists, then scan its Plugins/ dir. Any
107
- // failure (no workspace, no Plugins/ dir) degrades to an empty catalog — the
108
- // manual/tools must never break because skills can't be read.
114
+ // Ensure the default-branch clone exists, then scan its Skills/ and
115
+ // Plugins/ roots. Any failure (no workspace, no such dir) degrades to an
116
+ // empty catalog — the manual/tools must never break because skills can't
117
+ // be read.
109
118
  let wsId: string;
110
119
  try {
111
120
  wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
@@ -115,20 +124,34 @@ export class SkillService implements ISkillService {
115
124
  const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
116
125
 
117
126
  // Skills may be grouped in category subfolders (each carrying its own
118
- // access.md), so a SKILL.md can live at any depth under the root. Walk the
119
- // tree and treat every folder that directly contains a SKILL.md as a skill;
120
- // don't descend past it — its inner files are bundled assets, not nested
121
- // skills. The skill name is the leaf folder name; its path is the full
122
- // repo-relative folder (e.g. `Plugins/Development/coding-guidelines`).
127
+ // access.md), so a SKILL.md can live at any depth under either root. Walk
128
+ // the tree and treat every folder that directly contains a SKILL.md as a
129
+ // skill; don't descend past it — its inner files are bundled assets, not
130
+ // nested skills. The skill name is the leaf folder name; its path is the
131
+ // full repo-relative folder (e.g. `Skills/Engineering/deploy`).
132
+ //
133
+ // `.bevelignore` files INSIDE a root are honoured on the way down, the
134
+ // same layered rules the file tree applies: a repository that carries a
135
+ // build output beside its source (a `dist/` holding compiled copies of
136
+ // every skill) would otherwise list each skill twice and refuse the
137
+ // duplicate — the wrong one, half the time. The REPO-ROOT file is
138
+ // deliberately not consulted: it is where the template hides `Plugins/`
139
+ // from the Knowledge tree, and a rule that hides a root from the browser
140
+ // must not empty the catalog that root exists to feed.
123
141
  const out: ParsedSkill[] = [];
124
- const walk = async (dir: string, relFolder: string): Promise<void> => {
142
+ const walk = async (dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<void> => {
125
143
  let entries: import('node:fs').Dirent[];
126
144
  try {
127
145
  entries = await fs.readdir(dir, { withFileTypes: true });
128
146
  } catch {
129
147
  return;
130
148
  }
149
+ ignore = await ignore.extendedWith(dir);
131
150
  if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) {
151
+ // A skill folder is a LEAF whatever the ignore rules say of it: a rule
152
+ // naming its SKILL.md suppresses the skill, it does not turn the
153
+ // folder's assets into skills of their own.
154
+ if (ignore.isIgnored(path.join(dir, 'SKILL.md'), false)) return;
132
155
  let raw: string;
133
156
  try {
134
157
  raw = await fs.readFile(path.join(dir, 'SKILL.md'), 'utf-8');
@@ -144,26 +167,43 @@ export class SkillService implements ISkillService {
144
167
  const declared = resolveDeclaredId(fm.frontmatter, path.basename(dir));
145
168
  const name = isSafeSkillName(declared) ? declared : path.basename(dir);
146
169
  out.push({
147
- summary: { name, description: fm.description, version: fm.version, path: relFolder },
170
+ summary: {
171
+ name,
172
+ description: fm.description,
173
+ version: fm.version,
174
+ owner: fm.owner,
175
+ lifecycle: fm.lifecycle,
176
+ path: relFolder,
177
+ },
148
178
  body: fm.body,
149
179
  allowedTools: fm.allowedTools,
150
- files: await listBundledFiles(dir, relFolder),
180
+ files: await listBundledFiles(dir, relFolder, ignore),
151
181
  });
152
182
  return; // a skill folder is a leaf — its subfolders hold assets, not skills
153
183
  }
154
184
  for (const entry of entries) {
155
185
  if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
156
- await walk(path.join(dir, entry.name), `${relFolder}/${entry.name}`);
186
+ const abs = path.join(dir, entry.name);
187
+ if (ignore.isIgnored(abs, true)) continue;
188
+ await walk(abs, `${relFolder}/${entry.name}`, ignore);
157
189
  }
158
190
  };
159
- await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR);
191
+ // Each root starts its own ignore stack (the walk extends it with the
192
+ // root's own file first). Order is cosmetic: the sort below is by name
193
+ // then path, so a same-named pair resolves the same way regardless.
194
+ await walk(path.join(kbRoot, SKILLS_DIR), SKILLS_DIR, BevelIgnoreStack.empty());
195
+ await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR, BevelIgnoreStack.empty());
160
196
  // A skill's id (frontmatter `id`/`name`, else folder name) is how getSkill()
161
- // resolves it, so it must be unique. Sort by (name, path) for a deterministic
162
- // winner, then REFUSE later duplicates via the shared dedup — the same rule
163
- // tools use (no silent auto-suffix that would rebind an id under the caller).
197
+ // resolves it, so it must be unique. Sort by (name, root, path) for a
198
+ // deterministic winner — the shared root FIRST, since `Skills/` is a
199
+ // skill's canonical home and a same-named inline copy is the stale one —
200
+ // then REFUSE later duplicates via the shared dedup — the same rule tools
201
+ // use (no silent auto-suffix that would rebind an id under the caller).
202
+ const rootRank = (p: string) => (p === SKILLS_DIR || p.startsWith(`${SKILLS_DIR}/`) ? 0 : 1);
164
203
  out.sort(
165
204
  (a, b) =>
166
205
  a.summary.name.localeCompare(b.summary.name) ||
206
+ rootRank(a.summary.path) - rootRank(b.summary.path) ||
167
207
  a.summary.path.localeCompare(b.summary.path),
168
208
  );
169
209
  return dedupeById(out, (s) => s.summary.name, (s, id) =>
@@ -209,6 +249,10 @@ function scalarToString(value: unknown): string | undefined {
209
249
  export function parseSkillFrontmatter(raw: string): {
210
250
  description: string;
211
251
  version?: string;
252
+ /** `metadata.owner` (or a top-level `owner`) — the governance record's owner, verbatim. */
253
+ owner?: string;
254
+ /** `metadata.lifecycle` (or top-level) — e.g. `active`, `deprecated`, `retired`; lowercased. */
255
+ lifecycle?: string;
212
256
  allowedTools?: string[];
213
257
  body: string;
214
258
  /** The parsed frontmatter object (for shared id resolution: `id`/`name`). */
@@ -233,6 +277,11 @@ export function parseSkillFrontmatter(raw: string): {
233
277
  const description = typeof data.description === 'string' ? data.description.trim() : '';
234
278
  const metadata = (data.metadata ?? {}) as Record<string, unknown>;
235
279
  const version = scalarToString(data.version) ?? scalarToString(metadata.version);
280
+ // The governance record: `metadata.owner` / `metadata.lifecycle` first (the
281
+ // agentskills convention this catalog reads), a top-level spelling second.
282
+ const owner = (scalarToString(metadata.owner) ?? scalarToString(data.owner))?.trim() || undefined;
283
+ const lifecycle =
284
+ (scalarToString(metadata.lifecycle) ?? scalarToString(data.lifecycle))?.trim().toLowerCase() || undefined;
236
285
 
237
286
  // `allowed-tools` is a space-separated string (agentskills) or a YAML list.
238
287
  const at = data['allowed-tools'];
@@ -242,12 +291,25 @@ export function parseSkillFrontmatter(raw: string): {
242
291
  ? at.split(/\s+/).filter(Boolean)
243
292
  : undefined;
244
293
 
245
- return { description, version, allowedTools, body, frontmatter: data };
294
+ return { description, version, owner, lifecycle, allowedTools, body, frontmatter: data };
246
295
  }
247
296
 
248
- /** Repo-root-relative paths of every bundled file under a skill folder (excludes SKILL.md). */
249
- async function listBundledFiles(dir: string, relFolder: string): Promise<string[]> {
297
+ /**
298
+ * Repo-root-relative paths of every bundled file under a skill folder
299
+ * (excludes SKILL.md), under the same ignore rules the walk applied on the
300
+ * way down — a rule in the skill's own `.bevelignore` or any folder above
301
+ * hides an asset from the listing, and therefore (see `getSkill`) from
302
+ * being served.
303
+ */
304
+ async function listBundledFiles(dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<string[]> {
305
+ const hidden = (rel: string): boolean => {
306
+ const parts = rel.split('/');
307
+ for (let i = 1; i < parts.length; i++) {
308
+ if (ignore.isIgnored(path.join(dir, ...parts.slice(0, i)), true)) return true;
309
+ }
310
+ return ignore.isIgnored(path.join(dir, ...parts), false);
311
+ };
250
312
  return (await walkFiles(dir, () => true))
251
- .filter((rel) => rel !== 'SKILL.md')
313
+ .filter((rel) => rel !== 'SKILL.md' && !hidden(rel))
252
314
  .map((rel) => `${relFolder}/${rel}`);
253
315
  }
@@ -22,6 +22,12 @@ declare global {
22
22
  export interface ExternalApiKeySummary {
23
23
  id: string;
24
24
  label: string;
25
+ /**
26
+ * What the key was minted AS — `key` for one a person created by hand,
27
+ * another kind for one a flow minted on their behalf (a Claude link).
28
+ * Stored with the row; the label is the person's to edit and proves nothing.
29
+ */
30
+ kind: string;
25
31
  createdAt: number;
26
32
  lastUsedAt: number | null;
27
33
  revokedAt: number | null;
@@ -37,6 +43,19 @@ export interface MintedExternalApiKey {
37
43
  summary: ExternalApiKeySummary;
38
44
  }
39
45
 
46
+ /** The kind of a key a person creates by hand. */
47
+ export const DEFAULT_KEY_KIND = 'key';
48
+
49
+ /**
50
+ * How a key is minted. `kind` names one of the kinds the service was built
51
+ * with, each of which carries its own plaintext prefix (the tenant's for the
52
+ * default kind, `gho_` for a Claude link); an unknown kind is refused, since
53
+ * nothing would route its bearer back.
54
+ */
55
+ export interface MintOptions {
56
+ kind?: string;
57
+ }
58
+
40
59
  /**
41
60
  * Contract for connection-key (API token) lifecycle. Used by the MCP auth
42
61
  * middleware to resolve a Bearer token to a Bevel user, and by the settings
@@ -59,7 +78,7 @@ export interface IExternalApiKeyService {
59
78
  * plaintext is **only** returned here — there is no read path that can
60
79
  * surface it again.
61
80
  */
62
- mint(userId: string, label: string): Promise<MintedExternalApiKey>;
81
+ mint(userId: string, label: string, options?: MintOptions): Promise<MintedExternalApiKey>;
63
82
 
64
83
  /**
65
84
  * Resolve a plaintext token to the owning user. Returns null when the
@@ -8,36 +8,53 @@ import {
8
8
  TokenNotFoundError,
9
9
  TokenStillActiveError,
10
10
  } from './external-api-key.errors.js';
11
- import type {
12
- ExternalApiKeySummary,
13
- IExternalApiKeyService,
14
- MintedExternalApiKey,
11
+ import {
12
+ DEFAULT_KEY_KIND,
13
+ type ExternalApiKeySummary,
14
+ type IExternalApiKeyService,
15
+ type MintOptions,
16
+ type MintedExternalApiKey,
15
17
  } from './external-api-key.interface.js';
16
18
 
17
- // Plaintext format: `<tenant>_<43 base64url chars>`. 32 random bytes encoded
19
+ // Plaintext format: `<prefix><43 base64url chars>`. 32 random bytes encoded
18
20
  // base64url is 43 chars and gives 256 bits of entropy — overkill against
19
- // brute force, cheap to copy. The tenant prefix (default `bevel_`) lets the
20
- // auth middleware route key-vs-JWT requests without parsing both shapes.
21
+ // brute force, cheap to copy. The prefix lets the auth middleware route
22
+ // key-vs-JWT requests without parsing both shapes.
21
23
  const TOKEN_BYTES = 32;
22
24
 
23
25
  const MAX_LABEL_LEN = 200;
24
26
 
25
27
  export class ExternalApiKeyService implements IExternalApiKeyService {
28
+ private readonly prefixes: readonly string[];
29
+
26
30
  /**
27
31
  * @param keyPrefix Tenant-derived plaintext prefix (e.g. `bevel_`) — injected
28
32
  * from {@link AppConfig.externalApiKeyPrefix} so a deploy can brand its keys.
33
+ * Every key of the default kind is minted with it.
34
+ * @param kinds Other kinds a key may be minted as, each with the plaintext
35
+ * prefix that kind is spelled with — a Claude link's `gho_`, the shape
36
+ * claude.ai expects from a GitHub host. Same key, same table, same
37
+ * revocation; the KIND is stored on the row and is what tells such keys
38
+ * apart (a label is the person's to edit), the prefix is only what the
39
+ * outside world sees and what routes the bearer back here.
29
40
  */
30
41
  constructor(
31
42
  private readonly db: Database,
32
43
  private readonly keyPrefix: string,
33
- ) {}
44
+ private readonly kinds: Readonly<Record<string, string>> = {},
45
+ ) {
46
+ this.prefixes = [keyPrefix, ...Object.values(kinds)];
47
+ }
34
48
 
35
- /** True if a bearer string carries this tenant's external-API-key prefix. */
49
+ /** True if a bearer string carries one of this service's key prefixes. */
36
50
  looksLikeExternalApiKey(token: string): boolean {
37
- return typeof token === 'string' && token.startsWith(this.keyPrefix);
51
+ return typeof token === 'string' && this.prefixes.some((p) => token.startsWith(p));
38
52
  }
39
53
 
40
- async mint(userId: string, label: string): Promise<MintedExternalApiKey> {
54
+ async mint(userId: string, label: string, options: MintOptions = {}): Promise<MintedExternalApiKey> {
55
+ const kind = options.kind ?? DEFAULT_KEY_KIND;
56
+ const prefix = kind === DEFAULT_KEY_KIND ? this.keyPrefix : this.kinds[kind];
57
+ if (!prefix) throw new Error(`Unknown key kind "${kind}"`);
41
58
  const trimmed = (label ?? '').trim();
42
59
  if (trimmed.length === 0) {
43
60
  throw new InvalidTokenLabelError('Label cannot be empty');
@@ -48,12 +65,12 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
48
65
  );
49
66
  }
50
67
 
51
- const plaintext = this.keyPrefix + randomBytes(TOKEN_BYTES).toString('base64url');
68
+ const plaintext = prefix + randomBytes(TOKEN_BYTES).toString('base64url');
52
69
  const tokenHash = hashToken(plaintext);
53
70
 
54
71
  const [row] = await this.db
55
72
  .insert(externalApiKeys)
56
- .values({ userId, tokenHash, label: trimmed })
73
+ .values({ userId, tokenHash, label: trimmed, kind })
57
74
  .returning();
58
75
 
59
76
  return {
@@ -202,6 +219,7 @@ function toSummary(row: typeof externalApiKeys.$inferSelect): ExternalApiKeySumm
202
219
  return {
203
220
  id: row.id,
204
221
  label: row.label,
222
+ kind: row.kind,
205
223
  createdAt: row.createdAt.getTime(),
206
224
  lastUsedAt: row.lastUsedAt ? row.lastUsedAt.getTime() : null,
207
225
  revokedAt: row.revokedAt ? row.revokedAt.getTime() : null,
@@ -1,8 +1,31 @@
1
1
  import { describe, expect, it, vi, afterEach } from 'vitest';
2
- import { descriptorsFromMcpJson } from '../mcp-json-discovery.js';
2
+ import { descriptorsFromMcpJson, judgeMcpServerEntry } from '../mcp-json-discovery.js';
3
3
 
4
4
  afterEach(() => vi.restoreAllMocks());
5
5
 
6
+ describe('judgeMcpServerEntry', () => {
7
+ it('keeps every accepted header and env key — one spelled __proto__ included', () => {
8
+ // Parsed from text, as a repository file is: JSON.parse makes `__proto__`
9
+ // an own key, and a plain-object copy would turn it into a prototype
10
+ // assignment and lose it.
11
+ const http = judgeMcpServerEntry(
12
+ 'vendor',
13
+ JSON.parse('{"type":"streamable-http","url":"https://x.example","headers":{"__proto__":"p","X-A":"a"}}'),
14
+ );
15
+ expect(http.ok).toBe(true);
16
+ if (!http.ok || http.transport !== 'streamable-http') throw new Error('unreachable');
17
+ expect(Object.keys(http.entry.headers ?? {}).sort()).toEqual(['X-A', '__proto__']);
18
+ expect(http.entry.headers?.['__proto__']).toBe('p');
19
+
20
+ const stdio = judgeMcpServerEntry(
21
+ 'launch',
22
+ JSON.parse('{"type":"stdio","command":"run","env":{"__proto__":"p"}}'),
23
+ );
24
+ if (!stdio.ok || stdio.transport !== 'stdio') throw new Error('unreachable');
25
+ expect(stdio.entry.env?.['__proto__']).toBe('p');
26
+ });
27
+ });
28
+
6
29
  const MANIFEST = JSON.stringify({
7
30
  name: 'gtm',
8
31
  extensions: {
@@ -192,6 +192,8 @@ describe('manual namespaces are unique, not merely manual names', () => {
192
192
  */
193
193
  async function writeMcpServer(folder: string, serverName: string): Promise<void> {
194
194
  await mkdir(join(pluginsDir(), folder), { recursive: true });
195
+ // A plugin is a folder with a manifest; the servers ride in its mcp.json.
196
+ await writeFile(join(pluginsDir(), folder, 'plugin.json'), `{"name":"${folder.toLowerCase()}"}`);
195
197
  await writeFile(
196
198
  join(pluginsDir(), folder, 'mcp.json'),
197
199
  JSON.stringify({ mcpServers: { [serverName]: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),