@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,193 @@
1
+ import express from 'express';
2
+ import { spawn } from 'node:child_process';
3
+ import type { AuthUser } from '@bevel-software/platform-shared';
4
+ import type { MarketplaceRepoService } from './marketplace-repo.service.js';
5
+ import '../tool-auth/external-api-key.interface.js'; // Express Request augmentation (req.externalApiKeyId)
6
+
7
+ /** How a bearer string resolves to a user — the connection-key service's shape. */
8
+ export interface MarketplaceKeyResolver {
9
+ looksLikeExternalApiKey(token: string): boolean;
10
+ verifyAndLoadToken(plaintext: string): Promise<{ tokenId: string; user: AuthUser } | null>;
11
+ }
12
+
13
+ /**
14
+ * The per-user marketplace as a git remote:
15
+ *
16
+ * git clone https://key:<connection-key>@<host>/git/marketplace.git
17
+ *
18
+ * Anything that speaks git's smart HTTP protocol — Claude Code, Codex, `npx
19
+ * skills`, plain git — clones a repository whose `main` is the caller's
20
+ * compiled tree. The route authenticates, brings the caller's namespace up to
21
+ * date (`MarketplaceRepoService.ensureCompiled`), and hands the request to
22
+ * `git http-backend` with `GIT_NAMESPACE` set, so git itself serves the
23
+ * protocol and only that namespace's refs exist as far as the client can tell.
24
+ *
25
+ * AUTH: HTTP Basic with the connection key as the password (the username is
26
+ * anything — git sends whatever the URL carried), or the key as a Bearer
27
+ * token. The same keys the MCP endpoint takes, the same service resolving
28
+ * them, and the same metering stamp (`req.externalApiKeyId`), so a key's
29
+ * `lastUsedAt` moves when an agent refreshes its plugins. A 401 challenges
30
+ * with Basic so a client without credentials prompts for them.
31
+ *
32
+ * READ-ONLY: the receive-pack service is refused here by name, and the bare
33
+ * repository pins `http.receivepack=false` for good measure — a push would
34
+ * otherwise be enabled by http-backend the moment REMOTE_USER is set.
35
+ */
36
+ export function createMarketplaceGitRoutes(deps: {
37
+ repo: MarketplaceRepoService;
38
+ keys: MarketplaceKeyResolver;
39
+ /** The URL path the router is mounted at, e.g. `/git`. Used for PATH_INFO. */
40
+ mountPath: string;
41
+ }): express.Router {
42
+ const { repo, keys, mountPath } = deps;
43
+ const router = express.Router();
44
+ const repoRoute = `/${repo.repoName}`;
45
+
46
+ const challenge = (res: express.Response, message: string) => {
47
+ res.setHeader('WWW-Authenticate', 'Basic realm="hexis-marketplace"');
48
+ res.status(401).type('text/plain').send(message);
49
+ };
50
+
51
+ /** The connection key out of Basic (`user:key`, `key:` or `:key`) or Bearer. */
52
+ const keyOf = (header: string | undefined): string | null => {
53
+ if (!header) return null;
54
+ const space = header.indexOf(' ');
55
+ if (space < 0) return null;
56
+ const scheme = header.slice(0, space).toLowerCase();
57
+ const value = header.slice(space + 1).trim();
58
+ if (scheme === 'bearer') return value || null;
59
+ if (scheme !== 'basic') return null;
60
+ let decoded: string;
61
+ try {
62
+ decoded = Buffer.from(value, 'base64').toString('utf-8');
63
+ } catch {
64
+ return null;
65
+ }
66
+ const colon = decoded.indexOf(':');
67
+ const user = colon < 0 ? decoded : decoded.slice(0, colon);
68
+ const pass = colon < 0 ? '' : decoded.slice(colon + 1);
69
+ // Either half may carry the key: `https://key:<k>@` puts it in the
70
+ // password, `https://<k>@` in the username.
71
+ if (pass && keys.looksLikeExternalApiKey(pass)) return pass;
72
+ if (user && keys.looksLikeExternalApiKey(user)) return user;
73
+ return pass || user || null;
74
+ };
75
+
76
+ router.all(`${repoRoute}{/*rest}`, async (req, res) => {
77
+ const service = String(req.query.service ?? '');
78
+ const rest = `/${([] as string[]).concat((req.params as { rest?: string | string[] }).rest ?? []).join('/')}`;
79
+ // Refuse writes before touching anything — by service name on info/refs,
80
+ // and by endpoint on the POST.
81
+ if (service === 'git-receive-pack' || rest === '/git-receive-pack') {
82
+ res.status(403).type('text/plain').send('This repository is read-only.');
83
+ return;
84
+ }
85
+
86
+ const key = keyOf(req.headers.authorization);
87
+ if (!key || !keys.looksLikeExternalApiKey(key)) {
88
+ challenge(res, 'A connection key is required: https://key:<connection-key>@<host>/git/marketplace.git');
89
+ return;
90
+ }
91
+ let resolved: { tokenId: string; user: AuthUser } | null;
92
+ try {
93
+ resolved = await keys.verifyAndLoadToken(key);
94
+ } catch (err) {
95
+ console.error('[marketplace] connection-key verification failed:', err);
96
+ res.status(500).type('text/plain').send('Authentication backend unavailable');
97
+ return;
98
+ }
99
+ if (!resolved) {
100
+ challenge(res, 'Invalid or revoked connection key');
101
+ return;
102
+ }
103
+ req.userId = resolved.user.id;
104
+ req.userEmail = resolved.user.email;
105
+ req.externalApiKeyId = resolved.tokenId;
106
+
107
+ let ensured: { namespace: string };
108
+ try {
109
+ ensured = await repo.ensureCompiled({ id: resolved.user.id, email: resolved.user.email });
110
+ } catch (err) {
111
+ console.error('[marketplace] compile failed:', err);
112
+ res.status(500).type('text/plain').send('Could not prepare your marketplace');
113
+ return;
114
+ }
115
+
116
+ // CGI, as git documents it. PATH_INFO is the path below GIT_PROJECT_ROOT
117
+ // — `/marketplace.git/info/refs` — regardless of where we are mounted.
118
+ const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?') + 1) : '';
119
+ const env: NodeJS.ProcessEnv = {
120
+ ...process.env,
121
+ GIT_PROJECT_ROOT: repo.projectRoot,
122
+ GIT_HTTP_EXPORT_ALL: '1',
123
+ GIT_NAMESPACE: ensured.namespace,
124
+ PATH_INFO: `${repoRoute}${rest}`,
125
+ REQUEST_METHOD: req.method,
126
+ QUERY_STRING: query,
127
+ CONTENT_TYPE: req.headers['content-type'] ?? '',
128
+ REMOTE_USER: resolved.user.email,
129
+ REMOTE_ADDR: req.ip ?? '',
130
+ SERVER_PROTOCOL: 'HTTP/1.1',
131
+ HTTP_HOST: req.headers.host ?? '',
132
+ SCRIPT_NAME: mountPath,
133
+ };
134
+ if (req.headers['content-length']) env.CONTENT_LENGTH = String(req.headers['content-length']);
135
+ if (req.headers['content-encoding']) env.HTTP_CONTENT_ENCODING = String(req.headers['content-encoding']);
136
+ if (req.headers['git-protocol']) env.HTTP_GIT_PROTOCOL = String(req.headers['git-protocol']);
137
+
138
+ const child = spawn('git', ['http-backend'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
139
+ child.stderr.on('data', (chunk: Buffer) => console.warn(`[marketplace] http-backend: ${chunk.toString('utf-8').trim()}`));
140
+ child.on('error', (err) => {
141
+ console.error('[marketplace] could not run git http-backend:', err);
142
+ if (!res.headersSent) res.status(500).type('text/plain').send('git is unavailable');
143
+ });
144
+ req.pipe(child.stdin);
145
+ req.on('aborted', () => child.kill());
146
+
147
+ // Parse the CGI header block (`Status:`, `Content-Type:`, …) off stdout,
148
+ // then stream the body straight through.
149
+ let headerBuf = Buffer.alloc(0);
150
+ let headersDone = false;
151
+ // Backpressure: a slow client must not make Node buffer a whole pack. One
152
+ // path for every body write, the remainder of the header chunk included.
153
+ const writeBody = (bytes: Buffer) => {
154
+ if (!res.write(bytes)) {
155
+ child.stdout.pause();
156
+ res.once('drain', () => child.stdout.resume());
157
+ }
158
+ };
159
+ child.stdout.on('data', (chunk: Buffer) => {
160
+ if (headersDone) {
161
+ writeBody(chunk);
162
+ return;
163
+ }
164
+ headerBuf = Buffer.concat([headerBuf, chunk]);
165
+ const cut = headerBuf.indexOf('\r\n\r\n') >= 0 ? headerBuf.indexOf('\r\n\r\n') : headerBuf.indexOf('\n\n');
166
+ if (cut < 0) return;
167
+ const sepLen = headerBuf.indexOf('\r\n\r\n') >= 0 ? 4 : 2;
168
+ const head = headerBuf.subarray(0, cut).toString('utf-8');
169
+ let status = 200;
170
+ for (const line of head.split(/\r?\n/)) {
171
+ const colon = line.indexOf(':');
172
+ if (colon < 0) continue;
173
+ const name = line.slice(0, colon).trim();
174
+ const value = line.slice(colon + 1).trim();
175
+ if (name.toLowerCase() === 'status') status = Number.parseInt(value, 10) || 200;
176
+ else res.setHeader(name, value);
177
+ }
178
+ res.status(status);
179
+ headersDone = true;
180
+ const body = headerBuf.subarray(cut + sepLen);
181
+ if (body.length > 0) writeBody(body);
182
+ });
183
+ child.stdout.on('end', () => {
184
+ if (!headersDone) {
185
+ if (!res.headersSent) res.status(502).type('text/plain').send('git http-backend produced no response');
186
+ return;
187
+ }
188
+ res.end();
189
+ });
190
+ });
191
+
192
+ return router;
193
+ }
@@ -0,0 +1,78 @@
1
+ import express from 'express';
2
+ import '../../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
3
+ import {
4
+ GitHubFacadeUnavailableError,
5
+ type GitHubFacadeCredentialsService,
6
+ } from './github-facade-credentials.service.js';
7
+
8
+ export interface GitHubFacadeAdminRoutesDeps {
9
+ credentials: GitHubFacadeCredentialsService;
10
+ isAdmin(email: string | undefined): Promise<boolean>;
11
+ /** The deployment's public address; only its host is shown. */
12
+ publicUrl: string;
13
+ /** The marketplace URL people paste — the same one Claude Code clones. */
14
+ marketplaceUrl: string;
15
+ }
16
+
17
+ /**
18
+ * What an Owner needs to register this deployment in Claude's admin settings
19
+ * as a GitHub Enterprise Server, in the fields that form has — and the one
20
+ * verb on it, rotate. Admins only: the client secret is what makes claude.ai
21
+ * trust the token exchange. Never cached: after a rotation or a sign-out no
22
+ * copy of the old secrets may linger in a browser or a proxy.
23
+ *
24
+ * GET /api/admin/github-facade the credentials and the two URLs
25
+ * POST /api/admin/github-facade/rotate new credentials, all of them
26
+ */
27
+ export function createGitHubFacadeAdminRoutes(deps: GitHubFacadeAdminRoutesDeps): express.Router {
28
+ const router = express.Router();
29
+
30
+ const requireAdmin: express.RequestHandler = async (req, res, next) => {
31
+ if (!(await deps.isAdmin(req.userEmail))) {
32
+ res.status(403).json({ error: 'Admins only' });
33
+ return;
34
+ }
35
+ next();
36
+ };
37
+
38
+ const describe = async () => {
39
+ const creds = await deps.credentials.ensure();
40
+ return {
41
+ host: new URL(deps.publicUrl).host,
42
+ appId: creds.appId,
43
+ clientId: creds.clientId,
44
+ clientSecret: creds.clientSecret,
45
+ webhookSecret: creds.webhookSecret,
46
+ privateKeyPem: creds.privateKeyPem,
47
+ marketplaceUrl: deps.marketplaceUrl,
48
+ createdAt: creds.createdAt.getTime(),
49
+ rotatedAt: creds.rotatedAt ? creds.rotatedAt.getTime() : null,
50
+ };
51
+ };
52
+
53
+ const send = async (res: express.Response, body: () => Promise<unknown>) => {
54
+ try {
55
+ const payload = await body();
56
+ res.setHeader('Cache-Control', 'no-store');
57
+ res.json(payload);
58
+ } catch (err) {
59
+ if (err instanceof GitHubFacadeUnavailableError) {
60
+ res.status(409).json({ error: err.message });
61
+ return;
62
+ }
63
+ console.error('[github-facade admin]', err);
64
+ res.status(500).json({ error: 'Internal error' });
65
+ }
66
+ };
67
+
68
+ router.get('/admin/github-facade', requireAdmin, (_req, res) => send(res, describe));
69
+
70
+ router.post('/admin/github-facade/rotate', requireAdmin, (_req, res) =>
71
+ send(res, async () => {
72
+ await deps.credentials.rotate();
73
+ return describe();
74
+ }),
75
+ );
76
+
77
+ return router;
78
+ }
@@ -0,0 +1,145 @@
1
+ import { and, eq, gt, isNull } from 'drizzle-orm';
2
+ import type { Database } from '../../database/connection.js';
3
+ import { githubFacadeCodes } from '../../database/schema.js';
4
+
5
+ /**
6
+ * A one-time code the consent page issued, waiting for Anthropic's backend
7
+ * to exchange it. Looked up by the code's hash; the plaintext travels once,
8
+ * in the redirect.
9
+ */
10
+ export interface PendingFacadeCode {
11
+ codeHash: string;
12
+ userId: string;
13
+ clientId: string;
14
+ redirectUri: string;
15
+ expiresAt: Date;
16
+ }
17
+
18
+ /**
19
+ * Where pending codes live. The database in production, so the consent
20
+ * finish on one replica and the exchange on another see the same row.
21
+ *
22
+ * The rule "one live code per person" is the TABLE'S: the person is its
23
+ * primary key, so issuing is an upsert — the newest code overwrites the last
24
+ * in one statement, two finishes racing each other cannot both leave a row
25
+ * behind, and the table holds at most one row per user, which goes when the
26
+ * user does. That is the whole bound; nothing here ever sweeps, and no
27
+ * rotation of the client id can strand a row, because the client id is data
28
+ * the exchange checks rather than part of the key. Spending is one
29
+ * conditional update: the first exchange wins, on whichever replica, and
30
+ * every later attempt finds the code spent. Nothing in memory takes part in
31
+ * either guarantee.
32
+ */
33
+ export interface GitHubFacadeCodeStore {
34
+ /** Store a fresh code, replacing whatever the same person had. */
35
+ put(code: PendingFacadeCode): Promise<void>;
36
+ /** The live (unspent, unexpired) code with this hash, without spending it. */
37
+ peek(codeHash: string): Promise<PendingFacadeCode | null>;
38
+ /** Spend the code if it is live; null when it was not (unknown, spent, expired). */
39
+ consume(codeHash: string): Promise<PendingFacadeCode | null>;
40
+ }
41
+
42
+ export class DbGitHubFacadeCodeStore implements GitHubFacadeCodeStore {
43
+ constructor(private readonly db: Database) {}
44
+
45
+ async put(code: PendingFacadeCode): Promise<void> {
46
+ const fresh = {
47
+ clientId: code.clientId,
48
+ codeHash: code.codeHash,
49
+ redirectUri: code.redirectUri,
50
+ expiresAt: code.expiresAt,
51
+ consumedAt: null,
52
+ createdAt: new Date(),
53
+ };
54
+ await this.db
55
+ .insert(githubFacadeCodes)
56
+ .values({ userId: code.userId, ...fresh })
57
+ .onConflictDoUpdate({ target: githubFacadeCodes.userId, set: fresh });
58
+ }
59
+
60
+ async peek(codeHash: string): Promise<PendingFacadeCode | null> {
61
+ const [row] = await this.db
62
+ .select()
63
+ .from(githubFacadeCodes)
64
+ .where(live(codeHash))
65
+ .limit(1);
66
+ return row ? toPending(row) : null;
67
+ }
68
+
69
+ async consume(codeHash: string): Promise<PendingFacadeCode | null> {
70
+ const [row] = await this.db
71
+ .update(githubFacadeCodes)
72
+ .set({ consumedAt: new Date() })
73
+ .where(live(codeHash))
74
+ .returning();
75
+ return row ? toPending(row) : null;
76
+ }
77
+ }
78
+
79
+ /** The one definition of "live": this hash, unspent, unexpired. */
80
+ function live(codeHash: string) {
81
+ return and(
82
+ eq(githubFacadeCodes.codeHash, codeHash),
83
+ isNull(githubFacadeCodes.consumedAt),
84
+ gt(githubFacadeCodes.expiresAt, new Date()),
85
+ );
86
+ }
87
+
88
+ function toPending(row: typeof githubFacadeCodes.$inferSelect): PendingFacadeCode {
89
+ return {
90
+ codeHash: row.codeHash,
91
+ userId: row.userId,
92
+ clientId: row.clientId,
93
+ redirectUri: row.redirectUri,
94
+ expiresAt: row.expiresAt,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * For tests: the same rules over a map keyed the way the table is. Share one
100
+ * instance to model shared storage. Check-and-mark is synchronous, as the
101
+ * database's conditional update is atomic: two exchanges of one code cannot
102
+ * both win here either.
103
+ */
104
+ export class MemoryGitHubFacadeCodeStore implements GitHubFacadeCodeStore {
105
+ private readonly rows = new Map<string, PendingFacadeCode & { consumedAt: Date | null }>();
106
+
107
+ async put(code: PendingFacadeCode): Promise<void> {
108
+ this.rows.set(code.userId, { ...code, consumedAt: null });
109
+ }
110
+
111
+ async peek(codeHash: string): Promise<PendingFacadeCode | null> {
112
+ const row = this.findLive(codeHash);
113
+ return row ? strip(row) : null;
114
+ }
115
+
116
+ async consume(codeHash: string): Promise<PendingFacadeCode | null> {
117
+ const row = this.findLive(codeHash);
118
+ if (!row) return null;
119
+ row.consumedAt = new Date();
120
+ return strip(row);
121
+ }
122
+
123
+ /** How many rows the store holds — at most one per person. */
124
+ get size(): number {
125
+ return this.rows.size;
126
+ }
127
+
128
+ private findLive(codeHash: string) {
129
+ const now = Date.now();
130
+ for (const row of this.rows.values()) {
131
+ if (row.codeHash === codeHash && row.consumedAt === null && row.expiresAt.getTime() > now) return row;
132
+ }
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function strip(row: PendingFacadeCode & { consumedAt: Date | null }): PendingFacadeCode {
138
+ return {
139
+ codeHash: row.codeHash,
140
+ userId: row.userId,
141
+ clientId: row.clientId,
142
+ redirectUri: row.redirectUri,
143
+ expiresAt: row.expiresAt,
144
+ };
145
+ }
@@ -0,0 +1,208 @@
1
+ import { generateKeyPairSync, randomBytes, randomInt } from 'node:crypto';
2
+ import { and, eq } from 'drizzle-orm';
3
+ import type { Database } from '../../database/connection.js';
4
+ import { githubFacadeIdentity } from '../../database/schema.js';
5
+ import type { TokenCrypto } from '../../../shared/token-crypto.js';
6
+
7
+ /**
8
+ * What Claude's "Add manually" form for a GitHub Enterprise Server asks an
9
+ * Owner to paste, and what the token exchange later checks: the identity
10
+ * this deployment presents to claude.ai as if it were a GitHub App.
11
+ *
12
+ * Every field is generated HERE, once per deployment — nothing is registered
13
+ * anywhere else, because there is no GitHub: hexis is the host Claude talks
14
+ * to. The private key is handed over because the form requires one; the
15
+ * user-added marketplace flow never signs with it (the observed contract is
16
+ * the OAuth pair plus three REST calls), but a future organization
17
+ * marketplace would, and rotating everything together is simpler than
18
+ * explaining which half matters.
19
+ */
20
+ export interface GitHubFacadeCredentials {
21
+ appId: string;
22
+ clientId: string;
23
+ clientSecret: string;
24
+ webhookSecret: string;
25
+ privateKeyPem: string;
26
+ publicKeyPem: string;
27
+ createdAt: Date;
28
+ rotatedAt: Date | null;
29
+ }
30
+
31
+ /**
32
+ * Where the one credentials row lives — the database in production, memory
33
+ * in tests. THE row, not a copy of it: every replica reads the store on
34
+ * every use, so a rotation on one replica is what every other replica
35
+ * checks the very next exchange. The reads are rare (a connect, an admin
36
+ * page), and a cache would be a second source of truth with no invalidation.
37
+ *
38
+ * Both writes are conditional, so two replicas writing at once cannot
39
+ * disagree about what is stored: creation happens only when no row exists,
40
+ * and a rotation replaces only the set the caller was looking at. Either way
41
+ * the caller gets back what the store holds afterwards, never what it sent.
42
+ */
43
+ export interface GitHubFacadeCredentialsStore {
44
+ load(): Promise<GitHubFacadeCredentials | null>;
45
+ /**
46
+ * Store `creds` only if no row exists yet, and return whichever row
47
+ * exists afterwards — so two replicas initialising at once agree on one
48
+ * set, and the loser's generated values are simply dropped.
49
+ */
50
+ createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials>;
51
+ /**
52
+ * Replace the row only while it still carries `expectedClientId`, and
53
+ * return what is stored afterwards. Two admins rotating at once: the first
54
+ * write wins, the second finds a different client id, writes nothing, and
55
+ * is shown the winner's set — the only one Claude will accept.
56
+ */
57
+ replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials>;
58
+ }
59
+
60
+ const ROW_ID = 'default';
61
+
62
+ /**
63
+ * The database-backed store. Secrets are sealed with the deployment's secrets
64
+ * key, exactly as stored settings are; without that key there is nowhere safe
65
+ * to keep a client secret, so the store refuses rather than writing plaintext.
66
+ */
67
+ export class DbGitHubFacadeCredentialsStore implements GitHubFacadeCredentialsStore {
68
+ constructor(
69
+ private readonly db: Database,
70
+ private readonly crypto: TokenCrypto | null,
71
+ ) {}
72
+
73
+ async load(): Promise<GitHubFacadeCredentials | null> {
74
+ const [row] = await this.db
75
+ .select()
76
+ .from(githubFacadeIdentity)
77
+ .where(eq(githubFacadeIdentity.id, ROW_ID))
78
+ .limit(1);
79
+ if (!row) return null;
80
+ const crypto = this.requireCrypto();
81
+ return {
82
+ appId: row.appId,
83
+ clientId: row.clientId,
84
+ clientSecret: crypto.decrypt(row.clientSecret),
85
+ webhookSecret: crypto.decrypt(row.webhookSecret),
86
+ privateKeyPem: crypto.decrypt(row.privateKeyPem),
87
+ publicKeyPem: row.publicKeyPem,
88
+ createdAt: row.createdAt,
89
+ rotatedAt: row.rotatedAt,
90
+ };
91
+ }
92
+
93
+ async createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
94
+ await this.db
95
+ .insert(githubFacadeIdentity)
96
+ .values({ id: ROW_ID, ...this.seal(creds) })
97
+ .onConflictDoNothing({ target: githubFacadeIdentity.id });
98
+ return this.stored('insert');
99
+ }
100
+
101
+ async replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
102
+ await this.db
103
+ .update(githubFacadeIdentity)
104
+ .set(this.seal(creds))
105
+ .where(and(eq(githubFacadeIdentity.id, ROW_ID), eq(githubFacadeIdentity.clientId, expectedClientId)));
106
+ return this.stored('replace');
107
+ }
108
+
109
+ private async stored(after: string): Promise<GitHubFacadeCredentials> {
110
+ const row = await this.load();
111
+ if (!row) throw new Error(`claude bridge credentials vanished between ${after} and read`);
112
+ return row;
113
+ }
114
+
115
+ private seal(creds: GitHubFacadeCredentials) {
116
+ const crypto = this.requireCrypto();
117
+ return {
118
+ appId: creds.appId,
119
+ clientId: creds.clientId,
120
+ clientSecret: crypto.encrypt(creds.clientSecret),
121
+ webhookSecret: crypto.encrypt(creds.webhookSecret),
122
+ privateKeyPem: crypto.encrypt(creds.privateKeyPem),
123
+ publicKeyPem: creds.publicKeyPem,
124
+ createdAt: creds.createdAt,
125
+ rotatedAt: creds.rotatedAt,
126
+ };
127
+ }
128
+
129
+ private requireCrypto(): TokenCrypto {
130
+ if (!this.crypto) {
131
+ throw new GitHubFacadeUnavailableError(
132
+ 'SECRETS_ENC_KEY is not set, so the Claude connection credentials cannot be stored.',
133
+ );
134
+ }
135
+ return this.crypto;
136
+ }
137
+ }
138
+
139
+ export class GitHubFacadeUnavailableError extends Error {
140
+ constructor(message: string) {
141
+ super(message);
142
+ this.name = 'GitHubFacadeUnavailableError';
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Generates the credentials on first use and reads them from the store on
148
+ * every use after that. No in-process copy: the store is the one place the
149
+ * truth lives, whichever replica asks.
150
+ */
151
+ export class GitHubFacadeCredentialsService {
152
+ constructor(private readonly store: GitHubFacadeCredentialsStore) {}
153
+
154
+ /** The credentials, generated on first use. */
155
+ async ensure(): Promise<GitHubFacadeCredentials> {
156
+ const existing = await this.store.load();
157
+ if (existing) return existing;
158
+ return this.store.createIfAbsent(generateCredentials(null));
159
+ }
160
+
161
+ /**
162
+ * New credentials, all of them — replacing the set this call read, and
163
+ * returning whatever is stored afterwards. Every registration on the
164
+ * Claude side stops matching at once, which is the point of rotating: the
165
+ * Owner re-enters the new set, and connected users' tokens (connection
166
+ * keys) are untouched, since those are ours.
167
+ */
168
+ async rotate(): Promise<GitHubFacadeCredentials> {
169
+ const current = await this.ensure();
170
+ return this.store.replaceIfCurrent(current.clientId, generateCredentials(current.createdAt));
171
+ }
172
+ }
173
+
174
+ function generateCredentials(createdAt: Date | null): GitHubFacadeCredentials {
175
+ // The shapes GitHub uses, because the form was built for them: a numeric
176
+ // app id, an `Iv1.`-prefixed client id, hex secrets, a PKCS#1 RSA key.
177
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
178
+ modulusLength: 2048,
179
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
180
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
181
+ });
182
+ return {
183
+ appId: String(randomInt(100_000, 999_999)),
184
+ clientId: `Iv1.${randomBytes(8).toString('hex')}`,
185
+ clientSecret: randomBytes(20).toString('hex'),
186
+ webhookSecret: randomBytes(16).toString('hex'),
187
+ privateKeyPem: privateKey,
188
+ publicKeyPem: publicKey,
189
+ createdAt: createdAt ?? new Date(),
190
+ rotatedAt: createdAt ? new Date() : null,
191
+ };
192
+ }
193
+
194
+ /** For tests: the row kept in memory, with the same conditional writes. */
195
+ export class MemoryGitHubFacadeCredentialsStore implements GitHubFacadeCredentialsStore {
196
+ private row: GitHubFacadeCredentials | null = null;
197
+ async load(): Promise<GitHubFacadeCredentials | null> {
198
+ return this.row;
199
+ }
200
+ async createIfAbsent(creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
201
+ this.row ??= creds;
202
+ return this.row;
203
+ }
204
+ async replaceIfCurrent(expectedClientId: string, creds: GitHubFacadeCredentials): Promise<GitHubFacadeCredentials> {
205
+ if (this.row?.clientId === expectedClientId) this.row = creds;
206
+ return this.row!;
207
+ }
208
+ }