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.
Files changed (49) hide show
  1. package/dist/cli/App.js +158 -99
  2. package/dist/cli/App.js.map +1 -1
  3. package/dist/cli/attachments.js +14 -1
  4. package/dist/cli/attachments.js.map +1 -1
  5. package/dist/cli/components/KeyManager.js +3 -42
  6. package/dist/cli/components/KeyManager.js.map +1 -1
  7. package/dist/cli/components/McpMinitabs.js +689 -0
  8. package/dist/cli/components/McpMinitabs.js.map +1 -0
  9. package/dist/cli/components/minitabs.js +71 -0
  10. package/dist/cli/components/minitabs.js.map +1 -0
  11. package/dist/dynamo/engine.js +29 -7
  12. package/dist/dynamo/engine.js.map +1 -1
  13. package/dist/dynamo/prompt.js +2 -0
  14. package/dist/dynamo/prompt.js.map +1 -1
  15. package/dist/mcp/config.js +57 -3
  16. package/dist/mcp/config.js.map +1 -1
  17. package/dist/mcp/configWrite.js +40 -2
  18. package/dist/mcp/configWrite.js.map +1 -1
  19. package/dist/mcp/connection.js +51 -1
  20. package/dist/mcp/connection.js.map +1 -1
  21. package/dist/mcp/manager.js +65 -0
  22. package/dist/mcp/manager.js.map +1 -1
  23. package/dist/mcp/oauth/callback.js +171 -0
  24. package/dist/mcp/oauth/callback.js.map +1 -0
  25. package/dist/mcp/oauth/flow.js +415 -0
  26. package/dist/mcp/oauth/flow.js.map +1 -0
  27. package/dist/mcp/oauth/index.js +243 -0
  28. package/dist/mcp/oauth/index.js.map +1 -0
  29. package/dist/mcp/oauth/metadata.js +192 -0
  30. package/dist/mcp/oauth/metadata.js.map +1 -0
  31. package/dist/mcp/oauth/pkce.js +56 -0
  32. package/dist/mcp/oauth/pkce.js.map +1 -0
  33. package/dist/mcp/oauth/tokenStore.js +103 -0
  34. package/dist/mcp/oauth/tokenStore.js.map +1 -0
  35. package/dist/mcp/transport/http.js +33 -5
  36. package/dist/mcp/transport/http.js.map +1 -1
  37. package/dist/memory/session.js +4 -0
  38. package/dist/memory/session.js.map +1 -1
  39. package/dist/tools/backgroundShells.js +42 -4
  40. package/dist/tools/backgroundShells.js.map +1 -1
  41. package/dist/tools/mcpAdd.js +7 -0
  42. package/dist/tools/mcpAdd.js.map +1 -1
  43. package/dist/tools/subagent.js +41 -3
  44. package/dist/tools/subagent.js.map +1 -1
  45. package/dist/tools/todo.js +35 -0
  46. package/dist/tools/todo.js.map +1 -1
  47. package/dist/tools/verifyReport.js +144 -0
  48. package/dist/tools/verifyReport.js.map +1 -0
  49. 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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"}