@cybedefend/vibedefend 1.5.0 → 1.7.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.
- package/README.md +71 -2
- package/dist/api/gateway-request.js +49 -0
- package/dist/api/gateway-request.js.map +1 -0
- package/dist/api/projects.js +67 -0
- package/dist/api/projects.js.map +1 -1
- package/dist/api/repository-link.js +157 -0
- package/dist/api/repository-link.js.map +1 -0
- package/dist/auth/auth-store.js +107 -25
- package/dist/auth/auth-store.js.map +1 -1
- package/dist/auth/auth.js +21 -0
- package/dist/auth/auth.js.map +1 -1
- package/dist/clients/cursor.js +5 -1
- package/dist/clients/cursor.js.map +1 -1
- package/dist/clients/vscode.js +38 -29
- package/dist/clients/vscode.js.map +1 -1
- package/dist/commands/mcp/allow.js +219 -0
- package/dist/commands/mcp/allow.js.map +1 -0
- package/dist/commands/mcp/connect.js +337 -0
- package/dist/commands/mcp/connect.js.map +1 -0
- package/dist/commands/mcp/index.js +170 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/mcp/proxy.js +320 -0
- package/dist/commands/mcp/proxy.js.map +1 -0
- package/dist/commands/mcp/scan.js +349 -0
- package/dist/commands/mcp/scan.js.map +1 -0
- package/dist/commands/mcp/sync.js +215 -0
- package/dist/commands/mcp/sync.js.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostics.js +78 -12
- package/dist/diagnostics.js.map +1 -1
- package/dist/doctor.js +19 -0
- package/dist/doctor.js.map +1 -1
- package/dist/guards-evaluator/analysis-types.js +27 -0
- package/dist/guards-evaluator/analysis-types.js.map +1 -0
- package/dist/guards-evaluator/command-analysis.js +572 -0
- package/dist/guards-evaluator/command-analysis.js.map +1 -0
- package/dist/guards-evaluator/command-view.js +586 -0
- package/dist/guards-evaluator/command-view.js.map +1 -0
- package/dist/guards-evaluator/env.js +11 -0
- package/dist/guards-evaluator/env.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +91 -41
- package/dist/guards-evaluator/evaluate.js.map +1 -1
- package/dist/guards-evaluator/index.js +2 -1
- package/dist/guards-evaluator/index.js.map +1 -1
- package/dist/guards-evaluator/inline-code.js +259 -0
- package/dist/guards-evaluator/inline-code.js.map +1 -0
- package/dist/guards-evaluator/normalize.js +716 -0
- package/dist/guards-evaluator/normalize.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +13 -5
- package/dist/guards-evaluator/preprocess.js.map +1 -1
- package/dist/guards-evaluator/redact.js +19 -0
- package/dist/guards-evaluator/redact.js.map +1 -1
- package/dist/guards-evaluator/regex.js +25 -3
- package/dist/guards-evaluator/regex.js.map +1 -1
- package/dist/guards-evaluator/safe-commands.js +155 -0
- package/dist/guards-evaluator/safe-commands.js.map +1 -0
- package/dist/guards-evaluator/triage.js +315 -0
- package/dist/guards-evaluator/triage.js.map +1 -0
- package/dist/hook-runner.js +24679 -5079
- package/dist/hooks/install.js +38 -0
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/runtime/config.js +38 -0
- package/dist/hooks/runtime/config.js.map +1 -1
- package/dist/hooks/runtime/emit.js +94 -61
- package/dist/hooks/runtime/emit.js.map +1 -1
- package/dist/hooks/runtime/fetch-rules.js +22 -7
- package/dist/hooks/runtime/fetch-rules.js.map +1 -1
- package/dist/hooks/runtime/gateway-http-error.js +34 -0
- package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +139 -17
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
- package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
- package/dist/hooks/runtime/judge-cache.js +6 -0
- package/dist/hooks/runtime/judge-cache.js.map +1 -1
- package/dist/hooks/runtime/judge-triage.js +193 -0
- package/dist/hooks/runtime/judge-triage.js.map +1 -0
- package/dist/hooks/runtime/judge.js +7 -1
- package/dist/hooks/runtime/judge.js.map +1 -1
- package/dist/hooks/runtime/pre-compact.js +12 -6
- package/dist/hooks/runtime/pre-compact.js.map +1 -1
- package/dist/hooks/runtime/resolve.js +90 -8
- package/dist/hooks/runtime/resolve.js.map +1 -1
- package/dist/hooks/runtime/retry-policy.js +57 -0
- package/dist/hooks/runtime/retry-policy.js.map +1 -0
- package/dist/hooks/runtime/review-sessions.js +74 -0
- package/dist/hooks/runtime/review-sessions.js.map +1 -0
- package/dist/hooks/runtime/script-resolve.js +695 -0
- package/dist/hooks/runtime/script-resolve.js.map +1 -0
- package/dist/hooks/runtime/session-activation.js +5 -1
- package/dist/hooks/runtime/session-activation.js.map +1 -1
- package/dist/hooks/runtime/session-review.js +163 -45
- package/dist/hooks/runtime/session-review.js.map +1 -1
- package/dist/hooks/runtime/session-start.js +2 -1
- package/dist/hooks/runtime/session-start.js.map +1 -1
- package/dist/hooks/runtime/session-writes.js +232 -0
- package/dist/hooks/runtime/session-writes.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +26 -7
- package/dist/hooks/runtime/sniff.js.map +1 -1
- package/dist/hooks/runtime/target-resolve.js +336 -0
- package/dist/hooks/runtime/target-resolve.js.map +1 -0
- package/dist/hooks/runtime/unmapped-tools.js +93 -0
- package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
- package/dist/hooks/runtime/usage-block.js +4 -0
- package/dist/hooks/runtime/usage-block.js.map +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js +2 -1
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
- package/dist/hooks/runtime/wire-text.js +41 -0
- package/dist/hooks/runtime/wire-text.js.map +1 -0
- package/dist/hooks/runtime/work-dir.js +84 -0
- package/dist/hooks/runtime/work-dir.js.map +1 -0
- package/dist/index.js +45 -12
- package/dist/index.js.map +1 -1
- package/dist/install.js +174 -11
- package/dist/install.js.map +1 -1
- package/dist/link.js +74 -48
- package/dist/link.js.map +1 -1
- package/dist/login.js +77 -11
- package/dist/login.js.map +1 -1
- package/dist/mcp/auth/agent-sign-ins.js +174 -0
- package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
- package/dist/mcp/auth/callback.js +200 -0
- package/dist/mcp/auth/callback.js.map +1 -0
- package/dist/mcp/auth/connect.js +392 -0
- package/dist/mcp/auth/connect.js.map +1 -0
- package/dist/mcp/auth/discovery.js +399 -0
- package/dist/mcp/auth/discovery.js.map +1 -0
- package/dist/mcp/auth/oauth.js +326 -0
- package/dist/mcp/auth/oauth.js.map +1 -0
- package/dist/mcp/auth/probe.js +128 -0
- package/dist/mcp/auth/probe.js.map +1 -0
- package/dist/mcp/auth/secrets.js +446 -0
- package/dist/mcp/auth/secrets.js.map +1 -0
- package/dist/mcp/auth/store.js +260 -0
- package/dist/mcp/auth/store.js.map +1 -0
- package/dist/mcp/auth/tokenSource.js +55 -0
- package/dist/mcp/auth/tokenSource.js.map +1 -0
- package/dist/mcp/config/atomicWrite.js +98 -0
- package/dist/mcp/config/atomicWrite.js.map +1 -0
- package/dist/mcp/config/clients.js +171 -0
- package/dist/mcp/config/clients.js.map +1 -0
- package/dist/mcp/config/cybedefend-servers.js +48 -0
- package/dist/mcp/config/cybedefend-servers.js.map +1 -0
- package/dist/mcp/config/discover.js +114 -0
- package/dist/mcp/config/discover.js.map +1 -0
- package/dist/mcp/config/hook.js +123 -0
- package/dist/mcp/config/hook.js.map +1 -0
- package/dist/mcp/config/index.js +11 -0
- package/dist/mcp/config/index.js.map +1 -0
- package/dist/mcp/config/install.js +132 -0
- package/dist/mcp/config/install.js.map +1 -0
- package/dist/mcp/config/installed.js +37 -0
- package/dist/mcp/config/installed.js.map +1 -0
- package/dist/mcp/config/jsonEdit.js +119 -0
- package/dist/mcp/config/jsonEdit.js.map +1 -0
- package/dist/mcp/config/lock.js +102 -0
- package/dist/mcp/config/lock.js.map +1 -0
- package/dist/mcp/config/proxyBinary.js +211 -0
- package/dist/mcp/config/proxyBinary.js.map +1 -0
- package/dist/mcp/config/rewrite.js +388 -0
- package/dist/mcp/config/rewrite.js.map +1 -0
- package/dist/mcp/config/types.js +34 -0
- package/dist/mcp/config/types.js.map +1 -0
- package/dist/mcp/proxy/audit.js +83 -0
- package/dist/mcp/proxy/audit.js.map +1 -0
- package/dist/mcp/proxy/authInterceptor.js +119 -0
- package/dist/mcp/proxy/authInterceptor.js.map +1 -0
- package/dist/mcp/proxy/expand.js +93 -0
- package/dist/mcp/proxy/expand.js.map +1 -0
- package/dist/mcp/proxy/inject.js +412 -0
- package/dist/mcp/proxy/inject.js.map +1 -0
- package/dist/mcp/proxy/intercept.js +63 -0
- package/dist/mcp/proxy/intercept.js.map +1 -0
- package/dist/mcp/proxy/jsonrpc.js +153 -0
- package/dist/mcp/proxy/jsonrpc.js.map +1 -0
- package/dist/mcp/proxy/log.js +109 -0
- package/dist/mcp/proxy/log.js.map +1 -0
- package/dist/mcp/proxy/paths.js +57 -0
- package/dist/mcp/proxy/paths.js.map +1 -0
- package/dist/mcp/proxy/proxy.js +520 -0
- package/dist/mcp/proxy/proxy.js.map +1 -0
- package/dist/mcp/proxy/serverCheckTimer.js +75 -0
- package/dist/mcp/proxy/serverCheckTimer.js.map +1 -0
- package/dist/mcp/proxy/sessionLifecycle.js +29 -0
- package/dist/mcp/proxy/sessionLifecycle.js.map +1 -0
- package/dist/mcp/proxy/spec.js +203 -0
- package/dist/mcp/proxy/spec.js.map +1 -0
- package/dist/mcp/proxy/sse.js +77 -0
- package/dist/mcp/proxy/sse.js.map +1 -0
- package/dist/mcp/proxy/upstream.js +2 -0
- package/dist/mcp/proxy/upstream.js.map +1 -0
- package/dist/mcp/proxy/upstreamHttp.js +499 -0
- package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
- package/dist/mcp/proxy/upstreamStdio.js +188 -0
- package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
- package/dist/mcp/proxy/upstreams.js +143 -0
- package/dist/mcp/proxy/upstreams.js.map +1 -0
- package/dist/mcp/proxy/verifyInterceptor.js +718 -0
- package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
- package/dist/mcp/proxy/windowsCommand.js +99 -0
- package/dist/mcp/proxy/windowsCommand.js.map +1 -0
- package/dist/mcp/report/buffer.js +351 -0
- package/dist/mcp/report/buffer.js.map +1 -0
- package/dist/mcp/report/coalesce.js +165 -0
- package/dist/mcp/report/coalesce.js.map +1 -0
- package/dist/mcp/report/constants.js +51 -0
- package/dist/mcp/report/constants.js.map +1 -0
- package/dist/mcp/report/project.js +83 -0
- package/dist/mcp/report/project.js.map +1 -0
- package/dist/mcp/report/reporter.js +443 -0
- package/dist/mcp/report/reporter.js.map +1 -0
- package/dist/mcp/report/types.js +3 -0
- package/dist/mcp/report/types.js.map +1 -0
- package/dist/mcp/report/validate.js +179 -0
- package/dist/mcp/report/validate.js.map +1 -0
- package/dist/mcp/scan/args.js +228 -0
- package/dist/mcp/scan/args.js.map +1 -0
- package/dist/mcp/scan/results.js +253 -0
- package/dist/mcp/scan/results.js.map +1 -0
- package/dist/mcp/scan/types.js +40 -0
- package/dist/mcp/scan/types.js.map +1 -0
- package/dist/mcp/verify/api.js +514 -0
- package/dist/mcp/verify/api.js.map +1 -0
- package/dist/mcp/verify/backend.js +143 -0
- package/dist/mcp/verify/backend.js.map +1 -0
- package/dist/mcp/verify/cache.js +200 -0
- package/dist/mcp/verify/cache.js.map +1 -0
- package/dist/mcp/verify/contract-errors.js +198 -0
- package/dist/mcp/verify/contract-errors.js.map +1 -0
- package/dist/mcp/verify/digest.js +91 -0
- package/dist/mcp/verify/digest.js.map +1 -0
- package/dist/mcp/verify/factory.js +56 -0
- package/dist/mcp/verify/factory.js.map +1 -0
- package/dist/mcp/verify/identity.js +286 -0
- package/dist/mcp/verify/identity.js.map +1 -0
- package/dist/mcp/verify/policy-store.js +63 -0
- package/dist/mcp/verify/policy-store.js.map +1 -0
- package/dist/mcp/verify/policy.js +160 -0
- package/dist/mcp/verify/policy.js.map +1 -0
- package/dist/mcp/verify/pool.js +27 -0
- package/dist/mcp/verify/pool.js.map +1 -0
- package/dist/mcp/verify/token-refresh.js +44 -0
- package/dist/mcp/verify/token-refresh.js.map +1 -0
- package/dist/mcp/verify/token.js +67 -0
- package/dist/mcp/verify/token.js.map +1 -0
- package/dist/mcp/verify/verifier.js +1166 -0
- package/dist/mcp/verify/verifier.js.map +1 -0
- package/dist/org-gate.js +1 -1
- package/dist/org-gate.js.map +1 -1
- package/dist/org.js +146 -0
- package/dist/org.js.map +1 -0
- package/dist/project-create.js +129 -0
- package/dist/project-create.js.map +1 -0
- package/dist/prompts.js +21 -0
- package/dist/prompts.js.map +1 -1
- package/dist/re2.wasm +0 -0
- package/dist/status.js +61 -0
- package/dist/status.js.map +1 -1
- package/dist/uninstall.js +49 -4
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.js +62 -6
- package/dist/utils.js.map +1 -1
- 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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"}
|