@j0hanz/superfetch 2.2.1 → 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 +243 -494
- package/dist/cache.d.ts +2 -3
- package/dist/cache.js +51 -241
- package/dist/config.d.ts +6 -1
- package/dist/config.js +29 -34
- package/dist/crypto.d.ts +0 -1
- package/dist/crypto.js +0 -1
- package/dist/dom-noise-removal.d.ts +5 -0
- package/dist/dom-noise-removal.js +485 -0
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +8 -6
- package/dist/fetch.d.ts +0 -1
- package/dist/fetch.js +71 -61
- package/dist/host-normalization.d.ts +1 -0
- package/dist/host-normalization.js +47 -0
- package/dist/http-native.d.ts +5 -0
- package/dist/http-native.js +693 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -2
- package/dist/instructions.md +22 -20
- package/dist/json.d.ts +1 -0
- package/dist/json.js +29 -0
- package/dist/language-detection.d.ts +12 -0
- package/dist/language-detection.js +291 -0
- package/dist/markdown-cleanup.d.ts +18 -0
- package/dist/markdown-cleanup.js +283 -0
- 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 +1 -1
- package/dist/observability.js +15 -3
- package/dist/server-tuning.d.ts +9 -0
- package/dist/server-tuning.js +30 -0
- package/dist/session.d.ts +36 -0
- package/dist/session.js +159 -0
- package/dist/tools.d.ts +0 -1
- package/dist/tools.js +23 -33
- package/dist/transform-types.d.ts +80 -0
- package/dist/transform-types.js +5 -0
- package/dist/transform.d.ts +7 -53
- package/dist/transform.js +434 -856
- package/dist/type-guards.d.ts +1 -2
- package/dist/type-guards.js +1 -2
- package/dist/workers/transform-worker.d.ts +0 -1
- package/dist/workers/transform-worker.js +52 -43
- package/package.json +11 -12
- 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/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.d.ts +0 -90
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -1576
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.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.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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
export interface CacheEntry {
|
|
4
4
|
url: string;
|
|
@@ -38,6 +38,5 @@ export declare function keys(): readonly string[];
|
|
|
38
38
|
export declare function isEnabled(): boolean;
|
|
39
39
|
export declare function registerCachedContentResource(server: McpServer): void;
|
|
40
40
|
export declare function generateSafeFilename(url: string, title?: string, hashFallback?: string, extension?: string): string;
|
|
41
|
-
export declare function
|
|
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
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
2
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { ErrorCode, McpError, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { config } from './config.js';
|
|
5
5
|
import { sha256Hex } from './crypto.js';
|
|
6
6
|
import { getErrorMessage } from './errors.js';
|
|
7
|
+
import { stableStringify as stableJsonStringify } from './json.js';
|
|
7
8
|
import { logDebug, logWarn } from './observability.js';
|
|
8
|
-
import {
|
|
9
|
+
import { isObject } from './type-guards.js';
|
|
9
10
|
export function parseCachedPayload(raw) {
|
|
10
11
|
try {
|
|
11
12
|
const parsed = JSON.parse(raw);
|
|
@@ -31,7 +32,7 @@ function hasOptionalStringProperty(value, key) {
|
|
|
31
32
|
return typeof prop === 'string';
|
|
32
33
|
}
|
|
33
34
|
function isCachedPayload(value) {
|
|
34
|
-
if (!
|
|
35
|
+
if (!isObject(value))
|
|
35
36
|
return false;
|
|
36
37
|
if (!hasOptionalStringProperty(value, 'content'))
|
|
37
38
|
return false;
|
|
@@ -42,124 +43,16 @@ function isCachedPayload(value) {
|
|
|
42
43
|
return true;
|
|
43
44
|
}
|
|
44
45
|
const CACHE_HASH = {
|
|
45
|
-
URL_HASH_LENGTH:
|
|
46
|
-
VARY_HASH_LENGTH:
|
|
46
|
+
URL_HASH_LENGTH: 32,
|
|
47
|
+
VARY_HASH_LENGTH: 16,
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
-
MAX_STRING_LENGTH: 4096,
|
|
50
|
-
MAX_KEYS: 64,
|
|
51
|
-
MAX_ARRAY_LENGTH: 64,
|
|
52
|
-
MAX_DEPTH: 6,
|
|
53
|
-
MAX_NODES: 512,
|
|
54
|
-
};
|
|
55
|
-
function bumpStableStringifyNodeCount(state) {
|
|
56
|
-
state.nodes += 1;
|
|
57
|
-
return state.nodes <= CACHE_VARY_LIMITS.MAX_NODES;
|
|
58
|
-
}
|
|
59
|
-
function stableStringifyPrimitive(value) {
|
|
60
|
-
if (value === null || value === undefined) {
|
|
61
|
-
return '';
|
|
62
|
-
}
|
|
63
|
-
const json = JSON.stringify(value);
|
|
64
|
-
return typeof json === 'string' ? json : '';
|
|
65
|
-
}
|
|
66
|
-
function stableStringifyArray(value, state) {
|
|
67
|
-
if (value.length > CACHE_VARY_LIMITS.MAX_ARRAY_LENGTH) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const parts = ['['];
|
|
71
|
-
let length = 1;
|
|
72
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
73
|
-
if (index > 0) {
|
|
74
|
-
parts.push(',');
|
|
75
|
-
length += 1;
|
|
76
|
-
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
const entry = stableStringifyInner(value[index], state);
|
|
80
|
-
if (entry === null)
|
|
81
|
-
return null;
|
|
82
|
-
parts.push(entry);
|
|
83
|
-
length += entry.length;
|
|
84
|
-
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
parts.push(']');
|
|
88
|
-
length += 1;
|
|
89
|
-
return length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : parts.join('');
|
|
90
|
-
}
|
|
91
|
-
function stableStringifyRecord(value, state) {
|
|
92
|
-
const keys = Object.keys(value);
|
|
93
|
-
if (keys.length > CACHE_VARY_LIMITS.MAX_KEYS) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
keys.sort((a, b) => a.localeCompare(b));
|
|
97
|
-
const parts = ['{'];
|
|
98
|
-
let length = 1;
|
|
99
|
-
let isFirst = true;
|
|
100
|
-
for (const key of keys) {
|
|
101
|
-
const entryValue = value[key];
|
|
102
|
-
if (entryValue === undefined)
|
|
103
|
-
continue;
|
|
104
|
-
const encodedValue = stableStringifyInner(entryValue, state);
|
|
105
|
-
if (encodedValue === null)
|
|
106
|
-
return null;
|
|
107
|
-
const entry = `${JSON.stringify(key)}:${encodedValue}`;
|
|
108
|
-
if (!isFirst) {
|
|
109
|
-
parts.push(',');
|
|
110
|
-
length += 1;
|
|
111
|
-
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
parts.push(entry);
|
|
115
|
-
length += entry.length;
|
|
116
|
-
if (length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH)
|
|
117
|
-
return null;
|
|
118
|
-
isFirst = false;
|
|
119
|
-
}
|
|
120
|
-
parts.push('}');
|
|
121
|
-
length += 1;
|
|
122
|
-
return length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : parts.join('');
|
|
123
|
-
}
|
|
124
|
-
function stableStringifyObject(value, state) {
|
|
125
|
-
if (state.stack.has(value)) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
if (state.depth >= CACHE_VARY_LIMITS.MAX_DEPTH) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
state.stack.add(value);
|
|
132
|
-
state.depth += 1;
|
|
49
|
+
function stableStringify(value) {
|
|
133
50
|
try {
|
|
134
|
-
|
|
135
|
-
return stableStringifyArray(value, state);
|
|
136
|
-
}
|
|
137
|
-
return isRecord(value) ? stableStringifyRecord(value, state) : null;
|
|
51
|
+
return stableJsonStringify(value);
|
|
138
52
|
}
|
|
139
|
-
|
|
140
|
-
state.depth -= 1;
|
|
141
|
-
state.stack.delete(value);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function stableStringifyInner(value, state) {
|
|
145
|
-
if (!bumpStableStringifyNodeCount(state)) {
|
|
53
|
+
catch {
|
|
146
54
|
return null;
|
|
147
55
|
}
|
|
148
|
-
if (value === null || value === undefined) {
|
|
149
|
-
return 'null';
|
|
150
|
-
}
|
|
151
|
-
if (typeof value !== 'object') {
|
|
152
|
-
return stableStringifyPrimitive(value);
|
|
153
|
-
}
|
|
154
|
-
return stableStringifyObject(value, state);
|
|
155
|
-
}
|
|
156
|
-
function stableStringify(value) {
|
|
157
|
-
const state = {
|
|
158
|
-
depth: 0,
|
|
159
|
-
nodes: 0,
|
|
160
|
-
stack: new WeakSet(),
|
|
161
|
-
};
|
|
162
|
-
return stableStringifyInner(value, state);
|
|
163
56
|
}
|
|
164
57
|
function createHashFragment(input, length) {
|
|
165
58
|
return sha256Hex(input).substring(0, length);
|
|
@@ -174,8 +67,7 @@ function getVaryHash(vary) {
|
|
|
174
67
|
return undefined;
|
|
175
68
|
let varyString;
|
|
176
69
|
if (typeof vary === 'string') {
|
|
177
|
-
varyString =
|
|
178
|
-
vary.length > CACHE_VARY_LIMITS.MAX_STRING_LENGTH ? null : vary;
|
|
70
|
+
varyString = vary;
|
|
179
71
|
}
|
|
180
72
|
else {
|
|
181
73
|
varyString = stableStringify(vary);
|
|
@@ -204,15 +96,20 @@ export function parseCacheKey(cacheKey) {
|
|
|
204
96
|
return null;
|
|
205
97
|
return { namespace, urlHash };
|
|
206
98
|
}
|
|
99
|
+
function buildCacheResourceUri(namespace, urlHash) {
|
|
100
|
+
return `superfetch://cache/${namespace}/${urlHash}`;
|
|
101
|
+
}
|
|
207
102
|
export function toResourceUri(cacheKey) {
|
|
208
103
|
const parts = parseCacheKey(cacheKey);
|
|
209
104
|
if (!parts)
|
|
210
105
|
return null;
|
|
211
|
-
return
|
|
106
|
+
return buildCacheResourceUri(parts.namespace, parts.urlHash);
|
|
212
107
|
}
|
|
213
|
-
const contentCache = new
|
|
214
|
-
|
|
215
|
-
|
|
108
|
+
const contentCache = new LRUCache({
|
|
109
|
+
max: config.cache.maxKeys,
|
|
110
|
+
ttl: config.cache.ttl * 1000,
|
|
111
|
+
updateAgeOnGet: false,
|
|
112
|
+
});
|
|
216
113
|
const updateListeners = new Set();
|
|
217
114
|
export function onCacheUpdate(listener) {
|
|
218
115
|
updateListeners.add(listener);
|
|
@@ -231,47 +128,10 @@ function notifyCacheUpdate(cacheKey) {
|
|
|
231
128
|
listener(event);
|
|
232
129
|
}
|
|
233
130
|
}
|
|
234
|
-
function startCleanupLoop() {
|
|
235
|
-
if (cleanupController)
|
|
236
|
-
return;
|
|
237
|
-
cleanupController = new AbortController();
|
|
238
|
-
void runCleanupLoop(cleanupController.signal).catch((error) => {
|
|
239
|
-
if (error instanceof Error && error.name !== 'AbortError') {
|
|
240
|
-
logWarn('Cache cleanup loop failed', { error: getErrorMessage(error) });
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
async function runCleanupLoop(signal) {
|
|
245
|
-
const intervalMs = Math.floor(config.cache.ttl * 1000);
|
|
246
|
-
for await (const getNow of setIntervalPromise(intervalMs, Date.now, {
|
|
247
|
-
signal,
|
|
248
|
-
ref: false,
|
|
249
|
-
})) {
|
|
250
|
-
enforceCacheLimits(getNow());
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
function enforceCacheLimits(now) {
|
|
254
|
-
if (nextExpiryAt !== null && now < nextExpiryAt) {
|
|
255
|
-
trimCacheToMaxKeys();
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
let nextExpiry = null;
|
|
259
|
-
for (const [key, item] of contentCache.entries()) {
|
|
260
|
-
if (now > item.expiresAt) {
|
|
261
|
-
contentCache.delete(key);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (nextExpiry === null || item.expiresAt < nextExpiry) {
|
|
265
|
-
nextExpiry = item.expiresAt;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
nextExpiryAt = nextExpiry;
|
|
269
|
-
trimCacheToMaxKeys();
|
|
270
|
-
}
|
|
271
131
|
export function get(cacheKey) {
|
|
272
132
|
if (!isCacheReadable(cacheKey))
|
|
273
133
|
return undefined;
|
|
274
|
-
return runCacheOperation(cacheKey, 'Cache get error', () =>
|
|
134
|
+
return runCacheOperation(cacheKey, 'Cache get error', () => contentCache.get(cacheKey));
|
|
275
135
|
}
|
|
276
136
|
function isCacheReadable(cacheKey) {
|
|
277
137
|
return config.cache.enabled && Boolean(cacheKey);
|
|
@@ -288,28 +148,10 @@ function runCacheOperation(cacheKey, message, operation) {
|
|
|
288
148
|
return undefined;
|
|
289
149
|
}
|
|
290
150
|
}
|
|
291
|
-
function readCacheEntry(cacheKey) {
|
|
292
|
-
const now = Date.now();
|
|
293
|
-
return readCacheItem(cacheKey, now)?.entry;
|
|
294
|
-
}
|
|
295
|
-
function isExpired(item, now) {
|
|
296
|
-
return now > item.expiresAt;
|
|
297
|
-
}
|
|
298
|
-
function readCacheItem(cacheKey, now) {
|
|
299
|
-
const item = contentCache.get(cacheKey);
|
|
300
|
-
if (!item)
|
|
301
|
-
return undefined;
|
|
302
|
-
if (isExpired(item, now)) {
|
|
303
|
-
contentCache.delete(cacheKey);
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
return item;
|
|
307
|
-
}
|
|
308
151
|
export function set(cacheKey, content, metadata) {
|
|
309
152
|
if (!isCacheWritable(cacheKey, content))
|
|
310
153
|
return;
|
|
311
154
|
runCacheOperation(cacheKey, 'Cache set error', () => {
|
|
312
|
-
startCleanupLoop();
|
|
313
155
|
const now = Date.now();
|
|
314
156
|
const expiresAtMs = now + config.cache.ttl * 1000;
|
|
315
157
|
const entry = buildCacheEntry({
|
|
@@ -318,11 +160,11 @@ export function set(cacheKey, content, metadata) {
|
|
|
318
160
|
fetchedAtMs: now,
|
|
319
161
|
expiresAtMs,
|
|
320
162
|
});
|
|
321
|
-
persistCacheEntry(cacheKey, entry
|
|
163
|
+
persistCacheEntry(cacheKey, entry);
|
|
322
164
|
});
|
|
323
165
|
}
|
|
324
166
|
export function keys() {
|
|
325
|
-
return
|
|
167
|
+
return [...contentCache.keys()];
|
|
326
168
|
}
|
|
327
169
|
export function isEnabled() {
|
|
328
170
|
return config.cache.enabled;
|
|
@@ -336,33 +178,10 @@ function buildCacheEntry({ content, metadata, fetchedAtMs, expiresAtMs, }) {
|
|
|
336
178
|
...(metadata.title === undefined ? {} : { title: metadata.title }),
|
|
337
179
|
};
|
|
338
180
|
}
|
|
339
|
-
function persistCacheEntry(cacheKey, entry
|
|
340
|
-
contentCache.set(cacheKey,
|
|
341
|
-
if (nextExpiryAt === null || expiresAtMs < nextExpiryAt) {
|
|
342
|
-
nextExpiryAt = expiresAtMs;
|
|
343
|
-
}
|
|
344
|
-
trimCacheToMaxKeys();
|
|
181
|
+
function persistCacheEntry(cacheKey, entry) {
|
|
182
|
+
contentCache.set(cacheKey, entry);
|
|
345
183
|
notifyCacheUpdate(cacheKey);
|
|
346
184
|
}
|
|
347
|
-
function trimCacheToMaxKeys() {
|
|
348
|
-
if (contentCache.size <= config.cache.maxKeys)
|
|
349
|
-
return;
|
|
350
|
-
removeOldestEntries(contentCache.size - config.cache.maxKeys);
|
|
351
|
-
}
|
|
352
|
-
function removeOldestEntries(count) {
|
|
353
|
-
const iterator = contentCache.keys();
|
|
354
|
-
for (let removed = 0; removed < count; removed += 1) {
|
|
355
|
-
const next = iterator.next();
|
|
356
|
-
if (next.done)
|
|
357
|
-
break;
|
|
358
|
-
const removedKey = next.value;
|
|
359
|
-
const removedItem = contentCache.get(removedKey);
|
|
360
|
-
contentCache.delete(removedKey);
|
|
361
|
-
if (nextExpiryAt !== null && removedItem?.expiresAt === nextExpiryAt) {
|
|
362
|
-
nextExpiryAt = null;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
185
|
function logCacheError(message, cacheKey, error) {
|
|
367
186
|
logWarn(message, {
|
|
368
187
|
key: cacheKey.length > 100 ? cacheKey.slice(0, 100) : cacheKey,
|
|
@@ -371,18 +190,22 @@ function logCacheError(message, cacheKey, error) {
|
|
|
371
190
|
}
|
|
372
191
|
const CACHE_NAMESPACE = 'markdown';
|
|
373
192
|
const HASH_PATTERN = /^[a-f0-9.]+$/i;
|
|
193
|
+
const INVALID_CACHE_PARAMS_MESSAGE = 'Invalid cache resource parameters';
|
|
194
|
+
function throwInvalidCacheParams() {
|
|
195
|
+
throw new McpError(ErrorCode.InvalidParams, INVALID_CACHE_PARAMS_MESSAGE);
|
|
196
|
+
}
|
|
374
197
|
function resolveCacheParams(params) {
|
|
375
198
|
const parsed = requireRecordParams(params);
|
|
376
199
|
const namespace = requireParamString(parsed, 'namespace');
|
|
377
200
|
const urlHash = requireParamString(parsed, 'urlHash');
|
|
378
201
|
if (!isValidNamespace(namespace) || !isValidHash(urlHash)) {
|
|
379
|
-
|
|
202
|
+
throwInvalidCacheParams();
|
|
380
203
|
}
|
|
381
204
|
return { namespace, urlHash };
|
|
382
205
|
}
|
|
383
206
|
function requireRecordParams(value) {
|
|
384
|
-
if (!
|
|
385
|
-
|
|
207
|
+
if (!isObject(value)) {
|
|
208
|
+
throwInvalidCacheParams();
|
|
386
209
|
}
|
|
387
210
|
return value;
|
|
388
211
|
}
|
|
@@ -428,7 +251,7 @@ function resolveStringParam(value) {
|
|
|
428
251
|
function buildResourceEntry(namespace, urlHash) {
|
|
429
252
|
return {
|
|
430
253
|
name: `${namespace}:${urlHash}`,
|
|
431
|
-
uri:
|
|
254
|
+
uri: buildCacheResourceUri(namespace, urlHash),
|
|
432
255
|
description: `Cached content entry for ${namespace}`,
|
|
433
256
|
mimeType: 'text/markdown',
|
|
434
257
|
};
|
|
@@ -462,11 +285,11 @@ function attachInitializedGate(server) {
|
|
|
462
285
|
}
|
|
463
286
|
function getClientResourceCapabilities(server) {
|
|
464
287
|
const caps = server.server.getClientCapabilities();
|
|
465
|
-
if (!caps || !
|
|
288
|
+
if (!caps || !isObject(caps)) {
|
|
466
289
|
return { listChanged: false, subscribe: false };
|
|
467
290
|
}
|
|
468
291
|
const { resources } = caps;
|
|
469
|
-
if (!
|
|
292
|
+
if (!isObject(resources)) {
|
|
470
293
|
return { listChanged: false, subscribe: false };
|
|
471
294
|
}
|
|
472
295
|
const { listChanged, subscribe } = resources;
|
|
@@ -573,41 +396,32 @@ function buildMarkdownContentResponse(uri, content) {
|
|
|
573
396
|
],
|
|
574
397
|
};
|
|
575
398
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const { namespace, hash } = req.params;
|
|
581
|
-
if (!isSingleParam(namespace) || !isSingleParam(hash))
|
|
399
|
+
function parseDownloadParams(namespace, hash) {
|
|
400
|
+
const resolvedNamespace = resolveStringParam(namespace);
|
|
401
|
+
const resolvedHash = resolveStringParam(hash);
|
|
402
|
+
if (!resolvedNamespace || !resolvedHash)
|
|
582
403
|
return null;
|
|
583
|
-
if (!
|
|
404
|
+
if (!isValidNamespace(resolvedNamespace))
|
|
584
405
|
return null;
|
|
585
|
-
if (!
|
|
406
|
+
if (!isValidHash(resolvedHash))
|
|
586
407
|
return null;
|
|
587
|
-
|
|
588
|
-
return null;
|
|
589
|
-
return { namespace, hash };
|
|
408
|
+
return { namespace: resolvedNamespace, hash: resolvedHash };
|
|
590
409
|
}
|
|
591
410
|
function buildCacheKeyFromParams(params) {
|
|
592
411
|
return `${params.namespace}:${params.hash}`;
|
|
593
412
|
}
|
|
413
|
+
function sendJsonError(res, status, error, code) {
|
|
414
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
415
|
+
res.end(JSON.stringify({ error, code }));
|
|
416
|
+
}
|
|
594
417
|
function respondBadRequest(res, message) {
|
|
595
|
-
res
|
|
596
|
-
error: message,
|
|
597
|
-
code: 'BAD_REQUEST',
|
|
598
|
-
});
|
|
418
|
+
sendJsonError(res, 400, message, 'BAD_REQUEST');
|
|
599
419
|
}
|
|
600
420
|
function respondNotFound(res) {
|
|
601
|
-
res
|
|
602
|
-
error: 'Content not found or expired',
|
|
603
|
-
code: 'NOT_FOUND',
|
|
604
|
-
});
|
|
421
|
+
sendJsonError(res, 404, 'Content not found or expired', 'NOT_FOUND');
|
|
605
422
|
}
|
|
606
423
|
function respondServiceUnavailable(res) {
|
|
607
|
-
res
|
|
608
|
-
error: 'Download service is disabled',
|
|
609
|
-
code: 'SERVICE_UNAVAILABLE',
|
|
610
|
-
});
|
|
424
|
+
sendJsonError(res, 503, 'Download service is disabled', 'SERVICE_UNAVAILABLE');
|
|
611
425
|
}
|
|
612
426
|
export function generateSafeFilename(url, title, hashFallback, extension = '.md') {
|
|
613
427
|
const fromUrl = extractFilenameFromUrl(url);
|
|
@@ -714,14 +528,14 @@ function sendDownloadPayload(res, payload) {
|
|
|
714
528
|
res.setHeader('Content-Disposition', disposition);
|
|
715
529
|
res.setHeader('Cache-Control', `private, max-age=${config.cache.ttl}`);
|
|
716
530
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
717
|
-
res.
|
|
531
|
+
res.end(payload.content);
|
|
718
532
|
}
|
|
719
|
-
function handleDownload(
|
|
533
|
+
export function handleDownload(res, namespace, hash) {
|
|
720
534
|
if (!config.cache.enabled) {
|
|
721
535
|
respondServiceUnavailable(res);
|
|
722
536
|
return;
|
|
723
537
|
}
|
|
724
|
-
const params = parseDownloadParams(
|
|
538
|
+
const params = parseDownloadParams(namespace, hash);
|
|
725
539
|
if (!params) {
|
|
726
540
|
respondBadRequest(res, 'Invalid namespace or hash format');
|
|
727
541
|
return;
|
|
@@ -742,7 +556,3 @@ function handleDownload(req, res) {
|
|
|
742
556
|
logDebug('Serving download', { cacheKey, fileName: payload.fileName });
|
|
743
557
|
sendDownloadPayload(res, payload);
|
|
744
558
|
}
|
|
745
|
-
export function registerDownloadRoutes(app) {
|
|
746
|
-
app.get('/mcp/downloads/:namespace/:hash', handleDownload);
|
|
747
|
-
}
|
|
748
|
-
//# sourceMappingURL=cache.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ export declare const config: {
|
|
|
57
57
|
maxBlockLength: number;
|
|
58
58
|
minParagraphLength: number;
|
|
59
59
|
};
|
|
60
|
+
noiseRemoval: {
|
|
61
|
+
extraTokens: string[];
|
|
62
|
+
extraSelectors: string[];
|
|
63
|
+
};
|
|
60
64
|
logging: {
|
|
61
65
|
level: LogLevel;
|
|
62
66
|
};
|
|
@@ -68,6 +72,8 @@ export declare const config: {
|
|
|
68
72
|
security: {
|
|
69
73
|
blockedHosts: Set<string>;
|
|
70
74
|
blockedIpPatterns: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp];
|
|
75
|
+
blockedIpPattern: RegExp;
|
|
76
|
+
blockedIpv4MappedPattern: RegExp;
|
|
71
77
|
allowedHosts: Set<string>;
|
|
72
78
|
apiKey: string | undefined;
|
|
73
79
|
allowRemote: boolean;
|
|
@@ -83,4 +89,3 @@ export declare const config: {
|
|
|
83
89
|
};
|
|
84
90
|
export declare function enableHttpMode(): void;
|
|
85
91
|
export {};
|
|
86
|
-
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -33,39 +33,24 @@ const ALLOWED_LOG_LEVELS = new Set([
|
|
|
33
33
|
function isLogLevel(value) {
|
|
34
34
|
return ALLOWED_LOG_LEVELS.has(value);
|
|
35
35
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
return false;
|
|
39
|
-
return value < min;
|
|
36
|
+
function isOutsideRange(value, min, max) {
|
|
37
|
+
return ((min !== undefined && value < min) || (max !== undefined && value > max));
|
|
40
38
|
}
|
|
41
|
-
function
|
|
42
|
-
if (max === undefined)
|
|
43
|
-
return false;
|
|
44
|
-
return value > max;
|
|
45
|
-
}
|
|
46
|
-
function parseInteger(envValue, defaultValue, min, max) {
|
|
39
|
+
function parseIntegerValue(envValue, min, max) {
|
|
47
40
|
if (!envValue)
|
|
48
|
-
return
|
|
41
|
+
return null;
|
|
49
42
|
const parsed = parseInt(envValue, 10);
|
|
50
43
|
if (Number.isNaN(parsed))
|
|
51
|
-
return
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
54
|
-
if (isAboveMax(parsed, max))
|
|
55
|
-
return defaultValue;
|
|
44
|
+
return null;
|
|
45
|
+
if (isOutsideRange(parsed, min, max))
|
|
46
|
+
return null;
|
|
56
47
|
return parsed;
|
|
57
48
|
}
|
|
49
|
+
function parseInteger(envValue, defaultValue, min, max) {
|
|
50
|
+
return parseIntegerValue(envValue, min, max) ?? defaultValue;
|
|
51
|
+
}
|
|
58
52
|
function parseOptionalInteger(envValue, min, max) {
|
|
59
|
-
|
|
60
|
-
return undefined;
|
|
61
|
-
const parsed = parseInt(envValue, 10);
|
|
62
|
-
if (Number.isNaN(parsed))
|
|
63
|
-
return undefined;
|
|
64
|
-
if (isBelowMin(parsed, min))
|
|
65
|
-
return undefined;
|
|
66
|
-
if (isAboveMax(parsed, max))
|
|
67
|
-
return undefined;
|
|
68
|
-
return parsed;
|
|
53
|
+
return parseIntegerValue(envValue, min, max) ?? undefined;
|
|
69
54
|
}
|
|
70
55
|
function parseBoolean(envValue, defaultValue) {
|
|
71
56
|
if (!envValue)
|
|
@@ -88,6 +73,9 @@ function parseUrlEnv(value, name) {
|
|
|
88
73
|
}
|
|
89
74
|
return new URL(value);
|
|
90
75
|
}
|
|
76
|
+
function readUrlEnv(name) {
|
|
77
|
+
return parseUrlEnv(process.env[name], name);
|
|
78
|
+
}
|
|
91
79
|
function parseAllowedHosts(envValue) {
|
|
92
80
|
const hosts = new Set();
|
|
93
81
|
for (const entry of parseList(envValue)) {
|
|
@@ -123,16 +111,16 @@ const DEFAULT_TOOL_TIMEOUT_MS = TIMEOUT.DEFAULT_FETCH_TIMEOUT_MS +
|
|
|
123
111
|
5000;
|
|
124
112
|
function readCoreOAuthUrls() {
|
|
125
113
|
return {
|
|
126
|
-
issuerUrl:
|
|
127
|
-
authorizationUrl:
|
|
128
|
-
tokenUrl:
|
|
114
|
+
issuerUrl: readUrlEnv('OAUTH_ISSUER_URL'),
|
|
115
|
+
authorizationUrl: readUrlEnv('OAUTH_AUTHORIZATION_URL'),
|
|
116
|
+
tokenUrl: readUrlEnv('OAUTH_TOKEN_URL'),
|
|
129
117
|
};
|
|
130
118
|
}
|
|
131
119
|
function readOptionalOAuthUrls(baseUrl) {
|
|
132
120
|
return {
|
|
133
|
-
revocationUrl:
|
|
134
|
-
registrationUrl:
|
|
135
|
-
introspectionUrl:
|
|
121
|
+
revocationUrl: readUrlEnv('OAUTH_REVOCATION_URL'),
|
|
122
|
+
registrationUrl: readUrlEnv('OAUTH_REGISTRATION_URL'),
|
|
123
|
+
introspectionUrl: readUrlEnv('OAUTH_INTROSPECTION_URL'),
|
|
136
124
|
resourceUrl: parseUrlEnv(process.env.OAUTH_RESOURCE_URL, 'OAUTH_RESOURCE_URL') ??
|
|
137
125
|
new URL('/mcp', baseUrl),
|
|
138
126
|
};
|
|
@@ -158,7 +146,7 @@ function collectStaticTokens() {
|
|
|
158
146
|
if (process.env.API_KEY) {
|
|
159
147
|
staticTokens.add(process.env.API_KEY);
|
|
160
148
|
}
|
|
161
|
-
return
|
|
149
|
+
return [...staticTokens];
|
|
162
150
|
}
|
|
163
151
|
function buildAuthConfig(baseUrl) {
|
|
164
152
|
const urls = readOAuthUrls(baseUrl);
|
|
@@ -225,6 +213,10 @@ export const config = {
|
|
|
225
213
|
maxBlockLength: 5000,
|
|
226
214
|
minParagraphLength: 10,
|
|
227
215
|
},
|
|
216
|
+
noiseRemoval: {
|
|
217
|
+
extraTokens: parseList(process.env.SUPERFETCH_EXTRA_NOISE_TOKENS),
|
|
218
|
+
extraSelectors: parseList(process.env.SUPERFETCH_EXTRA_NOISE_SELECTORS),
|
|
219
|
+
},
|
|
228
220
|
logging: {
|
|
229
221
|
level: parseLogLevel(process.env.LOG_LEVEL),
|
|
230
222
|
},
|
|
@@ -262,6 +254,10 @@ export const config = {
|
|
|
262
254
|
/^::ffff:192\.168\./,
|
|
263
255
|
/^::ffff:169\.254\./,
|
|
264
256
|
],
|
|
257
|
+
// Combined regex patterns for fast IP blocking (used in fetch.ts)
|
|
258
|
+
// Split into two patterns to reduce complexity while maintaining performance
|
|
259
|
+
blockedIpPattern: /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|169\.254\.|100\.64\.|fc00:|fd00:|fe80:)/i,
|
|
260
|
+
blockedIpv4MappedPattern: /^::ffff:(?:127\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/i,
|
|
265
261
|
allowedHosts: parseAllowedHosts(process.env.ALLOWED_HOSTS),
|
|
266
262
|
apiKey: process.env.API_KEY,
|
|
267
263
|
allowRemote,
|
|
@@ -278,4 +274,3 @@ export const config = {
|
|
|
278
274
|
export function enableHttpMode() {
|
|
279
275
|
runtimeState.httpMode = true;
|
|
280
276
|
}
|
|
281
|
-
//# sourceMappingURL=config.js.map
|
package/dist/crypto.d.ts
CHANGED
package/dist/crypto.js
CHANGED