@j0hanz/superfetch 1.2.4 → 2.0.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.
Files changed (116) hide show
  1. package/README.md +116 -152
  2. package/dist/config/auth-config.d.ts +16 -0
  3. package/dist/config/auth-config.js +53 -0
  4. package/dist/config/constants.d.ts +11 -13
  5. package/dist/config/constants.js +1 -3
  6. package/dist/config/env-parsers.d.ts +7 -0
  7. package/dist/config/env-parsers.js +84 -0
  8. package/dist/config/formatting.d.ts +2 -2
  9. package/dist/config/index.d.ts +47 -53
  10. package/dist/config/index.js +25 -59
  11. package/dist/config/types/content.d.ts +1 -49
  12. package/dist/config/types/runtime.d.ts +8 -16
  13. package/dist/config/types/tools.d.ts +2 -28
  14. package/dist/http/accept-policy.d.ts +3 -0
  15. package/dist/http/accept-policy.js +45 -0
  16. package/dist/http/async-handler.d.ts +2 -0
  17. package/dist/http/async-handler.js +5 -0
  18. package/dist/http/auth-introspection.d.ts +2 -0
  19. package/dist/http/auth-introspection.js +141 -0
  20. package/dist/http/auth-static.d.ts +2 -0
  21. package/dist/http/auth-static.js +23 -0
  22. package/dist/http/auth.d.ts +3 -2
  23. package/dist/http/auth.js +98 -26
  24. package/dist/http/cors.d.ts +6 -6
  25. package/dist/http/cors.js +7 -42
  26. package/dist/http/download-routes.d.ts +0 -12
  27. package/dist/http/download-routes.js +21 -58
  28. package/dist/http/jsonrpc-http.d.ts +2 -0
  29. package/dist/http/jsonrpc-http.js +10 -0
  30. package/dist/http/mcp-routes.d.ts +0 -1
  31. package/dist/http/mcp-routes.js +43 -30
  32. package/dist/http/mcp-session-helpers.d.ts +0 -1
  33. package/dist/http/mcp-session-helpers.js +1 -1
  34. package/dist/http/mcp-session-transport.d.ts +7 -0
  35. package/dist/http/mcp-session-transport.js +57 -0
  36. package/dist/http/mcp-session.js +60 -73
  37. package/dist/http/mcp-validation.d.ts +1 -0
  38. package/dist/http/mcp-validation.js +11 -10
  39. package/dist/http/protocol-policy.d.ts +2 -0
  40. package/dist/http/protocol-policy.js +31 -0
  41. package/dist/http/rate-limit.js +5 -2
  42. package/dist/http/server-config.d.ts +1 -0
  43. package/dist/http/server-config.js +40 -0
  44. package/dist/http/server-middleware.d.ts +2 -9
  45. package/dist/http/server-middleware.js +96 -43
  46. package/dist/http/server-shutdown.d.ts +4 -0
  47. package/dist/http/server-shutdown.js +43 -0
  48. package/dist/http/server.js +52 -64
  49. package/dist/http/session-cleanup.js +1 -1
  50. package/dist/middleware/error-handler.js +1 -3
  51. package/dist/resources/cached-content.js +50 -108
  52. package/dist/resources/index.js +0 -82
  53. package/dist/server.js +51 -30
  54. package/dist/services/cache-keys.d.ts +7 -0
  55. package/dist/services/cache-keys.js +57 -0
  56. package/dist/services/cache.d.ts +1 -7
  57. package/dist/services/cache.js +53 -119
  58. package/dist/services/context.d.ts +0 -1
  59. package/dist/services/context.js +0 -7
  60. package/dist/services/extractor.js +10 -82
  61. package/dist/services/fetcher/agents.d.ts +2 -2
  62. package/dist/services/fetcher/agents.js +34 -95
  63. package/dist/services/fetcher/dns-selection.d.ts +2 -0
  64. package/dist/services/fetcher/dns-selection.js +72 -0
  65. package/dist/services/fetcher/interceptors.d.ts +0 -22
  66. package/dist/services/fetcher/interceptors.js +30 -13
  67. package/dist/services/fetcher/redirects.js +4 -3
  68. package/dist/services/fetcher/response.js +66 -31
  69. package/dist/services/fetcher.d.ts +1 -3
  70. package/dist/services/fetcher.js +14 -33
  71. package/dist/services/fifo-queue.d.ts +8 -0
  72. package/dist/services/fifo-queue.js +25 -0
  73. package/dist/services/logger.js +2 -2
  74. package/dist/services/metadata-collector.d.ts +1 -9
  75. package/dist/services/metadata-collector.js +71 -2
  76. package/dist/services/transform-worker-pool.d.ts +4 -14
  77. package/dist/services/transform-worker-pool.js +177 -129
  78. package/dist/services/transform-worker-types.d.ts +32 -0
  79. package/dist/services/transform-worker-types.js +14 -0
  80. package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
  81. package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
  82. package/dist/tools/handlers/fetch-single.shared.d.ts +1 -20
  83. package/dist/tools/handlers/fetch-single.shared.js +44 -87
  84. package/dist/tools/handlers/fetch-url.tool.d.ts +1 -1
  85. package/dist/tools/handlers/fetch-url.tool.js +46 -123
  86. package/dist/tools/index.js +21 -40
  87. package/dist/tools/schemas.d.ts +1 -51
  88. package/dist/tools/schemas.js +2 -108
  89. package/dist/tools/utils/cached-markdown.d.ts +5 -0
  90. package/dist/tools/utils/cached-markdown.js +46 -0
  91. package/dist/tools/utils/content-shaping.d.ts +4 -0
  92. package/dist/tools/utils/content-shaping.js +52 -0
  93. package/dist/tools/utils/content-transform.d.ts +2 -17
  94. package/dist/tools/utils/content-transform.js +120 -114
  95. package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
  96. package/dist/tools/utils/fetch-pipeline.js +65 -62
  97. package/dist/tools/utils/inline-content.d.ts +1 -2
  98. package/dist/tools/utils/inline-content.js +4 -7
  99. package/dist/transformers/markdown.transformer.js +109 -34
  100. package/dist/utils/cached-payload.d.ts +7 -0
  101. package/dist/utils/cached-payload.js +36 -0
  102. package/dist/utils/error-utils.js +1 -1
  103. package/dist/utils/filename-generator.js +21 -10
  104. package/dist/utils/guards.d.ts +1 -0
  105. package/dist/utils/guards.js +3 -0
  106. package/dist/utils/header-normalizer.d.ts +0 -3
  107. package/dist/utils/header-normalizer.js +3 -3
  108. package/dist/utils/tool-error-handler.d.ts +2 -2
  109. package/dist/utils/tool-error-handler.js +11 -38
  110. package/dist/utils/url-transformer.d.ts +7 -0
  111. package/dist/utils/url-transformer.js +147 -0
  112. package/dist/utils/url-validator.d.ts +1 -2
  113. package/dist/utils/url-validator.js +20 -93
  114. package/dist/workers/content-transform.worker.d.ts +1 -0
  115. package/dist/workers/content-transform.worker.js +40 -0
  116. package/package.json +13 -16
@@ -5,7 +5,9 @@ import { config } from '../config/index.js';
5
5
  import { logError, logInfo, logWarn } from '../services/logger.js';
6
6
  import { getErrorMessage } from '../utils/error-utils.js';
7
7
  import { createMcpServer } from '../server.js';
8
+ import { sendJsonRpcError } from './jsonrpc-http.js';
8
9
  import { createSlotTracker, ensureSessionCapacity, reserveSessionSlot, respondBadRequest, respondServerBusy, } from './mcp-session-helpers.js';
10
+ import { createTimeoutController, createTransportAdapter, } from './mcp-session-transport.js';
9
11
  import {} from './sessions.js';
10
12
  function startSessionInitTimeout(transport, tracker, clearInitTimeout, timeoutMs) {
11
13
  if (timeoutMs <= 0)
@@ -25,68 +27,23 @@ function startSessionInitTimeout(transport, tracker, clearInitTimeout, timeoutMs
25
27
  timeout.unref();
26
28
  return timeout;
27
29
  }
28
- function handleSessionInitialized(id, transport, options, tracker, clearInitTimeout) {
29
- clearInitTimeout();
30
- tracker.markInitialized();
31
- tracker.releaseSlot();
32
- const now = Date.now();
33
- options.sessionStore.set(id, {
34
- transport,
35
- createdAt: now,
36
- lastSeen: now,
37
- });
38
- logInfo('Session initialized', { sessionId: id });
39
- }
40
- function handleSessionClosed(id, options) {
41
- options.sessionStore.remove(id);
42
- logInfo('Session closed', { sessionId: id });
43
- }
44
- function handleTransportClose(transport, options, tracker, clearInitTimeout) {
45
- clearInitTimeout();
46
- if (!tracker.isInitialized()) {
47
- tracker.releaseSlot();
48
- }
49
- if (transport.sessionId) {
50
- options.sessionStore.remove(transport.sessionId);
51
- }
52
- }
53
- function createTransportForNewSession(options) {
54
- const tracker = createSlotTracker();
55
- let initTimeout = null;
56
- const clearInitTimeout = () => {
57
- if (!initTimeout)
58
- return;
59
- clearTimeout(initTimeout);
60
- initTimeout = null;
61
- };
62
- const transport = new StreamableHTTPServerTransport({
63
- sessionIdGenerator: () => randomUUID(),
64
- onsessioninitialized: (id) => {
65
- handleSessionInitialized(id, transport, options, tracker, clearInitTimeout);
66
- },
67
- onsessionclosed: (id) => {
68
- handleSessionClosed(id, options);
69
- },
70
- });
71
- transport.onclose = () => {
72
- handleTransportClose(transport, options, tracker, clearInitTimeout);
73
- };
74
- initTimeout = startSessionInitTimeout(transport, tracker, clearInitTimeout, config.server.sessionInitTimeoutMs);
75
- return { transport, releaseSlot: tracker.releaseSlot, clearInitTimeout };
76
- }
77
- function findExistingTransport(sessionId, options) {
78
- if (!sessionId) {
79
- return null;
30
+ async function connectTransportOrThrow(transport, clearInitTimeout, releaseSlot) {
31
+ const mcpServer = createMcpServer();
32
+ const transportAdapter = createTransportAdapter(transport);
33
+ try {
34
+ await mcpServer.connect(transportAdapter);
80
35
  }
81
- const existingSession = options.sessionStore.get(sessionId);
82
- if (!existingSession) {
83
- return null;
36
+ catch (error) {
37
+ clearInitTimeout();
38
+ releaseSlot();
39
+ void transport.close().catch((closeError) => {
40
+ logWarn('Failed to close transport after connect error', {
41
+ error: getErrorMessage(closeError),
42
+ });
43
+ });
44
+ logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
45
+ throw error;
84
46
  }
85
- options.sessionStore.touch(sessionId);
86
- return existingSession.transport;
87
- }
88
- function shouldInitializeSession(sessionId, body) {
89
- return !sessionId && isInitializeRequest(body);
90
47
  }
91
48
  async function createAndConnectTransport(options, res) {
92
49
  if (!ensureSessionCapacity(options.sessionStore, options.maxSessions, res, evictOldestSession)) {
@@ -96,25 +53,55 @@ async function createAndConnectTransport(options, res) {
96
53
  respondServerBusy(res);
97
54
  return null;
98
55
  }
99
- const { transport, releaseSlot, clearInitTimeout } = createTransportForNewSession(options);
100
- const mcpServer = createMcpServer();
101
- try {
102
- await mcpServer.connect(transport);
103
- }
104
- catch (error) {
56
+ const tracker = createSlotTracker();
57
+ const timeoutController = createTimeoutController();
58
+ const { clear: clearInitTimeout } = timeoutController;
59
+ const transport = new StreamableHTTPServerTransport({
60
+ sessionIdGenerator: () => randomUUID(),
61
+ });
62
+ transport.onclose = () => {
105
63
  clearInitTimeout();
106
- releaseSlot();
107
- logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
108
- throw error;
64
+ if (!tracker.isInitialized()) {
65
+ tracker.releaseSlot();
66
+ }
67
+ };
68
+ timeoutController.set(startSessionInitTimeout(transport, tracker, clearInitTimeout, config.server.sessionInitTimeoutMs));
69
+ await connectTransportOrThrow(transport, clearInitTimeout, tracker.releaseSlot);
70
+ const { sessionId } = transport;
71
+ if (typeof sessionId !== 'string') {
72
+ clearInitTimeout();
73
+ tracker.releaseSlot();
74
+ respondBadRequest(res);
75
+ return null;
109
76
  }
77
+ clearInitTimeout();
78
+ tracker.markInitialized();
79
+ tracker.releaseSlot();
80
+ const now = Date.now();
81
+ options.sessionStore.set(sessionId, {
82
+ transport,
83
+ createdAt: now,
84
+ lastSeen: now,
85
+ });
86
+ transport.onclose = () => {
87
+ options.sessionStore.remove(sessionId);
88
+ logInfo('Session closed');
89
+ };
90
+ logInfo('Session initialized');
110
91
  return transport;
111
92
  }
112
93
  export async function resolveTransportForPost(_req, res, body, sessionId, options) {
113
- const existingTransport = findExistingTransport(sessionId, options);
114
- if (existingTransport) {
115
- return existingTransport;
94
+ if (sessionId) {
95
+ const existingSession = options.sessionStore.get(sessionId);
96
+ if (existingSession) {
97
+ options.sessionStore.touch(sessionId);
98
+ return existingSession.transport;
99
+ }
100
+ // Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
101
+ sendJsonRpcError(res, -32600, 'Session not found', 404);
102
+ return null;
116
103
  }
117
- if (!shouldInitializeSession(sessionId, body)) {
104
+ if (!isInitializeRequest(body)) {
118
105
  respondBadRequest(res);
119
106
  return null;
120
107
  }
@@ -1,2 +1,3 @@
1
1
  import type { McpRequestBody } from '../config/types/runtime.js';
2
+ export declare function isJsonRpcBatchRequest(body: unknown): boolean;
2
3
  export declare function isMcpRequestBody(body: unknown): body is McpRequestBody;
@@ -1,13 +1,14 @@
1
- function isRecord(value) {
2
- return value !== null && typeof value === 'object';
1
+ import { z } from 'zod';
2
+ const paramsSchema = z.looseObject({});
3
+ const mcpRequestSchema = z.looseObject({
4
+ jsonrpc: z.literal('2.0'),
5
+ method: z.string().min(1),
6
+ id: z.union([z.string(), z.number()]).optional(),
7
+ params: paramsSchema.optional(),
8
+ });
9
+ export function isJsonRpcBatchRequest(body) {
10
+ return Array.isArray(body);
3
11
  }
4
12
  export function isMcpRequestBody(body) {
5
- if (!isRecord(body) || Array.isArray(body))
6
- return false;
7
- const { method, id, jsonrpc, params } = body;
8
- const methodValid = method === undefined || typeof method === 'string';
9
- const idValid = id === undefined || typeof id === 'string' || typeof id === 'number';
10
- const jsonrpcValid = jsonrpc === undefined || jsonrpc === '2.0';
11
- const paramsValid = params === undefined || typeof params === 'object';
12
- return methodValid && idValid && jsonrpcValid && paramsValid;
13
+ return mcpRequestSchema.safeParse(body).success;
13
14
  }
@@ -0,0 +1,2 @@
1
+ import type { Request, Response } from 'express';
2
+ export declare function ensureMcpProtocolVersionHeader(req: Request, res: Response): boolean;
@@ -0,0 +1,31 @@
1
+ import { sendJsonRpcError } from './jsonrpc-http.js';
2
+ const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version';
3
+ const MCP_PROTOCOL_VERSIONS = {
4
+ defaultVersion: '2025-03-26',
5
+ supported: new Set(['2025-03-26', '2025-11-25']),
6
+ };
7
+ function getHeaderValue(req, headerNameLower) {
8
+ const value = req.headers[headerNameLower];
9
+ if (typeof value === 'string')
10
+ return value;
11
+ if (Array.isArray(value))
12
+ return value[0] ?? null;
13
+ return null;
14
+ }
15
+ function setHeaderValue(req, headerNameLower, value) {
16
+ // Express exposes req.headers as a plain object, but the type is readonly-ish.
17
+ req.headers[headerNameLower] = value;
18
+ }
19
+ export function ensureMcpProtocolVersionHeader(req, res) {
20
+ const raw = getHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER);
21
+ const version = raw?.trim();
22
+ if (!version) {
23
+ setHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER, MCP_PROTOCOL_VERSIONS.defaultVersion);
24
+ return true;
25
+ }
26
+ if (!MCP_PROTOCOL_VERSIONS.supported.has(version)) {
27
+ sendJsonRpcError(res, -32600, `Unsupported MCP-Protocol-Version: ${version}`, 400);
28
+ return false;
29
+ }
30
+ return true;
31
+ }
@@ -13,7 +13,11 @@ export function createRateLimitMiddleware(options) {
13
13
  const stop = () => {
14
14
  cleanupController.abort();
15
15
  };
16
- const middleware = (req, res, next) => {
16
+ const middleware = createRateLimitHandler(store, options);
17
+ return { middleware, stop, store };
18
+ }
19
+ function createRateLimitHandler(store, options) {
20
+ return (req, res, next) => {
17
21
  if (shouldSkipRateLimit(req, options)) {
18
22
  next();
19
23
  return;
@@ -30,7 +34,6 @@ export function createRateLimitMiddleware(options) {
30
34
  }
31
35
  next();
32
36
  };
33
- return { middleware, stop, store };
34
37
  }
35
38
  async function startCleanupLoop(store, options, signal) {
36
39
  for await (const _ of setIntervalPromise(options.cleanupIntervalMs, undefined, { signal, ref: false })) {
@@ -0,0 +1 @@
1
+ export declare function assertHttpConfiguration(): void;
@@ -0,0 +1,40 @@
1
+ import { config } from '../config/index.js';
2
+ import { logError } from '../services/logger.js';
3
+ export function assertHttpConfiguration() {
4
+ ensureBindAllowed();
5
+ ensureStaticTokens();
6
+ if (config.auth.mode === 'oauth') {
7
+ ensureOauthConfiguration();
8
+ }
9
+ }
10
+ function ensureBindAllowed() {
11
+ const isLoopback = ['127.0.0.1', '::1', 'localhost'].includes(config.server.host);
12
+ if (!config.security.allowRemote && !isLoopback) {
13
+ logError('Refusing to bind to non-loopback host without ALLOW_REMOTE=true', { host: config.server.host });
14
+ process.exit(1);
15
+ }
16
+ if (config.security.allowRemote && config.auth.mode !== 'oauth') {
17
+ logError('Remote HTTP mode requires OAuth configuration; refusing to start');
18
+ process.exit(1);
19
+ }
20
+ }
21
+ function ensureStaticTokens() {
22
+ if (config.auth.mode === 'static' && config.auth.staticTokens.length === 0) {
23
+ logError('At least one static access token is required for HTTP mode');
24
+ process.exit(1);
25
+ }
26
+ }
27
+ function ensureOauthConfiguration() {
28
+ if (!config.auth.issuerUrl || !config.auth.authorizationUrl) {
29
+ logError('OAUTH_ISSUER_URL and OAUTH_AUTHORIZATION_URL are required for OAuth mode');
30
+ process.exit(1);
31
+ }
32
+ if (!config.auth.tokenUrl) {
33
+ logError('OAUTH_TOKEN_URL is required for OAuth mode');
34
+ process.exit(1);
35
+ }
36
+ if (!config.auth.introspectionUrl) {
37
+ logError('OAUTH_INTROSPECTION_URL is required for OAuth mode');
38
+ process.exit(1);
39
+ }
40
+ }
@@ -1,9 +1,2 @@
1
- import type { Express, NextFunction, Request, RequestHandler, Response } from 'express';
2
- export declare function buildCorsOptions(): {
3
- allowedOrigins: string[];
4
- allowAllOrigins: boolean;
5
- };
6
- export declare function createJsonParseErrorHandler(): (err: Error, _req: Request, res: Response, next: NextFunction) => void;
7
- export declare function createContextMiddleware(): (req: Request, _res: Response, next: NextFunction) => void;
8
- export declare function registerHealthRoute(app: Express): void;
9
- export declare function attachBaseMiddleware(app: Express, jsonParser: RequestHandler, rateLimitMiddleware: RequestHandler, authMiddleware: RequestHandler, corsMiddleware: RequestHandler): void;
1
+ import type { Express, RequestHandler } from 'express';
2
+ export declare function attachBaseMiddleware(app: Express, jsonParser: RequestHandler, rateLimitMiddleware: RequestHandler, corsMiddleware: RequestHandler): void;
@@ -1,45 +1,93 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { config } from '../config/index.js';
3
- import { bindToRequestContext, runWithRequestContext, } from '../services/context.js';
3
+ import { runWithRequestContext } from '../services/context.js';
4
4
  import { getSessionId } from './sessions.js';
5
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
+ }
6
53
  function normalizeHost(value) {
7
54
  const trimmed = value.trim().toLowerCase();
8
55
  if (!trimmed)
9
56
  return null;
10
- const first = trimmed.split(',')[0]?.trim();
57
+ const first = takeFirstHostValue(trimmed);
11
58
  if (!first)
12
59
  return null;
13
- if (first.startsWith('[')) {
14
- const end = first.indexOf(']');
15
- if (end === -1)
16
- return null;
17
- return first.slice(1, end);
18
- }
19
- const colonIndex = first.indexOf(':');
20
- if (colonIndex !== -1) {
21
- return first.slice(0, colonIndex);
22
- }
23
- return first;
60
+ const ipv6 = stripIpv6Brackets(first);
61
+ if (ipv6)
62
+ return ipv6;
63
+ return stripPortIfPresent(first);
24
64
  }
25
- function buildAllowedHosts() {
26
- const allowedHosts = new Set();
27
- const raw = process.env.ALLOWED_HOSTS ?? '';
28
- for (const entry of raw.split(',')) {
29
- const normalized = normalizeHost(entry);
30
- if (normalized) {
31
- allowedHosts.add(normalized);
32
- }
33
- }
65
+ function isWildcardHost(host) {
66
+ return host === '0.0.0.0' || host === '::';
67
+ }
68
+ function addLoopbackHosts(allowedHosts) {
34
69
  for (const host of LOOPBACK_HOSTS) {
35
70
  allowedHosts.add(host);
36
71
  }
72
+ }
73
+ function addConfiguredHost(allowedHosts) {
37
74
  const configuredHost = normalizeHost(config.server.host);
38
- if (configuredHost &&
39
- configuredHost !== '0.0.0.0' &&
40
- configuredHost !== '::') {
41
- allowedHosts.add(configuredHost);
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);
42
84
  }
85
+ }
86
+ function buildAllowedHosts() {
87
+ const allowedHosts = new Set();
88
+ addLoopbackHosts(allowedHosts);
89
+ addConfiguredHost(allowedHosts);
90
+ addExplicitAllowedHosts(allowedHosts);
43
91
  return allowedHosts;
44
92
  }
45
93
  function createHostValidationMiddleware() {
@@ -48,23 +96,29 @@ function createHostValidationMiddleware() {
48
96
  const hostHeader = typeof req.headers.host === 'string' ? req.headers.host : '';
49
97
  const normalized = normalizeHost(hostHeader);
50
98
  if (!normalized || !allowedHosts.has(normalized)) {
51
- res.status(403).json({
52
- error: 'Host not allowed',
53
- code: 'HOST_NOT_ALLOWED',
54
- });
99
+ respondHostNotAllowed(res);
55
100
  return;
56
101
  }
57
102
  next();
58
103
  };
59
104
  }
60
- export function buildCorsOptions() {
61
- const allowedOrigins = process.env.ALLOWED_ORIGINS
62
- ? process.env.ALLOWED_ORIGINS.split(',').map((o) => o.trim())
63
- : [];
64
- const allowAllOrigins = process.env.CORS_ALLOW_ALL === 'true';
65
- return { allowedOrigins, allowAllOrigins };
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
+ };
66
120
  }
67
- export function createJsonParseErrorHandler() {
121
+ function createJsonParseErrorHandler() {
68
122
  return (err, _req, res, next) => {
69
123
  if (err instanceof SyntaxError && 'body' in err) {
70
124
  res.status(400).json({
@@ -80,18 +134,17 @@ export function createJsonParseErrorHandler() {
80
134
  next();
81
135
  };
82
136
  }
83
- export function createContextMiddleware() {
137
+ function createContextMiddleware() {
84
138
  return (req, _res, next) => {
85
139
  const requestId = randomUUID();
86
140
  const sessionId = getSessionId(req);
87
141
  const context = sessionId === undefined ? { requestId } : { requestId, sessionId };
88
142
  runWithRequestContext(context, () => {
89
- const boundNext = bindToRequestContext(next);
90
- boundNext();
143
+ next();
91
144
  });
92
145
  };
93
146
  }
94
- export function registerHealthRoute(app) {
147
+ function registerHealthRoute(app) {
95
148
  app.get('/health', (_req, res) => {
96
149
  res.json({
97
150
  status: 'healthy',
@@ -101,13 +154,13 @@ export function registerHealthRoute(app) {
101
154
  });
102
155
  });
103
156
  }
104
- export function attachBaseMiddleware(app, jsonParser, rateLimitMiddleware, authMiddleware, corsMiddleware) {
157
+ export function attachBaseMiddleware(app, jsonParser, rateLimitMiddleware, corsMiddleware) {
105
158
  app.use(createHostValidationMiddleware());
159
+ app.use(createOriginValidationMiddleware());
106
160
  app.use(jsonParser);
107
161
  app.use(createContextMiddleware());
108
162
  app.use(createJsonParseErrorHandler());
109
163
  app.use(corsMiddleware);
110
164
  app.use('/mcp', rateLimitMiddleware);
111
- app.use(authMiddleware);
112
165
  registerHealthRoute(app);
113
166
  }
@@ -0,0 +1,4 @@
1
+ import type { Express } from 'express';
2
+ import type { SessionStore } from './sessions.js';
3
+ export declare function createShutdownHandler(server: ReturnType<Express['listen']>, sessionStore: SessionStore, sessionCleanupController: AbortController, stopRateLimitCleanup: () => void): (signal: string) => Promise<void>;
4
+ export declare function registerSignalHandlers(shutdown: (signal: string) => Promise<void>): void;
@@ -0,0 +1,43 @@
1
+ import { destroyAgents } from '../services/fetcher/agents.js';
2
+ import { logError, logInfo, logWarn } from '../services/logger.js';
3
+ import { getErrorMessage } from '../utils/error-utils.js';
4
+ export function createShutdownHandler(server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
5
+ return (signal) => shutdownServer(signal, server, sessionStore, sessionCleanupController, stopRateLimitCleanup);
6
+ }
7
+ async function shutdownServer(signal, server, sessionStore, sessionCleanupController, stopRateLimitCleanup) {
8
+ logInfo(`${signal} received, shutting down gracefully...`);
9
+ stopRateLimitCleanup();
10
+ sessionCleanupController.abort();
11
+ await closeSessions(sessionStore);
12
+ destroyAgents();
13
+ closeServer(server);
14
+ scheduleForcedShutdown(10000);
15
+ }
16
+ async function closeSessions(sessionStore) {
17
+ const sessions = sessionStore.clear();
18
+ await Promise.allSettled(sessions.map((session) => session.transport.close().catch((error) => {
19
+ logWarn('Failed to close session during shutdown', {
20
+ error: getErrorMessage(error),
21
+ });
22
+ })));
23
+ }
24
+ function closeServer(server) {
25
+ server.close(() => {
26
+ logInfo('HTTP server closed');
27
+ process.exit(0);
28
+ });
29
+ }
30
+ function scheduleForcedShutdown(timeoutMs) {
31
+ setTimeout(() => {
32
+ logError('Forced shutdown after timeout');
33
+ process.exit(1);
34
+ }, timeoutMs).unref();
35
+ }
36
+ export function registerSignalHandlers(shutdown) {
37
+ process.on('SIGINT', () => {
38
+ void shutdown('SIGINT');
39
+ });
40
+ process.on('SIGTERM', () => {
41
+ void shutdown('SIGTERM');
42
+ });
43
+ }