@j0hanz/superfetch 2.6.0 → 2.7.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.
@@ -5,6 +5,8 @@ import { isIP } from 'node:net';
5
5
  import { freemem, hostname, totalmem } from 'node:os';
6
6
  import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
7
7
  import process from 'node:process';
8
+ import { Writable } from 'node:stream';
9
+ import { pipeline } from 'node:stream/promises';
8
10
  import { setInterval as setIntervalPromise } from 'node:timers/promises';
9
11
  import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/server/auth/errors.js';
10
12
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -248,7 +250,7 @@ class JsonBodyReader {
248
250
  const { chunks, size } = await this.collectChunks(req, limit, signal);
249
251
  if (chunks.length === 0)
250
252
  return undefined;
251
- return Buffer.concat(chunks, size).toString();
253
+ return Buffer.concat(chunks, size).toString('utf8');
252
254
  }
253
255
  finally {
254
256
  this.detachAbortListener(signal, abortListener);
@@ -286,19 +288,34 @@ class JsonBodyReader {
286
288
  async collectChunks(req, limit, signal) {
287
289
  let size = 0;
288
290
  const chunks = [];
289
- try {
290
- for await (const chunk of req) {
291
- if (signal?.aborted || req.destroyed) {
292
- throw new JsonBodyError('read-failed', 'Request aborted');
291
+ const sink = new Writable({
292
+ write: (chunk, _encoding, callback) => {
293
+ try {
294
+ if (signal?.aborted || req.destroyed) {
295
+ callback(new JsonBodyError('read-failed', 'Request aborted'));
296
+ return;
297
+ }
298
+ const buf = this.normalizeChunk(chunk);
299
+ size += buf.length;
300
+ if (size > limit) {
301
+ req.destroy();
302
+ callback(new JsonBodyError('payload-too-large', 'Payload too large'));
303
+ return;
304
+ }
305
+ chunks.push(buf);
306
+ callback();
293
307
  }
294
- const buf = this.normalizeChunk(chunk);
295
- size += buf.length;
296
- if (size > limit) {
297
- req.destroy();
298
- throw new JsonBodyError('payload-too-large', 'Payload too large');
308
+ catch (err) {
309
+ callback(err instanceof Error ? err : new Error(String(err)));
299
310
  }
300
- chunks.push(buf);
311
+ },
312
+ });
313
+ try {
314
+ if (signal?.aborted || req.destroyed) {
315
+ throw new JsonBodyError('read-failed', 'Request aborted');
301
316
  }
317
+ await pipeline(req, sink, signal ? { signal } : undefined);
318
+ return { chunks, size };
302
319
  }
303
320
  catch (err) {
304
321
  if (err instanceof JsonBodyError)
@@ -308,13 +325,12 @@ class JsonBodyReader {
308
325
  }
309
326
  throw new JsonBodyError('read-failed', err instanceof Error ? err.message : String(err));
310
327
  }
311
- return { chunks, size };
312
328
  }
313
329
  normalizeChunk(chunk) {
314
330
  if (Buffer.isBuffer(chunk))
315
331
  return chunk;
316
332
  if (typeof chunk === 'string')
317
- return Buffer.from(chunk);
333
+ return Buffer.from(chunk, 'utf8');
318
334
  return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
319
335
  }
320
336
  }
@@ -556,8 +572,9 @@ class AuthService {
556
572
  return clean.href;
557
573
  }
558
574
  buildBasicAuthHeader(clientId, clientSecret) {
575
+ // Base64 is only an encoding for header transport; it is NOT encryption.
559
576
  const credentials = `${clientId}:${clientSecret ?? ''}`;
560
- return `Basic ${Buffer.from(credentials).toString('base64')}`;
577
+ return `Basic ${Buffer.from(credentials, 'utf8').toString('base64')}`;
561
578
  }
562
579
  buildIntrospectionRequest(token, resourceUrl, clientId, clientSecret) {
563
580
  const body = new URLSearchParams({
@@ -71,8 +71,10 @@ const HTML_TAGS = [
71
71
  ];
72
72
  const RUST_REGEX = /\b(?:fn|impl|struct|enum)\b/;
73
73
  const JS_REGEX = /\b(?:const|let|var|function|class|async|await|export|import)\b/;
74
- const PYTHON_REGEX = /\b(?:def|class|import|from)\b/;
74
+ const PYTHON_UNIQUE_REGEX = /\b(?:def |elif |except |finally:|yield |lambda |raise |pass$)/m;
75
+ const JS_SIGNAL_REGEX = /\b(?:const |let |var |function |require\(|=>|===|!==|console\.)/;
75
76
  const CSS_REGEX = /@media|@import|@keyframes/;
77
+ const CSS_PROPERTY_REGEX = /^\s*[a-z][\w-]*\s*:/;
76
78
  function containsJsxTag(code) {
77
79
  const len = code.length;
78
80
  for (let i = 0; i < len - 1; i++) {
@@ -129,7 +131,11 @@ function detectCssStructure(lines) {
129
131
  continue;
130
132
  const hasSelector = (trimmed.startsWith('.') || trimmed.startsWith('#')) &&
131
133
  trimmed.includes('{');
132
- if (hasSelector || (trimmed.includes(':') && trimmed.includes(';'))) {
134
+ if (hasSelector)
135
+ return true;
136
+ if (trimmed.includes(';') &&
137
+ CSS_PROPERTY_REGEX.test(trimmed) &&
138
+ !trimmed.includes('(')) {
133
139
  return true;
134
140
  }
135
141
  }
@@ -214,7 +220,25 @@ const LANGUAGES = [
214
220
  const l = ctx.lower;
215
221
  if (l.includes('print(') || l.includes('__name__'))
216
222
  return true;
217
- return PYTHON_REGEX.test(l);
223
+ if (l.includes('self.') || l.includes('elif '))
224
+ return true;
225
+ // Check for Python's None/True/False using original case (they are capitalized in Python)
226
+ if (ctx.code.includes('None') ||
227
+ ctx.code.includes('True') ||
228
+ ctx.code.includes('False')) {
229
+ return true;
230
+ }
231
+ // Python-unique keywords that JS doesn't have
232
+ if (PYTHON_UNIQUE_REGEX.test(l))
233
+ return true;
234
+ // Shared keywords (import, from, class) — only match if no JS signals present
235
+ if (/\b(?:import|from|class)\b/.test(l) &&
236
+ !JS_SIGNAL_REGEX.test(l) &&
237
+ !l.includes('{') &&
238
+ !l.includes("from '")) {
239
+ return true;
240
+ }
241
+ return false;
218
242
  },
219
243
  },
220
244
  {
@@ -238,7 +262,7 @@ const LANGUAGES = [
238
262
  },
239
263
  {
240
264
  lang: 'javascript',
241
- weight: 12,
265
+ weight: 15,
242
266
  match: (ctx) => JS_REGEX.test(ctx.lower),
243
267
  },
244
268
  {
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile, stat } from 'node:fs/promises';
3
3
  import process from 'node:process';
4
4
  import { z } from 'zod';
5
5
  import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -17,8 +17,14 @@ import { isObject } from './type-guards.js';
17
17
  * Icons + server info
18
18
  * ------------------------------------------------------------------------------------------------- */
19
19
  async function getLocalIcons(signal) {
20
+ const MAX_ICON_BYTES = 10 * 1024;
20
21
  try {
21
22
  const iconPath = new URL('../assets/logo.svg', import.meta.url);
23
+ if (signal?.aborted)
24
+ return undefined;
25
+ const { size } = await stat(iconPath);
26
+ if (size > MAX_ICON_BYTES)
27
+ return undefined;
22
28
  const base64 = await readFile(iconPath, {
23
29
  encoding: 'base64',
24
30
  ...(signal ? { signal } : {}),
package/dist/tasks.d.ts CHANGED
@@ -40,6 +40,7 @@ declare class TaskManager {
40
40
  getTask(taskId: string, ownerKey?: string): TaskState | undefined;
41
41
  updateTask(taskId: string, updates: Partial<Omit<TaskState, 'taskId' | 'createdAt'>>): void;
42
42
  cancelTask(taskId: string, ownerKey?: string): TaskState | undefined;
43
+ private collectPage;
43
44
  listTasks(options: {
44
45
  ownerKey: string;
45
46
  cursor?: string;
package/dist/tasks.js CHANGED
@@ -4,18 +4,21 @@ import { randomUUID } from 'node:crypto';
4
4
  import { setInterval } from 'node:timers';
5
5
  import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
6
6
  import { createUnrefTimeout } from './timer-utils.js';
7
- const DEFAULT_TTL_MS = 60000;
8
- const DEFAULT_POLL_INTERVAL_MS = 1000;
7
+ const DEFAULT_TTL_MS = 60_000;
8
+ const DEFAULT_POLL_INTERVAL_MS = 1_000;
9
9
  const DEFAULT_OWNER_KEY = 'default';
10
10
  const DEFAULT_PAGE_SIZE = 50;
11
- const TERMINAL_STATUSES = new Set([
12
- 'completed',
13
- 'failed',
14
- 'cancelled',
15
- ]);
11
+ const CLEANUP_INTERVAL_MS = 60_000;
12
+ const MAX_CURSOR_LENGTH = 256;
16
13
  function isTerminalStatus(status) {
17
- return TERMINAL_STATUSES.has(status);
14
+ return (status === 'completed' || status === 'failed' || status === 'cancelled');
18
15
  }
16
+ const asyncLocalStorageSnapshot = AsyncLocalStorage.snapshot;
17
+ const snapshotRunInContext = typeof asyncLocalStorageSnapshot === 'function'
18
+ ? asyncLocalStorageSnapshot
19
+ : () => (fn) => {
20
+ fn();
21
+ };
19
22
  class TaskManager {
20
23
  tasks = new Map();
21
24
  waiters = new Map();
@@ -24,28 +27,30 @@ class TaskManager {
24
27
  }
25
28
  startCleanupLoop() {
26
29
  const interval = setInterval(() => {
30
+ const now = Date.now();
27
31
  for (const [id, task] of this.tasks) {
28
- if (this.isExpired(task)) {
32
+ if (now - task._createdAtMs > task.ttl) {
29
33
  this.tasks.delete(id);
30
34
  }
31
35
  }
32
- }, 60000);
36
+ }, CLEANUP_INTERVAL_MS);
33
37
  interval.unref();
34
38
  }
35
39
  createTask(options, statusMessage = 'Task started', ownerKey = DEFAULT_OWNER_KEY) {
36
- const taskId = randomUUID();
37
- const now = new Date().toISOString();
40
+ const now = new Date();
41
+ const createdAt = now.toISOString();
38
42
  const task = {
39
- taskId,
43
+ taskId: randomUUID(),
40
44
  ownerKey,
41
45
  status: 'working',
42
46
  statusMessage,
43
- createdAt: now,
44
- lastUpdatedAt: now,
47
+ createdAt,
48
+ lastUpdatedAt: createdAt,
45
49
  ttl: options?.ttl ?? DEFAULT_TTL_MS,
46
50
  pollInterval: DEFAULT_POLL_INTERVAL_MS,
51
+ _createdAtMs: now.getTime(),
47
52
  };
48
- this.tasks.set(taskId, task);
53
+ this.tasks.set(task.taskId, task);
49
54
  return task;
50
55
  }
51
56
  getTask(taskId, ownerKey) {
@@ -78,9 +83,7 @@ class TaskManager {
78
83
  const task = this.getTask(taskId, ownerKey);
79
84
  if (!task)
80
85
  return undefined;
81
- if (task.status === 'completed' ||
82
- task.status === 'failed' ||
83
- task.status === 'cancelled') {
86
+ if (isTerminalStatus(task.status)) {
84
87
  throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task: already in terminal status '${task.status}'`);
85
88
  }
86
89
  this.updateTask(taskId, {
@@ -89,6 +92,27 @@ class TaskManager {
89
92
  });
90
93
  return this.tasks.get(taskId);
91
94
  }
95
+ collectPage(ownerKey, startIndex, pageSize) {
96
+ const page = [];
97
+ let currentIndex = 0;
98
+ const now = Date.now();
99
+ for (const task of this.tasks.values()) {
100
+ if (task.ownerKey !== ownerKey)
101
+ continue;
102
+ if (now - task._createdAtMs > task.ttl) {
103
+ this.tasks.delete(task.taskId);
104
+ continue;
105
+ }
106
+ if (currentIndex >= startIndex) {
107
+ page.push(task);
108
+ if (page.length > pageSize) {
109
+ break;
110
+ }
111
+ }
112
+ currentIndex++;
113
+ }
114
+ return page;
115
+ }
92
116
  listTasks(options) {
93
117
  const { ownerKey, cursor, limit } = options;
94
118
  const pageSize = limit && limit > 0 ? limit : DEFAULT_PAGE_SIZE;
@@ -96,36 +120,36 @@ class TaskManager {
96
120
  if (startIndex === null) {
97
121
  throw new McpError(ErrorCode.InvalidParams, 'Invalid cursor');
98
122
  }
99
- const allTasks = Array.from(this.tasks.values()).filter((task) => {
100
- if (task.ownerKey !== ownerKey)
101
- return false;
102
- if (this.isExpired(task)) {
103
- this.tasks.delete(task.taskId);
104
- return false;
105
- }
106
- return true;
107
- });
108
- const page = allTasks.slice(startIndex, startIndex + pageSize);
109
- const nextIndex = startIndex + page.length;
110
- const nextCursor = nextIndex < allTasks.length ? this.encodeCursor(nextIndex) : undefined;
123
+ const page = this.collectPage(ownerKey, startIndex, pageSize);
124
+ const hasMore = page.length > pageSize;
125
+ if (hasMore) {
126
+ page.pop();
127
+ }
128
+ const nextCursor = hasMore
129
+ ? this.encodeCursor(startIndex + page.length)
130
+ : undefined;
111
131
  return nextCursor ? { tasks: page, nextCursor } : { tasks: page };
112
132
  }
113
133
  async waitForTerminalTask(taskId, ownerKey, signal) {
114
- const task = this.getTask(taskId, ownerKey);
134
+ const task = this.tasks.get(taskId);
115
135
  if (!task)
116
136
  return undefined;
137
+ if (ownerKey && task.ownerKey !== ownerKey)
138
+ return undefined;
139
+ if (this.isExpired(task)) {
140
+ this.tasks.delete(taskId);
141
+ return undefined;
142
+ }
117
143
  if (isTerminalStatus(task.status))
118
144
  return task;
119
- const createdAtMs = Date.parse(task.createdAt);
120
- const deadlineMs = Number.isFinite(createdAtMs)
121
- ? createdAtMs + task.ttl
122
- : Number.NaN;
123
- if (Number.isFinite(deadlineMs) && deadlineMs <= Date.now()) {
145
+ const deadlineMs = task._createdAtMs + task.ttl;
146
+ const now = Date.now();
147
+ if (deadlineMs <= now) {
124
148
  this.tasks.delete(taskId);
125
149
  return undefined;
126
150
  }
127
151
  return new Promise((resolve, reject) => {
128
- const runInContext = AsyncLocalStorage.snapshot();
152
+ const runInContext = snapshotRunInContext();
129
153
  const resolveInContext = (value) => {
130
154
  runInContext(() => {
131
155
  resolve(value);
@@ -133,30 +157,12 @@ class TaskManager {
133
157
  };
134
158
  const rejectInContext = (error) => {
135
159
  runInContext(() => {
136
- if (error instanceof Error) {
137
- reject(error);
138
- }
139
- else {
140
- reject(new Error(String(error)));
141
- }
160
+ reject(error instanceof Error ? error : new Error(String(error)));
142
161
  });
143
162
  };
144
163
  let settled = false;
145
164
  let waiter = null;
146
165
  let deadlineTimeout;
147
- const settle = (fn) => {
148
- if (settled)
149
- return;
150
- settled = true;
151
- fn();
152
- };
153
- const onAbort = () => {
154
- settle(() => {
155
- cleanup();
156
- removeWaiter();
157
- rejectInContext(new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
158
- });
159
- };
160
166
  const cleanup = () => {
161
167
  if (deadlineTimeout) {
162
168
  deadlineTimeout.cancel();
@@ -167,17 +173,35 @@ class TaskManager {
167
173
  }
168
174
  };
169
175
  const removeWaiter = () => {
170
- const waiters = this.waiters.get(taskId);
171
- if (!waiters)
176
+ if (waiter) {
177
+ const set = this.waiters.get(taskId);
178
+ if (set) {
179
+ set.delete(waiter);
180
+ if (set.size === 0)
181
+ this.waiters.delete(taskId);
182
+ }
183
+ }
184
+ };
185
+ const settleOnce = (fn) => {
186
+ if (settled)
172
187
  return;
173
- if (waiter)
174
- waiters.delete(waiter);
175
- if (waiters.size === 0)
176
- this.waiters.delete(taskId);
188
+ settled = true;
189
+ fn();
190
+ };
191
+ const onAbort = () => {
192
+ settleOnce(() => {
193
+ cleanup();
194
+ removeWaiter();
195
+ rejectInContext(new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
196
+ });
177
197
  };
178
198
  waiter = (updated) => {
179
- settle(() => {
199
+ settleOnce(() => {
180
200
  cleanup();
201
+ if (updated.ownerKey !== ownerKey) {
202
+ resolveInContext(undefined);
203
+ return;
204
+ }
181
205
  resolveInContext(updated);
182
206
  });
183
207
  };
@@ -185,35 +209,27 @@ class TaskManager {
185
209
  onAbort();
186
210
  return;
187
211
  }
188
- const waiters = this.waiters.get(taskId) ?? new Set();
189
- waiters.add(waiter);
190
- this.waiters.set(taskId, waiters);
212
+ let set = this.waiters.get(taskId);
213
+ if (!set) {
214
+ set = new Set();
215
+ this.waiters.set(taskId, set);
216
+ }
217
+ set.add(waiter);
191
218
  if (signal) {
192
219
  signal.addEventListener('abort', onAbort, { once: true });
193
220
  }
194
- if (Number.isFinite(deadlineMs)) {
195
- const timeoutMs = Math.max(0, deadlineMs - Date.now());
196
- if (timeoutMs === 0) {
197
- settle(() => {
198
- cleanup();
199
- removeWaiter();
200
- this.tasks.delete(taskId);
201
- resolveInContext(undefined);
202
- });
203
- return;
204
- }
205
- deadlineTimeout = createUnrefTimeout(timeoutMs, { timeout: true });
206
- void deadlineTimeout.promise
207
- .then(() => {
208
- settle(() => {
209
- cleanup();
210
- removeWaiter();
211
- this.tasks.delete(taskId);
212
- resolveInContext(undefined);
213
- });
214
- })
215
- .catch(rejectInContext);
216
- }
221
+ const timeoutMs = Math.max(0, deadlineMs - Date.now());
222
+ deadlineTimeout = createUnrefTimeout(timeoutMs, { timeout: true });
223
+ void deadlineTimeout.promise
224
+ .then(() => {
225
+ settleOnce(() => {
226
+ cleanup();
227
+ removeWaiter();
228
+ this.tasks.delete(taskId);
229
+ resolveInContext(undefined);
230
+ });
231
+ })
232
+ .catch(rejectInContext);
217
233
  });
218
234
  }
219
235
  notifyWaiters(task) {
@@ -227,17 +243,18 @@ class TaskManager {
227
243
  waiter(task);
228
244
  }
229
245
  isExpired(task) {
230
- const createdAt = Date.parse(task.createdAt);
231
- if (!Number.isFinite(createdAt))
232
- return false;
233
- return Date.now() - createdAt > task.ttl;
246
+ return Date.now() - task._createdAtMs > task.ttl;
234
247
  }
235
248
  encodeCursor(index) {
236
- return Buffer.from(String(index)).toString('base64url');
249
+ return Buffer.from(String(index), 'utf8').toString('base64url');
237
250
  }
238
251
  decodeCursor(cursor) {
239
252
  try {
253
+ if (!isValidBase64UrlCursor(cursor))
254
+ return null;
240
255
  const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
256
+ if (!/^\d+$/u.test(decoded))
257
+ return null;
241
258
  const value = Number.parseInt(decoded, 10);
242
259
  if (!Number.isFinite(value) || value < 0)
243
260
  return null;
@@ -248,4 +265,21 @@ class TaskManager {
248
265
  }
249
266
  }
250
267
  }
268
+ function isValidBase64UrlCursor(cursor) {
269
+ if (!cursor)
270
+ return false;
271
+ if (cursor.length > MAX_CURSOR_LENGTH)
272
+ return false;
273
+ if (!/^[A-Za-z0-9_-]+={0,2}$/u.test(cursor))
274
+ return false;
275
+ const firstPaddingIndex = cursor.indexOf('=');
276
+ if (firstPaddingIndex !== -1) {
277
+ for (let i = firstPaddingIndex; i < cursor.length; i += 1) {
278
+ if (cursor[i] !== '=')
279
+ return false;
280
+ }
281
+ return cursor.length % 4 === 0;
282
+ }
283
+ return cursor.length % 4 !== 1;
284
+ }
251
285
  export const taskManager = new TaskManager();
package/dist/tools.d.ts CHANGED
@@ -47,6 +47,7 @@ export interface FetchPipelineOptions<T> {
47
47
  transform: (input: {
48
48
  buffer: Uint8Array;
49
49
  encoding: string;
50
+ truncated?: boolean;
50
51
  }, url: string) => T | Promise<T>;
51
52
  serialize?: (result: T) => string;
52
53
  deserialize?: (cached: string) => T | undefined;
@@ -116,6 +117,7 @@ interface SharedFetchOptions<T extends {
116
117
  readonly transform: (input: {
117
118
  buffer: Uint8Array;
118
119
  encoding: string;
120
+ truncated?: boolean;
119
121
  }, normalizedUrl: string) => T | Promise<T>;
120
122
  readonly serialize?: (result: T) => string;
121
123
  readonly deserialize?: (cached: string) => T | undefined;
package/dist/tools.js CHANGED
@@ -453,8 +453,8 @@ export async function executeFetchPipeline(options) {
453
453
  }
454
454
  }
455
455
  logDebug('Fetching URL', { url: resolvedUrl.normalizedUrl });
456
- const { buffer, encoding } = await fetchNormalizedUrlBuffer(resolvedUrl.normalizedUrl, withSignal(options.signal));
457
- const data = await options.transform({ buffer, encoding }, resolvedUrl.normalizedUrl);
456
+ const { buffer, encoding, truncated } = await fetchNormalizedUrlBuffer(resolvedUrl.normalizedUrl, withSignal(options.signal));
457
+ const data = await options.transform({ buffer, encoding, ...(truncated ? { truncated: true } : {}) }, resolvedUrl.normalizedUrl);
458
458
  if (cache.isEnabled()) {
459
459
  persistCache({
460
460
  cacheKey,
@@ -572,7 +572,8 @@ const markdownTransform = async (input, url, signal, skipNoiseRemoval) => {
572
572
  ...withSignal(signal),
573
573
  ...(skipNoiseRemoval ? { skipNoiseRemoval: true } : {}),
574
574
  });
575
- return { ...result, content: result.markdown };
575
+ const truncated = Boolean(result.truncated || input.truncated);
576
+ return { ...result, content: result.markdown, truncated };
576
577
  };
577
578
  function serializeMarkdownResult(result) {
578
579
  return JSON.stringify({
@@ -32,6 +32,7 @@ export interface ExtractedMetadata {
32
32
  description?: string;
33
33
  author?: string;
34
34
  image?: string;
35
+ favicon?: string;
35
36
  publishedAt?: string;
36
37
  modifiedAt?: string;
37
38
  }