@j0hanz/superfetch 2.0.0 → 2.1.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 +139 -46
- package/dist/cache.d.ts +42 -0
- package/dist/cache.js +565 -0
- package/dist/config/env-parsers.d.ts +1 -0
- package/dist/config/env-parsers.js +12 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +20 -8
- package/dist/config/types/content.d.ts +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +261 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +32 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +28 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.js +910 -0
- package/dist/http/auth.js +161 -2
- package/dist/http/base-middleware.d.ts +7 -0
- package/dist/http/base-middleware.js +143 -0
- package/dist/http/cors.d.ts +0 -5
- package/dist/http/cors.js +0 -6
- package/dist/http/download-routes.js +6 -2
- package/dist/http/error-handler.d.ts +2 -0
- package/dist/http/error-handler.js +55 -0
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/mcp-routes.d.ts +8 -2
- package/dist/http/mcp-routes.js +101 -8
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +5 -114
- package/dist/http/mcp-sessions.d.ts +41 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/rate-limit.js +2 -2
- package/dist/http/server-middleware.d.ts +6 -1
- package/dist/http/server-middleware.js +3 -117
- package/dist/http/server-shutdown.js +1 -1
- package/dist/http/server-tuning.d.ts +9 -0
- package/dist/http/server-tuning.js +45 -0
- package/dist/http/server.js +206 -9
- package/dist/http/session-cleanup.js +8 -5
- package/dist/http.d.ts +78 -0
- package/dist/http.js +1437 -0
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +94 -0
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +31 -30
- package/dist/observability.d.ts +16 -0
- package/dist/observability.js +78 -0
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +33 -33
- package/dist/server.js +21 -6
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache.d.ts +5 -4
- package/dist/services/cache.js +49 -45
- package/dist/services/context.d.ts +2 -0
- package/dist/services/context.js +3 -0
- package/dist/services/extractor.d.ts +1 -0
- package/dist/services/extractor.js +77 -40
- package/dist/services/fetcher/agents.js +1 -1
- package/dist/services/fetcher/dns-selection.js +1 -1
- package/dist/services/fetcher/interceptors.js +29 -60
- package/dist/services/fetcher/redirects.js +12 -4
- package/dist/services/fetcher/response.js +18 -8
- package/dist/services/fetcher.d.ts +23 -0
- package/dist/services/fetcher.js +553 -13
- package/dist/services/logger.js +4 -1
- package/dist/services/telemetry.d.ts +19 -0
- package/dist/services/telemetry.js +43 -0
- package/dist/services/transform-worker-pool.d.ts +10 -3
- package/dist/services/transform-worker-pool.js +213 -184
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -3
- package/dist/tools/handlers/fetch-single.shared.js +131 -2
- package/dist/tools/handlers/fetch-url.tool.d.ts +6 -0
- package/dist/tools/handlers/fetch-url.tool.js +56 -12
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +13 -1
- package/dist/tools/schemas.d.ts +2 -0
- package/dist/tools/schemas.js +8 -0
- package/dist/tools/utils/content-shaping.js +19 -4
- package/dist/tools/utils/content-transform-core.d.ts +5 -0
- package/dist/tools/utils/content-transform-core.js +180 -0
- package/dist/tools/utils/content-transform-workers.d.ts +1 -0
- package/dist/tools/utils/content-transform-workers.js +1 -0
- package/dist/tools/utils/content-transform.d.ts +2 -1
- package/dist/tools/utils/content-transform.js +37 -136
- package/dist/tools/utils/fetch-pipeline.js +47 -56
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +149 -0
- package/dist/tools.d.ts +104 -0
- package/dist/tools.js +421 -0
- package/dist/transform.d.ts +69 -0
- package/dist/transform.js +1509 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +5 -0
- package/dist/transformers/markdown.js +314 -0
- package/dist/transformers/markdown.transformer.js +2 -189
- package/dist/utils/cancellation.d.ts +1 -0
- package/dist/utils/cancellation.js +18 -0
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/filename-generator.js +14 -3
- package/dist/utils/host-normalizer.d.ts +1 -0
- package/dist/utils/host-normalizer.js +37 -0
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.js +12 -17
- package/dist/utils/url-redactor.d.ts +1 -0
- package/dist/utils/url-redactor.js +13 -0
- package/dist/utils/url-validator.js +35 -20
- package/dist/workers/transform-worker.js +82 -38
- package/package.json +13 -10
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { setInterval as setIntervalPromise } from 'node:timers/promises';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { config } from '../config/index.js';
|
|
6
|
+
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
7
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
8
|
+
import { createMcpServer } from '../server.js';
|
|
9
|
+
export function sendJsonRpcError(res, code, message, status = 400, id = null) {
|
|
10
|
+
res.status(status).json({
|
|
11
|
+
jsonrpc: '2.0',
|
|
12
|
+
error: {
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
},
|
|
16
|
+
id,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function getSessionId(req) {
|
|
20
|
+
const header = req.headers['mcp-session-id'];
|
|
21
|
+
return Array.isArray(header) ? header[0] : header;
|
|
22
|
+
}
|
|
23
|
+
export function createSessionStore(sessionTtlMs) {
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
return {
|
|
26
|
+
get: (sessionId) => sessions.get(sessionId),
|
|
27
|
+
touch: (sessionId) => {
|
|
28
|
+
touchSession(sessions, sessionId);
|
|
29
|
+
},
|
|
30
|
+
set: (sessionId, entry) => {
|
|
31
|
+
sessions.set(sessionId, entry);
|
|
32
|
+
},
|
|
33
|
+
remove: (sessionId) => removeSession(sessions, sessionId),
|
|
34
|
+
size: () => sessions.size,
|
|
35
|
+
clear: () => clearSessions(sessions),
|
|
36
|
+
evictExpired: () => evictExpiredSessions(sessions, sessionTtlMs),
|
|
37
|
+
evictOldest: () => evictOldestSession(sessions),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function touchSession(sessions, sessionId) {
|
|
41
|
+
const session = sessions.get(sessionId);
|
|
42
|
+
if (session) {
|
|
43
|
+
session.lastSeen = Date.now();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function removeSession(sessions, sessionId) {
|
|
47
|
+
const session = sessions.get(sessionId);
|
|
48
|
+
sessions.delete(sessionId);
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
function clearSessions(sessions) {
|
|
52
|
+
const entries = Array.from(sessions.values());
|
|
53
|
+
sessions.clear();
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
function evictExpiredSessions(sessions, sessionTtlMs) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const evicted = [];
|
|
59
|
+
for (const [id, session] of sessions.entries()) {
|
|
60
|
+
if (now - session.lastSeen > sessionTtlMs) {
|
|
61
|
+
sessions.delete(id);
|
|
62
|
+
evicted.push(session);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return evicted;
|
|
66
|
+
}
|
|
67
|
+
function evictOldestSession(sessions) {
|
|
68
|
+
let oldestId;
|
|
69
|
+
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
70
|
+
for (const [id, session] of sessions.entries()) {
|
|
71
|
+
if (session.lastSeen < oldestSeen) {
|
|
72
|
+
oldestSeen = session.lastSeen;
|
|
73
|
+
oldestId = id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!oldestId)
|
|
77
|
+
return undefined;
|
|
78
|
+
const session = sessions.get(oldestId);
|
|
79
|
+
sessions.delete(oldestId);
|
|
80
|
+
return session;
|
|
81
|
+
}
|
|
82
|
+
let inFlightSessions = 0;
|
|
83
|
+
export function reserveSessionSlot(store, maxSessions) {
|
|
84
|
+
if (store.size() + inFlightSessions >= maxSessions) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
inFlightSessions += 1;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function releaseSessionSlot() {
|
|
91
|
+
if (inFlightSessions > 0) {
|
|
92
|
+
inFlightSessions -= 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function createSlotTracker() {
|
|
96
|
+
let slotReleased = false;
|
|
97
|
+
let initialized = false;
|
|
98
|
+
return {
|
|
99
|
+
releaseSlot: () => {
|
|
100
|
+
if (slotReleased)
|
|
101
|
+
return;
|
|
102
|
+
slotReleased = true;
|
|
103
|
+
releaseSessionSlot();
|
|
104
|
+
},
|
|
105
|
+
markInitialized: () => {
|
|
106
|
+
initialized = true;
|
|
107
|
+
},
|
|
108
|
+
isInitialized: () => initialized,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function isServerAtCapacity(store, maxSessions) {
|
|
112
|
+
return store.size() + inFlightSessions >= maxSessions;
|
|
113
|
+
}
|
|
114
|
+
function tryEvictSlot(store, maxSessions, evictOldest) {
|
|
115
|
+
const currentSize = store.size();
|
|
116
|
+
const canFreeSlot = currentSize >= maxSessions &&
|
|
117
|
+
currentSize - 1 + inFlightSessions < maxSessions;
|
|
118
|
+
return canFreeSlot && evictOldest(store);
|
|
119
|
+
}
|
|
120
|
+
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }) {
|
|
121
|
+
if (!isServerAtCapacity(store, maxSessions)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (tryEvictSlot(store, maxSessions, evictOldest)) {
|
|
125
|
+
return !isServerAtCapacity(store, maxSessions);
|
|
126
|
+
}
|
|
127
|
+
respondServerBusy(res);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
function respondServerBusy(res) {
|
|
131
|
+
sendJsonRpcError(res, -32000, 'Server busy: maximum sessions reached', 503, null);
|
|
132
|
+
}
|
|
133
|
+
function respondBadRequest(res, id) {
|
|
134
|
+
sendJsonRpcError(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400, id);
|
|
135
|
+
}
|
|
136
|
+
function createTimeoutController() {
|
|
137
|
+
let initTimeout = null;
|
|
138
|
+
return {
|
|
139
|
+
clear: () => {
|
|
140
|
+
if (!initTimeout)
|
|
141
|
+
return;
|
|
142
|
+
clearTimeout(initTimeout);
|
|
143
|
+
initTimeout = null;
|
|
144
|
+
},
|
|
145
|
+
set: (timeout) => {
|
|
146
|
+
initTimeout = timeout;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function createTransportAdapter(transport) {
|
|
151
|
+
const adapter = buildTransportAdapter(transport);
|
|
152
|
+
attachTransportAccessors(adapter, transport);
|
|
153
|
+
return adapter;
|
|
154
|
+
}
|
|
155
|
+
function buildTransportAdapter(transport) {
|
|
156
|
+
return {
|
|
157
|
+
start: () => transport.start(),
|
|
158
|
+
send: (message, options) => transport.send(message, options),
|
|
159
|
+
close: () => transport.close(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createAccessorDescriptor(getter, setter) {
|
|
163
|
+
return {
|
|
164
|
+
get: getter,
|
|
165
|
+
...(setter ? { set: setter } : {}),
|
|
166
|
+
enumerable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createOnCloseDescriptor(transport) {
|
|
171
|
+
return createAccessorDescriptor(() => transport.onclose, (handler) => {
|
|
172
|
+
transport.onclose = handler;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function createOnErrorDescriptor(transport) {
|
|
176
|
+
return createAccessorDescriptor(() => transport.onerror, (handler) => {
|
|
177
|
+
transport.onerror = handler;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function createOnMessageDescriptor(transport) {
|
|
181
|
+
return createAccessorDescriptor(() => transport.onmessage, (handler) => {
|
|
182
|
+
transport.onmessage = handler;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function attachTransportAccessors(adapter, transport) {
|
|
186
|
+
Object.defineProperties(adapter, {
|
|
187
|
+
onclose: createOnCloseDescriptor(transport),
|
|
188
|
+
onerror: createOnErrorDescriptor(transport),
|
|
189
|
+
onmessage: createOnMessageDescriptor(transport),
|
|
190
|
+
sessionId: createAccessorDescriptor(() => transport.sessionId),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function startSessionInitTimeout({ transport, tracker, clearInitTimeout, timeoutMs, }) {
|
|
194
|
+
if (timeoutMs <= 0)
|
|
195
|
+
return null;
|
|
196
|
+
const timeout = setTimeout(() => {
|
|
197
|
+
clearInitTimeout();
|
|
198
|
+
if (tracker.isInitialized())
|
|
199
|
+
return;
|
|
200
|
+
tracker.releaseSlot();
|
|
201
|
+
void transport.close().catch((error) => {
|
|
202
|
+
logWarn('Failed to close stalled session', {
|
|
203
|
+
error: getErrorMessage(error),
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
logWarn('Session initialization timed out', { timeoutMs });
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
timeout.unref();
|
|
209
|
+
return timeout;
|
|
210
|
+
}
|
|
211
|
+
function createSessionTransport({ tracker, timeoutController, }) {
|
|
212
|
+
const transport = new StreamableHTTPServerTransport({
|
|
213
|
+
sessionIdGenerator: () => randomUUID(),
|
|
214
|
+
});
|
|
215
|
+
transport.onclose = () => {
|
|
216
|
+
timeoutController.clear();
|
|
217
|
+
if (!tracker.isInitialized()) {
|
|
218
|
+
tracker.releaseSlot();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
timeoutController.set(startSessionInitTimeout({
|
|
222
|
+
transport,
|
|
223
|
+
tracker,
|
|
224
|
+
clearInitTimeout: timeoutController.clear,
|
|
225
|
+
timeoutMs: config.server.sessionInitTimeoutMs,
|
|
226
|
+
}));
|
|
227
|
+
return transport;
|
|
228
|
+
}
|
|
229
|
+
async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlot, }) {
|
|
230
|
+
const mcpServer = createMcpServer();
|
|
231
|
+
const transportAdapter = createTransportAdapter(transport);
|
|
232
|
+
try {
|
|
233
|
+
await mcpServer.connect(transportAdapter);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
clearInitTimeout();
|
|
237
|
+
releaseSlot();
|
|
238
|
+
void transport.close().catch((closeError) => {
|
|
239
|
+
logWarn('Failed to close transport after connect error', {
|
|
240
|
+
error: getErrorMessage(closeError),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function evictExpiredSessionsWithClose(store) {
|
|
248
|
+
const evicted = store.evictExpired();
|
|
249
|
+
for (const session of evicted) {
|
|
250
|
+
void session.transport.close().catch((error) => {
|
|
251
|
+
logWarn('Failed to close expired session', {
|
|
252
|
+
error: getErrorMessage(error),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return evicted.length;
|
|
257
|
+
}
|
|
258
|
+
function evictOldestSessionWithClose(store) {
|
|
259
|
+
const session = store.evictOldest();
|
|
260
|
+
if (!session)
|
|
261
|
+
return false;
|
|
262
|
+
void session.transport.close().catch((error) => {
|
|
263
|
+
logWarn('Failed to close evicted session', {
|
|
264
|
+
error: getErrorMessage(error),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
function reserveSessionIfPossible({ options, res, }) {
|
|
270
|
+
if (!ensureSessionCapacity({
|
|
271
|
+
store: options.sessionStore,
|
|
272
|
+
maxSessions: options.maxSessions,
|
|
273
|
+
res,
|
|
274
|
+
evictOldest: evictOldestSessionWithClose,
|
|
275
|
+
})) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
279
|
+
respondServerBusy(res);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
function resolveSessionId({ transport, res, tracker, clearInitTimeout, }) {
|
|
285
|
+
const { sessionId } = transport;
|
|
286
|
+
if (typeof sessionId !== 'string') {
|
|
287
|
+
clearInitTimeout();
|
|
288
|
+
tracker.releaseSlot();
|
|
289
|
+
respondBadRequest(res, null);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return sessionId;
|
|
293
|
+
}
|
|
294
|
+
function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeout, }) {
|
|
295
|
+
clearInitTimeout();
|
|
296
|
+
tracker.markInitialized();
|
|
297
|
+
tracker.releaseSlot();
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
store.set(sessionId, {
|
|
300
|
+
transport,
|
|
301
|
+
createdAt: now,
|
|
302
|
+
lastSeen: now,
|
|
303
|
+
});
|
|
304
|
+
transport.onclose = () => {
|
|
305
|
+
store.remove(sessionId);
|
|
306
|
+
logInfo('Session closed');
|
|
307
|
+
};
|
|
308
|
+
logInfo('Session initialized');
|
|
309
|
+
}
|
|
310
|
+
async function createAndConnectTransport({ options, res, }) {
|
|
311
|
+
if (!reserveSessionIfPossible({ options, res }))
|
|
312
|
+
return null;
|
|
313
|
+
const tracker = createSlotTracker();
|
|
314
|
+
const timeoutController = createTimeoutController();
|
|
315
|
+
const transport = createSessionTransport({ tracker, timeoutController });
|
|
316
|
+
await connectTransportOrThrow({
|
|
317
|
+
transport,
|
|
318
|
+
clearInitTimeout: timeoutController.clear,
|
|
319
|
+
releaseSlot: tracker.releaseSlot,
|
|
320
|
+
});
|
|
321
|
+
const sessionId = resolveSessionId({
|
|
322
|
+
transport,
|
|
323
|
+
res,
|
|
324
|
+
tracker,
|
|
325
|
+
clearInitTimeout: timeoutController.clear,
|
|
326
|
+
});
|
|
327
|
+
if (!sessionId)
|
|
328
|
+
return null;
|
|
329
|
+
finalizeSession({
|
|
330
|
+
store: options.sessionStore,
|
|
331
|
+
transport,
|
|
332
|
+
sessionId,
|
|
333
|
+
tracker,
|
|
334
|
+
clearInitTimeout: timeoutController.clear,
|
|
335
|
+
});
|
|
336
|
+
return transport;
|
|
337
|
+
}
|
|
338
|
+
export async function resolveTransportForPost({ res, body, sessionId, options, }) {
|
|
339
|
+
if (sessionId) {
|
|
340
|
+
const existingSession = options.sessionStore.get(sessionId);
|
|
341
|
+
if (existingSession) {
|
|
342
|
+
options.sessionStore.touch(sessionId);
|
|
343
|
+
return existingSession.transport;
|
|
344
|
+
}
|
|
345
|
+
// Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
|
|
346
|
+
sendJsonRpcError(res, -32600, 'Session not found', 404, body.id ?? null);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
if (!isInitializeRequest(body)) {
|
|
350
|
+
respondBadRequest(res, body.id ?? null);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
evictExpiredSessionsWithClose(options.sessionStore);
|
|
354
|
+
return createAndConnectTransport({ options, res });
|
|
355
|
+
}
|
|
356
|
+
export function startSessionCleanupLoop(store, sessionTtlMs) {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
void runSessionCleanupLoop(store, sessionTtlMs, controller.signal).catch(handleSessionCleanupError);
|
|
359
|
+
return controller;
|
|
360
|
+
}
|
|
361
|
+
async function runSessionCleanupLoop(store, sessionTtlMs, signal) {
|
|
362
|
+
const intervalMs = getCleanupIntervalMs(sessionTtlMs);
|
|
363
|
+
for await (const getNow of setIntervalPromise(intervalMs, Date.now, {
|
|
364
|
+
signal,
|
|
365
|
+
ref: false,
|
|
366
|
+
})) {
|
|
367
|
+
handleSessionEvictions(store, getNow());
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function getCleanupIntervalMs(sessionTtlMs) {
|
|
371
|
+
return Math.min(Math.max(Math.floor(sessionTtlMs / 2), 10000), 60000);
|
|
372
|
+
}
|
|
373
|
+
function isAbortError(error) {
|
|
374
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
375
|
+
}
|
|
376
|
+
function handleSessionEvictions(store, now) {
|
|
377
|
+
const evicted = evictExpiredSessionsWithClose(store);
|
|
378
|
+
if (evicted > 0) {
|
|
379
|
+
logInfo('Expired sessions evicted', {
|
|
380
|
+
evicted,
|
|
381
|
+
timestamp: new Date(now).toISOString(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function handleSessionCleanupError(error) {
|
|
386
|
+
if (isAbortError(error)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
logWarn('Session cleanup loop failed', {
|
|
390
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
391
|
+
});
|
|
392
|
+
}
|
package/dist/http/rate-limit.js
CHANGED
|
@@ -36,8 +36,8 @@ function createRateLimitHandler(store, options) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
async function startCleanupLoop(store, options, signal) {
|
|
39
|
-
for await (const
|
|
40
|
-
evictStaleEntries(store, options,
|
|
39
|
+
for await (const getNow of setIntervalPromise(options.cleanupIntervalMs, Date.now, { signal, ref: false })) {
|
|
40
|
+
evictStaleEntries(store, options, getNow());
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
function evictStaleEntries(store, options, now) {
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { Express, RequestHandler } from 'express';
|
|
2
|
-
export declare function attachBaseMiddleware(
|
|
2
|
+
export declare function attachBaseMiddleware(options: {
|
|
3
|
+
app: Express;
|
|
4
|
+
jsonParser: RequestHandler;
|
|
5
|
+
rateLimitMiddleware: RequestHandler;
|
|
6
|
+
corsMiddleware: RequestHandler;
|
|
7
|
+
}): void;
|
|
@@ -1,123 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
3
|
import { runWithRequestContext } from '../services/context.js';
|
|
4
|
+
import { createHostValidationMiddleware, createOriginValidationMiddleware, } from './host-allowlist.js';
|
|
4
5
|
import { getSessionId } from './sessions.js';
|
|
5
|
-
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
6
|
-
function getNonEmptyStringHeader(value) {
|
|
7
|
-
if (typeof value !== 'string')
|
|
8
|
-
return null;
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
return trimmed === '' ? null : trimmed;
|
|
11
|
-
}
|
|
12
|
-
function respondHostNotAllowed(res) {
|
|
13
|
-
res.status(403).json({
|
|
14
|
-
error: 'Host not allowed',
|
|
15
|
-
code: 'HOST_NOT_ALLOWED',
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
function respondOriginNotAllowed(res) {
|
|
19
|
-
res.status(403).json({
|
|
20
|
-
error: 'Origin not allowed',
|
|
21
|
-
code: 'ORIGIN_NOT_ALLOWED',
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function tryParseOriginHostname(originHeader) {
|
|
25
|
-
try {
|
|
26
|
-
return new URL(originHeader).hostname.toLowerCase();
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function takeFirstHostValue(value) {
|
|
33
|
-
const first = value.split(',')[0];
|
|
34
|
-
if (!first)
|
|
35
|
-
return null;
|
|
36
|
-
const trimmed = first.trim();
|
|
37
|
-
return trimmed ? trimmed : null;
|
|
38
|
-
}
|
|
39
|
-
function stripIpv6Brackets(value) {
|
|
40
|
-
if (!value.startsWith('['))
|
|
41
|
-
return null;
|
|
42
|
-
const end = value.indexOf(']');
|
|
43
|
-
if (end === -1)
|
|
44
|
-
return null;
|
|
45
|
-
return value.slice(1, end);
|
|
46
|
-
}
|
|
47
|
-
function stripPortIfPresent(value) {
|
|
48
|
-
const colonIndex = value.indexOf(':');
|
|
49
|
-
if (colonIndex === -1)
|
|
50
|
-
return value;
|
|
51
|
-
return value.slice(0, colonIndex);
|
|
52
|
-
}
|
|
53
|
-
function normalizeHost(value) {
|
|
54
|
-
const trimmed = value.trim().toLowerCase();
|
|
55
|
-
if (!trimmed)
|
|
56
|
-
return null;
|
|
57
|
-
const first = takeFirstHostValue(trimmed);
|
|
58
|
-
if (!first)
|
|
59
|
-
return null;
|
|
60
|
-
const ipv6 = stripIpv6Brackets(first);
|
|
61
|
-
if (ipv6)
|
|
62
|
-
return ipv6;
|
|
63
|
-
return stripPortIfPresent(first);
|
|
64
|
-
}
|
|
65
|
-
function isWildcardHost(host) {
|
|
66
|
-
return host === '0.0.0.0' || host === '::';
|
|
67
|
-
}
|
|
68
|
-
function addLoopbackHosts(allowedHosts) {
|
|
69
|
-
for (const host of LOOPBACK_HOSTS) {
|
|
70
|
-
allowedHosts.add(host);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function addConfiguredHost(allowedHosts) {
|
|
74
|
-
const configuredHost = normalizeHost(config.server.host);
|
|
75
|
-
if (!configuredHost)
|
|
76
|
-
return;
|
|
77
|
-
if (isWildcardHost(configuredHost))
|
|
78
|
-
return;
|
|
79
|
-
allowedHosts.add(configuredHost);
|
|
80
|
-
}
|
|
81
|
-
function addExplicitAllowedHosts(allowedHosts) {
|
|
82
|
-
for (const host of config.security.allowedHosts) {
|
|
83
|
-
allowedHosts.add(host);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function buildAllowedHosts() {
|
|
87
|
-
const allowedHosts = new Set();
|
|
88
|
-
addLoopbackHosts(allowedHosts);
|
|
89
|
-
addConfiguredHost(allowedHosts);
|
|
90
|
-
addExplicitAllowedHosts(allowedHosts);
|
|
91
|
-
return allowedHosts;
|
|
92
|
-
}
|
|
93
|
-
function createHostValidationMiddleware() {
|
|
94
|
-
const allowedHosts = buildAllowedHosts();
|
|
95
|
-
return (req, res, next) => {
|
|
96
|
-
const hostHeader = typeof req.headers.host === 'string' ? req.headers.host : '';
|
|
97
|
-
const normalized = normalizeHost(hostHeader);
|
|
98
|
-
if (!normalized || !allowedHosts.has(normalized)) {
|
|
99
|
-
respondHostNotAllowed(res);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
next();
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function createOriginValidationMiddleware() {
|
|
106
|
-
const allowedHosts = buildAllowedHosts();
|
|
107
|
-
return (req, res, next) => {
|
|
108
|
-
const originHeader = getNonEmptyStringHeader(req.headers.origin);
|
|
109
|
-
if (!originHeader) {
|
|
110
|
-
next();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const originHostname = tryParseOriginHostname(originHeader);
|
|
114
|
-
if (!originHostname || !allowedHosts.has(originHostname)) {
|
|
115
|
-
respondOriginNotAllowed(res);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
next();
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
6
|
function createJsonParseErrorHandler() {
|
|
122
7
|
return (err, _req, res, next) => {
|
|
123
8
|
if (err instanceof SyntaxError && 'body' in err) {
|
|
@@ -154,7 +39,8 @@ function registerHealthRoute(app) {
|
|
|
154
39
|
});
|
|
155
40
|
});
|
|
156
41
|
}
|
|
157
|
-
export function attachBaseMiddleware(
|
|
42
|
+
export function attachBaseMiddleware(options) {
|
|
43
|
+
const { app, jsonParser, rateLimitMiddleware, corsMiddleware } = options;
|
|
158
44
|
app.use(createHostValidationMiddleware());
|
|
159
45
|
app.use(createOriginValidationMiddleware());
|
|
160
46
|
app.use(jsonParser);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { destroyAgents } from '../services/fetcher/agents.js';
|
|
2
2
|
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
3
|
-
import { getErrorMessage } from '../utils/error-
|
|
3
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
4
4
|
export function createShutdownHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
|
|
5
5
|
return (signal) => shutdownServer(signal, server, sessionStore, sessionCleanupController, stopRateLimitCleanup);
|
|
6
6
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface HttpServerTuningTarget {
|
|
2
|
+
headersTimeout?: number;
|
|
3
|
+
requestTimeout?: number;
|
|
4
|
+
keepAliveTimeout?: number;
|
|
5
|
+
closeIdleConnections?: () => void;
|
|
6
|
+
closeAllConnections?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function applyHttpServerTuning(server: HttpServerTuningTarget): void;
|
|
9
|
+
export declare function drainConnectionsOnShutdown(server: HttpServerTuningTarget): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { config } from '../config/index.js';
|
|
2
|
+
import { logDebug } from '../services/logger.js';
|
|
3
|
+
export function applyHttpServerTuning(server) {
|
|
4
|
+
const { headersTimeoutMs, requestTimeoutMs, keepAliveTimeoutMs } = config.server.http;
|
|
5
|
+
if (headersTimeoutMs !== undefined) {
|
|
6
|
+
server.headersTimeout = headersTimeoutMs;
|
|
7
|
+
}
|
|
8
|
+
if (requestTimeoutMs !== undefined) {
|
|
9
|
+
server.requestTimeout = requestTimeoutMs;
|
|
10
|
+
}
|
|
11
|
+
if (keepAliveTimeoutMs !== undefined) {
|
|
12
|
+
server.keepAliveTimeout = keepAliveTimeoutMs;
|
|
13
|
+
}
|
|
14
|
+
if (headersTimeoutMs !== undefined ||
|
|
15
|
+
requestTimeoutMs !== undefined ||
|
|
16
|
+
keepAliveTimeoutMs !== undefined) {
|
|
17
|
+
logDebug('Applied HTTP server tuning', {
|
|
18
|
+
headersTimeoutMs,
|
|
19
|
+
requestTimeoutMs,
|
|
20
|
+
keepAliveTimeoutMs,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function drainConnectionsOnShutdown(server) {
|
|
25
|
+
const { shutdownCloseAllConnections, shutdownCloseIdleConnections } = config.server.http;
|
|
26
|
+
if (shutdownCloseAllConnections) {
|
|
27
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
28
|
+
server.closeAllConnections();
|
|
29
|
+
logDebug('Closed all HTTP connections during shutdown');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
logDebug('HTTP server does not support closeAllConnections()');
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (shutdownCloseIdleConnections) {
|
|
37
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
38
|
+
server.closeIdleConnections();
|
|
39
|
+
logDebug('Closed idle HTTP connections during shutdown');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
logDebug('HTTP server does not support closeIdleConnections()');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|