mindweave 2.4.4 → 2.4.6
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/dist/cli/App.js +158 -99
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/attachments.js +14 -1
- package/dist/cli/attachments.js.map +1 -1
- package/dist/cli/components/KeyManager.js +3 -42
- package/dist/cli/components/KeyManager.js.map +1 -1
- package/dist/cli/components/McpMinitabs.js +689 -0
- package/dist/cli/components/McpMinitabs.js.map +1 -0
- package/dist/cli/components/minitabs.js +71 -0
- package/dist/cli/components/minitabs.js.map +1 -0
- package/dist/dynamo/engine.js +29 -7
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/prompt.js +2 -0
- package/dist/dynamo/prompt.js.map +1 -1
- package/dist/mcp/config.js +57 -3
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/configWrite.js +40 -2
- package/dist/mcp/configWrite.js.map +1 -1
- package/dist/mcp/connection.js +51 -1
- package/dist/mcp/connection.js.map +1 -1
- package/dist/mcp/manager.js +65 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/oauth/callback.js +171 -0
- package/dist/mcp/oauth/callback.js.map +1 -0
- package/dist/mcp/oauth/flow.js +415 -0
- package/dist/mcp/oauth/flow.js.map +1 -0
- package/dist/mcp/oauth/index.js +243 -0
- package/dist/mcp/oauth/index.js.map +1 -0
- package/dist/mcp/oauth/metadata.js +192 -0
- package/dist/mcp/oauth/metadata.js.map +1 -0
- package/dist/mcp/oauth/pkce.js +56 -0
- package/dist/mcp/oauth/pkce.js.map +1 -0
- package/dist/mcp/oauth/tokenStore.js +103 -0
- package/dist/mcp/oauth/tokenStore.js.map +1 -0
- package/dist/mcp/transport/http.js +33 -5
- package/dist/mcp/transport/http.js.map +1 -1
- package/dist/memory/session.js +4 -0
- package/dist/memory/session.js.map +1 -1
- package/dist/tools/backgroundShells.js +42 -4
- package/dist/tools/backgroundShells.js.map +1 -1
- package/dist/tools/mcpAdd.js +7 -0
- package/dist/tools/mcpAdd.js.map +1 -1
- package/dist/tools/subagent.js +41 -3
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/todo.js +35 -0
- package/dist/tools/todo.js.map +1 -1
- package/dist/tools/verifyReport.js +144 -0
- package/dist/tools/verifyReport.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — the one door the rest of Mindweave uses to authorize an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Everything under `oauth/` is a step; this is the sequence. Two entry points, and the
|
|
5
|
+
* split between them is the whole design:
|
|
6
|
+
*
|
|
7
|
+
* - `authHeaders()` runs on EVERY request and must be silent, fast and never open a
|
|
8
|
+
* browser. It hands back a token if one is stored, refreshes it first if it has
|
|
9
|
+
* expired, and returns nothing at all if there is no credential — a request with no
|
|
10
|
+
* Authorization header is how we learn a server wants one, so failing to produce a
|
|
11
|
+
* token is a normal outcome rather than an error.
|
|
12
|
+
* - `authenticate()` runs when a PERSON asked for it, and is the only thing allowed to
|
|
13
|
+
* open a browser and hold a port. It never runs on its own: a connection that hits a
|
|
14
|
+
* 401 goes to `needs-auth` and STOPS. A background retry that pops a browser window
|
|
15
|
+
* for a server the user forgot they configured is a worse outcome than a server that
|
|
16
|
+
* plainly says it needs signing in.
|
|
17
|
+
*
|
|
18
|
+
* An expired refresh is treated as no credential rather than as a failure: refresh tokens
|
|
19
|
+
* are revoked from dashboards all the time, and the right response is to offer signing in
|
|
20
|
+
* again, not to report a broken connection.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { authorizeUrl, discover, exchangeCode, isTransientNetworkError, refreshTokens, registerClient, revokeToken, TokenRequestError } from "./flow.js";
|
|
24
|
+
import { createPkce, randomState } from "./pkce.js";
|
|
25
|
+
import { findAvailablePort, redirectUri, waitForCallback } from "./callback.js";
|
|
26
|
+
import { clearAuth, isExpired, readAuth, serverKey, writeAuth } from "./tokenStore.js";
|
|
27
|
+
import { clientSecretFor } from "../config.js";
|
|
28
|
+
export { serverKey, clearAuth } from "./tokenStore.js";
|
|
29
|
+
export { parseWwwAuthenticate } from "./metadata.js";
|
|
30
|
+
/**
|
|
31
|
+
* The Authorization header for a server, or `{}` when there is nothing stored.
|
|
32
|
+
*
|
|
33
|
+
* Refreshes in place and persists the result, so a long session does not accumulate one
|
|
34
|
+
* expired token per hour. A refresh that fails is classified before anything is thrown
|
|
35
|
+
* away: only `invalid_grant` — the refresh token itself being dead — clears the
|
|
36
|
+
* credential. See the catch below for why both directions of that judgement are costly.
|
|
37
|
+
*/
|
|
38
|
+
export async function authHeaders(name, config, fetchImpl = fetch) {
|
|
39
|
+
const key = serverKey(name, config);
|
|
40
|
+
const stored = await readAuth(key);
|
|
41
|
+
if (!stored)
|
|
42
|
+
return {};
|
|
43
|
+
if (!isExpired(stored))
|
|
44
|
+
return { authorization: `Bearer ${stored.accessToken}` };
|
|
45
|
+
if (!stored.refreshToken) {
|
|
46
|
+
await clearAuth(key);
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const refreshed = await refreshTokens({
|
|
51
|
+
tokenEndpoint: stored.tokenEndpoint,
|
|
52
|
+
clientId: stored.clientId,
|
|
53
|
+
...(stored.clientSecret ? { clientSecret: stored.clientSecret } : {}),
|
|
54
|
+
refreshToken: stored.refreshToken,
|
|
55
|
+
...(stored.resource ? { resource: stored.resource } : {}),
|
|
56
|
+
}, fetchImpl);
|
|
57
|
+
const next = {
|
|
58
|
+
...stored,
|
|
59
|
+
accessToken: refreshed.accessToken,
|
|
60
|
+
...(refreshed.refreshToken ? { refreshToken: refreshed.refreshToken } : {}),
|
|
61
|
+
...(refreshed.expiresAt ? { expiresAt: refreshed.expiresAt } : {}),
|
|
62
|
+
...(refreshed.scope ? { scope: refreshed.scope } : {}),
|
|
63
|
+
};
|
|
64
|
+
await writeAuth(key, next);
|
|
65
|
+
return { authorization: `Bearer ${next.accessToken}` };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// WHAT KIND of failure decides whether the credential survives, and getting this
|
|
69
|
+
// wrong in either direction is expensive: clear too eagerly and a dropped packet
|
|
70
|
+
// signs the user out, keep too stubbornly and a revoked token is re-sent forever
|
|
71
|
+
// while the sign-in that would fix it is never offered.
|
|
72
|
+
const kind = error instanceof TokenRequestError ? error.kind : isTransientNetworkError(error) ? "transient" : "other";
|
|
73
|
+
if (kind !== "invalid_grant") {
|
|
74
|
+
// Not the credential's fault. Send nothing this time — the request will 401 and the
|
|
75
|
+
// connection will say so — but keep what we have for the next attempt.
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
// `invalid_grant` usually means the refresh token is dead. It also means exactly this
|
|
79
|
+
// when ANOTHER Mindweave session refreshed a moment ago on a server that rotates
|
|
80
|
+
// refresh tokens: ours became invalid because theirs succeeded. Re-reading before
|
|
81
|
+
// clearing is what tells the two apart, and without it a second open session is a
|
|
82
|
+
// reliable way to sign yourself out of the first.
|
|
83
|
+
const current = await readAuth(key);
|
|
84
|
+
if (current && current.accessToken !== stored.accessToken && !isExpired(current)) {
|
|
85
|
+
return { authorization: `Bearer ${current.accessToken}` };
|
|
86
|
+
}
|
|
87
|
+
await clearAuth(key);
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** How long to let the browser get on with it before offering the raw URL. Long enough
|
|
92
|
+
* that a working launch never shows it, short enough that someone staring at a terminal
|
|
93
|
+
* which appears to have done nothing is not left guessing. */
|
|
94
|
+
const URL_FALLBACK_DELAY_MS = 4_000;
|
|
95
|
+
/**
|
|
96
|
+
* Sign in to one server, end to end, and persist what comes back.
|
|
97
|
+
*
|
|
98
|
+
* The port is bound BEFORE the authorize URL is built, because the redirect URI is part
|
|
99
|
+
* of what gets signed into the request and part of what the client registers with, so it
|
|
100
|
+
* has to be the real one rather than a guess corrected later.
|
|
101
|
+
*/
|
|
102
|
+
export async function authenticate(options) {
|
|
103
|
+
const { name, config, challenge = {}, onUrl, signal, fetchImpl = fetch, openUrl = openInBrowser } = options;
|
|
104
|
+
if (config.type !== "http")
|
|
105
|
+
throw new Error("only http servers can be signed in to; a local command server has no authorization server");
|
|
106
|
+
const key = serverKey(name, config);
|
|
107
|
+
const overrides = config.oauth;
|
|
108
|
+
const { authServer, resource } = await discover(config.url, challenge, fetchImpl, overrides?.authServerMetadataUrl);
|
|
109
|
+
// A hand-registered client has an EXACT redirect URI recorded against it, so a
|
|
110
|
+
// configured client id and a configured port travel together — the random port the
|
|
111
|
+
// automatic flow picks would not match what was registered.
|
|
112
|
+
const port = overrides?.callbackPort ?? (await findAvailablePort());
|
|
113
|
+
const redirect = redirectUri(port);
|
|
114
|
+
// Three ways to have a client id, in order of how deliberate they are: named in the
|
|
115
|
+
// config by someone who registered it by hand, carried over from a previous sign-in, or
|
|
116
|
+
// registered now. Registering afresh on every attempt leaves a trail of one-shot clients
|
|
117
|
+
// on the authorization server, and several of them start refusing once that list gets long.
|
|
118
|
+
const configuredSecret = clientSecretFor(name);
|
|
119
|
+
const previous = await readAuth(key);
|
|
120
|
+
const registration = overrides?.clientId
|
|
121
|
+
? { clientId: overrides.clientId, ...(configuredSecret ? { clientSecret: configuredSecret } : {}) }
|
|
122
|
+
: previous?.clientId && previous.tokenEndpoint === authServer.tokenEndpoint
|
|
123
|
+
? { clientId: previous.clientId, ...(previous.clientSecret ?? configuredSecret ? { clientSecret: previous.clientSecret ?? configuredSecret } : {}) }
|
|
124
|
+
: await registerClient(authServer, redirect, fetchImpl);
|
|
125
|
+
const pkce = createPkce();
|
|
126
|
+
const state = randomState();
|
|
127
|
+
const scope = scopeFor(challenge, resource, authServer);
|
|
128
|
+
const url = authorizeUrl({
|
|
129
|
+
meta: authServer,
|
|
130
|
+
clientId: registration.clientId,
|
|
131
|
+
redirect,
|
|
132
|
+
pkce,
|
|
133
|
+
state,
|
|
134
|
+
...(scope ? { scope } : {}),
|
|
135
|
+
...(resource?.resource ? { resource: resource.resource } : {}),
|
|
136
|
+
});
|
|
137
|
+
// Start listening BEFORE the browser is told where to go. A fast redirect arriving at a
|
|
138
|
+
// port nobody is on yet is a flow that fails for no reason the user can see.
|
|
139
|
+
const waiting = waitForCallback({ port, state, ...(signal ? { signal } : {}) });
|
|
140
|
+
openUrl(url);
|
|
141
|
+
// THE URL IS A FALLBACK, NOT THE INSTRUCTION. It is 400 characters of query string, and
|
|
142
|
+
// printing it up front means every successful sign-in — the overwhelming majority —
|
|
143
|
+
// dumps a wall of unreadable text into the transcript to say something the browser
|
|
144
|
+
// window already said better. So it waits: if the browser opened, the callback lands
|
|
145
|
+
// first and the URL is never shown. If nothing opened, a few seconds of silence is
|
|
146
|
+
// exactly when someone starts wondering what to do, and that is when it appears.
|
|
147
|
+
const hint = setTimeout(() => onUrl?.(url), URL_FALLBACK_DELAY_MS);
|
|
148
|
+
hint.unref?.();
|
|
149
|
+
let code;
|
|
150
|
+
try {
|
|
151
|
+
({ code } = await waiting);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
clearTimeout(hint);
|
|
155
|
+
}
|
|
156
|
+
const tokens = await exchangeCode({
|
|
157
|
+
meta: authServer,
|
|
158
|
+
clientId: registration.clientId,
|
|
159
|
+
...(registration.clientSecret ? { clientSecret: registration.clientSecret } : {}),
|
|
160
|
+
redirect,
|
|
161
|
+
code,
|
|
162
|
+
verifier: pkce.verifier,
|
|
163
|
+
...(resource?.resource ? { resource: resource.resource } : {}),
|
|
164
|
+
}, fetchImpl);
|
|
165
|
+
await writeAuth(key, {
|
|
166
|
+
accessToken: tokens.accessToken,
|
|
167
|
+
...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
|
|
168
|
+
...(tokens.expiresAt ? { expiresAt: tokens.expiresAt } : {}),
|
|
169
|
+
...(tokens.scope ? { scope: tokens.scope } : {}),
|
|
170
|
+
clientId: registration.clientId,
|
|
171
|
+
...(registration.clientSecret ? { clientSecret: registration.clientSecret } : {}),
|
|
172
|
+
tokenEndpoint: authServer.tokenEndpoint,
|
|
173
|
+
// Kept so signing out can tell the server the token is finished with, rather than
|
|
174
|
+
// only forgetting it locally and leaving it valid until it expires on its own.
|
|
175
|
+
...(authServer.revocationEndpoint ? { revocationEndpoint: authServer.revocationEndpoint } : {}),
|
|
176
|
+
...(resource?.resource ? { resource: resource.resource } : {}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Forget a server's credential, and tell the server so where it will listen.
|
|
181
|
+
*
|
|
182
|
+
* The local record goes FIRST and unconditionally. Revocation is a courtesy that makes
|
|
183
|
+
* the token stop working rather than merely stop being used, but a server that is down,
|
|
184
|
+
* or that never implemented RFC 7009, must not be able to keep someone signed in.
|
|
185
|
+
*/
|
|
186
|
+
export async function signOut(name, config, fetchImpl = fetch) {
|
|
187
|
+
const key = serverKey(name, config);
|
|
188
|
+
const stored = await readAuth(key);
|
|
189
|
+
await clearAuth(key);
|
|
190
|
+
if (!stored?.revocationEndpoint)
|
|
191
|
+
return;
|
|
192
|
+
const base = {
|
|
193
|
+
revocationEndpoint: stored.revocationEndpoint,
|
|
194
|
+
clientId: stored.clientId,
|
|
195
|
+
...(stored.clientSecret ? { clientSecret: stored.clientSecret } : {}),
|
|
196
|
+
};
|
|
197
|
+
// The refresh token first: it is the one that would otherwise keep minting new access
|
|
198
|
+
// tokens long after this.
|
|
199
|
+
if (stored.refreshToken)
|
|
200
|
+
await revokeToken({ ...base, token: stored.refreshToken, hint: "refresh_token", accessToken: stored.accessToken }, fetchImpl);
|
|
201
|
+
await revokeToken({ ...base, token: stored.accessToken, hint: "access_token" }, fetchImpl);
|
|
202
|
+
}
|
|
203
|
+
/** Whether we hold anything for this server, without producing it. Drives whether the
|
|
204
|
+
* manage screen offers "Sign in" or "Sign out". */
|
|
205
|
+
export async function hasCredential(name, config) {
|
|
206
|
+
return (await readAuth(serverKey(name, config))) !== undefined;
|
|
207
|
+
}
|
|
208
|
+
/** The 401's scope, then the resource's, then the authorization server's own advertised
|
|
209
|
+
* list. The last one is a guess and is used only because some servers reject an
|
|
210
|
+
* authorize request carrying no scope at all. */
|
|
211
|
+
function scopeFor(challenge, resource, authServer) {
|
|
212
|
+
if (challenge.scope?.trim())
|
|
213
|
+
return challenge.scope.trim();
|
|
214
|
+
if (resource?.scopesSupported?.length)
|
|
215
|
+
return resource.scopesSupported.join(" ");
|
|
216
|
+
if (authServer.scopesSupported?.length)
|
|
217
|
+
return authServer.scopesSupported.join(" ");
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Hand a URL to whatever the user's machine calls a browser.
|
|
222
|
+
*
|
|
223
|
+
* Detached and with its streams discarded, because this must not become a child whose
|
|
224
|
+
* exit we wait on or whose output lands in the middle of the UI. A failure here is
|
|
225
|
+
* swallowed on purpose: the URL was already reported through `onUrl`, so a machine with
|
|
226
|
+
* no default browser leaves the user able to copy it rather than staring at an error
|
|
227
|
+
* about a program they did not ask us to run.
|
|
228
|
+
*/
|
|
229
|
+
function openInBrowser(url) {
|
|
230
|
+
try {
|
|
231
|
+
const child = process.platform === "win32"
|
|
232
|
+
? spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true })
|
|
233
|
+
: process.platform === "darwin"
|
|
234
|
+
? spawn("open", [url], { detached: true, stdio: "ignore" })
|
|
235
|
+
: spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
236
|
+
child.on("error", () => { });
|
|
237
|
+
child.unref();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Reported through onUrl already.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mcp/oauth/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* metadata.ts — finding out WHO can issue a token for a server, from the server itself.
|
|
3
|
+
*
|
|
4
|
+
* A remote MCP server that wants authorization answers an unauthenticated call with 401
|
|
5
|
+
* and a `WWW-Authenticate` header, and that header is the whole entry point: it names the
|
|
6
|
+
* URL of the server's Protected Resource Metadata (RFC 9728), which in turn names the
|
|
7
|
+
* authorization servers that can issue tokens for it, and each of those publishes its own
|
|
8
|
+
* metadata (RFC 8414) saying where to send someone to approve access and where to redeem
|
|
9
|
+
* the code afterwards. Three documents, fetched in that order, and none of them is
|
|
10
|
+
* configured by hand: the point of the chain is that adding a server is a URL and nothing
|
|
11
|
+
* else.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is PURE — parsing and URL construction only. The fetching lives in
|
|
14
|
+
* `flow.ts`, so the part with all the branching is the part a test can drive directly
|
|
15
|
+
* with a string, and the part doing network I/O stays small enough to read in one go.
|
|
16
|
+
*
|
|
17
|
+
* THE FALLBACKS ARE NOT OPTIONAL. RFC 9728 arrived long after the servers that are
|
|
18
|
+
* deployed today, and a large share of them either omit the header, omit the metadata
|
|
19
|
+
* document, or publish it at the pre-RFC location. Every lookup here therefore produces a
|
|
20
|
+
* LIST of candidate URLs in descending order of correctness, and the caller tries them in
|
|
21
|
+
* turn. The alternative is refusing to connect to most of the ecosystem on a technicality.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Parse a `WWW-Authenticate` header (pure).
|
|
25
|
+
*
|
|
26
|
+
* The grammar is `Scheme param=value, param="value"` and the values that matter here are
|
|
27
|
+
* routinely quoted and routinely contain `=` and `,` of their own (a metadata URL has a
|
|
28
|
+
* query string), so this walks the string rather than splitting it. A header we cannot
|
|
29
|
+
* make sense of yields an empty challenge rather than throwing: it costs us the shortcut,
|
|
30
|
+
* not the connection, because every field is a shortcut past a fallback.
|
|
31
|
+
*/
|
|
32
|
+
export function parseWwwAuthenticate(header) {
|
|
33
|
+
if (!header)
|
|
34
|
+
return {};
|
|
35
|
+
const challenge = {};
|
|
36
|
+
// Skip the scheme token ("Bearer"), then read `key=value` pairs. A quoted value runs to
|
|
37
|
+
// its closing quote and may hold anything; a bare value runs to the next comma.
|
|
38
|
+
const params = /([A-Za-z_-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^,\s]*))/g;
|
|
39
|
+
for (const match of header.matchAll(params)) {
|
|
40
|
+
const key = match[1].toLowerCase();
|
|
41
|
+
const value = (match[2] ?? match[3] ?? "").replace(/\\(.)/g, "$1");
|
|
42
|
+
if (!value)
|
|
43
|
+
continue;
|
|
44
|
+
if (key === "resource_metadata")
|
|
45
|
+
challenge.resourceMetadata = value;
|
|
46
|
+
else if (key === "scope")
|
|
47
|
+
challenge.scope = value;
|
|
48
|
+
else if (key === "error")
|
|
49
|
+
challenge.error = value;
|
|
50
|
+
}
|
|
51
|
+
return challenge;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Where a server's Protected Resource Metadata might live, best first (pure).
|
|
55
|
+
*
|
|
56
|
+
* RFC 9728 inserts the well-known segment BEFORE the resource's own path
|
|
57
|
+
* (`/.well-known/oauth-protected-resource/mcp`), which is the opposite of the OpenID
|
|
58
|
+
* convention and the single most common thing to get wrong, so the path-aware form leads
|
|
59
|
+
* and the root form follows for servers that publish one document for the whole host.
|
|
60
|
+
*/
|
|
61
|
+
export function protectedResourceUrls(serverUrl) {
|
|
62
|
+
const url = safeUrl(serverUrl);
|
|
63
|
+
if (!url)
|
|
64
|
+
return [];
|
|
65
|
+
const path = trimSlashes(url.pathname);
|
|
66
|
+
const out = [];
|
|
67
|
+
if (path)
|
|
68
|
+
out.push(`${url.origin}/.well-known/oauth-protected-resource/${path}`);
|
|
69
|
+
out.push(`${url.origin}/.well-known/oauth-protected-resource`);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Where an authorization server's metadata might live, best first (pure).
|
|
74
|
+
*
|
|
75
|
+
* Four shapes, because two specifications disagree about where the segment goes and both
|
|
76
|
+
* are deployed: RFC 8414 puts `.well-known` before the issuer's path, OpenID Connect
|
|
77
|
+
* Discovery appends it. An issuer with no path collapses to two.
|
|
78
|
+
*/
|
|
79
|
+
export function authServerUrls(issuer) {
|
|
80
|
+
const url = safeUrl(issuer);
|
|
81
|
+
if (!url)
|
|
82
|
+
return [];
|
|
83
|
+
const path = trimSlashes(url.pathname);
|
|
84
|
+
const out = [];
|
|
85
|
+
if (path) {
|
|
86
|
+
out.push(`${url.origin}/.well-known/oauth-authorization-server/${path}`);
|
|
87
|
+
out.push(`${url.origin}/${path}/.well-known/oauth-authorization-server`);
|
|
88
|
+
out.push(`${url.origin}/.well-known/openid-configuration/${path}`);
|
|
89
|
+
out.push(`${url.origin}/${path}/.well-known/openid-configuration`);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
out.push(`${url.origin}/.well-known/oauth-authorization-server`);
|
|
93
|
+
out.push(`${url.origin}/.well-known/openid-configuration`);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Read RFC 9728 metadata, or null if it is not that (pure). */
|
|
97
|
+
export function readResourceMetadata(json) {
|
|
98
|
+
const o = asObject(json);
|
|
99
|
+
if (!o)
|
|
100
|
+
return null;
|
|
101
|
+
const servers = stringArray(o.authorization_servers);
|
|
102
|
+
if (servers.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const out = { authorizationServers: servers };
|
|
105
|
+
const resource = asString(o.resource);
|
|
106
|
+
if (resource)
|
|
107
|
+
out.resource = resource;
|
|
108
|
+
const scopes = stringArray(o.scopes_supported);
|
|
109
|
+
if (scopes.length > 0)
|
|
110
|
+
out.scopesSupported = scopes;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** Read RFC 8414 / OIDC discovery metadata, or null if the required endpoints are absent
|
|
114
|
+
* (pure). Both endpoints are load bearing, so a document missing either is not usable
|
|
115
|
+
* even though it parsed. */
|
|
116
|
+
export function readAuthServerMetadata(json) {
|
|
117
|
+
const o = asObject(json);
|
|
118
|
+
if (!o)
|
|
119
|
+
return null;
|
|
120
|
+
const authorizationEndpoint = asString(o.authorization_endpoint);
|
|
121
|
+
const tokenEndpoint = asString(o.token_endpoint);
|
|
122
|
+
if (!authorizationEndpoint || !tokenEndpoint)
|
|
123
|
+
return null;
|
|
124
|
+
const out = {
|
|
125
|
+
issuer: asString(o.issuer) ?? "",
|
|
126
|
+
authorizationEndpoint,
|
|
127
|
+
tokenEndpoint,
|
|
128
|
+
};
|
|
129
|
+
const registration = asString(o.registration_endpoint);
|
|
130
|
+
if (registration)
|
|
131
|
+
out.registrationEndpoint = registration;
|
|
132
|
+
const revocation = asString(o.revocation_endpoint);
|
|
133
|
+
if (revocation)
|
|
134
|
+
out.revocationEndpoint = revocation;
|
|
135
|
+
const scopes = stringArray(o.scopes_supported);
|
|
136
|
+
if (scopes.length > 0)
|
|
137
|
+
out.scopesSupported = scopes;
|
|
138
|
+
const methods = stringArray(o.code_challenge_methods_supported);
|
|
139
|
+
if (methods.length > 0)
|
|
140
|
+
out.codeChallengeMethodsSupported = methods;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Does this authorization server support the only PKCE method we will use (pure)?
|
|
145
|
+
*
|
|
146
|
+
* A server that ADVERTISES its methods and does not list S256 is refused rather than
|
|
147
|
+
* downgraded. `plain` sends the verifier itself as the challenge, which protects against
|
|
148
|
+
* nothing, and quietly falling back to it would turn a loud incompatibility into a silent
|
|
149
|
+
* loss of the protection PKCE exists for. A server that advertises nothing is assumed to
|
|
150
|
+
* support S256, which is what the OAuth 2.1 draft requires of it anyway.
|
|
151
|
+
*/
|
|
152
|
+
export function supportsS256(meta) {
|
|
153
|
+
return meta.codeChallengeMethodsSupported === undefined || meta.codeChallengeMethodsSupported.includes("S256");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The scope string to ask for, or undefined to let the server decide (pure).
|
|
157
|
+
*
|
|
158
|
+
* The 401's own `scope` wins: it is the server saying what the call that just failed
|
|
159
|
+
* needed, which is more precise than anything advertised. Otherwise everything the
|
|
160
|
+
* resource says it supports, and failing that nothing at all — an empty `scope` parameter
|
|
161
|
+
* is meaningfully different from an absent one at several providers, so it is omitted
|
|
162
|
+
* rather than sent blank.
|
|
163
|
+
*/
|
|
164
|
+
export function scopeToRequest(challenge, resource) {
|
|
165
|
+
if (challenge.scope?.trim())
|
|
166
|
+
return challenge.scope.trim();
|
|
167
|
+
if (resource?.scopesSupported && resource.scopesSupported.length > 0)
|
|
168
|
+
return resource.scopesSupported.join(" ");
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
function safeUrl(raw) {
|
|
172
|
+
try {
|
|
173
|
+
const url = new URL(raw);
|
|
174
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url : null;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function trimSlashes(path) {
|
|
181
|
+
return path.replace(/^\/+|\/+$/g, "");
|
|
182
|
+
}
|
|
183
|
+
function asObject(value) {
|
|
184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
185
|
+
}
|
|
186
|
+
function asString(value) {
|
|
187
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
188
|
+
}
|
|
189
|
+
function stringArray(value) {
|
|
190
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim().length > 0) : [];
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../../src/mcp/oauth/metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pkce.ts — the proof that the code being redeemed is being redeemed by whoever asked.
|
|
3
|
+
*
|
|
4
|
+
* A CLI cannot keep a client secret: anything shipped in the binary or written to a
|
|
5
|
+
* config file is readable by anything else on the machine, so OAuth for native apps
|
|
6
|
+
* treats us as a PUBLIC client and closes the resulting hole with PKCE instead. We invent
|
|
7
|
+
* a random `verifier`, send only its SHA-256 (the `challenge`) when we send the user off
|
|
8
|
+
* to approve, and send the verifier itself when we redeem the code. Anything that
|
|
9
|
+
* intercepts the redirect gets a code it cannot spend, because it never saw the verifier.
|
|
10
|
+
*
|
|
11
|
+
* `state` is the other half and guards the other direction: it proves a callback arriving
|
|
12
|
+
* on our loopback port belongs to the flow WE started, rather than to something else on
|
|
13
|
+
* the machine firing a request at the port we just opened.
|
|
14
|
+
*
|
|
15
|
+
* All of it is one small pure-ish module on node:crypto so the encodings — which are the
|
|
16
|
+
* part that silently breaks, since base64url is not base64 and a single `+` will fail an
|
|
17
|
+
* exchange with a message that names none of this — live in exactly one place.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
20
|
+
/** RFC 7636 puts the verifier between 43 and 128 characters; 32 random bytes encode to
|
|
21
|
+
* 43, which is the shortest legal value and therefore the one with nothing to spare in
|
|
22
|
+
* it. Entropy is the point, not length, and 256 bits is not the weak link. */
|
|
23
|
+
const VERIFIER_BYTES = 32;
|
|
24
|
+
/** base64url: base64 with the two URL-hostile characters swapped and the padding dropped.
|
|
25
|
+
* Everything here goes into a query string or a form body, so the plain alphabet would
|
|
26
|
+
* need escaping at every call site and would be forgotten at one of them. */
|
|
27
|
+
function base64Url(buffer) {
|
|
28
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
29
|
+
}
|
|
30
|
+
/** A fresh verifier and its challenge. One per authorization attempt, never reused: a
|
|
31
|
+
* verifier that outlives its code is a secret with no expiry. */
|
|
32
|
+
export function createPkce() {
|
|
33
|
+
const verifier = base64Url(randomBytes(VERIFIER_BYTES));
|
|
34
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
35
|
+
return { verifier, challenge, method: "S256" };
|
|
36
|
+
}
|
|
37
|
+
/** An opaque value tying a callback to the request that caused it. */
|
|
38
|
+
export function randomState() {
|
|
39
|
+
return base64Url(randomBytes(VERIFIER_BYTES));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compare two `state` values without leaking where they first differ (pure).
|
|
43
|
+
*
|
|
44
|
+
* `===` on strings short-circuits at the first mismatched character, and the callback URL
|
|
45
|
+
* is attacker-supplied, so the comparison time is a signal about our secret. The amount
|
|
46
|
+
* of signal is small and the fix is three lines, which is the wrong trade to skip.
|
|
47
|
+
*/
|
|
48
|
+
export function stateMatches(expected, received) {
|
|
49
|
+
if (typeof received !== "string" || received.length !== expected.length)
|
|
50
|
+
return false;
|
|
51
|
+
let diff = 0;
|
|
52
|
+
for (let i = 0; i < expected.length; i++)
|
|
53
|
+
diff |= expected.charCodeAt(i) ^ received.charCodeAt(i);
|
|
54
|
+
return diff === 0;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../../src/mcp/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;+EAE+E;AAC/E,MAAM,cAAc,GAAG,EAAE,CAAC;AAW1B;;8EAE8E;AAC9E,SAAS,SAAS,CAAC,MAAc;IAC/B,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9F,CAAC;AAED;kEACkE;AAClE,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACjD,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,WAAW;IACzB,OAAO,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAAmC;IAChF,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACtF,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAClG,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tokenStore.ts — where a server's tokens live between runs.
|
|
3
|
+
*
|
|
4
|
+
* NOT the `.env` file the API keys use, and that is deliberate. A provider key is typed
|
|
5
|
+
* once by a person and belongs in a file a person can open; an OAuth token is issued by a
|
|
6
|
+
* machine, expires in an hour, is replaced silently by a refresh, and is meaningless to
|
|
7
|
+
* read. Putting the two in one file would mean a rewrite of the user's own config every
|
|
8
|
+
* time a token rotated, with their comments and ordering at risk each time.
|
|
9
|
+
*
|
|
10
|
+
* So: one JSON file of our own under the state directory, written whole, `0600`.
|
|
11
|
+
*
|
|
12
|
+
* KEYED BY NAME **AND** CONFIG, via `serverKey`. Keying by name alone would hand the
|
|
13
|
+
* token issued for one URL to whatever is configured under that name later — rename a
|
|
14
|
+
* server, point it at a different host, and the old credential follows it. Names are
|
|
15
|
+
* chosen by the user and reused freely; the URL is what the token was actually minted
|
|
16
|
+
* for, so both go into the key and changing either is a different server.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from "node:fs";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { stateRoot } from "../../memory/store.js";
|
|
22
|
+
/** Read on every call rather than cached at import, matching `stateRoot` itself, so a
|
|
23
|
+
* test can point this somewhere disposable before anything touches it. */
|
|
24
|
+
export function authFilePath() {
|
|
25
|
+
return join(stateRoot(), "mcp-auth.json");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The identity a credential is filed under (pure).
|
|
29
|
+
*
|
|
30
|
+
* A short hash of the URL rather than the URL itself: this string ends up as a JSON key
|
|
31
|
+
* in a file people will open when something is wrong, and a full URL with its query
|
|
32
|
+
* string makes that file unreadable. The name is kept in front of it so the entry is
|
|
33
|
+
* still recognisable at a glance.
|
|
34
|
+
*/
|
|
35
|
+
export function serverKey(name, config) {
|
|
36
|
+
// The HEADERS are part of the identity too, not just the URL. A configured header is
|
|
37
|
+
// frequently the thing that selects an account or a tenant at the other end, so a token
|
|
38
|
+
// minted while one was set has no business being sent after it changed — that is a
|
|
39
|
+
// credential for a context that no longer exists.
|
|
40
|
+
const identity = config.type === "http"
|
|
41
|
+
? JSON.stringify({ type: "http", url: config.url, headers: config.headers ?? {} })
|
|
42
|
+
: JSON.stringify({ type: "stdio", command: config.command, args: config.args });
|
|
43
|
+
const digest = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
44
|
+
return `${name}:${digest}`;
|
|
45
|
+
}
|
|
46
|
+
async function readFile() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(authFilePath(), "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
const servers = typeof parsed === "object" && parsed !== null ? parsed.servers : undefined;
|
|
51
|
+
return { servers: typeof servers === "object" && servers !== null ? servers : {} };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing is the normal case on a first run, and unreadable is not worth failing a
|
|
55
|
+
// connection over: both mean "no credential", and the flow that follows will mint one.
|
|
56
|
+
return { servers: {} };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replace the whole file, owner-readable only.
|
|
61
|
+
*
|
|
62
|
+
* Written to a temporary name and renamed, because a token file truncated by a crash
|
|
63
|
+
* mid-write is not a token file with one bad entry — it is a file that fails to parse and
|
|
64
|
+
* silently signs the user out of every server at once. The mode is set on the temporary
|
|
65
|
+
* file BEFORE the rename, so there is no window in which the real path exists readable.
|
|
66
|
+
*/
|
|
67
|
+
async function writeFile(file) {
|
|
68
|
+
const target = authFilePath();
|
|
69
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
70
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
71
|
+
await fs.writeFile(temp, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
72
|
+
await fs.rename(temp, target);
|
|
73
|
+
}
|
|
74
|
+
export async function readAuth(key) {
|
|
75
|
+
return (await readFile()).servers[key];
|
|
76
|
+
}
|
|
77
|
+
export async function writeAuth(key, auth) {
|
|
78
|
+
// Re-read immediately before writing rather than holding the file in memory: two
|
|
79
|
+
// servers can finish a refresh at the same moment, and the loser of a last-write-wins
|
|
80
|
+
// race would otherwise drop the winner's brand new token.
|
|
81
|
+
const file = await readFile();
|
|
82
|
+
file.servers[key] = auth;
|
|
83
|
+
await writeFile(file);
|
|
84
|
+
}
|
|
85
|
+
export async function clearAuth(key) {
|
|
86
|
+
const file = await readFile();
|
|
87
|
+
if (!(key in file.servers))
|
|
88
|
+
return;
|
|
89
|
+
delete file.servers[key];
|
|
90
|
+
await writeFile(file);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Is this token worth sending (pure)?
|
|
94
|
+
*
|
|
95
|
+
* A minute of headroom, because the token has to survive the round trip it is about to be
|
|
96
|
+
* spent on, and a clock that is thirty seconds fast would otherwise send something the
|
|
97
|
+
* server has already retired and turn a refresh into a 401.
|
|
98
|
+
*/
|
|
99
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
100
|
+
export function isExpired(auth, now = Date.now()) {
|
|
101
|
+
return auth.expiresAt !== undefined && auth.expiresAt - EXPIRY_SKEW_MS <= now;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=tokenStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenStore.js","sourceRoot":"","sources":["../../../src/mcp/oauth/tokenStore.ts"],"names":[],"mappings":"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"}
|