@cybedefend/vibedefend 1.4.7 → 1.6.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 (246) hide show
  1. package/README.md +77 -2
  2. package/dist/api/gateway-request.js +49 -0
  3. package/dist/api/gateway-request.js.map +1 -0
  4. package/dist/api/projects.js +67 -0
  5. package/dist/api/projects.js.map +1 -1
  6. package/dist/api/repository-link.js +157 -0
  7. package/dist/api/repository-link.js.map +1 -0
  8. package/dist/auth/auth-store.js +107 -25
  9. package/dist/auth/auth-store.js.map +1 -1
  10. package/dist/auth/auth.js +21 -0
  11. package/dist/auth/auth.js.map +1 -1
  12. package/dist/clients/cursor.js +5 -1
  13. package/dist/clients/cursor.js.map +1 -1
  14. package/dist/clients/vscode.js +38 -29
  15. package/dist/clients/vscode.js.map +1 -1
  16. package/dist/commands/mcp/allow.js +219 -0
  17. package/dist/commands/mcp/allow.js.map +1 -0
  18. package/dist/commands/mcp/connect.js +337 -0
  19. package/dist/commands/mcp/connect.js.map +1 -0
  20. package/dist/commands/mcp/index.js +170 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/mcp/proxy.js +264 -0
  23. package/dist/commands/mcp/proxy.js.map +1 -0
  24. package/dist/commands/mcp/scan.js +349 -0
  25. package/dist/commands/mcp/scan.js.map +1 -0
  26. package/dist/commands/mcp/sync.js +215 -0
  27. package/dist/commands/mcp/sync.js.map +1 -0
  28. package/dist/config.js +6 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/diagnostics.js +78 -12
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/doctor.js +19 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/guards-evaluator/analysis-types.js +27 -0
  35. package/dist/guards-evaluator/analysis-types.js.map +1 -0
  36. package/dist/guards-evaluator/command-analysis.js +572 -0
  37. package/dist/guards-evaluator/command-analysis.js.map +1 -0
  38. package/dist/guards-evaluator/command-view.js +586 -0
  39. package/dist/guards-evaluator/command-view.js.map +1 -0
  40. package/dist/guards-evaluator/env.js +11 -0
  41. package/dist/guards-evaluator/env.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +91 -41
  43. package/dist/guards-evaluator/evaluate.js.map +1 -1
  44. package/dist/guards-evaluator/index.js +2 -1
  45. package/dist/guards-evaluator/index.js.map +1 -1
  46. package/dist/guards-evaluator/inline-code.js +259 -0
  47. package/dist/guards-evaluator/inline-code.js.map +1 -0
  48. package/dist/guards-evaluator/normalize.js +716 -0
  49. package/dist/guards-evaluator/normalize.js.map +1 -0
  50. package/dist/guards-evaluator/preprocess.js +13 -5
  51. package/dist/guards-evaluator/preprocess.js.map +1 -1
  52. package/dist/guards-evaluator/redact.js +19 -0
  53. package/dist/guards-evaluator/redact.js.map +1 -1
  54. package/dist/guards-evaluator/regex.js +25 -3
  55. package/dist/guards-evaluator/regex.js.map +1 -1
  56. package/dist/guards-evaluator/safe-commands.js +155 -0
  57. package/dist/guards-evaluator/safe-commands.js.map +1 -0
  58. package/dist/guards-evaluator/triage.js +315 -0
  59. package/dist/guards-evaluator/triage.js.map +1 -0
  60. package/dist/hook-runner.js +24873 -5068
  61. package/dist/hooks/install.js +38 -0
  62. package/dist/hooks/install.js.map +1 -1
  63. package/dist/hooks/runtime/config.js +38 -0
  64. package/dist/hooks/runtime/config.js.map +1 -1
  65. package/dist/hooks/runtime/emit.js +126 -53
  66. package/dist/hooks/runtime/emit.js.map +1 -1
  67. package/dist/hooks/runtime/fetch-rules.js +27 -7
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -1
  69. package/dist/hooks/runtime/gateway-http-error.js +34 -0
  70. package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
  71. package/dist/hooks/runtime/guard-rules-cache.js +139 -17
  72. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
  73. package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
  74. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
  75. package/dist/hooks/runtime/judge-cache.js +6 -0
  76. package/dist/hooks/runtime/judge-cache.js.map +1 -1
  77. package/dist/hooks/runtime/judge-triage.js +193 -0
  78. package/dist/hooks/runtime/judge-triage.js.map +1 -0
  79. package/dist/hooks/runtime/judge.js +7 -1
  80. package/dist/hooks/runtime/judge.js.map +1 -1
  81. package/dist/hooks/runtime/pre-compact.js +21 -6
  82. package/dist/hooks/runtime/pre-compact.js.map +1 -1
  83. package/dist/hooks/runtime/resolve.js +108 -17
  84. package/dist/hooks/runtime/resolve.js.map +1 -1
  85. package/dist/hooks/runtime/retry-policy.js +57 -0
  86. package/dist/hooks/runtime/retry-policy.js.map +1 -0
  87. package/dist/hooks/runtime/review-sessions.js +74 -0
  88. package/dist/hooks/runtime/review-sessions.js.map +1 -0
  89. package/dist/hooks/runtime/script-resolve.js +695 -0
  90. package/dist/hooks/runtime/script-resolve.js.map +1 -0
  91. package/dist/hooks/runtime/session-activation.js +226 -0
  92. package/dist/hooks/runtime/session-activation.js.map +1 -0
  93. package/dist/hooks/runtime/session-review.js +174 -46
  94. package/dist/hooks/runtime/session-review.js.map +1 -1
  95. package/dist/hooks/runtime/session-start.js +35 -4
  96. package/dist/hooks/runtime/session-start.js.map +1 -1
  97. package/dist/hooks/runtime/session-writes.js +232 -0
  98. package/dist/hooks/runtime/session-writes.js.map +1 -0
  99. package/dist/hooks/runtime/sniff.js +26 -7
  100. package/dist/hooks/runtime/sniff.js.map +1 -1
  101. package/dist/hooks/runtime/target-resolve.js +336 -0
  102. package/dist/hooks/runtime/target-resolve.js.map +1 -0
  103. package/dist/hooks/runtime/unmapped-tools.js +93 -0
  104. package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
  105. package/dist/hooks/runtime/usage-block.js +4 -0
  106. package/dist/hooks/runtime/usage-block.js.map +1 -1
  107. package/dist/hooks/runtime/user-prompt-submit.js +8 -2
  108. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
  109. package/dist/hooks/runtime/wire-text.js +41 -0
  110. package/dist/hooks/runtime/wire-text.js.map +1 -0
  111. package/dist/hooks/runtime/work-dir.js +84 -0
  112. package/dist/hooks/runtime/work-dir.js.map +1 -0
  113. package/dist/index.js +45 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/install.js +174 -11
  116. package/dist/install.js.map +1 -1
  117. package/dist/link.js +74 -48
  118. package/dist/link.js.map +1 -1
  119. package/dist/login.js +99 -14
  120. package/dist/login.js.map +1 -1
  121. package/dist/mcp/auth/agent-sign-ins.js +174 -0
  122. package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
  123. package/dist/mcp/auth/callback.js +200 -0
  124. package/dist/mcp/auth/callback.js.map +1 -0
  125. package/dist/mcp/auth/connect.js +392 -0
  126. package/dist/mcp/auth/connect.js.map +1 -0
  127. package/dist/mcp/auth/discovery.js +399 -0
  128. package/dist/mcp/auth/discovery.js.map +1 -0
  129. package/dist/mcp/auth/oauth.js +326 -0
  130. package/dist/mcp/auth/oauth.js.map +1 -0
  131. package/dist/mcp/auth/probe.js +128 -0
  132. package/dist/mcp/auth/probe.js.map +1 -0
  133. package/dist/mcp/auth/secrets.js +446 -0
  134. package/dist/mcp/auth/secrets.js.map +1 -0
  135. package/dist/mcp/auth/store.js +260 -0
  136. package/dist/mcp/auth/store.js.map +1 -0
  137. package/dist/mcp/auth/tokenSource.js +55 -0
  138. package/dist/mcp/auth/tokenSource.js.map +1 -0
  139. package/dist/mcp/config/atomicWrite.js +98 -0
  140. package/dist/mcp/config/atomicWrite.js.map +1 -0
  141. package/dist/mcp/config/clients.js +171 -0
  142. package/dist/mcp/config/clients.js.map +1 -0
  143. package/dist/mcp/config/cybedefend-servers.js +48 -0
  144. package/dist/mcp/config/cybedefend-servers.js.map +1 -0
  145. package/dist/mcp/config/discover.js +114 -0
  146. package/dist/mcp/config/discover.js.map +1 -0
  147. package/dist/mcp/config/hook.js +123 -0
  148. package/dist/mcp/config/hook.js.map +1 -0
  149. package/dist/mcp/config/index.js +11 -0
  150. package/dist/mcp/config/index.js.map +1 -0
  151. package/dist/mcp/config/install.js +132 -0
  152. package/dist/mcp/config/install.js.map +1 -0
  153. package/dist/mcp/config/installed.js +37 -0
  154. package/dist/mcp/config/installed.js.map +1 -0
  155. package/dist/mcp/config/jsonEdit.js +119 -0
  156. package/dist/mcp/config/jsonEdit.js.map +1 -0
  157. package/dist/mcp/config/lock.js +102 -0
  158. package/dist/mcp/config/lock.js.map +1 -0
  159. package/dist/mcp/config/proxyBinary.js +211 -0
  160. package/dist/mcp/config/proxyBinary.js.map +1 -0
  161. package/dist/mcp/config/rewrite.js +388 -0
  162. package/dist/mcp/config/rewrite.js.map +1 -0
  163. package/dist/mcp/config/types.js +34 -0
  164. package/dist/mcp/config/types.js.map +1 -0
  165. package/dist/mcp/proxy/audit.js +83 -0
  166. package/dist/mcp/proxy/audit.js.map +1 -0
  167. package/dist/mcp/proxy/authInterceptor.js +119 -0
  168. package/dist/mcp/proxy/authInterceptor.js.map +1 -0
  169. package/dist/mcp/proxy/expand.js +93 -0
  170. package/dist/mcp/proxy/expand.js.map +1 -0
  171. package/dist/mcp/proxy/inject.js +397 -0
  172. package/dist/mcp/proxy/inject.js.map +1 -0
  173. package/dist/mcp/proxy/intercept.js +37 -0
  174. package/dist/mcp/proxy/intercept.js.map +1 -0
  175. package/dist/mcp/proxy/jsonrpc.js +153 -0
  176. package/dist/mcp/proxy/jsonrpc.js.map +1 -0
  177. package/dist/mcp/proxy/log.js +109 -0
  178. package/dist/mcp/proxy/log.js.map +1 -0
  179. package/dist/mcp/proxy/paths.js +53 -0
  180. package/dist/mcp/proxy/paths.js.map +1 -0
  181. package/dist/mcp/proxy/proxy.js +510 -0
  182. package/dist/mcp/proxy/proxy.js.map +1 -0
  183. package/dist/mcp/proxy/spec.js +203 -0
  184. package/dist/mcp/proxy/spec.js.map +1 -0
  185. package/dist/mcp/proxy/sse.js +77 -0
  186. package/dist/mcp/proxy/sse.js.map +1 -0
  187. package/dist/mcp/proxy/upstream.js +2 -0
  188. package/dist/mcp/proxy/upstream.js.map +1 -0
  189. package/dist/mcp/proxy/upstreamHttp.js +499 -0
  190. package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
  191. package/dist/mcp/proxy/upstreamStdio.js +188 -0
  192. package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
  193. package/dist/mcp/proxy/upstreams.js +143 -0
  194. package/dist/mcp/proxy/upstreams.js.map +1 -0
  195. package/dist/mcp/proxy/verifyInterceptor.js +559 -0
  196. package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
  197. package/dist/mcp/proxy/windowsCommand.js +99 -0
  198. package/dist/mcp/proxy/windowsCommand.js.map +1 -0
  199. package/dist/mcp/scan/args.js +228 -0
  200. package/dist/mcp/scan/args.js.map +1 -0
  201. package/dist/mcp/scan/results.js +253 -0
  202. package/dist/mcp/scan/results.js.map +1 -0
  203. package/dist/mcp/scan/types.js +40 -0
  204. package/dist/mcp/scan/types.js.map +1 -0
  205. package/dist/mcp/verify/api.js +475 -0
  206. package/dist/mcp/verify/api.js.map +1 -0
  207. package/dist/mcp/verify/backend.js +143 -0
  208. package/dist/mcp/verify/backend.js.map +1 -0
  209. package/dist/mcp/verify/cache.js +182 -0
  210. package/dist/mcp/verify/cache.js.map +1 -0
  211. package/dist/mcp/verify/contract-errors.js +198 -0
  212. package/dist/mcp/verify/contract-errors.js.map +1 -0
  213. package/dist/mcp/verify/digest.js +91 -0
  214. package/dist/mcp/verify/digest.js.map +1 -0
  215. package/dist/mcp/verify/factory.js +56 -0
  216. package/dist/mcp/verify/factory.js.map +1 -0
  217. package/dist/mcp/verify/identity.js +173 -0
  218. package/dist/mcp/verify/identity.js.map +1 -0
  219. package/dist/mcp/verify/policy-store.js +63 -0
  220. package/dist/mcp/verify/policy-store.js.map +1 -0
  221. package/dist/mcp/verify/policy.js +158 -0
  222. package/dist/mcp/verify/policy.js.map +1 -0
  223. package/dist/mcp/verify/pool.js +27 -0
  224. package/dist/mcp/verify/pool.js.map +1 -0
  225. package/dist/mcp/verify/token-refresh.js +44 -0
  226. package/dist/mcp/verify/token-refresh.js.map +1 -0
  227. package/dist/mcp/verify/token.js +67 -0
  228. package/dist/mcp/verify/token.js.map +1 -0
  229. package/dist/mcp/verify/verifier.js +1009 -0
  230. package/dist/mcp/verify/verifier.js.map +1 -0
  231. package/dist/org-gate.js +1 -1
  232. package/dist/org-gate.js.map +1 -1
  233. package/dist/org.js +146 -0
  234. package/dist/org.js.map +1 -0
  235. package/dist/project-create.js +129 -0
  236. package/dist/project-create.js.map +1 -0
  237. package/dist/prompts.js +21 -0
  238. package/dist/prompts.js.map +1 -1
  239. package/dist/re2.wasm +0 -0
  240. package/dist/status.js +61 -0
  241. package/dist/status.js.map +1 -1
  242. package/dist/uninstall.js +49 -4
  243. package/dist/uninstall.js.map +1 -1
  244. package/dist/utils.js +62 -6
  245. package/dist/utils.js.map +1 -1
  246. package/package.json +10 -3
@@ -0,0 +1,326 @@
1
+ /**
2
+ * OAuth 2.1 transport against a third-party MCP server: dynamic client
3
+ * registration (RFC 7591), code exchange, refresh.
4
+ *
5
+ * ─── Why VibeDefend authenticates as itself ─────────────────────────────────
6
+ *
7
+ * A third-party server's token belongs to whoever obtained it; there is
8
+ * nothing to borrow from another client's keychain, and the plan forbids
9
+ * looking. To read a catalogue behind an auth gate, VibeDefend runs its own
10
+ * OAuth 2.1 flow, like any other MCP client.
11
+ *
12
+ * ─── Public client, therefore PKCE ──────────────────────────────────────────
13
+ *
14
+ * This runs on a developer's machine, where no secret is protectable, so
15
+ * VibeDefend registers as a PUBLIC client (`token_endpoint_auth_method:
16
+ * none`, see `registerMcpClient`) and PKCE S256 is what secures the exchange.
17
+ * `discovery.ts` already refuses an authorization server that does not
18
+ * advertise S256 — never `plain`, which would make the protection useless for
19
+ * a client with no secret. The verifier/challenge pair itself is computed by
20
+ * `src/auth/pkce.ts`, already generic and already used by `vibedefend login`.
21
+ * This file only does OAuth TRANSPORT.
22
+ *
23
+ * ─── Partitioning ───────────────────────────────────────────────────────────
24
+ *
25
+ * A token obtained for Linear has no business at Notion — nor at the
26
+ * neighbour's on a shared MCP platform (Smithery, Composio, Zapier,
27
+ * Pipedream all publish N independent servers on a single host, told apart by
28
+ * PATH alone). The store (`store.ts`) is therefore keyed by the server's
29
+ * CANONICAL URL (`mcpAuthKey`), scheme and path included, never by host.
30
+ */
31
+ const DEFAULT_TIMEOUT_MS = 15_000;
32
+ const DEFAULT_PORTS = {
33
+ 'http:': '80',
34
+ 'https:': '443',
35
+ };
36
+ // ─── Canonical server identity ──────────────────────────────────────────────
37
+ /**
38
+ * Canonical identity of an MCP server: `scheme//host[:port]path`.
39
+ *
40
+ * This is the SAME projection as the backend's (`normalizeMcpUrl`,
41
+ * analysis-reporting-service/src/modules/mcp-match/mcp-url.util.ts): case
42
+ * normalised, trailing FQDN dot removed, default port omitted, trailing
43
+ * slash removed from the path, query and fragment dropped. Both sides have to
44
+ * name a server with the same string, or a verdict rendered over there no
45
+ * longer describes what we are asking about here.
46
+ *
47
+ * `null` for anything that is not http(s) — there is no MCP server identity
48
+ * to derive from it — or for an unparseable entry.
49
+ */
50
+ export function canonicalMcpUrl(url) {
51
+ if (!url || !url.trim())
52
+ return null;
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(url.trim());
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ const scheme = parsed.protocol.toLowerCase();
61
+ if (scheme !== 'http:' && scheme !== 'https:')
62
+ return null;
63
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, '');
64
+ const port = parsed.port && parsed.port !== DEFAULT_PORTS[scheme] ? `:${parsed.port}` : '';
65
+ const path = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/$/, '');
66
+ return `${scheme}//${host}${port}${path}`;
67
+ }
68
+ /**
69
+ * Storage key for an MCP credential, indexed by the server's CANONICAL URL.
70
+ *
71
+ * Not by host. Shared MCP platforms publish N independent servers on one
72
+ * host, told apart by PATH alone: a per-host key would merge their
73
+ * credentials, and a `.mcp.json` in a cloned repository declaring
74
+ * `https://<host>/@evil/tool/mcp` would be enough to send the legitimate
75
+ * neighbour's token out as `Authorization: Bearer`, with no user interaction
76
+ * at all. The scheme matters too: without it, `http://<host>/mcp` would
77
+ * replay a token obtained over https in the clear.
78
+ *
79
+ * The prefix keeps these entries from colliding with the CybeDefend ones in
80
+ * the same keychain (`Codex MCP Credentials`, see `src/auth/auth-store.ts`).
81
+ */
82
+ export function mcpAuthKey(serverUrl) {
83
+ return `mcp-oauth|${canonicalMcpUrl(serverUrl) ?? serverUrl.trim().toLowerCase()}`;
84
+ }
85
+ /**
86
+ * Raised when a SECRET-BEARING request — the code exchange, the refresh, or
87
+ * the dynamic registration itself — is answered with a REDIRECT (3xx) instead
88
+ * of a direct response.
89
+ *
90
+ * Without this name, such a status fell into the same `null` (or
91
+ * `{ok:false, reason:'unavailable'}` for `refreshMcpToken`) as an ordinary
92
+ * 500: indistinguishable from a plain server failure. A third-party MCP
93
+ * server that answers a request CARRYING a secret (`code`+`code_verifier`,
94
+ * `refresh_token`+`client_secret`, or the `redirect_uris` of the
95
+ * registration) with a redirect is not having an incident: it is TRYING to
96
+ * have that body replayed somewhere else. `redirect: 'manual'` keeps this
97
+ * file from following the redirect, but the body has already been sent ONCE
98
+ * to the address we asked, and the 3xx itself is the signal — not a failure
99
+ * to file with the others.
100
+ *
101
+ * `registerMcpClient` and `exchangeMcpCode` have NO return channel for a
102
+ * reason (`T | null`, by construction — see their docs): throwing is the only
103
+ * way to name this case without redrawing their whole contract for one
104
+ * branch. `refreshMcpToken`, which does have one, uses `'suspicious_redirect'`
105
+ * instead.
106
+ */
107
+ export class McpSuspiciousRedirectError extends Error {
108
+ status;
109
+ constructor(status, context) {
110
+ super(`MCP server responded to a secret-bearing ${context} request with a redirect (HTTP ${status}) ` +
111
+ 'instead of completing it — treated as an attempted exfiltration, not an ordinary network failure');
112
+ this.status = status;
113
+ this.name = 'McpSuspiciousRedirectError';
114
+ }
115
+ }
116
+ /** An HTTP redirect status (3xx) — the signal this module is built around. */
117
+ function isRedirectStatus(status) {
118
+ return status >= 300 && status < 400;
119
+ }
120
+ // ─── Dynamic client registration (RFC 7591) ─────────────────────────────────
121
+ /**
122
+ * Registers a public client, or returns `null`.
123
+ *
124
+ * Necessary: VibeDefend is pre-registered with no MCP server vendor, and
125
+ * asking the user to create an OAuth application by hand on every server
126
+ * would be unusable.
127
+ */
128
+ export async function registerMcpClient(registrationEndpoint, redirectUri, deps = {}) {
129
+ const f = deps.fetchImpl ?? globalThis.fetch;
130
+ let res;
131
+ try {
132
+ res = await f(registrationEndpoint, {
133
+ method: 'POST',
134
+ // A 307/308 would replay this request — BODY INCLUDED — against an
135
+ // origin we did not choose to talk to. The discovery requests carry no
136
+ // secret, but `redirect_uris` deserves the same guard.
137
+ redirect: 'manual',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ body: JSON.stringify({
140
+ client_name: 'VibeDefend',
141
+ redirect_uris: [redirectUri],
142
+ grant_types: ['authorization_code', 'refresh_token'],
143
+ response_types: ['code'],
144
+ // Public client: no secret is protectable on a developer machine, see
145
+ // the module header.
146
+ token_endpoint_auth_method: 'none',
147
+ application_type: 'native',
148
+ }),
149
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS),
150
+ });
151
+ }
152
+ catch {
153
+ return null; // an ORDINARY network failure, see McpSuspiciousRedirectError
154
+ }
155
+ if (isRedirectStatus(res.status)) {
156
+ throw new McpSuspiciousRedirectError(res.status, 'dynamic client registration');
157
+ }
158
+ if (!res.ok)
159
+ return null;
160
+ try {
161
+ const body = (await res.json());
162
+ const clientId = typeof body.client_id === 'string' ? body.client_id : null;
163
+ if (!clientId)
164
+ return null;
165
+ return {
166
+ clientId,
167
+ clientSecret: typeof body.client_secret === 'string' ? body.client_secret : null,
168
+ };
169
+ }
170
+ catch {
171
+ return null; // unreadable body, not an attempted redirect
172
+ }
173
+ }
174
+ /**
175
+ * Exchanges the authorization code and the PKCE `code_verifier` for a token,
176
+ * or returns `null`.
177
+ *
178
+ * `null` on EVERY failure (network, non-ok status, body with no
179
+ * `access_token`) — never interpolated text in the output. Unlike
180
+ * `refreshMcpToken`, no distinction of cause is needed here: an authorization
181
+ * code is not "refreshed", a failed exchange simply sends the user back to
182
+ * the start of the flow. That absence of a text channel structurally closes
183
+ * any risk of a secret leaking into the output.
184
+ */
185
+ export async function exchangeMcpCode(params) {
186
+ const f = params.fetchImpl ?? globalThis.fetch;
187
+ const body = new URLSearchParams({
188
+ grant_type: 'authorization_code',
189
+ code: params.code,
190
+ code_verifier: params.codeVerifier,
191
+ client_id: params.clientId,
192
+ redirect_uri: params.redirectUri,
193
+ // Without `resource`, a token obtained here would be replayable against
194
+ // another resource of the same authorization server.
195
+ resource: params.resource,
196
+ });
197
+ if (params.clientSecret)
198
+ body.set('client_secret', params.clientSecret);
199
+ let res;
200
+ try {
201
+ res = await f(params.tokenEndpoint, {
202
+ method: 'POST',
203
+ // `code` + `code_verifier` (and `client_secret`, if any) travel in THIS
204
+ // body. A 307/308 would replay it, body included, against an origin we
205
+ // did not choose to talk to — `undici` strips `Authorization` across
206
+ // origins, NOT the body.
207
+ redirect: 'manual',
208
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
209
+ body,
210
+ signal: AbortSignal.timeout(params.timeoutMs ?? DEFAULT_TIMEOUT_MS),
211
+ });
212
+ }
213
+ catch {
214
+ return null; // an ORDINARY network failure, see McpSuspiciousRedirectError
215
+ }
216
+ if (isRedirectStatus(res.status)) {
217
+ throw new McpSuspiciousRedirectError(res.status, 'code exchange');
218
+ }
219
+ if (!res.ok)
220
+ return null;
221
+ try {
222
+ const json = (await res.json());
223
+ const accessToken = typeof json.access_token === 'string' ? json.access_token : null;
224
+ if (!accessToken)
225
+ return null;
226
+ const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : null;
227
+ return {
228
+ accessToken,
229
+ refreshToken: typeof json.refresh_token === 'string' ? json.refresh_token : null,
230
+ // An absolute deadline: a relative duration becomes wrong the moment it
231
+ // is read back later, persisted and reloaded in the next session.
232
+ expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null,
233
+ };
234
+ }
235
+ catch {
236
+ return null; // unreadable body, not an attempted redirect
237
+ }
238
+ }
239
+ /**
240
+ * RFC 6749 §5.2: an authorization server refusing a dead `refresh_token`
241
+ * answers with the `invalid_grant` error code in the JSON body — the only
242
+ * signal we trust to erase local credentials.
243
+ *
244
+ * Unlike `token-exchange.ts` (Logto, a server WE control, where "400 or 401"
245
+ * is a good enough heuristic), a third-party MCP server is outside our
246
+ * control: a generic 400/401 can mean a CLIENT problem (a stale `client_id`
247
+ * after a server reset, say) just as easily as a dead refresh token. Erasing
248
+ * on an HTTP status alone would condemn the user to re-consent for a failure
249
+ * that is not theirs, so we require the EXPLICIT OAuth error code.
250
+ */
251
+ async function classifyRefreshError(res) {
252
+ try {
253
+ const json = (await res.json());
254
+ if (json.error === 'invalid_grant')
255
+ return 'invalid_grant';
256
+ }
257
+ catch {
258
+ // Unreadable body: not enough information to accuse this refresh token.
259
+ }
260
+ return 'unavailable';
261
+ }
262
+ /**
263
+ * Renews an expired token.
264
+ *
265
+ * Without this path an authorization does not survive its first expiry —
266
+ * measured on 2026-08-12 on a Linear token that expired after a few hours
267
+ * while its refresh token sat stored and unused.
268
+ */
269
+ export async function refreshMcpToken(params) {
270
+ const f = params.fetchImpl ?? globalThis.fetch;
271
+ const body = new URLSearchParams({
272
+ grant_type: 'refresh_token',
273
+ refresh_token: params.refreshToken,
274
+ client_id: params.clientId,
275
+ resource: params.resource,
276
+ });
277
+ if (params.clientSecret)
278
+ body.set('client_secret', params.clientSecret);
279
+ let res;
280
+ try {
281
+ res = await f(params.tokenEndpoint, {
282
+ method: 'POST',
283
+ // `refresh_token` + `client_secret` travel in THIS body — the same
284
+ // cross-origin replay risk as `exchangeMcpCode`.
285
+ redirect: 'manual',
286
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
287
+ body,
288
+ signal: AbortSignal.timeout(params.timeoutMs ?? DEFAULT_TIMEOUT_MS),
289
+ });
290
+ }
291
+ catch {
292
+ return { ok: false, reason: 'unavailable' };
293
+ }
294
+ // Named BEFORE the generic `!res.ok` below: otherwise `classifyRefreshError`
295
+ // would treat it like any other non-ok status (unreadable body →
296
+ // `'unavailable'`, indistinguishable from a plain 500).
297
+ if (isRedirectStatus(res.status)) {
298
+ return { ok: false, reason: 'suspicious_redirect' };
299
+ }
300
+ if (!res.ok) {
301
+ return { ok: false, reason: await classifyRefreshError(res) };
302
+ }
303
+ let json;
304
+ try {
305
+ json = (await res.json());
306
+ }
307
+ catch {
308
+ return { ok: false, reason: 'unavailable' };
309
+ }
310
+ const accessToken = typeof json.access_token === 'string' ? json.access_token : null;
311
+ if (!accessToken)
312
+ return { ok: false, reason: 'unavailable' };
313
+ const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : null;
314
+ return {
315
+ ok: true,
316
+ tokens: {
317
+ accessToken,
318
+ // A server that returns no new refresh token keeps the old one valid
319
+ // (RFC 6749 does not mandate rotation); losing it would break the NEXT
320
+ // renewal. A server that does issue a new one must be followed.
321
+ refreshToken: typeof json.refresh_token === 'string' ? json.refresh_token : params.refreshToken,
322
+ expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null,
323
+ },
324
+ };
325
+ }
326
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../src/mcp/auth/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Does this HTTP MCP server actually want a token?
3
+ *
4
+ * The plan is explicit: before starting an OAuth flow, try without one. A
5
+ * server that answers is left alone — no browser, no dynamic registration, no
6
+ * credential to keep safe for something that never needed it. Plenty of
7
+ * internal MCP servers sit behind a VPN and authenticate nobody.
8
+ *
9
+ * The probe is the real handshake, not a HEAD request: a server commonly lets
10
+ * `initialize` through and refuses `tools/list`, so stopping at the first
11
+ * exchange would file it as open when it is not.
12
+ */
13
+ import { canonicalMcpUrl } from './oauth.js';
14
+ const DEFAULT_TIMEOUT_MS = 15_000;
15
+ const PROTOCOL_VERSION = '2025-06-18';
16
+ /**
17
+ * Runs `initialize`, `notifications/initialized` and `tools/list` against an
18
+ * HTTP MCP server and reports what happened. Never throws.
19
+ */
20
+ export async function probeMcpHttp(serverUrl, deps = {}) {
21
+ const f = deps.fetchImpl ?? globalThis.fetch;
22
+ const timeout = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ const clientInfo = deps.clientInfo ?? { name: 'vibedefend', version: '0.0.0' };
24
+ let sessionId;
25
+ const post = async (body) => {
26
+ const headers = {
27
+ ...(deps.headers ?? {}),
28
+ 'content-type': 'application/json',
29
+ accept: 'application/json, text/event-stream',
30
+ };
31
+ if (deps.token)
32
+ headers.authorization = `Bearer ${deps.token}`;
33
+ if (sessionId)
34
+ headers['mcp-session-id'] = sessionId;
35
+ return f(serverUrl, {
36
+ method: 'POST',
37
+ // Same reasoning as everywhere else a request may carry a credential:
38
+ // a 3xx would replay it against an origin we did not choose.
39
+ redirect: 'manual',
40
+ headers,
41
+ body: JSON.stringify(body),
42
+ signal: AbortSignal.timeout(timeout),
43
+ });
44
+ };
45
+ const refusal = (res, at) => ({
46
+ verdict: 'refused',
47
+ refusedAt: at,
48
+ challenge: { status: res.status, wwwAuthenticate: res.headers.get('www-authenticate') },
49
+ });
50
+ try {
51
+ const initialize = await post({
52
+ jsonrpc: '2.0',
53
+ id: 1,
54
+ method: 'initialize',
55
+ params: { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo },
56
+ });
57
+ if (initialize.status === 401 || initialize.status === 403) {
58
+ return refusal(initialize, 'initialize');
59
+ }
60
+ if (!initialize.ok)
61
+ return { verdict: 'unusable' };
62
+ sessionId = initialize.headers.get('mcp-session-id') ?? undefined;
63
+ const initializeBody = await readMessage(initialize);
64
+ if (!initializeBody)
65
+ return { verdict: 'unusable' };
66
+ // A server that never sees `notifications/initialized` is entitled to
67
+ // refuse everything afterwards, which would read as an auth refusal.
68
+ await post({ jsonrpc: '2.0', method: 'notifications/initialized' }).catch(() => undefined);
69
+ const list = await post({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
70
+ if (list.status === 401 || list.status === 403)
71
+ return refusal(list, 'tools/list');
72
+ if (!list.ok)
73
+ return { verdict: 'unusable' };
74
+ const listBody = await readMessage(list);
75
+ if (!listBody)
76
+ return { verdict: 'unusable' };
77
+ // A JSON-RPC error is an answer, not a refusal: a server with no tools
78
+ // capability returns -32601 and is perfectly open.
79
+ const result = listBody.result;
80
+ const tools = Array.isArray(result?.tools) ? result.tools : [];
81
+ return { verdict: 'open', toolCount: tools.length, tools };
82
+ }
83
+ catch {
84
+ return { verdict: 'unreachable' };
85
+ }
86
+ }
87
+ /**
88
+ * Reads one JSON-RPC message out of a response, whether it came as JSON or as
89
+ * a one-shot SSE stream. The probe only needs the first one.
90
+ */
91
+ async function readMessage(res) {
92
+ const contentType = (res.headers.get('content-type') ?? '').toLowerCase();
93
+ let text;
94
+ try {
95
+ text = await res.text();
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ if (contentType.includes('text/event-stream')) {
101
+ for (const line of text.split(/\r?\n/)) {
102
+ if (!line.startsWith('data:'))
103
+ continue;
104
+ try {
105
+ return JSON.parse(line.slice(5).trim());
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ try {
114
+ return JSON.parse(text);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * The identity a probe result is filed under. Always the canonical URL, never
122
+ * the string as typed: `?api_key=` in a hosted server's URL is a credential
123
+ * and has no business in a storage key.
124
+ */
125
+ export function probeKey(serverUrl) {
126
+ return canonicalMcpUrl(serverUrl) ?? serverUrl;
127
+ }
128
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/mcp/auth/probe.ts"],"names":[],"mappings":"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"}