@j0hanz/superfetch 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +363 -363
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +13 -25
- package/dist/config.d.ts +0 -1
- package/dist/config.js +9 -7
- package/dist/crypto.d.ts +0 -1
- package/dist/crypto.js +0 -1
- package/dist/dom-noise-removal.d.ts +0 -1
- package/dist/dom-noise-removal.js +35 -32
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/fetch.d.ts +0 -1
- package/dist/fetch.js +45 -29
- package/dist/host-normalization.d.ts +1 -0
- package/dist/host-normalization.js +47 -0
- package/dist/http-native.d.ts +0 -1
- package/dist/http-native.js +73 -25
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/instructions.md +41 -41
- package/dist/json.d.ts +0 -1
- package/dist/json.js +0 -1
- package/dist/language-detection.d.ts +0 -1
- package/dist/language-detection.js +10 -2
- package/dist/markdown-cleanup.d.ts +0 -1
- package/dist/markdown-cleanup.js +10 -10
- package/dist/mcp-validator.d.ts +14 -0
- package/dist/mcp-validator.js +22 -0
- package/dist/mcp.d.ts +0 -1
- package/dist/mcp.js +0 -1
- package/dist/observability.d.ts +0 -1
- package/dist/observability.js +5 -3
- package/dist/server-tuning.d.ts +9 -0
- package/dist/server-tuning.js +30 -0
- package/dist/{http-utils.d.ts → session.d.ts} +0 -25
- package/dist/{http-utils.js → session.js} +11 -104
- package/dist/tools.d.ts +0 -1
- package/dist/tools.js +19 -29
- package/dist/transform-types.d.ts +0 -1
- package/dist/transform-types.js +0 -1
- package/dist/transform.d.ts +0 -1
- package/dist/transform.js +85 -79
- package/dist/type-guards.d.ts +0 -1
- package/dist/type-guards.js +0 -1
- package/dist/workers/transform-worker.d.ts +0 -1
- package/dist/workers/transform-worker.js +29 -19
- package/package.json +85 -85
- package/dist/cache.d.ts.map +0 -1
- package/dist/cache.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/crypto.d.ts.map +0 -1
- package/dist/crypto.js.map +0 -1
- package/dist/dom-noise-removal.d.ts.map +0 -1
- package/dist/dom-noise-removal.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js.map +0 -1
- package/dist/http-native.d.ts.map +0 -1
- package/dist/http-native.js.map +0 -1
- package/dist/http-utils.d.ts.map +0 -1
- package/dist/http-utils.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js.map +0 -1
- package/dist/language-detection.d.ts.map +0 -1
- package/dist/language-detection.js.map +0 -1
- package/dist/markdown-cleanup.d.ts.map +0 -1
- package/dist/markdown-cleanup.js.map +0 -1
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js.map +0 -1
- package/dist/observability.d.ts.map +0 -1
- package/dist/observability.js.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/transform-types.d.ts.map +0 -1
- package/dist/transform-types.js.map +0 -1
- package/dist/transform.d.ts.map +0 -1
- package/dist/transform.js.map +0 -1
- package/dist/type-guards.d.ts.map +0 -1
- package/dist/type-guards.js.map +0 -1
- package/dist/workers/transform-worker.d.ts.map +0 -1
- package/dist/workers/transform-worker.js.map +0 -1
package/dist/cache.d.ts
CHANGED
|
@@ -40,4 +40,3 @@ export declare function registerCachedContentResource(server: McpServer): void;
|
|
|
40
40
|
export declare function generateSafeFilename(url: string, title?: string, hashFallback?: string, extension?: string): string;
|
|
41
41
|
export declare function handleDownload(res: ServerResponse, namespace: string, hash: string): void;
|
|
42
42
|
export {};
|
|
43
|
-
//# sourceMappingURL=cache.d.ts.map
|
package/dist/cache.js
CHANGED
|
@@ -396,43 +396,32 @@ function buildMarkdownContentResponse(uri, content) {
|
|
|
396
396
|
],
|
|
397
397
|
};
|
|
398
398
|
}
|
|
399
|
-
function isSingleParam(value) {
|
|
400
|
-
return typeof value === 'string';
|
|
401
|
-
}
|
|
402
399
|
function parseDownloadParams(namespace, hash) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (!
|
|
400
|
+
const resolvedNamespace = resolveStringParam(namespace);
|
|
401
|
+
const resolvedHash = resolveStringParam(hash);
|
|
402
|
+
if (!resolvedNamespace || !resolvedHash)
|
|
406
403
|
return null;
|
|
407
|
-
if (!isValidNamespace(
|
|
404
|
+
if (!isValidNamespace(resolvedNamespace))
|
|
408
405
|
return null;
|
|
409
|
-
if (!isValidHash(
|
|
406
|
+
if (!isValidHash(resolvedHash))
|
|
410
407
|
return null;
|
|
411
|
-
return { namespace, hash };
|
|
408
|
+
return { namespace: resolvedNamespace, hash: resolvedHash };
|
|
412
409
|
}
|
|
413
410
|
function buildCacheKeyFromParams(params) {
|
|
414
411
|
return `${params.namespace}:${params.hash}`;
|
|
415
412
|
}
|
|
413
|
+
function sendJsonError(res, status, error, code) {
|
|
414
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
415
|
+
res.end(JSON.stringify({ error, code }));
|
|
416
|
+
}
|
|
416
417
|
function respondBadRequest(res, message) {
|
|
417
|
-
res
|
|
418
|
-
res.end(JSON.stringify({
|
|
419
|
-
error: message,
|
|
420
|
-
code: 'BAD_REQUEST',
|
|
421
|
-
}));
|
|
418
|
+
sendJsonError(res, 400, message, 'BAD_REQUEST');
|
|
422
419
|
}
|
|
423
420
|
function respondNotFound(res) {
|
|
424
|
-
res
|
|
425
|
-
res.end(JSON.stringify({
|
|
426
|
-
error: 'Content not found or expired',
|
|
427
|
-
code: 'NOT_FOUND',
|
|
428
|
-
}));
|
|
421
|
+
sendJsonError(res, 404, 'Content not found or expired', 'NOT_FOUND');
|
|
429
422
|
}
|
|
430
423
|
function respondServiceUnavailable(res) {
|
|
431
|
-
res
|
|
432
|
-
res.end(JSON.stringify({
|
|
433
|
-
error: 'Download service is disabled',
|
|
434
|
-
code: 'SERVICE_UNAVAILABLE',
|
|
435
|
-
}));
|
|
424
|
+
sendJsonError(res, 503, 'Download service is disabled', 'SERVICE_UNAVAILABLE');
|
|
436
425
|
}
|
|
437
426
|
export function generateSafeFilename(url, title, hashFallback, extension = '.md') {
|
|
438
427
|
const fromUrl = extractFilenameFromUrl(url);
|
|
@@ -567,4 +556,3 @@ export function handleDownload(res, namespace, hash) {
|
|
|
567
556
|
logDebug('Serving download', { cacheKey, fileName: payload.fileName });
|
|
568
557
|
sendDownloadPayload(res, payload);
|
|
569
558
|
}
|
|
570
|
-
//# sourceMappingURL=cache.js.map
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -73,6 +73,9 @@ function parseUrlEnv(value, name) {
|
|
|
73
73
|
}
|
|
74
74
|
return new URL(value);
|
|
75
75
|
}
|
|
76
|
+
function readUrlEnv(name) {
|
|
77
|
+
return parseUrlEnv(process.env[name], name);
|
|
78
|
+
}
|
|
76
79
|
function parseAllowedHosts(envValue) {
|
|
77
80
|
const hosts = new Set();
|
|
78
81
|
for (const entry of parseList(envValue)) {
|
|
@@ -108,16 +111,16 @@ const DEFAULT_TOOL_TIMEOUT_MS = TIMEOUT.DEFAULT_FETCH_TIMEOUT_MS +
|
|
|
108
111
|
5000;
|
|
109
112
|
function readCoreOAuthUrls() {
|
|
110
113
|
return {
|
|
111
|
-
issuerUrl:
|
|
112
|
-
authorizationUrl:
|
|
113
|
-
tokenUrl:
|
|
114
|
+
issuerUrl: readUrlEnv('OAUTH_ISSUER_URL'),
|
|
115
|
+
authorizationUrl: readUrlEnv('OAUTH_AUTHORIZATION_URL'),
|
|
116
|
+
tokenUrl: readUrlEnv('OAUTH_TOKEN_URL'),
|
|
114
117
|
};
|
|
115
118
|
}
|
|
116
119
|
function readOptionalOAuthUrls(baseUrl) {
|
|
117
120
|
return {
|
|
118
|
-
revocationUrl:
|
|
119
|
-
registrationUrl:
|
|
120
|
-
introspectionUrl:
|
|
121
|
+
revocationUrl: readUrlEnv('OAUTH_REVOCATION_URL'),
|
|
122
|
+
registrationUrl: readUrlEnv('OAUTH_REGISTRATION_URL'),
|
|
123
|
+
introspectionUrl: readUrlEnv('OAUTH_INTROSPECTION_URL'),
|
|
121
124
|
resourceUrl: parseUrlEnv(process.env.OAUTH_RESOURCE_URL, 'OAUTH_RESOURCE_URL') ??
|
|
122
125
|
new URL('/mcp', baseUrl),
|
|
123
126
|
};
|
|
@@ -271,4 +274,3 @@ export const config = {
|
|
|
271
274
|
export function enableHttpMode() {
|
|
272
275
|
runtimeState.httpMode = true;
|
|
273
276
|
}
|
|
274
|
-
//# sourceMappingURL=config.js.map
|
package/dist/crypto.d.ts
CHANGED
package/dist/crypto.js
CHANGED
|
@@ -56,6 +56,37 @@ const STRUCTURAL_TAGS = new Set([
|
|
|
56
56
|
'canvas',
|
|
57
57
|
]);
|
|
58
58
|
const ALWAYS_NOISE_TAGS = new Set(['nav', 'footer']);
|
|
59
|
+
const BASE_NOISE_SELECTORS = [
|
|
60
|
+
'nav',
|
|
61
|
+
'footer',
|
|
62
|
+
'header[class*="site"]',
|
|
63
|
+
'header[class*="nav"]',
|
|
64
|
+
'header[class*="menu"]',
|
|
65
|
+
'[role="banner"]',
|
|
66
|
+
'[role="navigation"]',
|
|
67
|
+
'[role="dialog"]',
|
|
68
|
+
'[style*="display: none"]',
|
|
69
|
+
'[style*="display:none"]',
|
|
70
|
+
'[hidden]',
|
|
71
|
+
'[aria-hidden="true"]',
|
|
72
|
+
];
|
|
73
|
+
const BASE_NOISE_SELECTOR = BASE_NOISE_SELECTORS.join(',');
|
|
74
|
+
const CANDIDATE_NOISE_SELECTOR = [
|
|
75
|
+
...STRUCTURAL_TAGS,
|
|
76
|
+
...ALWAYS_NOISE_TAGS,
|
|
77
|
+
'aside',
|
|
78
|
+
'header',
|
|
79
|
+
'[class]',
|
|
80
|
+
'[id]',
|
|
81
|
+
'[role]',
|
|
82
|
+
'[style]',
|
|
83
|
+
].join(',');
|
|
84
|
+
function buildNoiseSelector(extraSelectors) {
|
|
85
|
+
const extra = extraSelectors.filter((selector) => selector.trim().length > 0);
|
|
86
|
+
if (extra.length === 0)
|
|
87
|
+
return BASE_NOISE_SELECTOR;
|
|
88
|
+
return `${BASE_NOISE_SELECTOR},${extra.join(',')}`;
|
|
89
|
+
}
|
|
59
90
|
const NAVIGATION_ROLES = new Set([
|
|
60
91
|
'navigation',
|
|
61
92
|
'banner',
|
|
@@ -309,39 +340,12 @@ function removeNoiseNodes(nodes, shouldCheckNoise = true) {
|
|
|
309
340
|
function stripNoiseNodes(document) {
|
|
310
341
|
// Pass 1: Trusted selectors (Common noise)
|
|
311
342
|
// We trust these selectors match actual noise, so we skip the expensive isNoiseElement check
|
|
312
|
-
const baseSelectors = [
|
|
313
|
-
'nav',
|
|
314
|
-
'footer',
|
|
315
|
-
'header[class*="site"]',
|
|
316
|
-
'header[class*="nav"]',
|
|
317
|
-
'header[class*="menu"]',
|
|
318
|
-
'[role="banner"]',
|
|
319
|
-
'[role="navigation"]',
|
|
320
|
-
'[role="dialog"]',
|
|
321
|
-
'[style*="display: none"]',
|
|
322
|
-
'[style*="display:none"]',
|
|
323
|
-
'[hidden]',
|
|
324
|
-
'[aria-hidden="true"]',
|
|
325
|
-
];
|
|
326
343
|
// Add user-configured extra selectors
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
const potentialNoiseNodes = document.querySelectorAll(targetSelectors);
|
|
331
|
-
removeNoiseNodes(potentialNoiseNodes, false);
|
|
332
|
-
}
|
|
344
|
+
const targetSelectors = buildNoiseSelector(config.noiseRemoval.extraSelectors);
|
|
345
|
+
const potentialNoiseNodes = document.querySelectorAll(targetSelectors);
|
|
346
|
+
removeNoiseNodes(potentialNoiseNodes, false);
|
|
333
347
|
// Second pass: check remaining elements for noise patterns (promo, fixed positioning, etc.)
|
|
334
|
-
const
|
|
335
|
-
...STRUCTURAL_TAGS,
|
|
336
|
-
...ALWAYS_NOISE_TAGS,
|
|
337
|
-
'aside',
|
|
338
|
-
'header',
|
|
339
|
-
'[class]',
|
|
340
|
-
'[id]',
|
|
341
|
-
'[role]',
|
|
342
|
-
'[style]',
|
|
343
|
-
].join(',');
|
|
344
|
-
const allElements = document.querySelectorAll(candidateSelectors);
|
|
348
|
+
const allElements = document.querySelectorAll(CANDIDATE_NOISE_SELECTOR);
|
|
345
349
|
removeNoiseNodes(allElements, true);
|
|
346
350
|
}
|
|
347
351
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -479,4 +483,3 @@ export function removeNoiseFromHtml(html, document, baseUrl) {
|
|
|
479
483
|
return html;
|
|
480
484
|
}
|
|
481
485
|
}
|
|
482
|
-
//# sourceMappingURL=dom-noise-removal.js.map
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,4 +8,3 @@ export declare class FetchError extends Error {
|
|
|
8
8
|
export declare function getErrorMessage(error: unknown): string;
|
|
9
9
|
export declare function createErrorWithCode(message: string, code: string): NodeJS.ErrnoException;
|
|
10
10
|
export declare function isSystemError(error: unknown): error is NodeJS.ErrnoException;
|
|
11
|
-
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
CHANGED
package/dist/fetch.d.ts
CHANGED
package/dist/fetch.js
CHANGED
|
@@ -503,6 +503,12 @@ function createRateLimitError(url, headerValue) {
|
|
|
503
503
|
function createHttpError(url, status, statusText) {
|
|
504
504
|
return new FetchError(`HTTP ${status}: ${statusText}`, url, status);
|
|
505
505
|
}
|
|
506
|
+
function createTooManyRedirectsError(url) {
|
|
507
|
+
return new FetchError('Too many redirects', url);
|
|
508
|
+
}
|
|
509
|
+
function createMissingRedirectLocationError(url) {
|
|
510
|
+
return new FetchError('Redirect response missing Location header', url);
|
|
511
|
+
}
|
|
506
512
|
function createSizeLimitError(url, maxBytes) {
|
|
507
513
|
return new FetchError(`Response exceeds maximum size of ${maxBytes} bytes`, url);
|
|
508
514
|
}
|
|
@@ -534,21 +540,29 @@ function resolveErrorUrl(error, fallback) {
|
|
|
534
540
|
return requestUrl;
|
|
535
541
|
return fallback;
|
|
536
542
|
}
|
|
537
|
-
function
|
|
538
|
-
if (error
|
|
539
|
-
return
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (isTimeoutError(error)) {
|
|
543
|
-
return createTimeoutError(url, timeoutMs);
|
|
544
|
-
}
|
|
545
|
-
return createCanceledError(url);
|
|
543
|
+
function resolveAbortFetchError(error, url, timeoutMs) {
|
|
544
|
+
if (!isAbortError(error))
|
|
545
|
+
return null;
|
|
546
|
+
if (isTimeoutError(error)) {
|
|
547
|
+
return createTimeoutError(url, timeoutMs);
|
|
546
548
|
}
|
|
549
|
+
return createCanceledError(url);
|
|
550
|
+
}
|
|
551
|
+
function resolveUnexpectedFetchError(error, url) {
|
|
547
552
|
if (error instanceof Error) {
|
|
548
553
|
return createNetworkError(url, error.message);
|
|
549
554
|
}
|
|
550
555
|
return createUnknownError(url, 'Unexpected error');
|
|
551
556
|
}
|
|
557
|
+
function mapFetchError(error, fallbackUrl, timeoutMs) {
|
|
558
|
+
if (error instanceof FetchError)
|
|
559
|
+
return error;
|
|
560
|
+
const url = resolveErrorUrl(error, fallbackUrl);
|
|
561
|
+
const abortError = resolveAbortFetchError(error, url, timeoutMs);
|
|
562
|
+
if (abortError)
|
|
563
|
+
return abortError;
|
|
564
|
+
return resolveUnexpectedFetchError(error, url);
|
|
565
|
+
}
|
|
552
566
|
const fetchChannel = diagnosticsChannel.channel('superfetch.fetch');
|
|
553
567
|
function publishFetchEvent(event) {
|
|
554
568
|
if (!fetchChannel.hasSubscribers)
|
|
@@ -713,14 +727,14 @@ function assertRedirectWithinLimit(response, currentUrl, redirectLimit, redirect
|
|
|
713
727
|
if (redirectCount < redirectLimit)
|
|
714
728
|
return;
|
|
715
729
|
cancelResponseBody(response);
|
|
716
|
-
throw
|
|
730
|
+
throw createTooManyRedirectsError(currentUrl);
|
|
717
731
|
}
|
|
718
732
|
function getRedirectLocation(response, currentUrl) {
|
|
719
733
|
const location = response.headers.get('location');
|
|
720
734
|
if (location)
|
|
721
735
|
return location;
|
|
722
736
|
cancelResponseBody(response);
|
|
723
|
-
throw
|
|
737
|
+
throw createMissingRedirectLocationError(currentUrl);
|
|
724
738
|
}
|
|
725
739
|
function annotateRedirectError(error, url) {
|
|
726
740
|
if (!isObject(error))
|
|
@@ -737,26 +751,26 @@ function resolveRedirectTarget(baseUrl, location) {
|
|
|
737
751
|
}
|
|
738
752
|
return validateAndNormalizeUrl(resolved.href);
|
|
739
753
|
}
|
|
754
|
+
async function withRedirectErrorContext(url, fn) {
|
|
755
|
+
try {
|
|
756
|
+
return await fn();
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
annotateRedirectError(error, url);
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
740
763
|
export async function fetchWithRedirects(url, init, maxRedirects) {
|
|
741
764
|
let currentUrl = url;
|
|
742
765
|
const redirectLimit = Math.max(0, maxRedirects);
|
|
743
766
|
for (let redirectCount = 0; redirectCount <= redirectLimit; redirectCount += 1) {
|
|
744
|
-
const { response, nextUrl } = await
|
|
767
|
+
const { response, nextUrl } = await withRedirectErrorContext(currentUrl, () => performFetchCycle(currentUrl, init, redirectLimit, redirectCount));
|
|
745
768
|
if (!nextUrl) {
|
|
746
769
|
return { response, url: currentUrl };
|
|
747
770
|
}
|
|
748
771
|
currentUrl = nextUrl;
|
|
749
772
|
}
|
|
750
|
-
throw
|
|
751
|
-
}
|
|
752
|
-
async function performFetchCycleSafely(currentUrl, init, redirectLimit, redirectCount) {
|
|
753
|
-
try {
|
|
754
|
-
return await performFetchCycle(currentUrl, init, redirectLimit, redirectCount);
|
|
755
|
-
}
|
|
756
|
-
catch (error) {
|
|
757
|
-
annotateRedirectError(error, currentUrl);
|
|
758
|
-
throw error;
|
|
759
|
-
}
|
|
773
|
+
throw createTooManyRedirectsError(currentUrl);
|
|
760
774
|
}
|
|
761
775
|
function assertContentLengthWithinLimit(response, url, maxBytes) {
|
|
762
776
|
const contentLengthHeader = response.headers.get('content-length');
|
|
@@ -841,15 +855,18 @@ async function readStreamWithLimit(stream, url, maxBytes, signal) {
|
|
|
841
855
|
finalizeRead(state);
|
|
842
856
|
return { text: state.parts.join(''), size: state.total };
|
|
843
857
|
}
|
|
858
|
+
async function readResponseTextFallback(response, url, maxBytes) {
|
|
859
|
+
const text = await response.text();
|
|
860
|
+
const size = Buffer.byteLength(text);
|
|
861
|
+
if (size > maxBytes) {
|
|
862
|
+
throw createSizeLimitError(url, maxBytes);
|
|
863
|
+
}
|
|
864
|
+
return { text, size };
|
|
865
|
+
}
|
|
844
866
|
export async function readResponseText(response, url, maxBytes, signal) {
|
|
845
867
|
assertContentLengthWithinLimit(response, url, maxBytes);
|
|
846
868
|
if (!response.body) {
|
|
847
|
-
|
|
848
|
-
const size = Buffer.byteLength(text);
|
|
849
|
-
if (size > maxBytes) {
|
|
850
|
-
throw createSizeLimitError(url, maxBytes);
|
|
851
|
-
}
|
|
852
|
-
return { text, size };
|
|
869
|
+
return readResponseTextFallback(response, url, maxBytes);
|
|
853
870
|
}
|
|
854
871
|
return readStreamWithLimit(response.body, url, maxBytes, signal);
|
|
855
872
|
}
|
|
@@ -925,4 +942,3 @@ export async function fetchNormalizedUrl(normalizedUrl, options) {
|
|
|
925
942
|
const requestInit = buildRequestInit(headers, signal);
|
|
926
943
|
return fetchWithTelemetry(normalizedUrl, requestInit, timeoutMs);
|
|
927
944
|
}
|
|
928
|
-
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeHost(value: string): string | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
export function normalizeHost(value) {
|
|
3
|
+
const trimmed = value.trim().toLowerCase();
|
|
4
|
+
if (!trimmed)
|
|
5
|
+
return null;
|
|
6
|
+
const first = takeFirstHostValue(trimmed);
|
|
7
|
+
if (!first)
|
|
8
|
+
return null;
|
|
9
|
+
const ipv6 = stripIpv6Brackets(first);
|
|
10
|
+
if (ipv6)
|
|
11
|
+
return stripTrailingDots(ipv6);
|
|
12
|
+
if (isIpV6Literal(first)) {
|
|
13
|
+
return stripTrailingDots(first);
|
|
14
|
+
}
|
|
15
|
+
return stripTrailingDots(stripPortIfPresent(first));
|
|
16
|
+
}
|
|
17
|
+
function takeFirstHostValue(value) {
|
|
18
|
+
const first = value.split(',')[0];
|
|
19
|
+
if (!first)
|
|
20
|
+
return null;
|
|
21
|
+
const trimmed = first.trim();
|
|
22
|
+
return trimmed ? trimmed : null;
|
|
23
|
+
}
|
|
24
|
+
function stripIpv6Brackets(value) {
|
|
25
|
+
if (!value.startsWith('['))
|
|
26
|
+
return null;
|
|
27
|
+
const end = value.indexOf(']');
|
|
28
|
+
if (end === -1)
|
|
29
|
+
return null;
|
|
30
|
+
return value.slice(1, end);
|
|
31
|
+
}
|
|
32
|
+
function stripPortIfPresent(value) {
|
|
33
|
+
const colonIndex = value.indexOf(':');
|
|
34
|
+
if (colonIndex === -1)
|
|
35
|
+
return value;
|
|
36
|
+
return value.slice(0, colonIndex);
|
|
37
|
+
}
|
|
38
|
+
function isIpV6Literal(value) {
|
|
39
|
+
return isIP(value) === 6;
|
|
40
|
+
}
|
|
41
|
+
function stripTrailingDots(value) {
|
|
42
|
+
let result = value;
|
|
43
|
+
while (result.endsWith('.')) {
|
|
44
|
+
result = result.slice(0, -1);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
package/dist/http-native.d.ts
CHANGED
package/dist/http-native.js
CHANGED
|
@@ -8,10 +8,47 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
|
8
8
|
import { handleDownload } from './cache.js';
|
|
9
9
|
import { config, enableHttpMode } from './config.js';
|
|
10
10
|
import { timingSafeEqualUtf8 } from './crypto.js';
|
|
11
|
-
import {
|
|
11
|
+
import { normalizeHost } from './host-normalization.js';
|
|
12
|
+
import { acceptsEventStream, isJsonRpcBatchRequest, isMcpRequestBody, } from './mcp-validator.js';
|
|
12
13
|
import { createMcpServer } from './mcp.js';
|
|
13
14
|
import { logError, logInfo, logWarn } from './observability.js';
|
|
15
|
+
import { applyHttpServerTuning, drainConnectionsOnShutdown, } from './server-tuning.js';
|
|
16
|
+
import { composeCloseHandlers, createSessionStore, createSlotTracker, ensureSessionCapacity, reserveSessionSlot, startSessionCleanupLoop, } from './session.js';
|
|
14
17
|
import { isObject } from './type-guards.js';
|
|
18
|
+
function createTransportAdapter(transportImpl) {
|
|
19
|
+
const noopOnClose = () => { };
|
|
20
|
+
const noopOnError = () => { };
|
|
21
|
+
const noopOnMessage = () => { };
|
|
22
|
+
let oncloseHandler = noopOnClose;
|
|
23
|
+
let onerrorHandler = noopOnError;
|
|
24
|
+
let onmessageHandler = noopOnMessage;
|
|
25
|
+
return {
|
|
26
|
+
start: () => transportImpl.start(),
|
|
27
|
+
send: (message, options) => transportImpl.send(message, options),
|
|
28
|
+
close: () => transportImpl.close(),
|
|
29
|
+
get onclose() {
|
|
30
|
+
return oncloseHandler;
|
|
31
|
+
},
|
|
32
|
+
set onclose(handler) {
|
|
33
|
+
oncloseHandler = handler;
|
|
34
|
+
transportImpl.onclose = handler;
|
|
35
|
+
},
|
|
36
|
+
get onerror() {
|
|
37
|
+
return onerrorHandler;
|
|
38
|
+
},
|
|
39
|
+
set onerror(handler) {
|
|
40
|
+
onerrorHandler = handler;
|
|
41
|
+
transportImpl.onerror = handler;
|
|
42
|
+
},
|
|
43
|
+
get onmessage() {
|
|
44
|
+
return onmessageHandler;
|
|
45
|
+
},
|
|
46
|
+
set onmessage(handler) {
|
|
47
|
+
onmessageHandler = handler;
|
|
48
|
+
transportImpl.onmessage = handler;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
15
52
|
function shimResponse(res) {
|
|
16
53
|
const shim = res;
|
|
17
54
|
shim.status = function (code) {
|
|
@@ -144,26 +181,26 @@ function resolveOriginHost(origin) {
|
|
|
144
181
|
return null;
|
|
145
182
|
}
|
|
146
183
|
}
|
|
184
|
+
function rejectHostRequest(res, status, message) {
|
|
185
|
+
res.status(status).json({ error: message });
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
147
188
|
function validateHostAndOrigin(req, res) {
|
|
148
189
|
const host = resolveHostHeader(req);
|
|
149
190
|
if (!host) {
|
|
150
|
-
res
|
|
151
|
-
return false;
|
|
191
|
+
return rejectHostRequest(res, 400, 'Missing or invalid Host header');
|
|
152
192
|
}
|
|
153
193
|
if (!ALLOWED_HOSTS.has(host)) {
|
|
154
|
-
res
|
|
155
|
-
return false;
|
|
194
|
+
return rejectHostRequest(res, 403, 'Host not allowed');
|
|
156
195
|
}
|
|
157
196
|
const originHeader = getHeaderValue(req, 'origin');
|
|
158
197
|
if (originHeader) {
|
|
159
198
|
const originHost = resolveOriginHost(originHeader);
|
|
160
199
|
if (!originHost) {
|
|
161
|
-
res
|
|
162
|
-
return false;
|
|
200
|
+
return rejectHostRequest(res, 403, 'Invalid Origin header');
|
|
163
201
|
}
|
|
164
202
|
if (!ALLOWED_HOSTS.has(originHost)) {
|
|
165
|
-
res
|
|
166
|
-
return false;
|
|
203
|
+
return rejectHostRequest(res, 403, 'Origin not allowed');
|
|
167
204
|
}
|
|
168
205
|
}
|
|
169
206
|
return true;
|
|
@@ -318,24 +355,35 @@ async function verifyWithIntrospection(token) {
|
|
|
318
355
|
throw new InvalidTokenError('Token is inactive');
|
|
319
356
|
return buildIntrospectionAuthInfo(token, payload);
|
|
320
357
|
}
|
|
358
|
+
function resolveBearerToken(authHeader) {
|
|
359
|
+
const [type, token] = authHeader.split(' ');
|
|
360
|
+
if (type !== 'Bearer' || !token) {
|
|
361
|
+
throw new InvalidTokenError('Invalid Authorization header format');
|
|
362
|
+
}
|
|
363
|
+
return token;
|
|
364
|
+
}
|
|
365
|
+
function authenticateWithToken(token) {
|
|
366
|
+
return config.auth.mode === 'oauth'
|
|
367
|
+
? verifyWithIntrospection(token)
|
|
368
|
+
: Promise.resolve(verifyStaticToken(token));
|
|
369
|
+
}
|
|
370
|
+
function authenticateWithApiKey(req) {
|
|
371
|
+
const apiKey = getHeaderValue(req, 'x-api-key');
|
|
372
|
+
if (apiKey && config.auth.mode === 'static') {
|
|
373
|
+
return verifyStaticToken(apiKey);
|
|
374
|
+
}
|
|
375
|
+
if (apiKey && config.auth.mode === 'oauth') {
|
|
376
|
+
throw new InvalidTokenError('X-API-Key not supported for OAuth');
|
|
377
|
+
}
|
|
378
|
+
throw new InvalidTokenError('Missing Authorization header');
|
|
379
|
+
}
|
|
321
380
|
async function authenticate(req) {
|
|
322
381
|
const authHeader = req.headers.authorization;
|
|
323
382
|
if (!authHeader) {
|
|
324
|
-
|
|
325
|
-
if (apiKey && config.auth.mode === 'static') {
|
|
326
|
-
return verifyStaticToken(apiKey);
|
|
327
|
-
}
|
|
328
|
-
if (apiKey && config.auth.mode === 'oauth') {
|
|
329
|
-
throw new InvalidTokenError('X-API-Key not supported for OAuth');
|
|
330
|
-
}
|
|
331
|
-
throw new InvalidTokenError('Missing Authorization header');
|
|
383
|
+
return authenticateWithApiKey(req);
|
|
332
384
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
throw new InvalidTokenError('Invalid Authorization header format');
|
|
336
|
-
if (config.auth.mode === 'oauth')
|
|
337
|
-
return verifyWithIntrospection(token);
|
|
338
|
-
return verifyStaticToken(token);
|
|
385
|
+
const token = resolveBearerToken(authHeader);
|
|
386
|
+
return authenticateWithToken(token);
|
|
339
387
|
}
|
|
340
388
|
// --- MCP Routes ---
|
|
341
389
|
function sendError(res, code, message, status = 400, id = null) {
|
|
@@ -394,7 +442,8 @@ async function createNewSession(store, mcpServer, res, requestId) {
|
|
|
394
442
|
tracker.releaseSlot();
|
|
395
443
|
};
|
|
396
444
|
try {
|
|
397
|
-
|
|
445
|
+
const transport = createTransportAdapter(transportImpl);
|
|
446
|
+
await mcpServer.connect(transport);
|
|
398
447
|
}
|
|
399
448
|
catch (err) {
|
|
400
449
|
clearTimeout(initTimeout);
|
|
@@ -642,4 +691,3 @@ async function handleRequest(rawReq, rawRes, rateLimiter, ctx) {
|
|
|
642
691
|
// 5. Routing
|
|
643
692
|
await dispatchRequest(req, res, url, ctx);
|
|
644
693
|
}
|
|
645
|
-
//# sourceMappingURL=http-native.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED