@ni-c/mcp-hub 0.9.2 → 0.10.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/CHANGELOG.md +231 -0
- package/README.md +114 -54
- package/dist/admin.js +231 -7
- package/dist/admin.js.map +1 -1
- package/dist/auth/address.js +89 -0
- package/dist/auth/address.js.map +1 -0
- package/dist/auth/cimd.js +367 -0
- package/dist/auth/cimd.js.map +1 -0
- package/dist/auth/consent-page.js +4 -7
- package/dist/auth/consent-page.js.map +1 -1
- package/dist/auth/login-page.js +4 -7
- package/dist/auth/login-page.js.map +1 -1
- package/dist/auth/page.js +22 -0
- package/dist/auth/page.js.map +1 -1
- package/dist/auth/pinned-fetch.js +129 -0
- package/dist/auth/pinned-fetch.js.map +1 -0
- package/dist/auth/private-key-jwt.js +213 -0
- package/dist/auth/private-key-jwt.js.map +1 -0
- package/dist/auth/provider.js +102 -45
- package/dist/auth/provider.js.map +1 -1
- package/dist/auth/rate-limit.js +43 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/redirect-uri.js +58 -0
- package/dist/auth/redirect-uri.js.map +1 -0
- package/dist/auth/registration.js +146 -0
- package/dist/auth/registration.js.map +1 -0
- package/dist/auth/routes.js +67 -41
- package/dist/auth/routes.js.map +1 -1
- package/dist/auth/signed-token.js +49 -0
- package/dist/auth/signed-token.js.map +1 -0
- package/dist/auth/store.js +357 -5
- package/dist/auth/store.js.map +1 -1
- package/dist/auth/text.js +51 -0
- package/dist/auth/text.js.map +1 -0
- package/dist/config.js +130 -4
- package/dist/config.js.map +1 -1
- package/dist/health.js +22 -3
- package/dist/health.js.map +1 -1
- package/dist/hub.js +28 -0
- package/dist/hub.js.map +1 -1
- package/dist/index.js +123 -10
- package/dist/index.js.map +1 -1
- package/dist/proxy.js +25 -3
- package/dist/proxy.js.map +1 -1
- package/dist/stdio.js +27 -2
- package/dist/stdio.js.map +1 -1
- package/dist/supervisor.js +152 -8
- package/dist/supervisor.js.map +1 -1
- package/dist/tool-filter.js +58 -0
- package/dist/tool-filter.js.map +1 -0
- package/dist/upstream/auth.js +435 -0
- package/dist/upstream/auth.js.map +1 -0
- package/dist/upstream/login.js +94 -0
- package/dist/upstream/login.js.map +1 -0
- package/dist/upstream/provider.js +286 -0
- package/dist/upstream/provider.js.map +1 -0
- package/dist/upstream/routes.js +96 -0
- package/dist/upstream/routes.js.map +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { isPrivateAddress, resolvePublicAddress } from './address.js';
|
|
2
|
+
import { guardedRequest } from './pinned-fetch.js';
|
|
3
|
+
import { isSafeRedirectUri } from './redirect-uri.js';
|
|
4
|
+
import { clampDisplayName, logSafe } from './text.js';
|
|
5
|
+
// Lives in address.ts now — re-exported because it is part of this module's
|
|
6
|
+
// established surface and the test suite reaches for it here.
|
|
7
|
+
export { isPrivateAddress };
|
|
8
|
+
/**
|
|
9
|
+
* OAuth Client ID Metadata Documents (CIMD,
|
|
10
|
+
* draft-ietf-oauth-client-id-metadata-document-00), the registration mechanism
|
|
11
|
+
* the MCP specification prefers over RFC 7591 dynamic registration.
|
|
12
|
+
*
|
|
13
|
+
* The client hosts its own metadata at a stable HTTPS URL and uses that URL as
|
|
14
|
+
* its client_id; the authorization server fetches and validates the document
|
|
15
|
+
* instead of handing out credentials. Nothing is persisted here — the document
|
|
16
|
+
* is the source of truth and is re-fetched when the cache entry expires, so a
|
|
17
|
+
* client that changes its redirect URIs does not need to re-register.
|
|
18
|
+
*
|
|
19
|
+
* Everything in this file runs against a URL an unauthenticated caller chose,
|
|
20
|
+
* so the fetch is treated as hostile: no redirects, no private addresses, the
|
|
21
|
+
* checked address pinned into the connection, a hard size cap and a short
|
|
22
|
+
* timeout.
|
|
23
|
+
*/
|
|
24
|
+
/** Draft §6.6 recommends 5 kB; anything larger is not a client metadata document. */
|
|
25
|
+
const MAX_DOCUMENT_BYTES = 5 * 1024;
|
|
26
|
+
/** A key set is legitimately larger than a metadata document, but not by much:
|
|
27
|
+
* a handful of RSA keys is a couple of kilobytes. */
|
|
28
|
+
const MAX_JWKS_BYTES = 64 * 1024;
|
|
29
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
30
|
+
const MAX_CACHE_ENTRIES = 200;
|
|
31
|
+
const MIN_TTL_MS = 60_000;
|
|
32
|
+
const MAX_TTL_MS = 24 * 3600_000;
|
|
33
|
+
const DEFAULT_TTL_MS = 3600_000;
|
|
34
|
+
/** How long a rejected client_id is remembered, so a bad URL cannot be used to
|
|
35
|
+
* make the hub hammer a third party once per authorization request. */
|
|
36
|
+
const NEGATIVE_TTL_MS = 30_000;
|
|
37
|
+
/**
|
|
38
|
+
* The per-URL negative cache alone does not bound how often one host can be
|
|
39
|
+
* fetched: the query string is part of the client_id, so `?n=1`, `?n=2` and so
|
|
40
|
+
* on are all distinct identifiers pointing at the same server. Failures are
|
|
41
|
+
* therefore also counted per origin.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_ORIGIN_FAILURES = 10;
|
|
44
|
+
const ORIGIN_FAILURE_WINDOW_MS = 30_000;
|
|
45
|
+
const MAX_TRACKED_ORIGINS = 200;
|
|
46
|
+
const ASSERTION_SAFE_METHODS = new Set(['none', 'private_key_jwt']);
|
|
47
|
+
let fetchOverride;
|
|
48
|
+
/**
|
|
49
|
+
* Tests serve their metadata documents from a stub here. Client IDs are https
|
|
50
|
+
* URLs by specification and stay that way — the stub is what lets the suite
|
|
51
|
+
* exercise the real fetch path without a certificate authority. Passing
|
|
52
|
+
* undefined restores the global fetch.
|
|
53
|
+
*/
|
|
54
|
+
export function setCimdFetch(fn) {
|
|
55
|
+
fetchOverride = fn;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Draft §4.1: the identifier must be an https URL with a path component, no
|
|
59
|
+
* fragment, no credentials and no dot-segments. A `client_id` that fails this
|
|
60
|
+
* is not a metadata document URL at all and is looked up as a locally
|
|
61
|
+
* registered client instead — which is what keeps DCR working beside CIMD.
|
|
62
|
+
*/
|
|
63
|
+
export function isClientIdMetadataUrl(clientId) {
|
|
64
|
+
let url;
|
|
65
|
+
try {
|
|
66
|
+
url = new URL(clientId);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (url.protocol !== 'https:')
|
|
72
|
+
return false;
|
|
73
|
+
if (url.hash || url.username || url.password)
|
|
74
|
+
return false;
|
|
75
|
+
if (url.pathname === '' || url.pathname === '/')
|
|
76
|
+
return false;
|
|
77
|
+
// Dot segments are checked against the string the client sent, not the
|
|
78
|
+
// parsed URL: the URL parser resolves `/a/../b` away, and a percent-encoded
|
|
79
|
+
// `%2e%2e` survives parsing entirely while still traversing on the server
|
|
80
|
+
// that ultimately serves the document.
|
|
81
|
+
const rawPath = clientId.slice(url.origin.length).split(/[?#]/)[0];
|
|
82
|
+
return !rawPath.split('/').some(segment => /^(?:\.|%2e){1,2}$/i.test(segment));
|
|
83
|
+
}
|
|
84
|
+
export class CimdResolver {
|
|
85
|
+
options;
|
|
86
|
+
cache = new Map();
|
|
87
|
+
/** Collapses concurrent lookups of the same client_id into one fetch. */
|
|
88
|
+
inFlight = new Map();
|
|
89
|
+
/** Recent failures per origin, so distinct client_ids on one host cannot be
|
|
90
|
+
* used to keep fetching it. */
|
|
91
|
+
originFailures = new Map();
|
|
92
|
+
allowedOrigins;
|
|
93
|
+
constructor(options = {}) {
|
|
94
|
+
this.options = options;
|
|
95
|
+
this.allowedOrigins = new Set(options.allowedOrigins ?? []);
|
|
96
|
+
}
|
|
97
|
+
/** Undefined for every rejection; the reason is logged, never returned to
|
|
98
|
+
* the client, so an attacker cannot map out the admission policy. */
|
|
99
|
+
async resolve(clientId) {
|
|
100
|
+
const cached = this.cache.get(clientId);
|
|
101
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
102
|
+
// Refresh recency so the eviction below drops genuinely cold entries.
|
|
103
|
+
this.cache.delete(clientId);
|
|
104
|
+
this.cache.set(clientId, cached);
|
|
105
|
+
return cached.client;
|
|
106
|
+
}
|
|
107
|
+
const pending = this.inFlight.get(clientId);
|
|
108
|
+
if (pending)
|
|
109
|
+
return pending;
|
|
110
|
+
const promise = this.load(clientId).finally(() => this.inFlight.delete(clientId));
|
|
111
|
+
this.inFlight.set(clientId, promise);
|
|
112
|
+
return promise;
|
|
113
|
+
}
|
|
114
|
+
async load(clientId) {
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(clientId);
|
|
117
|
+
if (this.allowedOrigins.size > 0 && !this.allowedOrigins.has(url.origin)) {
|
|
118
|
+
return this.reject(clientId, `origin ${url.origin} is not in CIMD_ALLOWED_ORIGINS`);
|
|
119
|
+
}
|
|
120
|
+
if (this.originIsExhausted(url.origin)) {
|
|
121
|
+
// Counting this one too would keep extending the window for as long as
|
|
122
|
+
// the flood lasts, so the block could never lift.
|
|
123
|
+
return this.reject(clientId, `origin ${url.origin} failed too often recently`, false);
|
|
124
|
+
}
|
|
125
|
+
const pinned = await this.assertPublicHost(url.hostname);
|
|
126
|
+
const { document, ttl } = await this.fetchDocument(url, pinned);
|
|
127
|
+
const client = validateDocument(document, clientId);
|
|
128
|
+
this.remember(clientId, client, ttl);
|
|
129
|
+
console.log(`mcp-hub: fetched client metadata document ${logSafe(clientId)} (${logSafe(client.client_name ?? 'unnamed')})`);
|
|
130
|
+
return client;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return this.reject(clientId, error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
reject(clientId, reason, countAgainstOrigin = true) {
|
|
137
|
+
console.warn(`mcp-hub: refused client metadata document ${logSafe(clientId)}: ${logSafe(reason)}`);
|
|
138
|
+
this.remember(clientId, undefined, NEGATIVE_TTL_MS);
|
|
139
|
+
if (countAgainstOrigin)
|
|
140
|
+
this.recordOriginFailure(clientId);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
/** True once one origin has failed often enough that further lookups are
|
|
144
|
+
* refused without a request until the window passes. */
|
|
145
|
+
originIsExhausted(origin) {
|
|
146
|
+
const entry = this.originFailures.get(origin);
|
|
147
|
+
if (!entry || entry.resetAt <= Date.now())
|
|
148
|
+
return false;
|
|
149
|
+
return entry.count >= MAX_ORIGIN_FAILURES;
|
|
150
|
+
}
|
|
151
|
+
recordOriginFailure(clientId) {
|
|
152
|
+
let origin;
|
|
153
|
+
try {
|
|
154
|
+
origin = new URL(clientId).origin;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return; // not a URL, so there is no origin to hold responsible
|
|
158
|
+
}
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const entry = this.originFailures.get(origin);
|
|
161
|
+
if (!entry || entry.resetAt <= now) {
|
|
162
|
+
this.originFailures.set(origin, { count: 1, resetAt: now + ORIGIN_FAILURE_WINDOW_MS });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
entry.count++;
|
|
166
|
+
}
|
|
167
|
+
if (this.originFailures.size > MAX_TRACKED_ORIGINS) {
|
|
168
|
+
for (const [key, value] of this.originFailures) {
|
|
169
|
+
if (value.resetAt <= now)
|
|
170
|
+
this.originFailures.delete(key);
|
|
171
|
+
}
|
|
172
|
+
while (this.originFailures.size > MAX_TRACKED_ORIGINS) {
|
|
173
|
+
const oldest = this.originFailures.keys().next();
|
|
174
|
+
if (oldest.done)
|
|
175
|
+
break;
|
|
176
|
+
this.originFailures.delete(oldest.value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
remember(clientId, client, ttl) {
|
|
181
|
+
this.cache.delete(clientId);
|
|
182
|
+
this.cache.set(clientId, { client, expiresAt: Date.now() + ttl });
|
|
183
|
+
// Insertion order is recency order because a hit re-inserts; the oldest
|
|
184
|
+
// key is therefore the least recently used one.
|
|
185
|
+
while (this.cache.size > MAX_CACHE_ENTRIES) {
|
|
186
|
+
const oldest = this.cache.keys().next();
|
|
187
|
+
if (oldest.done)
|
|
188
|
+
break;
|
|
189
|
+
this.cache.delete(oldest.value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Draft §6.5: refuse every private or loopback address, so a client_id cannot
|
|
194
|
+
* be aimed at the hub's own network or a cloud metadata endpoint. Returns the
|
|
195
|
+
* address the connection is then pinned to.
|
|
196
|
+
*/
|
|
197
|
+
assertPublicHost(hostname) {
|
|
198
|
+
return resolvePublicAddress(hostname, this.options.allowPrivateAddresses);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The same guarded GET the metadata fetch uses, for the one other outbound
|
|
202
|
+
* request a client_id can trigger: the jwks_uri of a private_key_jwt client.
|
|
203
|
+
* Shaped for jose's customFetch hook, which reads only the status and the
|
|
204
|
+
* JSON body.
|
|
205
|
+
*/
|
|
206
|
+
safeFetch = async (input, init) => {
|
|
207
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
208
|
+
if (url.protocol !== 'https:' && !this.options.allowPrivateAddresses)
|
|
209
|
+
throw new Error(`${url.origin} is not https`);
|
|
210
|
+
const pinned = await this.assertPublicHost(url.hostname);
|
|
211
|
+
return this.fetchCapped(url, pinned, MAX_JWKS_BYTES, headersFrom(init));
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* One GET, with every guard applied and the body capped before anyone gets
|
|
215
|
+
* to look at it. The cap is what makes this safe to hand to jose, which
|
|
216
|
+
* would otherwise buffer a key set of any size the client cares to send.
|
|
217
|
+
*/
|
|
218
|
+
async fetchCapped(url, pinned, maxBytes, headers) {
|
|
219
|
+
const override = this.options.fetchImpl ?? fetchOverride;
|
|
220
|
+
if (!override && pinned) {
|
|
221
|
+
return guardedRequest(url, { pinnedAddress: pinned, headers, timeoutMs: FETCH_TIMEOUT_MS, maxBytes });
|
|
222
|
+
}
|
|
223
|
+
const response = await (override ?? fetch)(url, {
|
|
224
|
+
method: 'GET',
|
|
225
|
+
headers,
|
|
226
|
+
redirect: 'error',
|
|
227
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
228
|
+
});
|
|
229
|
+
return capResponse(response, maxBytes);
|
|
230
|
+
}
|
|
231
|
+
async fetchDocument(url, pinned) {
|
|
232
|
+
const response = await this.fetchCapped(url, pinned, MAX_DOCUMENT_BYTES, { Accept: 'application/json' });
|
|
233
|
+
if (!response.ok)
|
|
234
|
+
throw new Error(`HTTP ${response.status}`);
|
|
235
|
+
const contentType = response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() ?? '';
|
|
236
|
+
if (contentType !== 'application/json' && !contentType.endsWith('+json')) {
|
|
237
|
+
throw new Error(`content type ${contentType || 'missing'} is not JSON`);
|
|
238
|
+
}
|
|
239
|
+
let document;
|
|
240
|
+
try {
|
|
241
|
+
document = JSON.parse(await response.text());
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
throw new Error('body is not valid JSON');
|
|
245
|
+
}
|
|
246
|
+
return { document, ttl: cacheTtl(response.headers.get('cache-control')) };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Re-reads a response through the byte cap and hands back an equivalent one.
|
|
250
|
+
* A failed status is passed straight through: nothing reads its body. */
|
|
251
|
+
async function capResponse(response, limit) {
|
|
252
|
+
if (!response.ok)
|
|
253
|
+
return response;
|
|
254
|
+
const body = await readCapped(response, limit);
|
|
255
|
+
const headers = new Headers(response.headers);
|
|
256
|
+
// Both would now describe the original transfer rather than this body.
|
|
257
|
+
headers.delete('content-encoding');
|
|
258
|
+
headers.delete('content-length');
|
|
259
|
+
return new Response(body, { status: response.status, headers });
|
|
260
|
+
}
|
|
261
|
+
function headersFrom(init) {
|
|
262
|
+
const result = {};
|
|
263
|
+
const headers = init?.headers;
|
|
264
|
+
if (!headers)
|
|
265
|
+
return result;
|
|
266
|
+
if (headers instanceof Headers)
|
|
267
|
+
for (const [key, value] of headers)
|
|
268
|
+
result[key] = value;
|
|
269
|
+
else if (Array.isArray(headers))
|
|
270
|
+
for (const [key, value] of headers)
|
|
271
|
+
result[key] = value;
|
|
272
|
+
else
|
|
273
|
+
Object.assign(result, headers);
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
/** Reads at most `limit` bytes and refuses anything longer, without buffering
|
|
277
|
+
* a response an attacker made arbitrarily large. */
|
|
278
|
+
async function readCapped(response, limit) {
|
|
279
|
+
const declared = Number(response.headers.get('content-length'));
|
|
280
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
281
|
+
throw new Error(`document exceeds ${limit} bytes`);
|
|
282
|
+
const reader = response.body?.getReader();
|
|
283
|
+
if (!reader)
|
|
284
|
+
throw new Error('response has no body');
|
|
285
|
+
const chunks = [];
|
|
286
|
+
let size = 0;
|
|
287
|
+
try {
|
|
288
|
+
for (;;) {
|
|
289
|
+
const { done, value } = await reader.read();
|
|
290
|
+
if (done)
|
|
291
|
+
break;
|
|
292
|
+
size += value.byteLength;
|
|
293
|
+
if (size > limit)
|
|
294
|
+
throw new Error(`document exceeds ${limit} bytes`);
|
|
295
|
+
chunks.push(value);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
await reader.cancel().catch(() => { });
|
|
300
|
+
}
|
|
301
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
302
|
+
}
|
|
303
|
+
/** Draft §5: respect the client's cache headers, but never longer than a day
|
|
304
|
+
* and never so short that every authorization request refetches. */
|
|
305
|
+
function cacheTtl(cacheControl) {
|
|
306
|
+
if (cacheControl && /(^|,)\s*(no-store|no-cache)\s*(,|$)/i.test(cacheControl))
|
|
307
|
+
return MIN_TTL_MS;
|
|
308
|
+
const maxAge = cacheControl?.match(/(?:^|,)\s*max-age\s*=\s*(\d+)/i);
|
|
309
|
+
if (!maxAge)
|
|
310
|
+
return DEFAULT_TTL_MS;
|
|
311
|
+
return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, Number(maxAge[1]) * 1000));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Turns a fetched document into the client record the rest of the OAuth stack
|
|
315
|
+
* already understands. Throws with a reason for every rejection.
|
|
316
|
+
*
|
|
317
|
+
* No client_secret is ever produced: the SDK's client authentication middleware
|
|
318
|
+
* demands a secret exactly when the stored record has one, so a record without
|
|
319
|
+
* it is what makes a public client work — and the draft forbids symmetric
|
|
320
|
+
* secrets for CIMD clients outright.
|
|
321
|
+
*/
|
|
322
|
+
export function validateDocument(document, clientId) {
|
|
323
|
+
if (!document || typeof document !== 'object' || Array.isArray(document))
|
|
324
|
+
throw new Error('document is not a JSON object');
|
|
325
|
+
const metadata = document;
|
|
326
|
+
// Simple string comparison, per the draft: a document that could claim any
|
|
327
|
+
// identity would let one origin impersonate another.
|
|
328
|
+
if (metadata.client_id !== clientId)
|
|
329
|
+
throw new Error('client_id does not match the document URL');
|
|
330
|
+
// The name goes on the consent page, so it is held to what a name can be:
|
|
331
|
+
// one line, and short enough not to displace what sits below it.
|
|
332
|
+
const clientName = clampDisplayName(metadata.client_name);
|
|
333
|
+
if (clientName === undefined)
|
|
334
|
+
throw new Error('client_name is missing');
|
|
335
|
+
if ('client_secret' in metadata || 'client_secret_expires_at' in metadata) {
|
|
336
|
+
throw new Error('document carries a client_secret');
|
|
337
|
+
}
|
|
338
|
+
const authMethod = metadata.token_endpoint_auth_method;
|
|
339
|
+
if (authMethod !== undefined) {
|
|
340
|
+
if (typeof authMethod !== 'string' || !ASSERTION_SAFE_METHODS.has(authMethod)) {
|
|
341
|
+
throw new Error(`token_endpoint_auth_method ${String(authMethod)} needs a shared secret`);
|
|
342
|
+
}
|
|
343
|
+
if (authMethod === 'private_key_jwt' && !metadata.jwks_uri && !metadata.jwks) {
|
|
344
|
+
throw new Error('private_key_jwt without jwks or jwks_uri');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const redirectUris = metadata.redirect_uris;
|
|
348
|
+
if (!Array.isArray(redirectUris) || redirectUris.length === 0)
|
|
349
|
+
throw new Error('redirect_uris is missing or empty');
|
|
350
|
+
for (const uri of redirectUris) {
|
|
351
|
+
if (typeof uri !== 'string')
|
|
352
|
+
throw new Error('redirect_uris contains a non-string entry');
|
|
353
|
+
// Same rule the MCP specification states for every client: https, or a
|
|
354
|
+
// loopback address for native clients. A metadata document does not get
|
|
355
|
+
// the private-use schemes dynamic registration allows.
|
|
356
|
+
if (!isSafeRedirectUri(uri, { allowPrivateUseSchemes: false })) {
|
|
357
|
+
throw new Error(`redirect_uri ${uri} is neither https nor loopback`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
...metadata,
|
|
362
|
+
client_id: clientId,
|
|
363
|
+
client_name: clientName,
|
|
364
|
+
redirect_uris: redirectUris
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=cimd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cimd.js","sourceRoot":"","sources":["../../src/auth/cimd.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { escapeHtml, renderPage } from './page.js';
|
|
1
|
+
import { escapeHtml, renderPage, renderIdentity } from './page.js';
|
|
2
2
|
/**
|
|
3
3
|
* Shown when a signed-in user is asked to authorize a client they have not
|
|
4
4
|
* confirmed before. This is the only thing standing between a live session
|
|
5
5
|
* cookie and a code, so it must never be submitted on the user's behalf:
|
|
6
6
|
* the CSRF token ties the form to their session.
|
|
7
7
|
*/
|
|
8
|
-
export function renderConsentPage(requestToken, csrfToken, redirectUri,
|
|
8
|
+
export function renderConsentPage(requestToken, csrfToken, redirectUri, identity = {}) {
|
|
9
9
|
return renderPage('mcp-hub authorization', `<form method="post" action="consent">
|
|
10
10
|
<h1>Authorize access?</h1>
|
|
11
|
-
<p>${clientName ? `<strong>${escapeHtml(clientName)}</strong> is asking` : 'An application is asking'} for access.</p>
|
|
12
|
-
|
|
13
|
-
<code class="target">${escapeHtml(resource ?? 'Every server on this hub')}</code>
|
|
14
|
-
<p class="label">Codes will be sent to</p>
|
|
15
|
-
<code class="target">${escapeHtml(redirectUri)}</code>
|
|
11
|
+
<p>${identity.clientName ? `<strong>${escapeHtml(identity.clientName)}</strong> is asking` : 'An application is asking'} for access.</p>
|
|
12
|
+
${renderIdentity(redirectUri, identity)}
|
|
16
13
|
<p>Only continue if you started this yourself. The name above is chosen by the application; the address is not.</p>
|
|
17
14
|
<input type="hidden" name="request" value="${escapeHtml(requestToken)}">
|
|
18
15
|
<input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consent-page.js","sourceRoot":"","sources":["../../src/auth/consent-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"consent-page.js","sourceRoot":"","sources":["../../src/auth/consent-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGnE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB,EAAE,SAAiB,EAAE,WAAmB,EAAE,QAAQ,GAAmB,EAAE;IAC3H,OAAO,UAAU,CACf,uBAAuB,EACvB;;OAEG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC,CAAC,0BAA0B;EACvH,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC;;+CAEQ,UAAU,CAAC,YAAY,CAAC;4CAC3B,UAAU,CAAC,SAAS,CAAC;;;QAGzD,CACL,CAAC;AACJ,CAAC"}
|
package/dist/auth/login-page.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { escapeHtml, renderPage } from './page.js';
|
|
1
|
+
import { escapeHtml, renderPage, renderIdentity } from './page.js';
|
|
2
2
|
/**
|
|
3
3
|
* Entering the password here is what approves the client, so the page names
|
|
4
4
|
* both the client and where its codes would be sent — the client name is
|
|
5
5
|
* self-declared at registration and anyone can register, the URL cannot lie.
|
|
6
6
|
*/
|
|
7
|
-
export function renderLoginPage(requestToken, redirectUri,
|
|
7
|
+
export function renderLoginPage(requestToken, redirectUri, identity = {}, errorMessage) {
|
|
8
8
|
return renderPage('mcp-hub login', `<form method="post" action="login">
|
|
9
9
|
<h1>mcp-hub</h1>
|
|
10
|
-
<p>${clientName ? `Authorize <strong>${escapeHtml(clientName)}</strong>` : 'Authorization required'}</p>
|
|
11
|
-
|
|
12
|
-
<code class="target">${escapeHtml(resource ?? 'Every server on this hub')}</code>
|
|
13
|
-
<p class="label">Codes will be sent to</p>
|
|
14
|
-
<code class="target">${escapeHtml(redirectUri)}</code>
|
|
10
|
+
<p>${identity.clientName ? `Authorize <strong>${escapeHtml(identity.clientName)}</strong>` : 'Authorization required'}</p>
|
|
11
|
+
${renderIdentity(redirectUri, identity)}
|
|
15
12
|
<input type="password" name="password" placeholder="Password" autofocus required autocomplete="current-password">
|
|
16
13
|
<input type="hidden" name="request" value="${escapeHtml(requestToken)}">
|
|
17
14
|
${errorMessage ? `<p class="error">${escapeHtml(errorMessage)}</p>` : ''}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-page.js","sourceRoot":"","sources":["../../src/auth/login-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"login-page.js","sourceRoot":"","sources":["../../src/auth/login-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGnE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,YAAoB,EAAE,WAAmB,EAAE,QAAQ,GAAmB,EAAE,EAAE,YAAqB;IAC7H,OAAO,UAAU,CACf,eAAe,EACf;;OAEG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,qBAAqB,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,wBAAwB;EACrH,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC;;+CAEQ,UAAU,CAAC,YAAY,CAAC;IACnE,YAAY,CAAC,CAAC,CAAC,oBAAoB,UAAU,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;;QAElE,CACL,CAAC;AACJ,CAAC"}
|
package/dist/auth/page.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export function escapeHtml(value) {
|
|
2
2
|
return value.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* The block both pages share: what access is requested, where codes would go
|
|
6
|
+
* and — for a Client ID Metadata Document client — the URL that vouches for
|
|
7
|
+
* the name above it. The document URL is the one part of a CIMD client's
|
|
8
|
+
* identity that cannot be invented, which is why the specification asks for it
|
|
9
|
+
* to be shown; a client whose redirect URIs are all local cannot be attributed
|
|
10
|
+
* to anything at all, so that gets said outright.
|
|
11
|
+
*/
|
|
12
|
+
export function renderIdentity(redirectUri, identity) {
|
|
13
|
+
const lines = [
|
|
14
|
+
' <p class="label">Requested access</p>',
|
|
15
|
+
` <code class="target">${escapeHtml(identity.resource ?? 'Every server on this hub')}</code>`
|
|
16
|
+
];
|
|
17
|
+
if (identity.clientId) {
|
|
18
|
+
lines.push(' <p class="label">Identified by</p>', ` <code class="target">${escapeHtml(identity.clientId)}</code>`);
|
|
19
|
+
}
|
|
20
|
+
lines.push(' <p class="label">Codes will be sent to</p>', ` <code class="target">${escapeHtml(redirectUri)}</code>`);
|
|
21
|
+
if (identity.loopbackOnly) {
|
|
22
|
+
lines.push(' <p class="error">This client only accepts codes on this machine, so any program running here could be the one asking.</p>');
|
|
23
|
+
}
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
4
26
|
/** Shared chrome for the two interactive pages, login and consent. */
|
|
5
27
|
export function renderPage(title, body) {
|
|
6
28
|
return `<!doctype html>
|
package/dist/auth/page.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/auth/page.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAY;IACpD,OAAO;;;;;;SAMA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;EAexB,IAAI;;QAEE,CAAC;AACT,CAAC"}
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/auth/page.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC;AAaD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB,EAAE,QAAwB;IAC1E,MAAM,KAAK,GAAG;QACZ,yCAAyC;QACzC,0BAA0B,UAAU,CAAC,QAAQ,CAAC,QAAQ,IAAI,0BAA0B,CAAC,SAAS;KAC/F,CAAC;IACF,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,sCAAsC,EAAE,0BAA0B,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACvH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,8CAA8C,EAAE,0BAA0B,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IACvH,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,6HAA6H,CAAC,CAAC;IAC5I,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAY;IACpD,OAAO;;;;;;SAMA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;EAexB,IAAI;;QAEE,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
/**
|
|
5
|
+
* An HTTPS request that connects to an address someone already vetted.
|
|
6
|
+
*
|
|
7
|
+
* Checking a hostname and then handing it to `fetch` leaves a gap: the check
|
|
8
|
+
* resolves the name once, `fetch` resolves it again, and a DNS zone the caller
|
|
9
|
+
* controls can answer differently the second time. A short TTL alternating
|
|
10
|
+
* between a public address and `169.254.169.254` wins that race reliably. The
|
|
11
|
+
* only way to close it is to stop resolving twice — so the vetted address is
|
|
12
|
+
* pinned into the connection here, while the hostname is still what TLS
|
|
13
|
+
* validates the certificate against.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately `node:https` rather than a custom `fetch` dispatcher: undici is
|
|
16
|
+
* not a dependency of this project, and its `Agent` would be the only reason
|
|
17
|
+
* to add one.
|
|
18
|
+
*/
|
|
19
|
+
/** Answers every lookup with the one address that was already checked. */
|
|
20
|
+
export function pinnedLookup(address) {
|
|
21
|
+
const family = net.isIP(address);
|
|
22
|
+
return ((hostname, options, callback) => {
|
|
23
|
+
const done = (typeof options === 'function' ? options : callback);
|
|
24
|
+
const wantsAll = typeof options === 'object' && options !== null && options.all === true;
|
|
25
|
+
if (wantsAll)
|
|
26
|
+
done(null, [{ address, family }]);
|
|
27
|
+
else
|
|
28
|
+
done(null, address, family);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function guardedRequest(url, options) {
|
|
32
|
+
// Only https reaches this in production — the caller rejects anything else
|
|
33
|
+
// before resolving. Plain http is here so the transport can be exercised
|
|
34
|
+
// without a certificate authority, and for the local-development escape hatch.
|
|
35
|
+
const transport = url.protocol === 'http:' ? http : https;
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let settled = false;
|
|
38
|
+
// Declared up here because finish() may run before the timer below is
|
|
39
|
+
// assigned — an immediate connection error would otherwise hit the
|
|
40
|
+
// temporal dead zone instead of rejecting.
|
|
41
|
+
let deadline;
|
|
42
|
+
const finish = (error, response) => {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(deadline);
|
|
47
|
+
if (error)
|
|
48
|
+
reject(error);
|
|
49
|
+
else
|
|
50
|
+
resolve(response);
|
|
51
|
+
};
|
|
52
|
+
const request = transport.request({
|
|
53
|
+
hostname: url.hostname.replace(/^\[|\]$/g, ''),
|
|
54
|
+
port: url.port || (url.protocol === 'http:' ? 80 : 443),
|
|
55
|
+
path: `${url.pathname}${url.search}`,
|
|
56
|
+
method: options.method ?? 'GET',
|
|
57
|
+
// No compression: this path decodes nothing, and a compressed body
|
|
58
|
+
// would make the byte cap below measure the wrong thing.
|
|
59
|
+
headers: {
|
|
60
|
+
...options.headers,
|
|
61
|
+
host: url.host,
|
|
62
|
+
'accept-encoding': 'identity',
|
|
63
|
+
...(options.body !== undefined ? { 'content-length': String(Buffer.byteLength(options.body)) } : {})
|
|
64
|
+
},
|
|
65
|
+
lookup: pinnedLookup(options.pinnedAddress)
|
|
66
|
+
}, response => {
|
|
67
|
+
const status = response.statusCode ?? 0;
|
|
68
|
+
// The caller asked for no redirects; a hop would be resolved and
|
|
69
|
+
// connected without going through the address check again.
|
|
70
|
+
if (status >= 300 && status < 400) {
|
|
71
|
+
response.destroy();
|
|
72
|
+
request.destroy();
|
|
73
|
+
finish(new Error(`refused to follow a redirect (HTTP ${status})`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const declared = Number(response.headers['content-length']);
|
|
77
|
+
if (Number.isFinite(declared) && declared > options.maxBytes) {
|
|
78
|
+
response.destroy();
|
|
79
|
+
request.destroy();
|
|
80
|
+
finish(new Error(`response exceeds ${options.maxBytes} bytes`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let size = 0;
|
|
85
|
+
response.on('data', (chunk) => {
|
|
86
|
+
size += chunk.byteLength;
|
|
87
|
+
if (size > options.maxBytes) {
|
|
88
|
+
response.destroy();
|
|
89
|
+
request.destroy();
|
|
90
|
+
finish(new Error(`response exceeds ${options.maxBytes} bytes`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
chunks.push(chunk);
|
|
94
|
+
});
|
|
95
|
+
response.on('error', error => finish(error));
|
|
96
|
+
response.on('end', () => {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
const headers = new Headers();
|
|
100
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
if (Array.isArray(value))
|
|
105
|
+
for (const entry of value)
|
|
106
|
+
headers.append(key, entry);
|
|
107
|
+
else
|
|
108
|
+
headers.set(key, value);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A header the Fetch API will not represent is not one this
|
|
112
|
+
// caller reads; dropping it beats failing the whole request.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// A zero-length body has to be null: Response refuses an empty
|
|
116
|
+
// buffer for the statuses that may not carry one.
|
|
117
|
+
finish(undefined, new Response(chunks.length > 0 ? Buffer.concat(chunks) : null, { status, headers }));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// Bounds the whole exchange, not just socket inactivity — a body trickled
|
|
121
|
+
// a byte at a time would otherwise keep the request alive indefinitely.
|
|
122
|
+
deadline = setTimeout(() => {
|
|
123
|
+
request.destroy(new Error(`timed out after ${options.timeoutMs}ms`));
|
|
124
|
+
}, options.timeoutMs);
|
|
125
|
+
request.on('error', error => finish(error));
|
|
126
|
+
request.end(options.body);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=pinned-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-fetch.js","sourceRoot":"","sources":["../../src/auth/pinned-fetch.ts"],"names":[],"mappings":"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"}
|