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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -0,0 +1,443 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import http from 'node:http';
6
+ import { randomBytes } from 'node:crypto';
7
+ import express from 'express';
8
+ import type { AuthUser } from '@bevel-software/platform-shared';
9
+
10
+ import { MarketplaceRepoService, type MarketplaceCompiler } from '../marketplace-repo.service.js';
11
+ import {
12
+ GitHubFacadeCredentialsService,
13
+ GitHubFacade,
14
+ MemoryGitHubFacadeCodeStore,
15
+ MemoryGitHubFacadeCredentialsStore,
16
+ CLAUDE_CONSUMER,
17
+ GITHUB_LINK_KEY_KIND,
18
+ GITHUB_LINK_KEY_PREFIX,
19
+ createGitHubFacadeAdminRoutes,
20
+ createGitHubFacadeRoutes,
21
+ } from '../github-facade/index.js';
22
+ import { createOAuthConsentRoutes } from '../../mcp/oauth/oauth-consent.routes.js';
23
+ import type { BevelOAuthProvider } from '../../mcp/oauth/bevel-oauth-provider.js';
24
+ import type { VirtualTree } from '../../plugins/compile/compile-marketplace.js';
25
+
26
+ /**
27
+ * The GitHub-shaped surface, driven exactly as claude.ai drove the facade
28
+ * that recorded the contract: the authorize redirect, the consent finish,
29
+ * the code exchange with our client id and secret, then — with the token
30
+ * that came back — repository, head commit, zipball. Two people, two trees,
31
+ * a token that reads only its own — and two REPLICAS over one store, since
32
+ * the replica that issued a code is not the one asked to exchange it.
33
+ */
34
+
35
+ const CALLBACK = 'https://claude.ai/connect/github/callback';
36
+ const FRONTEND = 'http://app.test';
37
+ const PUBLIC = 'https://ops:hunter2@kb.acme.com/some/path';
38
+ const STATE_SECRET = 'state-secret';
39
+
40
+ const users: Record<string, AuthUser> = {
41
+ alice: { id: 'user-alice', email: 'alice@x.io', name: 'Alice' } as AuthUser,
42
+ bob: { id: 'user-bob', email: 'bob@x.io', name: 'Bob' } as AuthUser,
43
+ };
44
+
45
+ function tree(files: Record<string, string>, sourceCommit: string): VirtualTree & { sourceCommit: string } {
46
+ return {
47
+ files: new Map(Object.entries(files).map(([k, v]) => [k, Buffer.from(v)])),
48
+ warnings: [],
49
+ plugins: [],
50
+ sourceCommit,
51
+ };
52
+ }
53
+
54
+ /** Connection keys, in memory: what the service does minus the database. */
55
+ function makeKeys() {
56
+ const byToken = new Map<string, { tokenId: string; user: AuthUser; label: string; kind: string }>();
57
+ const kinds: Record<string, string> = { key: 'bevel_', [GITHUB_LINK_KEY_KIND]: GITHUB_LINK_KEY_PREFIX };
58
+ return {
59
+ byToken,
60
+ looksLikeExternalApiKey: (t: string) => Object.values(kinds).some((p) => t.startsWith(p)),
61
+ verifyAndLoadToken: async (t: string) => {
62
+ const hit = byToken.get(t);
63
+ return hit ? { tokenId: hit.tokenId, user: hit.user } : null;
64
+ },
65
+ mint: async (userId: string, label: string, options: { kind?: string } = {}) => {
66
+ const kind = options.kind ?? 'key';
67
+ const prefix = kinds[kind];
68
+ if (!prefix) throw new Error(`Unknown key kind "${kind}"`);
69
+ const user = Object.values(users).find((u) => u.id === userId)!;
70
+ const plaintext = prefix + randomBytes(16).toString('base64url');
71
+ byToken.set(plaintext, { tokenId: `tok-${byToken.size + 1}`, user, label, kind });
72
+ return {
73
+ plaintext,
74
+ summary: { id: `tok-${byToken.size}`, label, kind, createdAt: Date.now(), lastUsedAt: null, revokedAt: null },
75
+ };
76
+ },
77
+ };
78
+ }
79
+
80
+ /** One replica: its own bridge over the SHARED stores, its own HTTP listener. */
81
+ async function replica(shared: {
82
+ credentials: MemoryGitHubFacadeCredentialsStore;
83
+ codes: MemoryGitHubFacadeCodeStore;
84
+ keys: ReturnType<typeof makeKeys>;
85
+ repo: MarketplaceRepoService;
86
+ admins: Set<string>;
87
+ }) {
88
+ const credentials = new GitHubFacadeCredentialsService(shared.credentials);
89
+ const facade = new GitHubFacade({
90
+ credentials,
91
+ codes: shared.codes,
92
+ keys: shared.keys,
93
+ consumers: [CLAUDE_CONSUMER],
94
+ stateSecret: STATE_SECRET,
95
+ publicFrontendUrl: FRONTEND,
96
+ });
97
+ const app = express();
98
+ app.use(
99
+ createGitHubFacadeRoutes({ facade, keys: shared.keys, repo: shared.repo, owner: 'git', repoName: 'marketplace', publicUrl: PUBLIC }),
100
+ );
101
+ // The consent routes as the SPA reaches them: behind a session. The
102
+ // session here is a header naming the person.
103
+ const session: express.RequestHandler = (req, _res, next) => {
104
+ const who = req.header('x-test-user');
105
+ if (who && users[who]) {
106
+ req.userId = users[who].id;
107
+ req.userEmail = users[who].email;
108
+ }
109
+ next();
110
+ };
111
+ const provider = {
112
+ clientsStore: { getClient: async () => undefined },
113
+ issueAuthCode: async () => {
114
+ throw new Error('the SDK code path must not be reached for a Claude link');
115
+ },
116
+ } as unknown as BevelOAuthProvider;
117
+ app.use(
118
+ '/api',
119
+ session,
120
+ express.json(),
121
+ createOAuthConsentRoutes({
122
+ provider,
123
+ stateSecret: STATE_SECRET,
124
+ facade: {
125
+ isFacadeRequest: (st) => facade.isFacadeRequest(st),
126
+ clientNameFor: (st) => facade.clientNameFor(st),
127
+ completeConsent: (userId, st) => facade.completeConsent(userId, st),
128
+ },
129
+ }),
130
+ createGitHubFacadeAdminRoutes({
131
+ credentials,
132
+ isAdmin: async (email) => shared.admins.has(email ?? ''),
133
+ publicUrl: PUBLIC,
134
+ marketplaceUrl: 'https://kb.acme.com/git/marketplace.git',
135
+ }),
136
+ );
137
+ const server = await new Promise<http.Server>((resolve) => {
138
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
139
+ });
140
+ const { port } = server.address() as { port: number };
141
+ return { credentials, base: `http://127.0.0.1:${port}`, close: () => new Promise<void>((r) => server.close(() => r())) };
142
+ }
143
+
144
+ describe('the GitHub facade', () => {
145
+ let root: string;
146
+ let shared: Parameters<typeof replica>[0];
147
+ let a: Awaited<ReturnType<typeof replica>>;
148
+ let b: Awaited<ReturnType<typeof replica>>;
149
+
150
+ beforeEach(async () => {
151
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-bridge-'));
152
+ const trees: Record<string, Record<string, string>> = {
153
+ 'alice@x.io': { 'README.md': 'alice\n', '.claude-plugin/marketplace.json': '{"name":"hexis"}\n', 'plugins/gtm/skills/deploy/SKILL.md': 'ship\n' },
154
+ 'bob@x.io': { 'README.md': 'bob\n', '.claude-plugin/marketplace.json': '{"name":"hexis"}\n' },
155
+ };
156
+ const compiler: MarketplaceCompiler = {
157
+ sourceCommit: async () => 'aaa111',
158
+ compileFor: async ({ userEmail }) => tree(trees[userEmail] ?? {}, 'aaa111'),
159
+ };
160
+ shared = {
161
+ credentials: new MemoryGitHubFacadeCredentialsStore(),
162
+ codes: new MemoryGitHubFacadeCodeStore(),
163
+ keys: makeKeys(),
164
+ repo: new MarketplaceRepoService(path.join(root, 'marketplace.git'), compiler),
165
+ admins: new Set(['alice@x.io']),
166
+ };
167
+ a = await replica(shared);
168
+ b = await replica(shared);
169
+ });
170
+ afterEach(async () => {
171
+ await a.close();
172
+ await b.close();
173
+ await fs.rm(root, { recursive: true, force: true });
174
+ });
175
+
176
+ /** The browser's half: authorize on one replica, approve, and come back with a code. */
177
+ async function approve(base: string, who: string, state = 'claude-state'): Promise<string> {
178
+ const creds = await a.credentials.ensure();
179
+ const authorize = await fetch(
180
+ `${base}/login/oauth/authorize?client_id=${encodeURIComponent(creds.clientId)}&redirect_uri=${encodeURIComponent(CALLBACK)}&state=${state}`,
181
+ { redirect: 'manual' },
182
+ );
183
+ expect(authorize.status).toBe(302);
184
+ const location = new URL(authorize.headers.get('location')!);
185
+ expect(location.origin + location.pathname).toBe(`${FRONTEND}/connect`);
186
+ const signed = location.searchParams.get('oauth')!;
187
+
188
+ const request = await fetch(`${base}/api/mcp/oauth/request?state=${encodeURIComponent(signed)}`, {
189
+ headers: { 'x-test-user': who },
190
+ });
191
+ expect(await request.json()).toEqual({ clientName: 'Claude', scope: null, resource: null });
192
+
193
+ const complete = await fetch(`${base}/api/mcp/oauth/complete`, {
194
+ method: 'POST',
195
+ headers: { 'content-type': 'application/json', 'x-test-user': who },
196
+ body: JSON.stringify({ state: signed }),
197
+ });
198
+ expect(complete.status).toBe(200);
199
+ const redirectTo = new URL(((await complete.json()) as { redirectTo: string }).redirectTo);
200
+ expect(redirectTo.origin + redirectTo.pathname).toBe(CALLBACK);
201
+ expect(redirectTo.searchParams.get('state')).toBe(state);
202
+ return redirectTo.searchParams.get('code')!;
203
+ }
204
+
205
+ /** Anthropic's half: the exchange, on whichever replica the load balancer picked. */
206
+ async function exchange(base: string, body: Record<string, string>, accept = 'application/vnd.github+json') {
207
+ return fetch(`${base}/login/oauth/access_token`, {
208
+ method: 'POST',
209
+ headers: { 'content-type': 'application/json', accept },
210
+ body: JSON.stringify(body),
211
+ });
212
+ }
213
+
214
+ async function connect(who: string): Promise<string> {
215
+ const creds = await a.credentials.ensure();
216
+ const code = await approve(a.base, who);
217
+ // The exchange lands on the OTHER replica.
218
+ const token = await exchange(b.base, { client_id: creds.clientId, client_secret: creds.clientSecret, code });
219
+ expect(token.status).toBe(200);
220
+ const body = (await token.json()) as { access_token: string; token_type: string; scope: string };
221
+ expect(body.token_type).toBe('bearer');
222
+ expect(body.access_token.startsWith(GITHUB_LINK_KEY_PREFIX)).toBe(true);
223
+ return body.access_token;
224
+ }
225
+
226
+ const api = (base: string, token: string, p: string) =>
227
+ fetch(`${base}/api/v3${p}`, {
228
+ headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28' },
229
+ });
230
+
231
+ it('connects a person across replicas and mints them a Claude-link key', async () => {
232
+ const token = await connect('alice');
233
+ const minted = shared.keys.byToken.get(token)!;
234
+ expect(minted.user.id).toBe('user-alice');
235
+ expect(minted.kind).toBe(GITHUB_LINK_KEY_KIND);
236
+ expect(minted.label).toBe(CLAUDE_CONSUMER.keyLabel);
237
+ });
238
+
239
+ it('serves the repository, the head commit and the zipball of that person’s own tree — from the origin, never the configured userinfo', async () => {
240
+ const token = await connect('alice');
241
+
242
+ const repoRes = await api(b.base, token, '/repos/git/marketplace');
243
+ expect(repoRes.status).toBe(200);
244
+ const repoBody = (await repoRes.json()) as { full_name: string; private: boolean; default_branch: string; clone_url: string; url: string };
245
+ expect(repoBody).toMatchObject({ full_name: 'git/marketplace', private: true, default_branch: 'main' });
246
+ expect(repoBody.clone_url).toBe('https://kb.acme.com/git/marketplace.git');
247
+ expect(JSON.stringify(repoBody)).not.toContain('hunter2');
248
+ expect(JSON.stringify(repoBody)).not.toContain('/some/path');
249
+
250
+ const commits = await api(a.base, token, '/repos/git/marketplace/commits?per_page=1');
251
+ expect(commits.status).toBe(200);
252
+ const [head] = (await commits.json()) as { sha: string; commit: { message: string }; url: string }[];
253
+ expect(head.sha).toMatch(/^[0-9a-f]{40}$/);
254
+ expect(head.commit.message).toContain('aaa111');
255
+ expect(head.url).toContain('https://kb.acme.com/api/v3/');
256
+
257
+ const zip = await api(b.base, token, `/repos/git/marketplace/zipball/${head.sha}`);
258
+ expect(zip.status).toBe(200);
259
+ expect(zip.headers.get('content-type')).toBe('application/zip');
260
+ const bytes = Buffer.from(await zip.arrayBuffer());
261
+ expect(bytes.subarray(0, 2).toString()).toBe('PK');
262
+ const names = bytes.toString('latin1');
263
+ const prefix = `git-marketplace-${head.sha.slice(0, 7)}/`;
264
+ expect(names).toContain(`${prefix}README.md`);
265
+ expect(names).toContain(`${prefix}plugins/gtm/skills/deploy/SKILL.md`);
266
+ });
267
+
268
+ it('keeps every person to their own tree: a foreign sha and an unknown sha are not found', async () => {
269
+ const alice = await connect('alice');
270
+ const bob = await connect('bob');
271
+ const aliceHead = ((await (await api(a.base, alice, '/repos/git/marketplace/commits?per_page=1')).json()) as { sha: string }[])[0].sha;
272
+ const bobHead = ((await (await api(a.base, bob, '/repos/git/marketplace/commits?per_page=1')).json()) as { sha: string }[])[0].sha;
273
+ expect(aliceHead).not.toBe(bobHead);
274
+ expect((await api(a.base, bob, `/repos/git/marketplace/zipball/${aliceHead}`)).status).toBe(404);
275
+ expect((await api(a.base, bob, `/repos/git/marketplace/zipball/${'0'.repeat(40)}`)).status).toBe(404);
276
+ const bobZip = Buffer.from(await (await api(a.base, bob, `/repos/git/marketplace/zipball/${bobHead}`)).arrayBuffer()).toString('latin1');
277
+ expect(bobZip).not.toContain('plugins/gtm');
278
+ });
279
+
280
+ it('spends a code exactly once across replicas, and only after every other check passed', async () => {
281
+ const creds = await a.credentials.ensure();
282
+ const code = await approve(a.base, 'alice');
283
+ const good = { client_id: creds.clientId, client_secret: creds.clientSecret, code };
284
+
285
+ // Wrong about something else: the code survives to be retried.
286
+ const badRedirect = await exchange(b.base, { ...good, redirect_uri: 'https://claude.ai/elsewhere' });
287
+ expect(badRedirect.status).toBe(400);
288
+ expect(((await badRedirect.json()) as { error: string }).error).toBe('redirect_uri_mismatch');
289
+ const badSecret = await exchange(b.base, { ...good, client_secret: 'not-it' });
290
+ expect(badSecret.status).toBe(401);
291
+
292
+ // Then the first correct exchange wins, on either replica, and the code is spent for both.
293
+ expect((await exchange(b.base, good)).status).toBe(200);
294
+ const replayHere = await exchange(a.base, good);
295
+ expect(replayHere.status).toBe(400);
296
+ expect(((await replayHere.json()) as { error: string }).error).toBe('bad_verification_code');
297
+ expect((await exchange(b.base, good)).status).toBe(400);
298
+ });
299
+
300
+ it('keeps at most one live code per person: mashing Finish never grows the store, even concurrently', async () => {
301
+ for (let i = 0; i < 5; i++) await approve(a.base, 'alice', `s${i}`);
302
+ await approve(b.base, 'bob');
303
+ expect(shared.codes.size).toBe(2);
304
+ // Finishes racing each other on two replicas still leave ONE row, and
305
+ // only the code that landed last is live.
306
+ const codes = await Promise.all([approve(a.base, 'alice', 'x'), approve(b.base, 'alice', 'y'), approve(a.base, 'alice', 'z')]);
307
+ expect(shared.codes.size).toBe(2);
308
+ const creds = await a.credentials.ensure();
309
+ const results = await Promise.all(
310
+ codes.map((code) => exchange(b.base, { client_id: creds.clientId, client_secret: creds.clientSecret, code })),
311
+ );
312
+ expect(results.filter((r) => r.status === 200)).toHaveLength(1);
313
+ // Nor does a rotated client id strand a row under the old one: the
314
+ // person's row is the person's, whatever client it was last issued for.
315
+ await a.credentials.rotate();
316
+ await approve(a.base, 'alice', 'after-rotation');
317
+ expect(shared.codes.size).toBe(2);
318
+ });
319
+
320
+ it('two exchanges of one code at the same instant mint exactly one token', async () => {
321
+ const creds = await a.credentials.ensure();
322
+ const code = await approve(a.base, 'alice');
323
+ const body = { client_id: creds.clientId, client_secret: creds.clientSecret, code };
324
+ const before = shared.keys.byToken.size;
325
+ const results = await Promise.all([exchange(a.base, body), exchange(b.base, body), exchange(a.base, body)]);
326
+ expect(results.map((r) => r.status).sort()).toEqual([200, 400, 400]);
327
+ expect(shared.keys.byToken.size).toBe(before + 1);
328
+ });
329
+
330
+ it('a rotation returns what is stored, so two admins rotating at once are shown the same set', async () => {
331
+ const [x, y] = await Promise.all([a.credentials.rotate(), b.credentials.rotate()]);
332
+ const stored = await shared.credentials.load();
333
+ expect(x.clientId).toBe(stored!.clientId);
334
+ expect(y.clientId).toBe(stored!.clientId);
335
+ });
336
+
337
+ it('answers the token endpoint in the encoding the client negotiated, failures included', async () => {
338
+ const creds = await a.credentials.ensure();
339
+ const form = 'application/x-www-form-urlencoded';
340
+ const failed = await exchange(a.base, { client_id: creds.clientId, client_secret: creds.clientSecret, code: 'nope' }, form);
341
+ expect(failed.status).toBe(400);
342
+ expect(failed.headers.get('content-type')).toContain(form);
343
+ expect(new URLSearchParams(await failed.text()).get('error')).toBe('bad_verification_code');
344
+ const code = await approve(a.base, 'alice');
345
+ const ok = await exchange(b.base, { client_id: creds.clientId, client_secret: creds.clientSecret, code }, form);
346
+ expect(ok.headers.get('content-type')).toContain(form);
347
+ expect(new URLSearchParams(await ok.text()).get('token_type')).toBe('bearer');
348
+ });
349
+
350
+ it('refuses what it should: wrong redirect host, unknown client, no token, other repos', async () => {
351
+ const creds = await a.credentials.ensure();
352
+ const elsewhere = await fetch(
353
+ `${a.base}/login/oauth/authorize?client_id=${creds.clientId}&redirect_uri=${encodeURIComponent('https://evil.example/cb')}`,
354
+ { redirect: 'manual' },
355
+ );
356
+ expect(elsewhere.status).toBe(400);
357
+ const wrongClient = await fetch(
358
+ `${a.base}/login/oauth/authorize?client_id=Iv1.nope&redirect_uri=${encodeURIComponent(CALLBACK)}`,
359
+ { redirect: 'manual' },
360
+ );
361
+ expect(wrongClient.status).toBe(400);
362
+
363
+ const token = await connect('alice');
364
+ const noToken = await fetch(`${a.base}/api/v3/repos/git/marketplace`);
365
+ expect(noToken.status).toBe(401);
366
+ expect(noToken.headers.get('www-authenticate')).toContain('Bearer');
367
+ expect((await api(a.base, 'gho_unknown', '/repos/git/marketplace')).status).toBe(401);
368
+ expect((await api(a.base, token, '/repos/someone/else')).status).toBe(404);
369
+ expect((await api(a.base, token, '/user')).status).toBe(404);
370
+ expect((await api(a.base, token, '/repos/git/marketplace/zipball/main..HEAD')).status).toBe(404);
371
+ });
372
+
373
+ it('a consent finish that is not a Claude link still goes to the SDK, and a Claude link without a bridge is a client error', async () => {
374
+ const { signAuthRequest } = await import('../../mcp/oauth/oauth-state.js');
375
+ const mcpState = signAuthRequest(STATE_SECRET, { c: 'mcp-client', r: 'http://localhost/cb', cc: 'challenge' });
376
+ const complete = await fetch(`${a.base}/api/mcp/oauth/complete`, {
377
+ method: 'POST',
378
+ headers: { 'content-type': 'application/json', 'x-test-user': 'alice' },
379
+ body: JSON.stringify({ state: mcpState }),
380
+ });
381
+ expect(complete.status).toBe(500); // the stub SDK path throws — proving it was the one asked
382
+
383
+ // A deployment whose consent routes were built without the bridge.
384
+ const bare = express();
385
+ bare.use('/api', (req, _res, next) => { req.userId = 'user-alice'; next(); }, express.json(), createOAuthConsentRoutes({
386
+ provider: { clientsStore: { getClient: async () => undefined } } as unknown as BevelOAuthProvider,
387
+ stateSecret: STATE_SECRET,
388
+ }));
389
+ const server = await new Promise<http.Server>((resolve) => { const s = bare.listen(0, '127.0.0.1', () => resolve(s)); });
390
+ const { port } = server.address() as { port: number };
391
+ const ghState = signAuthRequest(STATE_SECRET, { c: 'Iv1.x', r: CALLBACK, gh: true });
392
+ try {
393
+ const request = await fetch(`http://127.0.0.1:${port}/api/mcp/oauth/request?state=${encodeURIComponent(ghState)}`);
394
+ expect(request.status).toBe(400);
395
+ const finish = await fetch(`http://127.0.0.1:${port}/api/mcp/oauth/complete`, {
396
+ method: 'POST',
397
+ headers: { 'content-type': 'application/json' },
398
+ body: JSON.stringify({ state: ghState }),
399
+ });
400
+ expect(finish.status).toBe(400);
401
+ } finally {
402
+ await new Promise<void>((r) => server.close(() => r()));
403
+ }
404
+ });
405
+
406
+ it('hands an admin the registration fields uncached, and a rotation on one replica is what the other checks', async () => {
407
+ const forbidden = await fetch(`${a.base}/api/admin/github-facade`, { headers: { 'x-test-user': 'bob' } });
408
+ expect(forbidden.status).toBe(403);
409
+ const shown = await fetch(`${a.base}/api/admin/github-facade`, { headers: { 'x-test-user': 'alice' } });
410
+ expect(shown.status).toBe(200);
411
+ expect(shown.headers.get('cache-control')).toBe('no-store');
412
+ const before = (await shown.json()) as Record<string, string>;
413
+ expect(before.host).toBe('kb.acme.com');
414
+ expect(before.marketplaceUrl).toBe('https://kb.acme.com/git/marketplace.git');
415
+ expect(before.clientId).toMatch(/^Iv1\.[0-9a-f]{16}$/);
416
+ expect(before.privateKeyPem).toContain('BEGIN RSA PRIVATE KEY');
417
+ expect(before.appId).toMatch(/^\d{6}$/);
418
+
419
+ // Rotate on replica B…
420
+ const rotated = await fetch(`${b.base}/api/admin/github-facade/rotate`, { method: 'POST', headers: { 'x-test-user': 'alice' } });
421
+ expect(rotated.headers.get('cache-control')).toBe('no-store');
422
+ const after = (await rotated.json()) as Record<string, string>;
423
+ for (const field of ['appId', 'clientId', 'clientSecret', 'webhookSecret', 'privateKeyPem']) {
424
+ expect(after[field]).not.toBe(before[field]);
425
+ }
426
+ // …and replica A, which served the old set a moment ago, now refuses it and accepts the new.
427
+ const stale = await fetch(
428
+ `${a.base}/login/oauth/authorize?client_id=${before.clientId}&redirect_uri=${encodeURIComponent(CALLBACK)}`,
429
+ { redirect: 'manual' },
430
+ );
431
+ expect(stale.status).toBe(400);
432
+ const fresh = await fetch(
433
+ `${a.base}/login/oauth/authorize?client_id=${after.clientId}&redirect_uri=${encodeURIComponent(CALLBACK)}`,
434
+ { redirect: 'manual' },
435
+ );
436
+ expect(fresh.status).toBe(302);
437
+ });
438
+
439
+ it('initialises once even when two replicas race for the first credentials', async () => {
440
+ const [x, y] = await Promise.all([a.credentials.ensure(), b.credentials.ensure()]);
441
+ expect(x.clientId).toBe(y.clientId);
442
+ });
443
+ });
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import http from 'node:http';
6
+ import { execFile } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import express from 'express';
9
+ import type { AuthUser } from '@bevel-software/platform-shared';
10
+
11
+ import { MarketplaceRepoService, type MarketplaceCompiler } from '../marketplace-repo.service.js';
12
+ import { createMarketplaceGitRoutes } from '../git-http.routes.js';
13
+ import type { VirtualTree } from '../../plugins/compile/compile-marketplace.js';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+ // No credential helper and no prompt: a 401 must FAIL the command, not open
17
+ // the OS credential manager (which would hang the test on Windows). No
18
+ // autocrlf either, so the bytes the server sent are the bytes we read back.
19
+ const git = (args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) =>
20
+ execFileAsync('git', ['-c', 'credential.helper=', '-c', 'core.autocrlf=false', ...args], {
21
+ cwd: opts.cwd,
22
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', ...opts.env },
23
+ });
24
+
25
+ /**
26
+ * The marketplace as a git remote, driven the way a client drives it: real
27
+ * `git clone` and `git pull` over HTTP against the express router, with a
28
+ * stub compiler standing in for the knowledge base. What is under test is
29
+ * the namespace isolation, the fast-forward contract, the auth, and the
30
+ * read-only guarantee — not the compiler.
31
+ */
32
+
33
+ const users: Record<string, AuthUser> = {
34
+ 'bevel_alice': { id: 'user-alice', email: 'alice@x.io', name: 'Alice' } as AuthUser,
35
+ 'bevel_bob': { id: 'user-bob', email: 'bob@x.io', name: 'Bob' } as AuthUser,
36
+ };
37
+
38
+ function tree(files: Record<string, string>, sourceCommit: string): VirtualTree & { sourceCommit: string } {
39
+ return {
40
+ files: new Map(Object.entries(files).map(([k, v]) => [k, Buffer.from(v)])),
41
+ warnings: [],
42
+ plugins: [],
43
+ sourceCommit,
44
+ };
45
+ }
46
+
47
+ describe('marketplace git endpoint', () => {
48
+ let root: string;
49
+ let repo: MarketplaceRepoService;
50
+ let server: http.Server;
51
+ let base: string;
52
+ /** What the "knowledge base" currently is, per user. */
53
+ let source = 'aaa111';
54
+ let trees: Record<string, Record<string, string>>;
55
+
56
+ beforeEach(async () => {
57
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-mp-'));
58
+ source = 'aaa111';
59
+ trees = {
60
+ 'alice@x.io': { 'README.md': 'alice v1\n', 'plugins/gtm/skills/deploy/SKILL.md': '---\n---\nship\n' },
61
+ 'bob@x.io': { 'README.md': 'bob v1\n' },
62
+ };
63
+ const compiler: MarketplaceCompiler = {
64
+ sourceCommit: async () => source,
65
+ compileFor: async ({ userEmail }) => tree(trees[userEmail] ?? {}, source),
66
+ };
67
+ repo = new MarketplaceRepoService(path.join(root, 'data', 'marketplace.git'), compiler);
68
+ const app = express();
69
+ app.use(
70
+ '/git',
71
+ createMarketplaceGitRoutes({
72
+ repo,
73
+ keys: {
74
+ looksLikeExternalApiKey: (t) => t.startsWith('bevel_'),
75
+ verifyAndLoadToken: async (t) => (users[t] ? { tokenId: `tok-${t}`, user: users[t] } : null),
76
+ },
77
+ mountPath: '/git',
78
+ }),
79
+ );
80
+ server = await new Promise<http.Server>((resolve) => {
81
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
82
+ });
83
+ const { port } = server.address() as { port: number };
84
+ base = `http://127.0.0.1:${port}/git/marketplace.git`;
85
+ });
86
+ afterEach(async () => {
87
+ await new Promise<void>((resolve) => server.close(() => resolve()));
88
+ await fs.rm(root, { recursive: true, force: true });
89
+ });
90
+
91
+ const withKey = (key: string) => base.replace('http://', `http://key:${key}@`);
92
+ const clone = async (key: string, dir: string) => git(['clone', '--quiet', withKey(key), dir]);
93
+ const read = (dir: string, rel: string) => fs.readFile(path.join(dir, rel), 'utf-8');
94
+
95
+ it('clones each caller their own tree, and nothing of anyone else\'s', async () => {
96
+ const a = path.join(root, 'alice');
97
+ const b = path.join(root, 'bob');
98
+ await clone('bevel_alice', a);
99
+ await clone('bevel_bob', b);
100
+ expect(await read(a, 'README.md')).toBe('alice v1\n');
101
+ expect(await read(a, 'plugins/gtm/skills/deploy/SKILL.md')).toContain('ship');
102
+ expect(await read(b, 'README.md')).toBe('bob v1\n');
103
+ await expect(read(b, 'plugins/gtm/skills/deploy/SKILL.md')).rejects.toThrow();
104
+ // One object store underneath: both namespaces live in the same bare repo.
105
+ const { stdout } = await git(['-C', repo.repoDir, 'for-each-ref', '--format=%(refname)', 'refs/namespaces/']);
106
+ // Each namespace carries a HEAD symref beside its branch; the branches are the point.
107
+ expect(stdout.split('\n').filter((r) => r.endsWith('/refs/heads/main')).sort()).toEqual([
108
+ 'refs/namespaces/u-user-alice/refs/heads/main',
109
+ 'refs/namespaces/u-user-bob/refs/heads/main',
110
+ ]);
111
+ });
112
+
113
+ it('pulls fast-forward after the knowledge base moves, and does not recompile when it has not', async () => {
114
+ const a = path.join(root, 'alice');
115
+ await clone('bevel_alice', a);
116
+ const first = (await git(['-C', a, 'rev-parse', 'HEAD'])).stdout.trim();
117
+
118
+ // Same source: a pull is a no-op with the same head.
119
+ expect((await repo.ensureCompiled({ id: 'user-alice', email: 'alice@x.io' })).compiled).toBe(false);
120
+ await git(['-C', a, 'pull', '--quiet', '--ff-only']);
121
+ expect((await git(['-C', a, 'rev-parse', 'HEAD'])).stdout.trim()).toBe(first);
122
+
123
+ // The KB moves and access is withdrawn from the skill: one new commit,
124
+ // the file is gone, and the pull is still a fast-forward.
125
+ source = 'bbb222';
126
+ trees['alice@x.io'] = { 'README.md': 'alice v2\n' };
127
+ await git(['-C', a, 'pull', '--quiet', '--ff-only']);
128
+ expect(await read(a, 'README.md')).toBe('alice v2\n');
129
+ await expect(read(a, 'plugins/gtm/skills/deploy/SKILL.md')).rejects.toThrow();
130
+ const log = (await git(['-C', a, 'log', '--format=%s'])).stdout.trim().split('\n');
131
+ expect(log).toEqual(['Compile marketplace from bbb222', 'Compile marketplace from aaa111']);
132
+ });
133
+
134
+ it('restores a namespace HEAD lost between the branch update and the symref — on the unchanged path', async () => {
135
+ await repo.ensureCompiled({ id: 'user-alice', email: 'alice@x.io' });
136
+ const head = 'refs/namespaces/u-user-alice/HEAD';
137
+ await git(['-C', repo.repoDir, 'symbolic-ref', '--delete', head]);
138
+ await expect(git(['-C', repo.repoDir, 'symbolic-ref', head])).rejects.toThrow();
139
+ // Same source, same tree: nothing to compile — and still a served namespace.
140
+ expect((await repo.ensureCompiled({ id: 'user-alice', email: 'alice@x.io' })).compiled).toBe(false);
141
+ expect((await git(['-C', repo.repoDir, 'symbolic-ref', head])).stdout.trim()).toBe(
142
+ 'refs/namespaces/u-user-alice/refs/heads/main',
143
+ );
144
+ const a = path.join(root, 'alice-again');
145
+ await clone('bevel_alice', a);
146
+ expect(await read(a, 'README.md')).toBe('alice v1\n');
147
+ });
148
+
149
+ it('a moved source with an identical tree writes no new commit', async () => {
150
+ const a = path.join(root, 'alice');
151
+ await clone('bevel_alice', a);
152
+ source = 'ccc333';
153
+ const r = await repo.ensureCompiled({ id: 'user-alice', email: 'alice@x.io' });
154
+ expect(r.compiled).toBe(false);
155
+ await git(['-C', a, 'pull', '--quiet', '--ff-only']);
156
+ expect((await git(['-C', a, 'log', '--format=%s'])).stdout.trim().split('\n')).toHaveLength(1);
157
+ });
158
+
159
+ it('refuses a missing or unknown key with a Basic challenge, and refuses pushes', { timeout: 60_000 }, async () => {
160
+ await expect(git(['clone', '--quiet', base, path.join(root, 'anon')])).rejects.toThrow();
161
+ await expect(git(['clone', '--quiet', withKey('bevel_nobody'), path.join(root, 'bad')])).rejects.toThrow();
162
+ const res = await fetch(`${base}/info/refs?service=git-upload-pack`);
163
+ expect(res.status).toBe(401);
164
+ expect(res.headers.get('www-authenticate')).toContain('Basic');
165
+
166
+ const a = path.join(root, 'alice');
167
+ await clone('bevel_alice', a);
168
+ await fs.writeFile(path.join(a, 'evil.md'), 'no');
169
+ await git(['-C', a, 'add', 'evil.md']);
170
+ await git(['-C', a, 'commit', '--quiet', '-m', 'push attempt'], {
171
+ env: { GIT_AUTHOR_NAME: 'x', GIT_AUTHOR_EMAIL: 'x@x', GIT_COMMITTER_NAME: 'x', GIT_COMMITTER_EMAIL: 'x@x' },
172
+ });
173
+ await expect(git(['-C', a, 'push', '--quiet', 'origin', 'HEAD:main'])).rejects.toThrow();
174
+ const push = await fetch(`${base}/info/refs?service=git-receive-pack`, {
175
+ headers: { authorization: `Basic ${Buffer.from('key:bevel_alice').toString('base64')}` },
176
+ });
177
+ expect(push.status).toBe(403);
178
+ });
179
+ });