@bevel-software/platform-core-backend 0.33.0 → 0.35.0

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 (602) hide show
  1. package/agent-guide/access-control.md +146 -289
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +239 -8
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/skills.md +67 -42
  11. package/agent-guide/start-here.md +65 -0
  12. package/agent-guide/tool-manuals.md +133 -155
  13. package/agent-guide/tools.md +67 -0
  14. package/agent-guide/where-a-new-file-goes.md +45 -28
  15. package/dist/core/catalog-revision.d.ts +12 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +23 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +41 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts.map +1 -1
  23. package/dist/core/create-core-server.js +15 -8
  24. package/dist/core/create-core-server.js.map +1 -1
  25. package/dist/core/create-core-services.d.ts +22 -5
  26. package/dist/core/create-core-services.d.ts.map +1 -1
  27. package/dist/core/create-core-services.js +44 -16
  28. package/dist/core/create-core-services.js.map +1 -1
  29. package/dist/core/lifecycle.d.ts +16 -0
  30. package/dist/core/lifecycle.d.ts.map +1 -1
  31. package/dist/core/lifecycle.js +12 -2
  32. package/dist/core/lifecycle.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/access/access-control.interface.d.ts +39 -9
  37. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.d.ts +15 -3
  39. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.js +239 -75
  41. package/dist/modules/access/access-control.service.js.map +1 -1
  42. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  43. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  44. package/dist/modules/access/access-mutation.service.js +48 -51
  45. package/dist/modules/access/access-mutation.service.js.map +1 -1
  46. package/dist/modules/access/access-view.d.ts.map +1 -1
  47. package/dist/modules/access/access-view.js +12 -11
  48. package/dist/modules/access/access-view.js.map +1 -1
  49. package/dist/modules/access/access.routes.d.ts +3 -0
  50. package/dist/modules/access/access.routes.d.ts.map +1 -1
  51. package/dist/modules/access/access.routes.js +58 -33
  52. package/dist/modules/access/access.routes.js.map +1 -1
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  54. package/dist/modules/access/groups-admin.routes.js +4 -2
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  56. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  58. package/dist/modules/access/groups-admin.service.js +80 -38
  59. package/dist/modules/access/groups-admin.service.js.map +1 -1
  60. package/dist/modules/access/groups-edit.d.ts +1 -1
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  62. package/dist/modules/access/groups-edit.js +1 -1
  63. package/dist/modules/access/groups-edit.js.map +1 -1
  64. package/dist/modules/access/index.d.ts +1 -1
  65. package/dist/modules/access/index.d.ts.map +1 -1
  66. package/dist/modules/access/reference-scan.d.ts +116 -24
  67. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  68. package/dist/modules/access/reference-scan.js +305 -44
  69. package/dist/modules/access/reference-scan.js.map +1 -1
  70. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  71. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  72. package/dist/modules/access/roles-admin.service.js +32 -23
  73. package/dist/modules/access/roles-admin.service.js.map +1 -1
  74. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  75. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  76. package/dist/modules/access-model/access-grammar.js +60 -20
  77. package/dist/modules/access-model/access-grammar.js.map +1 -1
  78. package/dist/modules/access-model/access-splice.d.ts +22 -18
  79. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  80. package/dist/modules/access-model/access-splice.js +20 -14
  81. package/dist/modules/access-model/access-splice.js.map +1 -1
  82. package/dist/modules/access-model/group-files.d.ts +1 -0
  83. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  84. package/dist/modules/access-model/group-files.js +5 -1
  85. package/dist/modules/access-model/group-files.js.map +1 -1
  86. package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
  87. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  88. package/dist/modules/agent-guide/agent-guide.js +103 -13
  89. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  90. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  91. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  92. package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
  93. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  94. package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
  95. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  96. package/dist/modules/agent-guide/guide-budget.js +67 -0
  97. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  98. package/dist/modules/agent-guide/index.d.ts +2 -1
  99. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  100. package/dist/modules/agent-guide/index.js +2 -1
  101. package/dist/modules/agent-guide/index.js.map +1 -1
  102. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  103. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  104. package/dist/modules/agent-instructions/compose.js +7 -1
  105. package/dist/modules/agent-instructions/compose.js.map +1 -1
  106. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  107. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  108. package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
  109. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  110. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  111. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  112. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  113. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  114. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  115. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  116. package/dist/modules/agent-scope/connection-scope.js +165 -0
  117. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  118. package/dist/modules/agent-scope/index.d.ts +3 -0
  119. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  120. package/dist/modules/agent-scope/index.js +3 -0
  121. package/dist/modules/agent-scope/index.js.map +1 -0
  122. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  123. package/dist/modules/audit/agent-audit.service.js +2 -0
  124. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  125. package/dist/modules/audit/audit.contract.d.ts +6 -0
  126. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  127. package/dist/modules/audit/audit.contract.js.map +1 -1
  128. package/dist/modules/auth/account-admission.d.ts +12 -0
  129. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  130. package/dist/modules/auth/account-admission.js +15 -0
  131. package/dist/modules/auth/account-admission.js.map +1 -1
  132. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  133. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  134. package/dist/modules/auth/auth.middleware.js +42 -4
  135. package/dist/modules/auth/auth.middleware.js.map +1 -1
  136. package/dist/modules/auth/auth.routes.d.ts +13 -9
  137. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  138. package/dist/modules/auth/auth.routes.js +35 -29
  139. package/dist/modules/auth/auth.routes.js.map +1 -1
  140. package/dist/modules/auth/auth.service.d.ts +55 -6
  141. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  142. package/dist/modules/auth/auth.service.js +128 -34
  143. package/dist/modules/auth/auth.service.js.map +1 -1
  144. package/dist/modules/auth/index.d.ts +1 -1
  145. package/dist/modules/auth/index.d.ts.map +1 -1
  146. package/dist/modules/auth/index.js +1 -1
  147. package/dist/modules/auth/index.js.map +1 -1
  148. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  149. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  150. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  151. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  152. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  153. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  154. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  155. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  156. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  157. package/dist/modules/database/core-schema.d.ts +68 -0
  158. package/dist/modules/database/core-schema.d.ts.map +1 -1
  159. package/dist/modules/database/core-schema.js +33 -0
  160. package/dist/modules/database/core-schema.js.map +1 -1
  161. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  162. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  163. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  164. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  165. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  166. package/dist/modules/embed/embed.tools.js +5 -0
  167. package/dist/modules/embed/embed.tools.js.map +1 -1
  168. package/dist/modules/kb-fs/index.d.ts +2 -0
  169. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  170. package/dist/modules/kb-fs/index.js +1 -0
  171. package/dist/modules/kb-fs/index.js.map +1 -1
  172. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  173. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  174. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  175. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  176. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  177. package/dist/modules/kb-fs/repo-path.js +22 -7
  178. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  179. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  180. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  181. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  182. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  183. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  184. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  185. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  186. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  187. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  188. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  189. package/dist/modules/mcp/mcp.routes.js +39 -4
  190. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  191. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  192. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  193. package/dist/modules/mcp/mcp.service.js +74 -48
  194. package/dist/modules/mcp/mcp.service.js.map +1 -1
  195. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  196. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  197. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  198. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  199. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  200. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  201. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  202. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  203. package/dist/modules/plugins/index.d.ts +1 -1
  204. package/dist/modules/plugins/index.d.ts.map +1 -1
  205. package/dist/modules/plugins/index.js.map +1 -1
  206. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  207. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  208. package/dist/modules/plugins/plugin-links.js.map +1 -1
  209. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  210. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  211. package/dist/modules/plugins/plugins.routes.js +32 -5
  212. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  213. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
  214. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  215. package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
  216. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  217. package/dist/modules/secrets-vault/index.d.ts +1 -1
  218. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  219. package/dist/modules/secrets-vault/index.js +1 -1
  220. package/dist/modules/secrets-vault/index.js.map +1 -1
  221. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
  222. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  223. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  224. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  225. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  226. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  227. package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
  228. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  229. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  230. package/dist/modules/settings/oidc-check.js +5 -17
  231. package/dist/modules/settings/oidc-check.js.map +1 -1
  232. package/dist/modules/skills/index.d.ts +3 -3
  233. package/dist/modules/skills/index.d.ts.map +1 -1
  234. package/dist/modules/skills/index.js +0 -2
  235. package/dist/modules/skills/index.js.map +1 -1
  236. package/dist/modules/skills/skills.contract.d.ts +16 -0
  237. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  238. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  239. package/dist/modules/skills/skills.service.js +6 -1
  240. package/dist/modules/skills/skills.service.js.map +1 -1
  241. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  242. package/dist/modules/skills/skills.tools.js +25 -13
  243. package/dist/modules/skills/skills.tools.js.map +1 -1
  244. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  245. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  246. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  247. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  248. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  249. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  250. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  251. package/dist/modules/tool-auth/index.d.ts +1 -1
  252. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  253. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  254. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  255. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  256. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  257. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  258. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  259. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  260. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  261. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  262. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  263. package/dist/modules/tool-helpers/tool-context.js +3 -0
  264. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  265. package/dist/modules/tool-helpers/tool-def.js +1 -1
  266. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  267. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  268. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  269. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  270. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  271. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  272. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  273. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  274. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  275. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  276. package/dist/modules/tool-helpers/validate-token.js +1 -1
  277. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  278. package/dist/modules/tool-manuals/index.d.ts +3 -3
  279. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  280. package/dist/modules/tool-manuals/index.js +0 -2
  281. package/dist/modules/tool-manuals/index.js.map +1 -1
  282. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  283. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  284. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  285. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  286. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
  287. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  288. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  289. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  290. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  291. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  292. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
  293. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  294. package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
  295. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  296. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
  297. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  298. package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
  299. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  300. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  301. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  302. package/dist/modules/tool-registry/manual.routes.js +27 -7
  303. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  304. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  305. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  306. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  307. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  308. package/dist/modules/workflow/agent-tools/workflow.tools.js +36 -27
  309. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  310. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  311. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  312. package/dist/modules/workflow/git/git.service.js +20 -35
  313. package/dist/modules/workflow/git/git.service.js.map +1 -1
  314. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  315. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  316. package/dist/modules/workflow/workflow.service.js +3 -0
  317. package/dist/modules/workflow/workflow.service.js.map +1 -1
  318. package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
  319. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  320. package/dist/modules/workspace/agent-access.gate.js +1 -1
  321. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  322. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  323. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  324. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  325. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  326. package/dist/modules/workspace/index.d.ts +8 -0
  327. package/dist/modules/workspace/index.d.ts.map +1 -1
  328. package/dist/modules/workspace/index.js +8 -0
  329. package/dist/modules/workspace/index.js.map +1 -1
  330. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  331. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  332. package/dist/modules/workspace/nul-scan.js +55 -0
  333. package/dist/modules/workspace/nul-scan.js.map +1 -0
  334. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  335. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  336. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  337. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  338. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  339. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  340. package/dist/modules/workspace/ripgrep-search.js +657 -0
  341. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  342. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  343. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  344. package/dist/modules/workspace/ripgrep-version.js +31 -0
  345. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  346. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  347. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  348. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  349. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  350. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  351. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  352. package/dist/modules/workspace/worker-pool.js.map +1 -1
  353. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  354. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  355. package/dist/modules/workspace/workspace-tmp.js +435 -0
  356. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  357. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  358. package/dist/modules/workspace/workspace.routes.js +5 -26
  359. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  360. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  361. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  362. package/dist/modules/workspace/workspace.service.js +86 -6
  363. package/dist/modules/workspace/workspace.service.js.map +1 -1
  364. package/dist/modules/workspace/workspace.tools.d.ts +3 -2
  365. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  366. package/dist/modules/workspace/workspace.tools.js +531 -137
  367. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  368. package/dist/shared/oauth-token-error.d.ts +23 -0
  369. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  370. package/dist/shared/oauth-token-error.js +47 -0
  371. package/dist/shared/oauth-token-error.js.map +1 -0
  372. package/dist/shared/path-order.d.ts +28 -8
  373. package/dist/shared/path-order.d.ts.map +1 -1
  374. package/dist/shared/path-order.js +37 -8
  375. package/dist/shared/path-order.js.map +1 -1
  376. package/dist/shared/read-capped.d.ts +19 -0
  377. package/dist/shared/read-capped.d.ts.map +1 -0
  378. package/dist/shared/read-capped.js +45 -0
  379. package/dist/shared/read-capped.js.map +1 -0
  380. package/migrations/0022_session_version.sql +6 -0
  381. package/migrations/0023_agent_path.sql +3 -0
  382. package/migrations/meta/0022_snapshot.json +2360 -0
  383. package/migrations/meta/0023_snapshot.json +2378 -0
  384. package/migrations/meta/_journal.json +14 -0
  385. package/package.json +4 -3
  386. package/src/__tests__/hexis-catalog.ts +144 -0
  387. package/src/__tests__/pg-scratch.test.ts +69 -0
  388. package/src/__tests__/pg-scratch.ts +31 -0
  389. package/src/__tests__/real-disk.ts +21 -0
  390. package/src/__tests__/slow-ripgrep.ts +65 -0
  391. package/src/__tests__/tokens.ts +19 -0
  392. package/src/__tests__/tool-manuals.ts +32 -0
  393. package/src/__tests__/tool-wiring.ts +25 -0
  394. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  395. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  396. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  397. package/src/core/__tests__/gated-boot.test.ts +8 -1
  398. package/src/core/__tests__/lifecycle.test.ts +46 -13
  399. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  400. package/src/core/catalog-revision.ts +24 -2
  401. package/src/core/core-ports.ts +42 -0
  402. package/src/core/create-core-server.ts +14 -1
  403. package/src/core/create-core-services.ts +62 -15
  404. package/src/core/lifecycle.ts +26 -2
  405. package/src/index.ts +1 -1
  406. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  407. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  408. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  409. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  410. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  411. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  412. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  413. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  414. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  415. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  416. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  417. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  418. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  419. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  420. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  421. package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
  422. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  423. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  424. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  425. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  426. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  427. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  428. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  429. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  430. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  431. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  432. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  433. package/src/modules/access/access-control.interface.ts +43 -10
  434. package/src/modules/access/access-control.service.ts +250 -72
  435. package/src/modules/access/access-mutation.service.ts +48 -53
  436. package/src/modules/access/access-view.ts +12 -10
  437. package/src/modules/access/access.routes.ts +66 -37
  438. package/src/modules/access/groups-admin.routes.ts +4 -2
  439. package/src/modules/access/groups-admin.service.ts +114 -44
  440. package/src/modules/access/groups-edit.ts +1 -1
  441. package/src/modules/access/index.ts +1 -1
  442. package/src/modules/access/reference-scan.ts +319 -46
  443. package/src/modules/access/roles-admin.service.ts +43 -26
  444. package/src/modules/access/synced-groups-writer.ts +1 -1
  445. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  446. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  447. package/src/modules/access-model/access-grammar.ts +65 -20
  448. package/src/modules/access-model/access-splice.ts +31 -23
  449. package/src/modules/access-model/group-files.ts +5 -0
  450. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
  451. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
  452. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  453. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  454. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  455. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  456. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  457. package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
  458. package/src/modules/agent-guide/agent-guide.ts +120 -13
  459. package/src/modules/agent-guide/guide-budget.ts +73 -0
  460. package/src/modules/agent-guide/index.ts +12 -0
  461. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
  462. package/src/modules/agent-instructions/compose.ts +7 -1
  463. package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
  464. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  465. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  466. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  467. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  468. package/src/modules/agent-scope/connection-scope.ts +199 -0
  469. package/src/modules/agent-scope/index.ts +15 -0
  470. package/src/modules/audit/agent-audit.service.ts +2 -0
  471. package/src/modules/audit/audit.contract.ts +6 -0
  472. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  473. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  474. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  475. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  476. package/src/modules/auth/account-admission.ts +19 -0
  477. package/src/modules/auth/auth.middleware.ts +45 -4
  478. package/src/modules/auth/auth.routes.ts +46 -29
  479. package/src/modules/auth/auth.service.ts +135 -34
  480. package/src/modules/auth/index.ts +2 -0
  481. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  482. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  483. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  484. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  485. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  486. package/src/modules/database/core-schema.ts +33 -0
  487. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  488. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  489. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  490. package/src/modules/embed/embed.tools.ts +5 -0
  491. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  492. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  493. package/src/modules/kb-fs/index.ts +2 -0
  494. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  495. package/src/modules/kb-fs/repo-path.ts +33 -7
  496. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  497. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  498. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  499. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  500. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  501. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  502. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  503. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  504. package/src/modules/mcp/mcp.routes.ts +39 -3
  505. package/src/modules/mcp/mcp.service.ts +112 -52
  506. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  507. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  508. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  509. package/src/modules/plugins/index.ts +1 -1
  510. package/src/modules/plugins/plugin-links.ts +19 -2
  511. package/src/modules/plugins/plugins.routes.ts +35 -4
  512. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  513. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
  514. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  515. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  516. package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
  517. package/src/modules/secrets-vault/index.ts +1 -1
  518. package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
  519. package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
  520. package/src/modules/settings/oidc-check.ts +5 -18
  521. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  522. package/src/modules/skills/index.ts +7 -2
  523. package/src/modules/skills/skills.contract.ts +17 -0
  524. package/src/modules/skills/skills.service.ts +5 -1
  525. package/src/modules/skills/skills.tools.ts +45 -14
  526. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  527. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  528. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  529. package/src/modules/tool-auth/index.ts +1 -1
  530. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  531. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  532. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
  533. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  534. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  535. package/src/modules/tool-helpers/tool-context.ts +10 -0
  536. package/src/modules/tool-helpers/tool-def.ts +1 -1
  537. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  538. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  539. package/src/modules/tool-helpers/validate-token.ts +4 -2
  540. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  541. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  542. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  543. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
  544. package/src/modules/tool-manuals/index.ts +6 -2
  545. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  546. package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
  547. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  548. package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
  549. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
  550. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
  551. package/src/modules/tool-registry/manual.routes.ts +26 -5
  552. package/src/modules/tool-registry/tool.contract.ts +7 -0
  553. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  554. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  556. package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
  557. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  558. package/src/modules/workflow/git/git.service.ts +29 -48
  559. package/src/modules/workflow/workflow.service.ts +5 -0
  560. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  561. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  562. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  563. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  564. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  565. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  566. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  567. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  568. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
  569. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  570. package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
  571. package/src/modules/workspace/agent-access.gate.ts +1 -1
  572. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  573. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  574. package/src/modules/workspace/index.ts +20 -0
  575. package/src/modules/workspace/nul-scan.ts +50 -0
  576. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  577. package/src/modules/workspace/ripgrep-search.ts +715 -0
  578. package/src/modules/workspace/ripgrep-version.ts +35 -0
  579. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  580. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  581. package/src/modules/workspace/worker-pool.ts +3 -4
  582. package/src/modules/workspace/workspace-tmp.ts +457 -0
  583. package/src/modules/workspace/workspace.routes.ts +6 -27
  584. package/src/modules/workspace/workspace.service.ts +92 -15
  585. package/src/modules/workspace/workspace.tools.ts +574 -176
  586. package/src/shared/__tests__/read-capped.test.ts +68 -0
  587. package/src/shared/oauth-token-error.ts +46 -0
  588. package/src/shared/path-order.ts +37 -8
  589. package/src/shared/read-capped.ts +40 -0
  590. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  591. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  592. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  593. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  594. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  595. package/agent-guide/finding-things.md +0 -7
  596. package/agent-guide/introduction.md +0 -27
  597. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  598. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  599. package/dist/modules/workspace/bounded-matcher.js +0 -200
  600. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  601. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  602. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -1,9 +1,10 @@
1
1
  import { describe, expect, it, vi, afterEach } from 'vitest';
2
2
  import { createHash } from 'node:crypto';
3
- import { DbSecretsVaultService } from '../db-secrets-vault.service.js';
4
- import { SecretOAuthError } from '../secrets-vault.contract.js';
3
+ import { DbSecretsVaultService, InvalidSecretError, SecretOAuthError } from '../index.js';
5
4
  import { TokenCrypto } from '../../../shared/token-crypto.js';
6
- import type { Database } from '../../database/connection.js';
5
+ import type { Database } from '../../database/index.js';
6
+ import { setLogger } from '../../../shared/logging.js';
7
+ import type { ILogger, LogFields } from '../../../shared/logger.contract.js';
7
8
 
8
9
  const ENC_KEY = Buffer.alloc(32, 7).toString('base64');
9
10
  const crypto = new TokenCrypto(ENC_KEY);
@@ -595,3 +596,429 @@ describe('DbSecretsVaultService — forceRefresh (a downstream rejected the toke
595
596
  expect(fetchMock).not.toHaveBeenCalled();
596
597
  });
597
598
  });
599
+
600
+ describe("DbSecretsVaultService — a refused token request names the provider's error code, never its text", () => {
601
+ const confidentialRow = (blob: Record<string, unknown>) =>
602
+ sharedRow({
603
+ id: 'user-row-1',
604
+ userId: 'user-1',
605
+ oauthMeta: { ...PUBLIC_META, publicClient: false },
606
+ valueEncrypted: crypto.encrypt(JSON.stringify(blob)),
607
+ });
608
+ const SECRET = 'client-secret-1';
609
+ const BLOB = { clientSecret: SECRET, pendingVerifier: 'the-verifier' };
610
+ const INVALID_CLIENT =
611
+ 'Token endpoint returned HTTP 400 (invalid_client). The client id or client secret this sign-in uses is not accepted by the provider. The tool owner should check the declared client id and set the client secret again.';
612
+
613
+ async function exchangeFailure(
614
+ body: string | ReadableStream<Uint8Array>,
615
+ status = 400,
616
+ blob: Record<string, unknown> = BLOB,
617
+ ): Promise<SecretOAuthError> {
618
+ const { db } = makeFakeDb([[confidentialRow(blob)]]);
619
+ const svc = new DbSecretsVaultService(db, ENC_KEY);
620
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(body, { status })));
621
+ try {
622
+ await svc.completeOAuth('user-1', 'user-row-1', 'the-code', 'https://bevel.example.com/cb');
623
+ } catch (err) {
624
+ expect(err).toBeInstanceOf(SecretOAuthError);
625
+ return err as SecretOAuthError;
626
+ }
627
+ throw new Error('the exchange was expected to fail');
628
+ }
629
+
630
+ it('names an OAuth error code and says what to do about it, with the status', async () => {
631
+ const client = await exchangeFailure(
632
+ JSON.stringify({ error: 'invalid_client', error_description: 'Client authentication failed' }),
633
+ );
634
+ expect(client.message).toBe(INVALID_CLIENT);
635
+ expect(client.providerStatus).toBe(400);
636
+ const grant = await exchangeFailure(JSON.stringify({ error: 'invalid_grant' }));
637
+ expect(grant.message).toBe(
638
+ 'Token endpoint returned HTTP 400 (invalid_grant). The provider refused the grant: for a sign-in, a redirect URL mismatch or an expired or reused authorization code; for a refresh, a refresh token that has expired or been revoked. Sign in again.',
639
+ );
640
+ const scope = await exchangeFailure(JSON.stringify({ error: 'invalid_scope' }));
641
+ expect(scope.message).toBe(
642
+ "Token endpoint returned HTTP 400 (invalid_scope). The provider refused the scopes this sign-in asks for. The tool owner should check the declared scopes against the provider's app.",
643
+ );
644
+ const unauthorized = await exchangeFailure(JSON.stringify({ error: 'unauthorized_client' }));
645
+ expect(unauthorized.message).toBe(
646
+ "Token endpoint returned HTTP 400 (unauthorized_client). The provider does not allow this client to use this grant. The tool owner should check the provider's app settings.",
647
+ );
648
+ });
649
+
650
+ it("names a provider's own status code (HubSpot's shape) with the sentence of its OAuth equivalent", async () => {
651
+ const err = await exchangeFailure(
652
+ JSON.stringify({ status: 'BAD_CLIENT_SECRET', message: 'client secret does not match', correlationId: 'c1' }),
653
+ );
654
+ expect(err.message).toBe(INVALID_CLIENT.replace('(invalid_client)', '(BAD_CLIENT_SECRET)'));
655
+ });
656
+
657
+ it('takes nothing of the body but the code: descriptions, unknown shapes, plain text and cut bodies leave no trace', async () => {
658
+ const withDescription = JSON.stringify({ error: 'invalid_client', error_description: `secret ${SECRET} rejected` });
659
+ expect((await exchangeFailure(withDescription)).message).toBe(INVALID_CLIENT);
660
+ for (const body of [
661
+ JSON.stringify({ detail: `${SECRET} rejected` }),
662
+ `<html>${SECRET} rejected</html>`,
663
+ `${SECRET} rejected\nmore`,
664
+ JSON.stringify({ error: 'invalid_client', error_description: SECRET }).slice(0, 30),
665
+ JSON.stringify(`${SECRET} rejected`),
666
+ JSON.stringify([`${SECRET} rejected`]),
667
+ '{}',
668
+ '',
669
+ ]) {
670
+ expect((await exchangeFailure(body)).message).toBe('Token endpoint returned HTTP 400');
671
+ }
672
+ });
673
+
674
+ it('takes a code only when it is exactly one Hexis knows: anything else in that field is not a code', async () => {
675
+ // A secret, a secret with a suffix, a near-miss of a known code, a
676
+ // prototype key, free text: none is on the list, none reaches a message.
677
+ for (const code of [
678
+ 'wordy_secret',
679
+ 'wordy_secret_rejected',
680
+ 'Invalid_Client',
681
+ 'invalid_client ',
682
+ 'invalid_token',
683
+ 'constructor',
684
+ 'bad code',
685
+ '',
686
+ ]) {
687
+ const err = await exchangeFailure(JSON.stringify({ error: code }), 400, {
688
+ clientSecret: 'wordy_secret',
689
+ pendingVerifier: 'the-verifier',
690
+ });
691
+ expect(err.message).toBe('Token endpoint returned HTTP 400');
692
+ }
693
+ });
694
+
695
+ it('keeps the status when the refusal body cannot be read, so a refused refresh still retires the grant', async () => {
696
+ const failing = () =>
697
+ new ReadableStream<Uint8Array>({
698
+ pull() {
699
+ throw new Error('connection reset');
700
+ },
701
+ });
702
+ const err = await exchangeFailure(failing(), 401);
703
+ expect(err.message).toBe('Token endpoint returned HTTP 401');
704
+ expect(err.providerStatus).toBe(401);
705
+
706
+ const { db, captured } = makeFakeDb([
707
+ [confidentialRow({ clientSecret: SECRET, tokens: { access_token: 'at', refresh_token: 'rt', expires_at: 1 } })],
708
+ undefined,
709
+ ]);
710
+ const svc = new DbSecretsVaultService(db, ENC_KEY);
711
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(failing(), { status: 400 })));
712
+ await expect(svc.forceRefresh('user-1', KEY)).resolves.toBe('rejected');
713
+ expect(JSON.parse(crypto.decrypt(captured.set[0].valueEncrypted))).toEqual({ clientSecret: SECRET });
714
+ });
715
+
716
+ it('reads a refusal body only up to its cap and cancels the rest of the stream', async () => {
717
+ // Without the cap this never returns: the stream has no end.
718
+ let pulls = 0;
719
+ let cancelled = false;
720
+ const endless = new ReadableStream<Uint8Array>({
721
+ pull(controller) {
722
+ pulls += 1;
723
+ controller.enqueue(new TextEncoder().encode('z'.repeat(1024)));
724
+ },
725
+ cancel() {
726
+ cancelled = true;
727
+ },
728
+ });
729
+ const err = await exchangeFailure(endless, 502);
730
+ expect(err.message).toBe('Token endpoint returned HTTP 502');
731
+ expect(pulls).toBeLessThanOrEqual(3);
732
+ expect(cancelled).toBe(true);
733
+ });
734
+
735
+ it('a refused refresh is logged with the key, the outcome and the code as the sign-in is wiped', async () => {
736
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
737
+ const row = confidentialRow({
738
+ clientSecret: SECRET,
739
+ tokens: { access_token: 'at-old', refresh_token: 'rt-old', expires_at: 1 },
740
+ });
741
+ const { db } = makeFakeDb([[row], undefined]);
742
+ const svc = new DbSecretsVaultService(db, ENC_KEY);
743
+ vi.stubGlobal(
744
+ 'fetch',
745
+ vi.fn(
746
+ async () =>
747
+ new Response(JSON.stringify({ error: 'invalid_grant', error_description: 'rt-old was revoked' }), {
748
+ status: 400,
749
+ }),
750
+ ),
751
+ );
752
+
753
+ await expect(svc.forceRefresh('user-1', KEY)).resolves.toBe('rejected');
754
+
755
+ const line = warn.mock.calls.map((c) => String(c[0])).find((m) => m.includes('refresh refused'));
756
+ expect(line).toContain(`refresh refused for "${KEY}", sign-in wiped: Token endpoint returned HTTP 400 (invalid_grant).`);
757
+ expect(line).not.toContain('rt-old');
758
+ warn.mockRestore();
759
+ });
760
+
761
+ it('a refused refresh that lost to a concurrent rotation logs that the rotated sign-in was kept, not wiped', async () => {
762
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
763
+ const row = confidentialRow({
764
+ clientSecret: SECRET,
765
+ tokens: { access_token: 'at-old', refresh_token: 'rt-old', expires_at: 1 },
766
+ });
767
+ const rotated = crypto.encrypt(
768
+ JSON.stringify({ clientSecret: SECRET, tokens: { access_token: 'rotated-at', refresh_token: 'rt-2' } }),
769
+ );
770
+ // The row moved on between our read and our guarded wipe: the wipe matches nothing.
771
+ const { db, updates } = whereAwareDb({ ...row, valueEncrypted: rotated }, row.valueEncrypted);
772
+ const svc = new DbSecretsVaultService(db, ENC_KEY);
773
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })));
774
+
775
+ await expect(svc.forceRefresh('user-1', KEY)).resolves.toBe('refreshed');
776
+ expect(updates).toEqual([]);
777
+
778
+ const line = warn.mock.calls.map((c) => String(c[0])).find((m) => m.includes('refresh refused'));
779
+ expect(line).toContain(`refresh refused for "${KEY}", the row had been rotated by a concurrent refresh meanwhile`);
780
+ expect(line).toContain('the rotated sign-in is kept: Token endpoint returned HTTP 400 (invalid_grant)');
781
+ expect(line).not.toContain('sign-in wiped');
782
+ warn.mockRestore();
783
+ });
784
+
785
+ it('a refused refresh whose wipe matched nothing because the row was replaced logs that nothing was wiped', async () => {
786
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
787
+ const row = confidentialRow({
788
+ clientSecret: SECRET,
789
+ tokens: { access_token: 'at-old', refresh_token: 'rt-old', expires_at: 1 },
790
+ });
791
+ // Read as the OAuth row it was; by the time the wipe writes, an edit has
792
+ // made it a static value, so the guarded wipe matches nothing and no grant
793
+ // is there to serve: `rejected`, yet nothing was wiped.
794
+ const { db, updates } = whereAwareDb(
795
+ { ...row, kind: 'static', valueEncrypted: crypto.encrypt(JSON.stringify({ value: 'x' })) },
796
+ { valueEncrypted: row.valueEncrypted, kind: 'oauth' },
797
+ );
798
+ const svc = new DbSecretsVaultService(db, ENC_KEY);
799
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400 })));
800
+
801
+ await expect(svc.forceRefresh('user-1', KEY)).resolves.toBe('rejected');
802
+ expect(updates).toEqual([]);
803
+
804
+ const line = warn.mock.calls.map((c) => String(c[0])).find((m) => m.includes('refresh refused'));
805
+ expect(line).toContain(`refresh refused for "${KEY}", the row had been removed or replaced meanwhile; nothing wiped:`);
806
+ expect(line).not.toContain('sign-in wiped');
807
+ warn.mockRestore();
808
+ });
809
+ });
810
+
811
+ describe('DbSecretsVaultService — a declared sign-in follows the live declaration', () => {
812
+ // What the owner's client-secret save stored: client id A and its endpoints.
813
+ const SAVED_META = {
814
+ authorizationUrl: 'https://old.example.com/authorize',
815
+ tokenUrl: 'https://old.example.com/token',
816
+ clientId: 'client-A',
817
+ scopes: ['crm.read'],
818
+ authParams: {},
819
+ pkce: true,
820
+ resource: 'https://mcp.example.com/',
821
+ };
822
+ // What the tool file reads now: client id B.
823
+ const LIVE = {
824
+ authorizationUrl: 'https://app.example.com/oauth/authorize',
825
+ tokenUrl: 'https://api.example.com/oauth/v1/token',
826
+ clientId: 'client-B',
827
+ scopes: ['crm.read', 'crm.write'],
828
+ authParams: { optional_scope: 'tickets' },
829
+ pkce: true,
830
+ resource: 'https://mcp.example.com/',
831
+ };
832
+ const confidentialShared = () =>
833
+ sharedRow({ oauthMeta: SAVED_META, valueEncrypted: crypto.encrypt(JSON.stringify({ clientSecret: 'cs-owner' })) });
834
+
835
+ const begin = (svc: DbSecretsVaultService, provider: Record<string, unknown> = LIVE) =>
836
+ svc.beginToolOAuthByKey({
837
+ userId: 'user-1',
838
+ key: KEY,
839
+ redirectUri: 'https://bevel.example.com/api/secrets/oauth/callback',
840
+ state: 's',
841
+ provider: provider as never,
842
+ });
843
+
844
+ it('builds the consent URL from the live declaration, not the stored copy, and records it on the caller row', async () => {
845
+ const { db, captured } = makeFakeDb([[confidentialShared()], [], [{ id: 'user-row-1' }]]);
846
+ const { url } = await begin(new DbSecretsVaultService(db, ENC_KEY));
847
+
848
+ const consent = new URL(url);
849
+ expect(consent.origin + consent.pathname).toBe(LIVE.authorizationUrl);
850
+ expect(consent.searchParams.get('client_id')).toBe('client-B');
851
+ expect(consent.searchParams.get('resource')).toBe(LIVE.resource);
852
+ expect(consent.searchParams.get('scope')).toBe('crm.read crm.write');
853
+ expect(consent.searchParams.get('optional_scope')).toBe('tickets');
854
+ const blob = JSON.parse(crypto.decrypt(captured.values[0].valueEncrypted));
855
+ expect(consent.searchParams.get('code_challenge')).toBe(
856
+ createHash('sha256').update(blob.pendingVerifier).digest('base64url'),
857
+ );
858
+ // The secret is the owner's, unchanged; the configuration is the file's.
859
+ expect(blob.clientSecret).toBe('cs-owner');
860
+ expect(captured.values[0].oauthMeta).toMatchObject({ clientId: 'client-B', tokenUrl: LIVE.tokenUrl });
861
+ expect(captured.values[0].oauthMeta.publicClient).toBeUndefined();
862
+ });
863
+
864
+ it('omits PKCE when the declaration opts out', async () => {
865
+ const { db, captured } = makeFakeDb([[confidentialShared()], [], [{ id: 'user-row-1' }]]);
866
+ const { url } = await begin(new DbSecretsVaultService(db, ENC_KEY), { ...LIVE, pkce: false });
867
+ expect(new URL(url).searchParams.get('code_challenge')).toBeNull();
868
+ expect(JSON.parse(crypto.decrypt(captured.values[0].valueEncrypted)).pendingVerifier).toBeUndefined();
869
+ });
870
+
871
+ it('the exchange that completes it posts to the live token endpoint with client id B and the stored secret', async () => {
872
+ const start = makeFakeDb([[confidentialShared()], [], [{ id: 'user-row-1' }]]);
873
+ await begin(new DbSecretsVaultService(start.db, ENC_KEY));
874
+ // The callback reads the caller row exactly as the start wrote it.
875
+ const written = start.captured.values[0];
876
+ const userRow = sharedRow({ id: 'user-row-1', userId: 'user-1', ...written });
877
+ const { db } = makeFakeDb([[userRow], undefined]);
878
+ const fetchMock = vi.fn<(url: unknown, init?: RequestInit) => Promise<Response>>(
879
+ async () =>
880
+ new Response(JSON.stringify({ access_token: 'at', refresh_token: 'rt', expires_in: 3600 }), { status: 200 }),
881
+ );
882
+ vi.stubGlobal('fetch', fetchMock);
883
+
884
+ await new DbSecretsVaultService(db, ENC_KEY).completeOAuth('user-1', 'user-row-1', 'code', 'https://bevel.example.com/cb');
885
+
886
+ expect(String(fetchMock.mock.calls[0][0])).toBe(LIVE.tokenUrl);
887
+ const body = new URLSearchParams(String(fetchMock.mock.calls[0][1]?.body));
888
+ expect(body.get('client_id')).toBe('client-B');
889
+ expect(body.get('client_secret')).toBe('cs-owner');
890
+ expect(body.get('resource')).toBe(LIVE.resource);
891
+ });
892
+
893
+ it('a later refresh of that grant uses the configuration the consent was given for', async () => {
894
+ const userRow = sharedRow({
895
+ id: 'user-row-1',
896
+ userId: 'user-1',
897
+ oauthMeta: { ...LIVE },
898
+ valueEncrypted: crypto.encrypt(
899
+ JSON.stringify({ clientSecret: 'cs-owner', tokens: { access_token: 'old', refresh_token: 'rt', expires_at: 1_000 } }),
900
+ ),
901
+ });
902
+ const { db } = makeFakeDb([[userRow], undefined]);
903
+ const fetchMock = vi.fn<(url: unknown, init?: RequestInit) => Promise<Response>>(
904
+ async () =>
905
+ new Response(JSON.stringify({ access_token: 'fresh', expires_in: 3600 }), { status: 200 }),
906
+ );
907
+ vi.stubGlobal('fetch', fetchMock);
908
+
909
+ await expect(
910
+ new DbSecretsVaultService(db, ENC_KEY, undefined, async () => 'user' as const).resolve('user-1', KEY),
911
+ ).resolves.toBe('fresh');
912
+ expect(String(fetchMock.mock.calls[0][0])).toBe(LIVE.tokenUrl);
913
+ expect(new URLSearchParams(String(fetchMock.mock.calls[0][1]?.body)).get('client_id')).toBe('client-B');
914
+ });
915
+
916
+ it.each([
917
+ ['an http:// token endpoint', { tokenUrl: 'http://api.example.com/token' }, 'tokenUrl must use https'],
918
+ ['a loopback token endpoint', { tokenUrl: 'https://127.0.0.1/token' }, 'tokenUrl host is not allowed'],
919
+ ['a private token endpoint', { tokenUrl: 'https://10.0.0.5/token' }, 'tokenUrl host is not allowed'],
920
+ ['a link-local authorize endpoint', { authorizationUrl: 'https://169.254.169.254/authorize' }, 'authorizationUrl host is not allowed'],
921
+ ])('refuses %s with the save’s validation message, writing and sending nothing', async (_name, over, message) => {
922
+ const { db } = makeFakeDb([[confidentialShared()]]);
923
+ const fetchMock = vi.fn();
924
+ vi.stubGlobal('fetch', fetchMock);
925
+ const err = await begin(new DbSecretsVaultService(db, ENC_KEY), { ...LIVE, ...over }).catch((e: unknown) => e);
926
+ expect(err).toBeInstanceOf(InvalidSecretError);
927
+ expect((err as Error).message).toBe(message);
928
+ expect(db.insert).not.toHaveBeenCalled();
929
+ expect(fetchMock).not.toHaveBeenCalled();
930
+ });
931
+
932
+ it("refuses with \"owner hasn't finished\" when the shared row holds no secret, whatever it says of itself", async () => {
933
+ // A row the platform once registered as a public client, under a file that
934
+ // now declares its own client: the declared client is confidential.
935
+ const { db } = makeFakeDb([[sharedRow()]]);
936
+ await expect(begin(new DbSecretsVaultService(db, ENC_KEY))).rejects.toThrow("hasn't finished setting this up");
937
+ expect(db.insert).not.toHaveBeenCalled();
938
+ });
939
+
940
+ it('keeps the tokens a re-entered sign-in already holds when the declaration is unchanged, key order notwithstanding', async () => {
941
+ // jsonb does not keep key order: the stored copy reads back reordered.
942
+ const reordered = Object.fromEntries(Object.entries({ ...LIVE }).reverse());
943
+ const existing = sharedRow({
944
+ id: 'user-row-1',
945
+ userId: 'user-1',
946
+ oauthMeta: reordered,
947
+ valueEncrypted: crypto.encrypt(JSON.stringify({ clientSecret: 'cs-owner', tokens: { access_token: 'held' } })),
948
+ });
949
+ const { db, captured } = makeFakeDb([[confidentialShared()], [existing], [{ id: 'user-row-1' }]]);
950
+ await begin(new DbSecretsVaultService(db, ENC_KEY));
951
+ expect(JSON.parse(crypto.decrypt(captured.values[0].valueEncrypted)).tokens).toEqual({ access_token: 'held' });
952
+ });
953
+
954
+ it('does not carry tokens minted under client A into a sign-in under client B', async () => {
955
+ const existing = sharedRow({
956
+ id: 'user-row-1',
957
+ userId: 'user-1',
958
+ oauthMeta: SAVED_META,
959
+ valueEncrypted: crypto.encrypt(JSON.stringify({ clientSecret: 'cs-owner', tokens: { access_token: 'under-A' } })),
960
+ });
961
+ const { db, captured } = makeFakeDb([[confidentialShared()], [existing], [{ id: 'user-row-1' }]]);
962
+ await begin(new DbSecretsVaultService(db, ENC_KEY));
963
+ expect(JSON.parse(crypto.decrypt(captured.values[0].valueEncrypted)).tokens).toBeUndefined();
964
+ });
965
+
966
+ it('a platform-registered client (no provider handed over) still starts from its stored row', async () => {
967
+ const { db, captured } = makeFakeDb([[sharedRow()], [], [{ id: 'user-row-1' }]]);
968
+ const { url } = await new DbSecretsVaultService(db, ENC_KEY).beginToolOAuthByKey({
969
+ userId: 'user-1',
970
+ key: KEY,
971
+ redirectUri: 'https://bevel.example.com/cb',
972
+ state: 's',
973
+ });
974
+ expect(new URL(url).searchParams.get('client_id')).toBe('dcr-client-1');
975
+ expect(captured.values[0].oauthMeta).toEqual(PUBLIC_META);
976
+ });
977
+
978
+ // The moved property, pinned in both directions: the secret used to be bound to
979
+ // the token endpoint recorded at its save; it now goes where the file says, and
980
+ // the move leaves a line naming both origins (never a path) for an operator.
981
+ describe('where the stored secret goes when the declaration moves the token endpoint', () => {
982
+ const capture = () => {
983
+ const lines: Array<{ message: string; fields?: LogFields }> = [];
984
+ const make = (): ILogger => ({
985
+ debug: () => {},
986
+ info: () => {},
987
+ warn: (message, fields) => void lines.push({ message, fields }),
988
+ error: () => {},
989
+ child: () => make(),
990
+ });
991
+ const previous = setLogger(make());
992
+ return { lines, restore: () => setLogger(previous) };
993
+ };
994
+
995
+ it('follows the file to the new endpoint, and says so with both origins', async () => {
996
+ const log = capture();
997
+ try {
998
+ const { db, captured } = makeFakeDb([[confidentialShared()], [], [{ id: 'user-row-1' }]]);
999
+ await begin(new DbSecretsVaultService(db, ENC_KEY));
1000
+ expect(captured.values[0].oauthMeta.tokenUrl).toBe(LIVE.tokenUrl);
1001
+ expect(JSON.parse(crypto.decrypt(captured.values[0].valueEncrypted)).clientSecret).toBe('cs-owner');
1002
+ expect(log.lines).toEqual([
1003
+ {
1004
+ message: `sign-in on ${KEY}: the declared token endpoint differs from the one recorded when the client secret was saved`,
1005
+ fields: { savedOrigin: 'https://old.example.com', declaredOrigin: 'https://api.example.com' },
1006
+ },
1007
+ ]);
1008
+ } finally {
1009
+ log.restore();
1010
+ }
1011
+ });
1012
+
1013
+ it('says nothing when the declared token endpoint is the one recorded at the save', async () => {
1014
+ const log = capture();
1015
+ try {
1016
+ const { db } = makeFakeDb([[confidentialShared()], [], [{ id: 'user-row-1' }]]);
1017
+ await begin(new DbSecretsVaultService(db, ENC_KEY), { ...LIVE, tokenUrl: SAVED_META.tokenUrl });
1018
+ expect(log.lines).toEqual([]);
1019
+ } finally {
1020
+ log.restore();
1021
+ }
1022
+ });
1023
+ });
1024
+ });
@@ -0,0 +1,218 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import express from 'express';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { createSecretsVaultRoutes, InvalidSecretError, SecretOAuthError } from '../index.js';
5
+ import type { ToolManualSummary, ToolVariable } from '../../tool-manuals/index.js';
6
+
7
+ /**
8
+ * The tool-scoped sign-in start follows the tool file as it reads NOW
9
+ * (`liveDeclaration`), not the catalog's cached scan and not the copy the
10
+ * owner's client-secret save stored. A client the platform registered itself
11
+ * (oauth-auto) is described by no file and still starts from its stored row.
12
+ *
13
+ * The incident: the GTM plugin's HubSpot connector was replaced, the file got
14
+ * the new client id, the new secret was saved — and every sign-in still asked
15
+ * for consent under the old client id.
16
+ */
17
+
18
+ const USER = 'owner@x.com';
19
+ const ENDPOINTS = {
20
+ authorizationUrl: 'https://app.hubspot.example/oauth/authorize',
21
+ tokenUrl: 'https://api.hubspot.example/oauth/v1/token',
22
+ };
23
+
24
+ const manual = (variables: ToolVariable[], setup?: ToolManualSummary['setup']): ToolManualSummary => ({
25
+ slug: 'hubspot',
26
+ name: 'hubspot',
27
+ path: 'Plugins/GTM/.claude-plugin/plugin.json',
28
+ type: 'mcp',
29
+ variables,
30
+ ...(setup ? { setup } : {}),
31
+ });
32
+ const signIn = (oauth?: ToolVariable['oauth']): ToolVariable => ({
33
+ name: 'HUBSPOT_TOKEN',
34
+ scope: 'user',
35
+ ...(oauth ? { oauth } : {}),
36
+ });
37
+
38
+ let httpServer: HttpServer | undefined;
39
+ afterEach(async () => {
40
+ await new Promise<void>((r) => (httpServer ? httpServer.close(() => r()) : r()));
41
+ httpServer = undefined;
42
+ });
43
+
44
+ /** One server whose catalog says `catalog` and whose file says `live`. */
45
+ async function serve(opts: {
46
+ catalog: ToolManualSummary[];
47
+ live: { manual: ToolManualSummary; variable?: ToolVariable } | null;
48
+ begin?: (input: Record<string, unknown>) => Promise<{ id: string; url: string }>;
49
+ canWrite?: boolean;
50
+ varName?: string;
51
+ /** The live read fails outright (the workspace cannot be opened) instead of answering. */
52
+ liveError?: Error;
53
+ }) {
54
+ const beginToolOAuthByKey = vi.fn(
55
+ opts.begin ?? (async () => ({ id: 'row-1', url: 'https://app.hubspot.example/oauth/authorize?client_id=x' })),
56
+ );
57
+ const putSharedOAuthClientSecret = vi.fn(async () => {});
58
+ const liveDeclaration = vi.fn(async () => {
59
+ if (opts.liveError) throw opts.liveError;
60
+ return opts.live;
61
+ });
62
+ const deps = {
63
+ secretsVault: { beginToolOAuthByKey, putSharedOAuthClientSecret },
64
+ toolManualService: { listAccessible: async () => opts.catalog, liveDeclaration },
65
+ accessControl: { canRead: async () => true, canWrite: async () => opts.canWrite ?? false },
66
+ connectionProbe: {},
67
+ kb: { defaultWorkspaceId: () => 'main' },
68
+ stateSecret: 'state-secret',
69
+ publicBackendUrl: 'http://localhost:3000',
70
+ publicFrontendUrl: 'http://localhost:5173',
71
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0];
72
+ const app = express();
73
+ app.use(express.json());
74
+ app.use((req, _res, next) => {
75
+ req.userId = 'user-1';
76
+ req.userEmail = USER;
77
+ next();
78
+ });
79
+ app.use('/api', createSecretsVaultRoutes(deps));
80
+ httpServer = await new Promise<HttpServer>((resolve) => {
81
+ const s = app.listen(0, () => resolve(s));
82
+ });
83
+ const base = `http://127.0.0.1:${(httpServer.address() as { port: number }).port}/api/secrets/tools/hubspot/vars/${opts.varName ?? 'HUBSPOT_TOKEN'}`;
84
+ return {
85
+ beginToolOAuthByKey,
86
+ putSharedOAuthClientSecret,
87
+ liveDeclaration,
88
+ start: () => fetch(`${base}/oauth/start`, { method: 'POST' }),
89
+ saveSecret: () =>
90
+ fetch(`${base}/oauth/admin`, {
91
+ method: 'PUT',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ clientSecret: 'cs' }),
94
+ }),
95
+ };
96
+ }
97
+
98
+ describe('POST …/oauth/start — a declared sign-in follows the live declaration', () => {
99
+ it('hands the vault the file’s client id, endpoints, PKCE and resource, even when the catalog still says otherwise', async () => {
100
+ const stale = manual([signIn({ clientId: 'client-A', ...ENDPOINTS })], { kind: 'oauth-manual' });
101
+ const fresh = manual([signIn({ clientId: 'client-B', ...ENDPOINTS, scopes: ['crm.read'], resource: 'https://mcp.hubspot.example/' })], {
102
+ kind: 'oauth-manual',
103
+ });
104
+ const s = await serve({ catalog: [stale], live: { manual: fresh, variable: fresh.variables![0] } });
105
+
106
+ const res = await s.start();
107
+ expect(res.status).toBe(200);
108
+ expect(s.liveDeclaration).toHaveBeenCalledWith(USER, 'hubspot', 'HUBSPOT_TOKEN');
109
+ expect(s.beginToolOAuthByKey).toHaveBeenCalledWith(
110
+ expect.objectContaining({
111
+ key: 'hubspot_HUBSPOT_TOKEN',
112
+ scopes: ['crm.read'],
113
+ provider: {
114
+ ...ENDPOINTS,
115
+ clientId: 'client-B',
116
+ scopes: ['crm.read'],
117
+ authParams: undefined,
118
+ pkce: true,
119
+ resource: 'https://mcp.hubspot.example/',
120
+ },
121
+ }),
122
+ );
123
+ });
124
+
125
+ it('a declaration opting out of PKCE is handed over as such', async () => {
126
+ const fresh = manual([signIn({ clientId: 'client-B', ...ENDPOINTS, pkce: false })]);
127
+ const s = await serve({ catalog: [fresh], live: { manual: fresh, variable: fresh.variables![0] } });
128
+ expect((await s.start()).status).toBe(200);
129
+ expect(s.beginToolOAuthByKey.mock.calls[0][0]).toMatchObject({ provider: { pkce: false } });
130
+ });
131
+
132
+ it('refuses a declaration whose endpoints could not be discovered with the discovery reason — the save’s own wording', async () => {
133
+ const reason = 'no authorization-server metadata at https://mcp.hubspot.example';
134
+ const stuck = manual([signIn({ clientId: 'client-B' })], { kind: 'oauth-manual', reason });
135
+ const s = await serve({ catalog: [stuck], live: { manual: stuck, variable: stuck.variables![0] }, canWrite: true });
136
+
137
+ const res = await s.start();
138
+ expect(res.status).toBe(422);
139
+ const { error } = (await res.json()) as { error: string };
140
+ expect(error).toBe(`The sign-in endpoints for this server aren't known yet: ${reason}.`);
141
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
142
+
143
+ const saved = await s.saveSecret();
144
+ expect(saved.status).toBe(422);
145
+ expect(((await saved.json()) as { error: string }).error).toBe(error);
146
+ });
147
+
148
+ it('refuses with "owner hasn’t finished" when the oauth block was removed from the file, never using the stored row', async () => {
149
+ const stale = manual([signIn({ clientId: 'client-A', ...ENDPOINTS })], { kind: 'oauth-manual' });
150
+ const fresh = manual([signIn()]);
151
+ const s = await serve({ catalog: [stale], live: { manual: fresh, variable: fresh.variables![0] } });
152
+
153
+ const res = await s.start();
154
+ expect(res.status).toBe(409);
155
+ expect(((await res.json()) as { error: string }).error).toBe("This tool's owner hasn't finished setting this up");
156
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it('passes on the vault’s refusals: a missing secret, and an endpoint that fails validation', async () => {
160
+ const fresh = manual([signIn({ clientId: 'client-B', ...ENDPOINTS, tokenUrl: 'http://127.0.0.1/token' })]);
161
+ const refusing = await serve({
162
+ catalog: [fresh],
163
+ live: { manual: fresh, variable: fresh.variables![0] },
164
+ begin: async () => {
165
+ throw new InvalidSecretError('tokenUrl must use https');
166
+ },
167
+ });
168
+ const invalid = await refusing.start();
169
+ expect(invalid.status).toBe(422);
170
+ expect(((await invalid.json()) as { error: string }).error).toBe('tokenUrl must use https');
171
+ await new Promise<void>((r) => httpServer!.close(() => r()));
172
+
173
+ const unset = await serve({
174
+ catalog: [fresh],
175
+ live: { manual: fresh, variable: fresh.variables![0] },
176
+ begin: async () => {
177
+ throw new SecretOAuthError("This tool's owner hasn't finished setting this up");
178
+ },
179
+ });
180
+ const res = await unset.start();
181
+ expect(res.status).toBe(409);
182
+ expect(((await res.json()) as { error: string }).error).toBe("This tool's owner hasn't finished setting this up");
183
+ });
184
+
185
+ it('a platform-registered sign-in (oauth-auto) starts from its stored row: no provider is handed over', async () => {
186
+ const auto = manual(
187
+ [{ name: 'MCP_OAUTH', scope: 'user', oauth: { clientId: 'dcr-1', ...ENDPOINTS } }],
188
+ { kind: 'oauth-auto' },
189
+ );
190
+ // The file declares no such variable: the catalog's discovery synthesised it.
191
+ const s = await serve({ catalog: [auto], live: { manual: manual([]) }, varName: 'MCP_OAUTH' });
192
+ expect((await s.start()).status).toBe(200);
193
+ const input = s.beginToolOAuthByKey.mock.calls[0][0];
194
+ expect(input).toMatchObject({ key: 'hubspot_MCP_OAUTH' });
195
+ expect(input.provider).toBeUndefined();
196
+ });
197
+
198
+ it('answers 404 when neither the file nor the catalog knows the tool', async () => {
199
+ const s = await serve({ catalog: [], live: null });
200
+ expect((await s.start()).status).toBe(404);
201
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('answers 500 when the tool files cannot be read, never a misconfiguration verdict from the catalog', async () => {
205
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
206
+ // The catalog still holds a declared sign-in; a live read that fails must
207
+ // not fall back to it and answer "owner hasn't finished" (409) or "not
208
+ // found" (404) for what is a server fault.
209
+ const stale = manual([signIn({ clientId: 'client-A', ...ENDPOINTS })], { kind: 'oauth-manual' });
210
+ const s = await serve({ catalog: [stale], live: null, liveError: new Error('workspace unavailable') });
211
+
212
+ const res = await s.start();
213
+ expect(res.status).toBe(500);
214
+ expect(((await res.json()) as { error: string }).error).toBe('Internal error');
215
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
216
+ error.mockRestore();
217
+ });
218
+ });