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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +101 -9
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +20 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +105 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +12 -5
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +18 -6
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +26 -1
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +213 -71
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access.routes.d.ts.map +1 -1
  23. package/dist/modules/access/access.routes.js +59 -7
  24. package/dist/modules/access/access.routes.js.map +1 -1
  25. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  26. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  27. package/dist/modules/access-model/access-grammar.js +95 -1
  28. package/dist/modules/access-model/access-grammar.js.map +1 -1
  29. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  30. package/dist/modules/access-model/group-files.js +4 -1
  31. package/dist/modules/access-model/group-files.js.map +1 -1
  32. package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
  33. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
  34. package/dist/modules/access-model/kb-read-filter.js +15 -0
  35. package/dist/modules/access-model/kb-read-filter.js.map +1 -1
  36. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  37. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  38. package/dist/modules/access-model/plugin-principals.js +125 -0
  39. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  40. package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
  41. package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
  42. package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
  43. package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
  44. package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
  45. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
  46. package/dist/modules/connection-probe/connection-probe.service.js +684 -0
  47. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
  48. package/dist/modules/connection-probe/index.d.ts +3 -0
  49. package/dist/modules/connection-probe/index.d.ts.map +1 -0
  50. package/dist/modules/connection-probe/index.js +3 -0
  51. package/dist/modules/connection-probe/index.js.map +1 -0
  52. package/dist/modules/database/core-schema.d.ts +326 -0
  53. package/dist/modules/database/core-schema.d.ts.map +1 -1
  54. package/dist/modules/database/core-schema.js +52 -0
  55. package/dist/modules/database/core-schema.js.map +1 -1
  56. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  57. package/dist/modules/diff/diff.routes.js +3 -5
  58. package/dist/modules/diff/diff.routes.js.map +1 -1
  59. package/dist/modules/kb-fs/mutex.d.ts +37 -0
  60. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/mutex.js +48 -5
  62. package/dist/modules/kb-fs/mutex.js.map +1 -1
  63. package/dist/modules/kb-sync/index.d.ts +6 -0
  64. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  65. package/dist/modules/kb-sync/index.js +5 -0
  66. package/dist/modules/kb-sync/index.js.map +1 -0
  67. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  68. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  69. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  70. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  71. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  72. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  73. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  74. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  75. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  76. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  77. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  78. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  79. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  80. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  81. package/dist/modules/kb-sync/sync-auth.js +62 -0
  82. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  83. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  84. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  85. package/dist/modules/kb-sync/sync-payload.js +105 -0
  86. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  87. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  88. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  89. package/dist/modules/marketplace/git-http.routes.js +190 -0
  90. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  91. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  92. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  93. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  94. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  95. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  96. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  97. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  98. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  99. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  100. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  101. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  102. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  103. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  104. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  105. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  106. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  107. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  108. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  109. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  110. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  111. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  112. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  113. package/dist/modules/marketplace/github-facade/index.js +6 -0
  114. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  115. package/dist/modules/marketplace/index.d.ts +4 -0
  116. package/dist/modules/marketplace/index.d.ts.map +1 -0
  117. package/dist/modules/marketplace/index.js +4 -0
  118. package/dist/modules/marketplace/index.js.map +1 -0
  119. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  120. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  121. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  122. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  123. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  124. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  125. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  126. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  127. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  128. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  129. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  130. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  131. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  132. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  133. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  134. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  135. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  136. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  137. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  138. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  139. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  140. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  141. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  142. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  143. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  144. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  145. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  146. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  147. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  148. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  149. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  150. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  151. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  152. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  153. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  154. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  155. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  156. package/dist/modules/plugins/discovery/native.source.js +102 -0
  157. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  158. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  159. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  160. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  161. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  162. package/dist/modules/plugins/index.d.ts +10 -1
  163. package/dist/modules/plugins/index.d.ts.map +1 -1
  164. package/dist/modules/plugins/index.js +8 -0
  165. package/dist/modules/plugins/index.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  167. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  168. package/dist/modules/plugins/plugin-links.js +160 -0
  169. package/dist/modules/plugins/plugin-links.js.map +1 -0
  170. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  171. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  172. package/dist/modules/plugins/plugin-links.service.js +304 -0
  173. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  174. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  175. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  177. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  178. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  179. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  180. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  181. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  182. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  183. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  185. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  186. package/dist/modules/plugins/plugins.routes.js +120 -12
  187. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  188. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  189. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  190. package/dist/modules/plugins/plugins.service.js +108 -47
  191. package/dist/modules/plugins/plugins.service.js.map +1 -1
  192. package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
  193. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
  195. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
  197. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  198. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  199. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  200. package/dist/modules/settings/deployment-settings.service.js +82 -2
  201. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  202. package/dist/modules/settings/setup.routes.d.ts +26 -0
  203. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  204. package/dist/modules/settings/setup.routes.js +9 -1
  205. package/dist/modules/settings/setup.routes.js.map +1 -1
  206. package/dist/modules/skills/index.d.ts +1 -0
  207. package/dist/modules/skills/index.d.ts.map +1 -1
  208. package/dist/modules/skills/index.js +1 -0
  209. package/dist/modules/skills/index.js.map +1 -1
  210. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  211. package/dist/modules/skills/pending-skills.service.js +9 -4
  212. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  213. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  214. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  215. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  216. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  217. package/dist/modules/skills/skills.contract.d.ts +31 -0
  218. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  219. package/dist/modules/skills/skills.routes.d.ts +28 -3
  220. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  221. package/dist/modules/skills/skills.routes.js +42 -3
  222. package/dist/modules/skills/skills.routes.js.map +1 -1
  223. package/dist/modules/skills/skills.service.d.ts +4 -0
  224. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  225. package/dist/modules/skills/skills.service.js +83 -22
  226. package/dist/modules/skills/skills.service.js.map +1 -1
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  228. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  229. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  230. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  232. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  233. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  234. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  235. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  236. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
  238. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  239. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  240. package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
  241. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  242. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
  243. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
  245. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  246. package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
  247. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  248. package/dist/modules/workflow/git/git.service.d.ts +58 -3
  249. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  250. package/dist/modules/workflow/git/git.service.js +325 -60
  251. package/dist/modules/workflow/git/git.service.js.map +1 -1
  252. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  253. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  254. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  255. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  256. package/dist/modules/workflow/workflow.routes.d.ts +2 -1
  257. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  258. package/dist/modules/workflow/workflow.routes.js +98 -9
  259. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +133 -9
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +462 -52
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  268. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  270. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  271. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  272. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  273. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  274. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspace.routes.js +11 -14
  276. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  277. package/dist/modules/workspace/workspace.service.d.ts +38 -1
  278. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.service.js +131 -20
  280. package/dist/modules/workspace/workspace.service.js.map +1 -1
  281. package/dist/shared/domain-errors.d.ts +20 -0
  282. package/dist/shared/domain-errors.d.ts.map +1 -1
  283. package/dist/shared/domain-errors.js +29 -0
  284. package/dist/shared/domain-errors.js.map +1 -1
  285. package/dist/shared/fs-errors.d.ts +8 -0
  286. package/dist/shared/fs-errors.d.ts.map +1 -0
  287. package/dist/shared/fs-errors.js +11 -0
  288. package/dist/shared/fs-errors.js.map +1 -0
  289. package/dist/shared/fs-walk.d.ts +20 -6
  290. package/dist/shared/fs-walk.d.ts.map +1 -1
  291. package/dist/shared/fs-walk.js +35 -27
  292. package/dist/shared/fs-walk.js.map +1 -1
  293. package/dist/shared/kb-walk.d.ts +52 -0
  294. package/dist/shared/kb-walk.d.ts.map +1 -0
  295. package/dist/shared/kb-walk.js +76 -0
  296. package/dist/shared/kb-walk.js.map +1 -0
  297. package/dist/shared/path-order.d.ts +18 -0
  298. package/dist/shared/path-order.d.ts.map +1 -0
  299. package/dist/shared/path-order.js +35 -0
  300. package/dist/shared/path-order.js.map +1 -0
  301. package/dist/shared/token-crypto.d.ts +17 -2
  302. package/dist/shared/token-crypto.d.ts.map +1 -1
  303. package/dist/shared/token-crypto.js +25 -8
  304. package/dist/shared/token-crypto.js.map +1 -1
  305. package/dist/shared/ttl-cache.d.ts +28 -1
  306. package/dist/shared/ttl-cache.d.ts.map +1 -1
  307. package/dist/shared/ttl-cache.js +33 -1
  308. package/dist/shared/ttl-cache.js.map +1 -1
  309. package/kb-template/.bevelignore +1 -1
  310. package/kb-template/AGENTS.md +95 -30
  311. package/kb-template/Skills/.gitkeep +0 -0
  312. package/migrations/0005_github_facade.sql +25 -0
  313. package/migrations/meta/0005_snapshot.json +1717 -0
  314. package/migrations/meta/_journal.json +7 -0
  315. package/package.json +6 -6
  316. package/src/__tests__/core-config.admin.test.ts +21 -0
  317. package/src/__tests__/kb-layout-config.test.ts +110 -0
  318. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  319. package/src/core/catalog-cache-invalidation.ts +92 -0
  320. package/src/core/create-core-server.ts +126 -7
  321. package/src/core/create-core-services.ts +173 -41
  322. package/src/core-config.ts +14 -7
  323. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  324. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  325. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  326. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  327. package/src/modules/access/access-control.interface.ts +20 -9
  328. package/src/modules/access/access-control.service.ts +240 -78
  329. package/src/modules/access/access.routes.ts +96 -14
  330. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  331. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  332. package/src/modules/access-model/access-grammar.ts +114 -2
  333. package/src/modules/access-model/group-files.ts +4 -0
  334. package/src/modules/access-model/kb-read-filter.ts +22 -0
  335. package/src/modules/access-model/plugin-principals.ts +137 -0
  336. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
  337. package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
  338. package/src/modules/connection-probe/connection-probe.service.ts +734 -0
  339. package/src/modules/connection-probe/index.ts +2 -0
  340. package/src/modules/database/core-schema.ts +54 -0
  341. package/src/modules/diff/diff.routes.ts +9 -5
  342. package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
  343. package/src/modules/kb-fs/mutex.ts +50 -5
  344. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  345. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  346. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  347. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  348. package/src/modules/kb-sync/index.ts +19 -0
  349. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  350. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  351. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  352. package/src/modules/kb-sync/sync-auth.ts +118 -0
  353. package/src/modules/kb-sync/sync-payload.ts +143 -0
  354. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  355. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  356. package/src/modules/marketplace/git-http.routes.ts +193 -0
  357. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  358. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  359. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  360. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  361. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  362. package/src/modules/marketplace/github-facade/index.ts +25 -0
  363. package/src/modules/marketplace/index.ts +3 -0
  364. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  365. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  366. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  367. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  368. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  369. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  370. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  371. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  372. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  373. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  374. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  375. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  376. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  377. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  378. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  379. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  380. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  381. package/src/modules/plugins/discovery/native.source.ts +122 -0
  382. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  383. package/src/modules/plugins/index.ts +10 -0
  384. package/src/modules/plugins/plugin-links.service.ts +363 -0
  385. package/src/modules/plugins/plugin-links.ts +184 -0
  386. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  387. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  388. package/src/modules/plugins/plugins.contract.ts +28 -1
  389. package/src/modules/plugins/plugins.routes.ts +128 -11
  390. package/src/modules/plugins/plugins.service.ts +106 -53
  391. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
  392. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
  393. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
  394. package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
  395. package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
  396. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  397. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  398. package/src/modules/settings/deployment-settings.service.ts +95 -2
  399. package/src/modules/settings/setup.routes.ts +24 -0
  400. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  401. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  402. package/src/modules/skills/index.ts +1 -0
  403. package/src/modules/skills/pending-skills.service.ts +7 -5
  404. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  405. package/src/modules/skills/skills.contract.ts +32 -0
  406. package/src/modules/skills/skills.routes.ts +59 -3
  407. package/src/modules/skills/skills.service.ts +84 -22
  408. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  409. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  410. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
  411. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  412. package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
  413. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
  414. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
  415. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
  416. package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
  417. package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
  418. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  419. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
  420. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
  421. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  422. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
  423. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
  424. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  425. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  426. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
  427. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  428. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  429. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  430. package/src/modules/workflow/git/git.service.ts +354 -63
  431. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  432. package/src/modules/workflow/workflow.routes.ts +104 -4
  433. package/src/modules/workflow/workflow.service.ts +505 -64
  434. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  435. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  436. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  437. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  438. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  439. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  440. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  441. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  442. package/src/modules/workspace/workspace.routes.ts +17 -21
  443. package/src/modules/workspace/workspace.service.ts +128 -19
  444. package/src/shared/__tests__/filename.test.ts +0 -0
  445. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  446. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  447. package/src/shared/__tests__/path-order.test.ts +24 -0
  448. package/src/shared/__tests__/token-crypto.test.ts +34 -0
  449. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  450. package/src/shared/domain-errors.ts +29 -0
  451. package/src/shared/fs-errors.ts +10 -0
  452. package/src/shared/fs-walk.ts +38 -23
  453. package/src/shared/kb-walk.ts +95 -0
  454. package/src/shared/path-order.ts +33 -0
  455. package/src/shared/token-crypto.ts +28 -10
  456. package/src/shared/ttl-cache.ts +33 -1
@@ -0,0 +1,2 @@
1
+ export * from './connection-probe.contract.js';
2
+ export { ConnectionProbeService } from './connection-probe.service.js';
@@ -183,6 +183,12 @@ export const externalApiKeys = pgTable('api_tokens', {
183
183
  userId: uuid('user_id').notNull().references(() => users.id),
184
184
  tokenHash: text('token_hash').notNull().unique(),
185
185
  label: text('label').notNull(),
186
+ /**
187
+ * What the key was minted as: `key` by hand, or a flow's own kind (a
188
+ * Claude link). The one fact that tells such keys apart — the label is
189
+ * free text the person may edit or imitate.
190
+ */
191
+ kind: text('kind').default('key').notNull(),
186
192
  createdAt: timestamp('created_at').defaultNow().notNull(),
187
193
  lastUsedAt: timestamp('last_used_at'),
188
194
  revokedAt: timestamp('revoked_at'),
@@ -490,3 +496,51 @@ export const deploymentSettings = pgTable('deployment_settings', {
490
496
  updatedBy: uuid('updated_by').references(() => users.id, { onDelete: 'set null' }),
491
497
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
492
498
  });
499
+
500
+ /**
501
+ * What this deployment presents to claude.ai as a "GitHub Enterprise Server"
502
+ * so that Cowork and claude.ai can add the per-user marketplace: the app id,
503
+ * client id and secrets an Owner pastes into Claude's admin settings. ONE row,
504
+ * generated on first use, replaced whole on rotate. Secrets are sealed with
505
+ * the secrets key, as stored settings are — see
506
+ * `marketplace/github-facade/github-facade-credentials.service.ts`.
507
+ */
508
+ export const githubFacadeIdentity = pgTable('github_facade_identity', {
509
+ id: text('id').primaryKey(),
510
+ appId: text('app_id').notNull(),
511
+ clientId: text('client_id').notNull(),
512
+ /** Sealed. */
513
+ clientSecret: text('client_secret').notNull(),
514
+ /** Sealed. */
515
+ webhookSecret: text('webhook_secret').notNull(),
516
+ /** Sealed — PKCS#1 PEM. */
517
+ privateKeyPem: text('private_key_pem').notNull(),
518
+ publicKeyPem: text('public_key_pem').notNull(),
519
+ createdAt: timestamp('created_at').defaultNow().notNull(),
520
+ rotatedAt: timestamp('rotated_at'),
521
+ });
522
+
523
+ /**
524
+ * One-time codes the Claude connect flow issues on the consent page and
525
+ * Anthropic's backend exchanges seconds later — in the database so the
526
+ * replica that issued a code and the replica asked to exchange it agree.
527
+ *
528
+ * Keyed by the PERSON: "one live code per person" is the table's own rule,
529
+ * not a cleanup's. Issuing upserts their row — the newest code overwrites
530
+ * the last in one statement — so the table holds at most one row per user,
531
+ * for as long as the user exists (the row goes with them), and nothing ever
532
+ * has to sweep it. The client id is data the exchange checks, not part of
533
+ * the key: the bridge has one client, and a rotated client id must not leave
534
+ * a dead row behind under the old one. The code's hash is the unique lookup
535
+ * an exchange uses, spent by a conditional update. See
536
+ * `marketplace/github-facade/github-facade-codes.store.ts`.
537
+ */
538
+ export const githubFacadeCodes = pgTable('github_facade_codes', {
539
+ userId: uuid('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
540
+ clientId: text('client_id').notNull(),
541
+ codeHash: text('code_hash').notNull().unique(),
542
+ redirectUri: text('redirect_uri').notNull(),
543
+ expiresAt: timestamp('expires_at').notNull(),
544
+ consumedAt: timestamp('consumed_at'),
545
+ createdAt: timestamp('created_at').defaultNow().notNull(),
546
+ });
@@ -3,7 +3,7 @@ import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared
3
3
  import type { IDiffService } from './diff.interface.js';
4
4
  import type { AuthService } from '../auth/auth.service.js';
5
5
  import type { IAccessControl } from '../access/access-control.interface.js';
6
- import { toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
6
+ import { canReadWorkspacePath, toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
7
7
  import { WorkflowDomainError, WorkflowValidationError } from '../../shared/domain-errors.js';
8
8
  import { LockingFilesystem } from '../kb-fs/locking-filesystem.js';
9
9
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
@@ -138,10 +138,14 @@ export function createDiffRoutes(
138
138
  }
139
139
 
140
140
  /** Read gate for a single change path (mirrors GET /review/file). Returns true to proceed. */
141
- async function canReadPath(workspaceId: string, email: string, pathParam: string): Promise<boolean> {
142
- const rel = toKbRelative(pathParam, kbDirName);
143
- // Non-KB paths (toKbRelative → null) carry no read rules and pass.
144
- return rel === null || (await accessControl.canRead(workspaceId, email, rel));
141
+ function canReadPath(workspaceId: string, email: string, pathParam: string): Promise<boolean> {
142
+ return canReadWorkspacePath(
143
+ (w, e, p) => accessControl.canRead(w, e, p),
144
+ workspaceId,
145
+ email,
146
+ kbDirName,
147
+ pathParam,
148
+ );
145
149
  }
146
150
 
147
151
  router.get('/workspace/:id/review', async (req, res) => {
@@ -8,6 +8,113 @@ function deferred<T = void>() {
8
8
  }
9
9
 
10
10
  describe('WorkspaceMutex', () => {
11
+ describe('runAll', () => {
12
+ /**
13
+ * The property nesting cannot provide: `run(a, () => run(b, fn))` releases
14
+ * nothing, but between taking `a` and taking `b` the second key is free,
15
+ * so a single-key task can slip in, run to completion, and be gone before
16
+ * the pair operation's body starts. `runAll` reserves the set in one
17
+ * synchronous step, so it cannot be interleaved that way.
18
+ */
19
+ it('reserves every key before any of them can be taken by another task', async () => {
20
+ const mtx = new WorkspaceMutex();
21
+ const order: string[] = [];
22
+ const release = deferred();
23
+
24
+ // Something already holds 'a', so the pair operation cannot start yet —
25
+ // this is the window a nested acquisition would leave 'b' open in.
26
+ const holder = mtx.run('a', async () => {
27
+ order.push('holder-start');
28
+ await release.promise;
29
+ order.push('holder-end');
30
+ });
31
+ await new Promise((r) => setImmediate(r));
32
+
33
+ const pair = mtx.runAll(['a', 'b'], async () => {
34
+ order.push('pair');
35
+ });
36
+ // Submitted while the pair is still waiting on 'a'. If 'b' were taken
37
+ // only after 'a' were granted, this would run first.
38
+ const single = mtx.run('b', async () => {
39
+ order.push('single-b');
40
+ });
41
+
42
+ release.resolve();
43
+ await Promise.all([holder, pair, single]);
44
+ expect(order).toEqual(['holder-start', 'holder-end', 'pair', 'single-b']);
45
+ });
46
+
47
+ it('lets a task on an unrelated key run in parallel', async () => {
48
+ const mtx = new WorkspaceMutex();
49
+ const order: string[] = [];
50
+ const release = deferred();
51
+
52
+ const holder = mtx.run('a', async () => {
53
+ await release.promise;
54
+ order.push('holder');
55
+ });
56
+ await new Promise((r) => setImmediate(r));
57
+
58
+ const pair = mtx.runAll(['a', 'b'], async () => void order.push('pair'));
59
+ const other = mtx.run('c', async () => void order.push('other'));
60
+
61
+ await other;
62
+ expect(order).toEqual(['other']);
63
+ release.resolve();
64
+ await Promise.all([holder, pair]);
65
+ expect(order).toEqual(['other', 'holder', 'pair']);
66
+ });
67
+
68
+ /**
69
+ * A failing task on ONE key must not release the others. `Promise.all`
70
+ * settles on the first rejection rather than waiting for the rest, so
71
+ * combining the predecessors without catching each one first would start
72
+ * this task while another key was still held.
73
+ */
74
+ it('waits for every predecessor even when one of them fails first', async () => {
75
+ const mtx = new WorkspaceMutex();
76
+ const order: string[] = [];
77
+ const failA = deferred();
78
+ const finishB = deferred();
79
+
80
+ const failing = mtx.run('a', async () => {
81
+ await failA.promise;
82
+ throw new Error('a-failed');
83
+ });
84
+ failing.catch(() => undefined); // handled below; keep the rejection quiet
85
+ const running = mtx.run('b', async () => {
86
+ await finishB.promise;
87
+ order.push('b-end');
88
+ });
89
+
90
+ // Queued while BOTH predecessors are still pending.
91
+ const pair = mtx.runAll(['a', 'b'], async () => void order.push('pair'));
92
+
93
+ failA.resolve();
94
+ await expect(failing).rejects.toThrow('a-failed');
95
+ await new Promise((r) => setImmediate(r));
96
+ // 'a' has failed, but 'b' still holds its key — the pair must wait.
97
+ expect(order).toEqual([]);
98
+
99
+ finishB.resolve();
100
+ await Promise.all([running, pair]);
101
+ expect(order).toEqual(['b-end', 'pair']);
102
+ });
103
+
104
+ it('releases every key when the body throws', async () => {
105
+ const mtx = new WorkspaceMutex();
106
+ await expect(mtx.runAll(['a', 'b'], async () => { throw new Error('boom'); })).rejects.toThrow('boom');
107
+ // Both keys must be usable again, or one failure wedges them forever.
108
+ await expect(mtx.run('a', async () => 'a-ok')).resolves.toBe('a-ok');
109
+ await expect(mtx.run('b', async () => 'b-ok')).resolves.toBe('b-ok');
110
+ });
111
+
112
+ it('collapses a duplicate key instead of waiting on itself', async () => {
113
+ const mtx = new WorkspaceMutex();
114
+ await expect(mtx.runAll(['a', 'a'], async () => 'done')).resolves.toBe('done');
115
+ });
116
+ });
117
+
11
118
  it('serializes tasks for the same key in submission order', async () => {
12
119
  const mtx = new WorkspaceMutex();
13
120
  const order: string[] = [];
@@ -8,14 +8,59 @@ export class WorkspaceMutex {
8
8
  private readonly tails = new Map<string, Promise<unknown>>();
9
9
 
10
10
  async run<T>(key: string, fn: () => Promise<T>): Promise<T> {
11
- const prev = this.tails.get(key) ?? Promise.resolve();
12
- const next = prev.catch(() => undefined).then(fn);
13
- this.tails.set(key, next);
11
+ return this.runAll([key], fn);
12
+ }
13
+
14
+ /**
15
+ * Reserve SEVERAL keys for one operation, atomically.
16
+ *
17
+ * Nesting `run(a, () => run(b, fn))` does NOT do this: between taking `a`
18
+ * and taking `b` there is a gap in which another operation can take `b`,
19
+ * finish, and release it — so an operation holding both keys can still be
20
+ * interleaved with one holding just the second. For branch lifecycle that
21
+ * gap is the whole bug: a change request being opened `Y -> X` would hold
22
+ * one branch's key while the other was deleted out from under it.
23
+ *
24
+ * This reserves the set in ONE synchronous step. The tails are read and
25
+ * rewritten without an await between, so no other call can observe a
26
+ * half-taken set: a concurrent `run` on any of these keys either already
27
+ * sits in `prev` (it goes first) or chains onto `next` (it goes after).
28
+ * Within one call nothing is ever held while waiting for something else,
29
+ * so callers that do not nest need no ordering discipline and cannot
30
+ * deadlock.
31
+ *
32
+ * NESTING is the one way to reintroduce hold-and-wait, so never nest
33
+ * `run`/`runAll` calls on the same instance. An inner call chains behind
34
+ * everything already queued on its keys, which can include a `runAll` that
35
+ * is itself waiting for the outer call to finish: `run(b, () => run(a, …))`
36
+ * was harmless when every operation took a single key, but against a
37
+ * concurrent `runAll([a, b])` it deadlocks — outer waits inner, inner
38
+ * waits the pair, the pair waits outer. No caller nests today; keep it
39
+ * that way, or hand the full key set to one `runAll` at the top.
40
+ *
41
+ * Duplicate keys collapse; the operation waits on each distinct key once.
42
+ *
43
+ * Each predecessor is caught INDIVIDUALLY, before the `Promise.all`. A bare
44
+ * `Promise.all` settles on the first rejection rather than waiting for the
45
+ * rest, so one key's task failing early would start this one while another
46
+ * key was still held — a failure on any key would punch a hole in the
47
+ * mutual exclusion of every key beside it. Catching first makes the
48
+ * combined promise wait for all of them to settle, however each ends.
49
+ */
50
+ async runAll<T>(keys: string[], fn: () => Promise<T>): Promise<T> {
51
+ const distinct = [...new Set(keys)];
52
+ const prev = Promise.all(
53
+ distinct.map((k) => (this.tails.get(k) ?? Promise.resolve()).catch(() => undefined)),
54
+ );
55
+ const next = prev.then(fn);
56
+ for (const k of distinct) this.tails.set(k, next);
14
57
  try {
15
58
  return await next;
16
59
  } finally {
17
- if (this.tails.get(key) === next) {
18
- this.tails.delete(key);
60
+ for (const k of distinct) {
61
+ if (this.tails.get(k) === next) {
62
+ this.tails.delete(k);
63
+ }
19
64
  }
20
65
  }
21
66
  }
@@ -0,0 +1,297 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import type { Server } from 'node:http';
3
+ import express from 'express';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { createKbSyncRoutes, isSyncRawBodyPath } from '../kb-sync.routes.js';
6
+ import type { IKbSyncService, SyncResult } from '../kb-sync.interface.js';
7
+
8
+ const SECRET = 'a-very-long-and-random-sync-secret';
9
+
10
+ const SYNCED: SyncResult = {
11
+ status: 'synced',
12
+ results: [{ branch: 'main', outcome: 'updated', from: 'aaa', to: 'bbb' }],
13
+ changeRequests: { closedDeletedBranch: 0 },
14
+ };
15
+
16
+ let server: Server | undefined;
17
+ afterEach(() => {
18
+ server?.close();
19
+ server = undefined;
20
+ });
21
+
22
+ async function mount(opts: {
23
+ secret?: string;
24
+ result?: SyncResult;
25
+ admin?: boolean;
26
+ /** Install the production-shaped global JSON parser in front of the router. */
27
+ globalJson?: boolean;
28
+ }): Promise<{ base: string; sync: ReturnType<typeof vi.fn> }> {
29
+ // A suite may mount twice in one test; the earlier server must not outlive it.
30
+ server?.close();
31
+ const sync = vi.fn(async () => opts.result ?? SYNCED);
32
+ const kbSync: IKbSyncService = { sync };
33
+ const app = express();
34
+ // Mirrors production: the global JSON parser skips `/api/sync`, so the
35
+ // router's own raw parser sees the bytes. Installing `express.json()` here
36
+ // would hide exactly the wiring bug the signature path depends on.
37
+ if (opts.globalJson) {
38
+ const json = express.json();
39
+ app.use((req, res, next) => (isSyncRawBodyPath(req.path) ? next() : json(req, res, next)));
40
+ }
41
+ app.use(
42
+ '/api',
43
+ createKbSyncRoutes({
44
+ kbSync,
45
+ syncSecret: () => opts.secret ?? SECRET,
46
+ authService: {
47
+ verifyToken: (token: string) => {
48
+ if (token !== 'jwt-ok') throw new Error('bad token');
49
+ return { userId: 'u1', email: 'person@example.com' };
50
+ },
51
+ },
52
+ adminAccess: { isAdmin: async () => opts.admin === true },
53
+ }),
54
+ );
55
+ await new Promise<void>((resolve) => {
56
+ server = app.listen(0, resolve);
57
+ });
58
+ const address = server!.address();
59
+ if (typeof address === 'string' || !address) throw new Error('no port');
60
+ return { base: `http://127.0.0.1:${address.port}/api/sync`, sync };
61
+ }
62
+
63
+ function post(url: string, init: { headers?: Record<string, string>; body?: string } = {}) {
64
+ return fetch(url, { method: 'POST', headers: init.headers ?? {}, body: init.body });
65
+ }
66
+
67
+ describe('POST /api/sync — credentials', () => {
68
+ it('401 with no credential, and advertises the bearer challenge', async () => {
69
+ const { base, sync } = await mount({});
70
+ const res = await post(base);
71
+ expect(res.status).toBe(401);
72
+ expect(res.headers.get('www-authenticate')).toContain('Bearer');
73
+ expect(sync).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it('401 with the wrong secret', async () => {
77
+ const { base, sync } = await mount({});
78
+ const res = await post(base, { headers: { authorization: 'Bearer wrong' } });
79
+ expect(res.status).toBe(401);
80
+ expect(sync).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it('503 when no secret is configured and a hook presents one', async () => {
84
+ const { base } = await mount({ secret: '' });
85
+ const res = await post(base, { headers: { authorization: `Bearer ${SECRET}` } });
86
+ expect(res.status).toBe(503);
87
+ expect(((await res.json()) as { error: string }).error).toContain('KB_SYNC_SECRET');
88
+ });
89
+
90
+ it("an admin's session works; a non-admin's does not", async () => {
91
+ const admin = await mount({ admin: true });
92
+ expect((await post(admin.base, { headers: { authorization: 'Bearer jwt-ok' } })).status).toBe(200);
93
+ const person = await mount({ admin: false });
94
+ expect((await post(person.base, { headers: { authorization: 'Bearer jwt-ok' } })).status).toBe(401);
95
+ });
96
+
97
+ it('accepts the GitLab token header', async () => {
98
+ const { base } = await mount({});
99
+ const res = await post(base, { headers: { 'x-gitlab-token': SECRET } });
100
+ expect(res.status).toBe(200);
101
+ });
102
+
103
+ it('accepts a GitHub signature computed over the exact bytes received', async () => {
104
+ const { base, sync } = await mount({});
105
+ const body = '{"ref":"refs/heads/main","after":"b"}';
106
+ const sig = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex');
107
+ const res = await post(base, {
108
+ headers: { 'x-hub-signature-256': sig, 'content-type': 'application/json' },
109
+ body,
110
+ });
111
+ expect(res.status).toBe(200);
112
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['main'] }));
113
+ });
114
+ });
115
+
116
+ describe('POST /api/sync — body and result', () => {
117
+ it('an empty body syncs everything', async () => {
118
+ const { base, sync } = await mount({});
119
+ const res = await post(base, { headers: { authorization: `Bearer ${SECRET}` } });
120
+ expect(res.status).toBe(200);
121
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: 'all' }));
122
+ expect(await res.json()).toEqual(SYNCED);
123
+ });
124
+
125
+ it('an explicit body names the branches', async () => {
126
+ const { base, sync } = await mount({});
127
+ await post(base, {
128
+ headers: { authorization: `Bearer ${SECRET}`, 'content-type': 'application/json' },
129
+ body: JSON.stringify({ branches: ['main', 'ali/x'] }),
130
+ });
131
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['main', 'ali/x'] }));
132
+ });
133
+
134
+ it('an Azure DevOps push payload names the pushed branch', async () => {
135
+ const { base, sync } = await mount({});
136
+ await post(base, {
137
+ headers: { authorization: `Bearer ${SECRET}`, 'content-type': 'application/json' },
138
+ body: JSON.stringify({
139
+ eventType: 'git.push',
140
+ resource: { refUpdates: [{ name: 'refs/heads/ali/x' }] },
141
+ }),
142
+ });
143
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['ali/x'] }));
144
+ });
145
+
146
+ it('400 for a branch name git would refuse, and for a body that is not JSON', async () => {
147
+ const { base, sync } = await mount({});
148
+ const bad = await post(base, {
149
+ headers: { authorization: `Bearer ${SECRET}`, 'content-type': 'application/json' },
150
+ body: JSON.stringify({ branches: ['--upload-pack=x'] }),
151
+ });
152
+ expect(bad.status).toBe(400);
153
+ const notJson = await post(base, {
154
+ headers: { authorization: `Bearer ${SECRET}`, 'content-type': 'application/json' },
155
+ body: '{not json',
156
+ });
157
+ expect(notJson.status).toBe(400);
158
+ expect(sync).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('409 when any branch is in conflict — the message rides in the body', async () => {
162
+ const { base } = await mount({
163
+ result: {
164
+ status: 'partial',
165
+ results: [
166
+ { branch: 'ali/x', outcome: 'updated', from: 'a', to: 'b' },
167
+ {
168
+ branch: 'main',
169
+ outcome: 'conflict',
170
+ conflictedPaths: ['Plugins/x/SKILL.md'],
171
+ error: 'main is not in sync yet: Plugins/x/SKILL.md changed both in Hexis and on the git host. Recovery is queued; if this stays, open the files on main in Hexis, keep what you want, and save.',
172
+ },
173
+ ],
174
+ changeRequests: { closedDeletedBranch: 0 },
175
+ },
176
+ });
177
+ const res = await post(base, { headers: { authorization: `Bearer ${SECRET}` } });
178
+ expect(res.status).toBe(409);
179
+ const body = (await res.json()) as SyncResult;
180
+ expect(body.results[1]).toMatchObject({ outcome: 'conflict' });
181
+ expect((body.results[1] as { error: string }).error).toContain('Recovery is queued');
182
+ });
183
+
184
+ it('503 when a branch could not be pulled and none is in conflict', async () => {
185
+ const { base } = await mount({
186
+ result: {
187
+ status: 'partial',
188
+ results: [{ branch: 'main', outcome: 'error', error: 'could not read from remote' }],
189
+ changeRequests: { closedDeletedBranch: 0 },
190
+ },
191
+ });
192
+ expect((await post(base, { headers: { authorization: `Bearer ${SECRET}` } })).status).toBe(503);
193
+ });
194
+ });
195
+
196
+ describe('POST /api/sync/<branch> — the branch in the URL', () => {
197
+ it('syncs exactly that branch, spelled as segments or percent-encoded', async () => {
198
+ for (const spelling of ['main', 'ali/new-skill', 'ali%2Fnew-skill']) {
199
+ const { base, sync } = await mount({});
200
+ const res = await post(`${base}/${spelling}`, { headers: { authorization: `Bearer ${SECRET}` } });
201
+ expect(res.status).toBe(200);
202
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: [decodeURIComponent(spelling)] }));
203
+ server?.close();
204
+ }
205
+ });
206
+
207
+ it('ignores the body for branch selection — a GitHub Action can post anything', async () => {
208
+ const { base, sync } = await mount({});
209
+ await post(`${base}/main`, {
210
+ headers: { authorization: `Bearer ${SECRET}`, 'content-type': 'application/json' },
211
+ body: JSON.stringify({ branches: ['other'] }),
212
+ });
213
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['main'] }));
214
+ });
215
+
216
+ it('still checks the credential first', async () => {
217
+ const { base, sync } = await mount({});
218
+ expect((await post(`${base}/main`)).status).toBe(401);
219
+ expect(sync).not.toHaveBeenCalled();
220
+ });
221
+
222
+ it('400 for a branch name git would refuse', async () => {
223
+ const { base, sync } = await mount({});
224
+ const res = await post(`${base}/--upload-pack=x`, { headers: { authorization: `Bearer ${SECRET}` } });
225
+ expect(res.status).toBe(400);
226
+ expect(sync).not.toHaveBeenCalled();
227
+ });
228
+ });
229
+
230
+ describe('POST /api/sync — behind the production JSON parser', () => {
231
+ it('the branch route still sees the raw bytes, so a GitHub signature verifies', async () => {
232
+ const { base, sync } = await mount({ globalJson: true });
233
+ const body = '{"ref":"refs/heads/main"}';
234
+ const sig = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex');
235
+ const res = await post(`${base}/main`, {
236
+ headers: { 'x-hub-signature-256': sig, 'content-type': 'application/json' },
237
+ body,
238
+ });
239
+ expect(res.status).toBe(200);
240
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['main'] }));
241
+ });
242
+
243
+ it('and so does the bare route', async () => {
244
+ const { base } = await mount({ globalJson: true });
245
+ const body = '{"ref":"refs/heads/main"}';
246
+ const sig = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex');
247
+ const res = await post(base, {
248
+ headers: { 'x-hub-signature-256': sig, 'content-type': 'application/json' },
249
+ body,
250
+ });
251
+ expect(res.status).toBe(200);
252
+ });
253
+
254
+ it('isSyncRawBodyPath covers both shapes and nothing else', () => {
255
+ expect(isSyncRawBodyPath('/api/sync')).toBe(true);
256
+ expect(isSyncRawBodyPath('/api/sync/main')).toBe(true);
257
+ expect(isSyncRawBodyPath('/api/sync/ali/new-skill')).toBe(true);
258
+ expect(isSyncRawBodyPath('/api/synchronise')).toBe(false);
259
+ expect(isSyncRawBodyPath('/api/setup/status')).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe('POST /api/sync — the response marker', () => {
264
+ it('every answer carries the header a proxy never sets, including the 503 and the 401', async () => {
265
+ const ok = await mount({});
266
+ expect((await post(ok.base, { headers: { authorization: `Bearer ${SECRET}` } })).headers.get('x-hexis-sync')).toBe('result');
267
+ server?.close();
268
+ const down = await mount({
269
+ result: {
270
+ status: 'partial',
271
+ results: [{ branch: 'main', outcome: 'error', error: 'could not read from remote' }],
272
+ changeRequests: { closedDeletedBranch: 0 },
273
+ },
274
+ });
275
+ const res = await post(down.base, { headers: { authorization: `Bearer ${SECRET}` } });
276
+ expect(res.status).toBe(503);
277
+ expect(res.headers.get('x-hexis-sync')).toBe('result');
278
+ expect((await post(down.base)).headers.get('x-hexis-sync')).toBe('result');
279
+ });
280
+ });
281
+
282
+ describe('POST /api/Sync — an odd-cased URL', () => {
283
+ it('is matched by Express and by the raw-body carve-out alike, so it works exactly like the lower-case one', async () => {
284
+ const { base, sync } = await mount({ globalJson: true });
285
+ const oddCase = base.replace('/api/sync', '/api/Sync');
286
+ const body = '{"ref":"refs/heads/main"}';
287
+ const sig = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex');
288
+ const res = await post(`${oddCase}/main`, {
289
+ headers: { 'x-hub-signature-256': sig, 'content-type': 'application/json' },
290
+ body,
291
+ });
292
+ expect(res.status).toBe(200);
293
+ expect(sync).toHaveBeenCalledWith(expect.objectContaining({ branches: ['main'] }));
294
+ expect(isSyncRawBodyPath('/api/Sync')).toBe(true);
295
+ expect(isSyncRawBodyPath('/API/SYNC/main')).toBe(true);
296
+ });
297
+ });