@juspay/neurolink 13.8.16 → 13.8.18
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/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +83 -8
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/expressBody.d.ts +33 -0
- package/dist/server/utils/expressBody.js +353 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +255 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/docs-site/static/search-index.json +75 -52
- package/package.json +171 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -10,6 +10,7 @@ import { ProviderFactory } from "../../factories/providerFactory.js";
|
|
|
10
10
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
11
11
|
import { tracers } from "../../telemetry/tracers.js";
|
|
12
12
|
import { logger } from "../../utils/logger.js";
|
|
13
|
+
import { vetRegexPattern } from "../../middleware/utils/regexSafety.js";
|
|
13
14
|
import { rerank } from "../reranker/reranker.js";
|
|
14
15
|
/**
|
|
15
16
|
* Creates a vector query tool for semantic search
|
|
@@ -132,11 +133,19 @@ export function createVectorQueryTool(config, vectorStore) {
|
|
|
132
133
|
},
|
|
133
134
|
};
|
|
134
135
|
}
|
|
136
|
+
/** Longest `$regex` filter pattern accepted; a secondary cap behind vetting. */
|
|
137
|
+
const MAX_FILTER_REGEX_LENGTH = 200;
|
|
138
|
+
/** Longest metadata value a `$regex` filter is run against. */
|
|
139
|
+
const MAX_FILTER_REGEX_SUBJECT = 10_000;
|
|
140
|
+
/** Distinct patterns remembered per store before the verdict cache is reset. */
|
|
141
|
+
const MAX_FILTER_REGEX_CACHE = 256;
|
|
135
142
|
/**
|
|
136
143
|
* In-memory vector store implementation for testing and development
|
|
137
144
|
*/
|
|
138
145
|
export class InMemoryVectorStore {
|
|
139
146
|
vectors = new Map();
|
|
147
|
+
/** Vetted `$regex` patterns: the compiled expression, or null when refused. */
|
|
148
|
+
regexFilterCache = new Map();
|
|
140
149
|
/**
|
|
141
150
|
* Add vectors to an index
|
|
142
151
|
*/
|
|
@@ -272,23 +281,14 @@ export class InMemoryVectorStore {
|
|
|
272
281
|
return false;
|
|
273
282
|
}
|
|
274
283
|
if ("$regex" in ops) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
? fieldValue.slice(0, 10_000)
|
|
284
|
-
: "";
|
|
285
|
-
regexMatches = re.test(testValue);
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// Invalid regex pattern — treat as non-match
|
|
289
|
-
regexMatches = false;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
284
|
+
// The pattern is chosen by the model and the value it runs on
|
|
285
|
+
// comes from ingested documents, so both halves of a ReDoS are
|
|
286
|
+
// attacker-influenced. A length cap alone does not bound
|
|
287
|
+
// backtracking (`^(a+)+$` is 7 characters); `vetRegexPattern`
|
|
288
|
+
// reads the pattern's structure and refuses the ones that can
|
|
289
|
+
// backtrack catastrophically. A refused or invalid pattern is a
|
|
290
|
+
// non-match, as an invalid one always was.
|
|
291
|
+
const regexMatches = this.matchesRegexFilter(ops.$regex, fieldValue);
|
|
292
292
|
if (!regexMatches) {
|
|
293
293
|
return false;
|
|
294
294
|
}
|
|
@@ -304,6 +304,43 @@ export class InMemoryVectorStore {
|
|
|
304
304
|
}
|
|
305
305
|
return true;
|
|
306
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Evaluate a `$regex` filter operator. Patterns are vetted once per store
|
|
309
|
+
* and the verdict cached, so a refused pattern costs one analysis.
|
|
310
|
+
*/
|
|
311
|
+
matchesRegexFilter(pattern, fieldValue) {
|
|
312
|
+
if (typeof pattern !== "string" ||
|
|
313
|
+
pattern.length > MAX_FILTER_REGEX_LENGTH) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
let compiled = this.regexFilterCache.get(pattern);
|
|
317
|
+
if (compiled === undefined) {
|
|
318
|
+
const verdict = vetRegexPattern(pattern);
|
|
319
|
+
if (verdict.safe) {
|
|
320
|
+
try {
|
|
321
|
+
compiled = new RegExp(pattern);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
compiled = null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
logger.debug("[InMemoryVectorStore] Refused a $regex filter pattern that could stall the process", { reason: verdict.reason });
|
|
329
|
+
compiled = null;
|
|
330
|
+
}
|
|
331
|
+
if (this.regexFilterCache.size >= MAX_FILTER_REGEX_CACHE) {
|
|
332
|
+
this.regexFilterCache.clear();
|
|
333
|
+
}
|
|
334
|
+
this.regexFilterCache.set(pattern, compiled);
|
|
335
|
+
}
|
|
336
|
+
if (compiled === null) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const testValue = typeof fieldValue === "string"
|
|
340
|
+
? fieldValue.slice(0, MAX_FILTER_REGEX_SUBJECT)
|
|
341
|
+
: "";
|
|
342
|
+
return compiled.test(testValue);
|
|
343
|
+
}
|
|
307
344
|
/**
|
|
308
345
|
* Calculate cosine similarity between two vectors
|
|
309
346
|
*/
|
|
@@ -7,6 +7,8 @@ import { EventEmitter } from "events";
|
|
|
7
7
|
import type { ExternalServerManager } from "../../mcp/externalServerManager.js";
|
|
8
8
|
import type { MCPToolRegistry } from "../../mcp/toolRegistry.js";
|
|
9
9
|
import type { NeuroLink } from "../../neurolink.js";
|
|
10
|
+
/** The interface an adapter binds unless told otherwise: this host only. */
|
|
11
|
+
export declare const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
10
12
|
import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerCarriedContext, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
|
|
11
13
|
/**
|
|
12
14
|
* Abstract base class for server adapters
|
|
@@ -29,7 +31,47 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
|
|
|
29
31
|
protected activeConnections: Map<string, TrackedConnection>;
|
|
30
32
|
protected readonly shutdownConfig: RequiredShutdownConfig;
|
|
31
33
|
private carryRequestedByRoutes;
|
|
34
|
+
private sensitiveRoutesRegistered;
|
|
35
|
+
private sensitiveRoutesAllowedOpen;
|
|
32
36
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Whether middleware marked `authenticates: true` is registered (the
|
|
39
|
+
* built-in authentication middleware marks itself). The tool and MCP
|
|
40
|
+
* execution routes ask on every request; `registerAllRoutes()` wires them.
|
|
41
|
+
*/
|
|
42
|
+
hasAuthenticationMiddleware(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Called by `registerAllRoutes()`: routes that act on the host (tool and
|
|
45
|
+
* MCP execution) are mounted, so `start()` warns when no authentication
|
|
46
|
+
* middleware guards them. The check runs at start, when the middleware set
|
|
47
|
+
* is final, so registering authentication after the routes is fine.
|
|
48
|
+
*/
|
|
49
|
+
noteSensitiveRoutes(options?: {
|
|
50
|
+
allowUnauthenticated?: boolean;
|
|
51
|
+
}): void;
|
|
52
|
+
/**
|
|
53
|
+
* The warning promised by `noteSensitiveRoutes()`, always on stderr: a
|
|
54
|
+
* `logger.warn` is dropped unless debug logging is on, which would hide a
|
|
55
|
+
* notice meant for the operator who just started the server.
|
|
56
|
+
*/
|
|
57
|
+
protected warnIfSensitiveRoutesUnauthenticated(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Give the handler a way to notice that the client is gone or the request
|
|
60
|
+
* is over time. The controller is this request's own unless middleware
|
|
61
|
+
* already attached one that the adapter carried; `applyTimeout` arms the
|
|
62
|
+
* configured request timeout, which the caller leaves off for a streaming
|
|
63
|
+
* route (a stream legitimately outlives it, as under Hono's own timeout
|
|
64
|
+
* middleware). `release()` disarms the timer once the response is written.
|
|
65
|
+
*/
|
|
66
|
+
protected attachRequestAbort(ctx: ServerContext, options: {
|
|
67
|
+
onClientClose: (listener: () => void) => void;
|
|
68
|
+
applyTimeout: boolean;
|
|
69
|
+
}): {
|
|
70
|
+
controller: AbortController;
|
|
71
|
+
deadline: Promise<never> | undefined;
|
|
72
|
+
timedOut: () => boolean;
|
|
73
|
+
release: () => void;
|
|
74
|
+
};
|
|
33
75
|
/**
|
|
34
76
|
* Initialize the underlying server framework
|
|
35
77
|
* May be async: adapters that dynamically import their framework package
|
|
@@ -146,6 +188,7 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
|
|
|
146
188
|
query?: Record<string, string>;
|
|
147
189
|
params?: Record<string, string>;
|
|
148
190
|
body?: unknown;
|
|
191
|
+
remoteAddress?: string;
|
|
149
192
|
}): ServerContext;
|
|
150
193
|
/**
|
|
151
194
|
* Register built-in middleware
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js";
|
|
8
8
|
import { withTimeout } from "../../utils/errorHandling.js";
|
|
9
|
+
import { MAX_TIMER_MS } from "../../constants/timeouts.js";
|
|
9
10
|
import { logger } from "../../utils/logger.js";
|
|
10
11
|
import { markContextCarried } from "../utils/carriedContextMark.js";
|
|
12
|
+
import { isLoopbackAddress } from "../utils/clientAddress.js";
|
|
13
|
+
import { pathStartsWithSegment } from "../utils/pathMatch.js";
|
|
11
14
|
import { tryImport } from "../../utils/tryImport.js";
|
|
12
|
-
import { DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ShutdownTimeoutError, } from "../errors.js";
|
|
15
|
+
import { ConfigurationError, DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ServerTimeoutError, ShutdownTimeoutError, } from "../errors.js";
|
|
16
|
+
/** The interface an adapter binds unless told otherwise: this host only. */
|
|
17
|
+
export const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
13
18
|
/**
|
|
14
19
|
* Abstract base class for server adapters
|
|
15
20
|
* Provides common functionality and defines the interface for framework-specific implementations
|
|
@@ -37,6 +42,9 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
37
42
|
// Set by enableMiddlewareStateCarry(); the carryMiddlewareState option is the
|
|
38
43
|
// other way in.
|
|
39
44
|
carryRequestedByRoutes = false;
|
|
45
|
+
// Set by noteSensitiveRoutes(); read when the server starts.
|
|
46
|
+
sensitiveRoutesRegistered = false;
|
|
47
|
+
sensitiveRoutesAllowedOpen = false;
|
|
40
48
|
constructor(neurolink, config = {}) {
|
|
41
49
|
super();
|
|
42
50
|
this.neurolink = neurolink;
|
|
@@ -53,7 +61,9 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
53
61
|
// Apply defaults
|
|
54
62
|
this.config = {
|
|
55
63
|
port: config.port ?? 3000,
|
|
56
|
-
|
|
64
|
+
// Loopback unless the operator names an interface: the routes run tools
|
|
65
|
+
// and remove MCP servers, so reachability is opt-in.
|
|
66
|
+
host: config.host ?? DEFAULT_SERVER_HOST,
|
|
57
67
|
basePath: config.basePath ?? "/api",
|
|
58
68
|
cors: {
|
|
59
69
|
enabled: config.cors?.enabled ?? true,
|
|
@@ -85,6 +95,8 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
85
95
|
"Too many requests, please try again later",
|
|
86
96
|
skipPaths: config.rateLimit?.skipPaths,
|
|
87
97
|
keyGenerator: config.rateLimit?.keyGenerator,
|
|
98
|
+
trustProxy: config.rateLimit?.trustProxy ?? false,
|
|
99
|
+
trustedProxies: config.rateLimit?.trustedProxies ?? [],
|
|
88
100
|
},
|
|
89
101
|
bodyParser: {
|
|
90
102
|
enabled: config.bodyParser?.enabled ?? true,
|
|
@@ -107,6 +119,100 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
107
119
|
shutdown: this.shutdownConfig,
|
|
108
120
|
carryMiddlewareState: config.carryMiddlewareState ?? false,
|
|
109
121
|
};
|
|
122
|
+
// A wildcard origin with credentials is what the CORS specification
|
|
123
|
+
// forbids and what an adapter reflecting the request's Origin would grant:
|
|
124
|
+
// any page could make credentialed calls. Refuse it for every adapter.
|
|
125
|
+
if (this.config.cors.enabled &&
|
|
126
|
+
this.config.cors.credentials &&
|
|
127
|
+
this.config.cors.origins.includes("*")) {
|
|
128
|
+
throw new ConfigurationError('cors.credentials cannot be combined with the wildcard origin "*"; list the origins that may send credentials', { origins: this.config.cors.origins });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether middleware marked `authenticates: true` is registered (the
|
|
133
|
+
* built-in authentication middleware marks itself). The tool and MCP
|
|
134
|
+
* execution routes ask on every request; `registerAllRoutes()` wires them.
|
|
135
|
+
*/
|
|
136
|
+
hasAuthenticationMiddleware() {
|
|
137
|
+
return this.middlewares.some((middleware) => middleware.authenticates === true);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Called by `registerAllRoutes()`: routes that act on the host (tool and
|
|
141
|
+
* MCP execution) are mounted, so `start()` warns when no authentication
|
|
142
|
+
* middleware guards them. The check runs at start, when the middleware set
|
|
143
|
+
* is final, so registering authentication after the routes is fine.
|
|
144
|
+
*/
|
|
145
|
+
noteSensitiveRoutes(options) {
|
|
146
|
+
this.sensitiveRoutesRegistered = true;
|
|
147
|
+
if (options?.allowUnauthenticated) {
|
|
148
|
+
this.sensitiveRoutesAllowedOpen = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The warning promised by `noteSensitiveRoutes()`, always on stderr: a
|
|
153
|
+
* `logger.warn` is dropped unless debug logging is on, which would hide a
|
|
154
|
+
* notice meant for the operator who just started the server.
|
|
155
|
+
*/
|
|
156
|
+
warnIfSensitiveRoutesUnauthenticated() {
|
|
157
|
+
if (!this.sensitiveRoutesRegistered ||
|
|
158
|
+
this.sensitiveRoutesAllowedOpen ||
|
|
159
|
+
this.hasAuthenticationMiddleware()) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const where = isLoopbackAddress(this.config.host)
|
|
163
|
+
? `loopback only (${this.config.host})`
|
|
164
|
+
: `${this.config.host}, reachable from the network`;
|
|
165
|
+
logger.alwaysStderr(`[ServerAdapter] WARNING: no authentication middleware is registered and the server listens on ${where}. ` +
|
|
166
|
+
"The tool and MCP execution routes answer 403 until authentication middleware (marked authenticates: true) is registered, " +
|
|
167
|
+
"or allowUnauthenticated: true / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1 opts out of that gate.");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Give the handler a way to notice that the client is gone or the request
|
|
171
|
+
* is over time. The controller is this request's own unless middleware
|
|
172
|
+
* already attached one that the adapter carried; `applyTimeout` arms the
|
|
173
|
+
* configured request timeout, which the caller leaves off for a streaming
|
|
174
|
+
* route (a stream legitimately outlives it, as under Hono's own timeout
|
|
175
|
+
* middleware). `release()` disarms the timer once the response is written.
|
|
176
|
+
*/
|
|
177
|
+
attachRequestAbort(ctx, options) {
|
|
178
|
+
const controller = ctx.abortController ?? new AbortController();
|
|
179
|
+
ctx.abortController = controller;
|
|
180
|
+
ctx.abortSignal = controller.signal;
|
|
181
|
+
let timedOut = false;
|
|
182
|
+
let timer;
|
|
183
|
+
const timeoutMs = this.config.timeout;
|
|
184
|
+
// Node turns an over-range delay into 1 ms; keep long request budgets
|
|
185
|
+
// at the timer limit, as the readiness deadline does through withTimeout.
|
|
186
|
+
const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
|
|
187
|
+
const deadline = options.applyTimeout && timeoutMs > 0
|
|
188
|
+
? new Promise((_, reject) => {
|
|
189
|
+
timer = setTimeout(() => {
|
|
190
|
+
timedOut = true;
|
|
191
|
+
const error = new ServerTimeoutError(timeoutMs, `${ctx.method} ${ctx.path}`, ctx.requestId);
|
|
192
|
+
controller.abort(error);
|
|
193
|
+
reject(error);
|
|
194
|
+
}, delayMs);
|
|
195
|
+
})
|
|
196
|
+
: undefined;
|
|
197
|
+
// A rejection nobody awaits (the handler won on the race) must not
|
|
198
|
+
// surface as unhandled.
|
|
199
|
+
deadline?.catch(() => undefined);
|
|
200
|
+
options.onClientClose(() => {
|
|
201
|
+
if (!controller.signal.aborted) {
|
|
202
|
+
controller.abort(new Error("Client disconnected"));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
controller,
|
|
207
|
+
deadline,
|
|
208
|
+
timedOut: () => timedOut,
|
|
209
|
+
release: () => {
|
|
210
|
+
if (timer) {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
timer = undefined;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
110
216
|
}
|
|
111
217
|
/**
|
|
112
218
|
* The listening Node `http.Server`, once `start()` has run and the runtime
|
|
@@ -237,11 +343,17 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
237
343
|
}
|
|
238
344
|
}
|
|
239
345
|
// Register all routes in the group with prefix applied
|
|
346
|
+
const prefix = this.normalizePath(group.prefix);
|
|
347
|
+
// The group's root segment ("/api" of "/api/health"): a route whose path
|
|
348
|
+
// already lives under it is a full path, as every built-in route is.
|
|
349
|
+
// Testing only the whole prefix put `/api/version`, declared in the health
|
|
350
|
+
// group, at `/api/health/api/version`.
|
|
351
|
+
const root = `/${prefix.split("/").filter(Boolean)[0] ?? ""}`;
|
|
240
352
|
for (const route of group.routes) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const prefixedPath = this.normalizePath(needsPrefix ? `${
|
|
353
|
+
const needsPrefix = prefix !== "/" &&
|
|
354
|
+
!pathStartsWithSegment(route.path, prefix) &&
|
|
355
|
+
!pathStartsWithSegment(route.path, root);
|
|
356
|
+
const prefixedPath = this.normalizePath(needsPrefix ? `${prefix}${route.path}` : route.path);
|
|
245
357
|
const prefixedRoute = {
|
|
246
358
|
...route,
|
|
247
359
|
path: prefixedPath,
|
|
@@ -336,6 +448,7 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
336
448
|
query: options.query ?? {},
|
|
337
449
|
params: options.params ?? {},
|
|
338
450
|
body: options.body,
|
|
451
|
+
remoteAddress: options.remoteAddress,
|
|
339
452
|
neurolink: this.neurolink,
|
|
340
453
|
toolRegistry: this.toolRegistry,
|
|
341
454
|
externalServerManager: this.externalServerManager,
|
|
@@ -13,6 +13,7 @@ export declare class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
13
13
|
private server?;
|
|
14
14
|
private frameworkInitialized;
|
|
15
15
|
private sockets;
|
|
16
|
+
private warnedNoClientAddress;
|
|
16
17
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
17
18
|
/**
|
|
18
19
|
* Initialize Express framework asynchronously
|
|
@@ -22,6 +23,8 @@ export declare class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
22
23
|
* Initialize Express framework with async imports
|
|
23
24
|
*/
|
|
24
25
|
private initializeFrameworkAsync;
|
|
26
|
+
/** The address a request is counted against; `undefined` when there is none. */
|
|
27
|
+
private clientKeyFor;
|
|
25
28
|
/**
|
|
26
29
|
* Override initialize to ensure async framework setup
|
|
27
30
|
*/
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
6
6
|
import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js";
|
|
7
7
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
8
|
+
import { resolveClientAddress } from "../utils/clientAddress.js";
|
|
9
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
10
|
+
import { pathMatchesAny } from "../utils/pathMatch.js";
|
|
8
11
|
import { isErrorResponse } from "../utils/validation.js";
|
|
12
|
+
import { createBoundedBodyParser } from "../utils/expressBody.js";
|
|
9
13
|
import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
|
|
14
|
+
/** How long `listen()` may take; `config.timeout` is the per-request budget. */
|
|
15
|
+
const STARTUP_TIMEOUT_MS = 30_000;
|
|
10
16
|
/**
|
|
11
17
|
* Express-specific server adapter
|
|
12
18
|
*/
|
|
@@ -15,6 +21,8 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
15
21
|
server;
|
|
16
22
|
frameworkInitialized = false;
|
|
17
23
|
sockets = new Set();
|
|
24
|
+
// The rate limiter counts nothing it cannot attribute, and says so once.
|
|
25
|
+
warnedNoClientAddress = false;
|
|
18
26
|
constructor(neurolink, config = {}) {
|
|
19
27
|
super(neurolink, config);
|
|
20
28
|
}
|
|
@@ -41,8 +49,10 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
41
49
|
const bodyLimit = this.config.bodyParser.maxSize ||
|
|
42
50
|
this.config.bodyParser.jsonLimit ||
|
|
43
51
|
"10mb";
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const jsonLimit = this.config.bodyParser.jsonLimit || bodyLimit;
|
|
53
|
+
// body-parser drains a body it refuses, so each is read through a bound.
|
|
54
|
+
this.app.use(createBoundedBodyParser(express.json({ limit: jsonLimit }), jsonLimit));
|
|
55
|
+
this.app.use(createBoundedBodyParser(express.urlencoded({ extended: true, limit: bodyLimit }), bodyLimit));
|
|
46
56
|
// Uploads (multipart, octet-stream, audio/*) reach the route as bytes.
|
|
47
57
|
this.app.use(express.raw({
|
|
48
58
|
type: (req) => isRawBodyContentType(req.headers["content-type"]),
|
|
@@ -74,6 +84,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
74
84
|
const limiter = rateLimit({
|
|
75
85
|
windowMs,
|
|
76
86
|
max: this.config.rateLimit.maxRequests,
|
|
87
|
+
// The key is resolved here, from the TCP peer or, behind a declared
|
|
88
|
+
// proxy, the client it reports; the library's own reading of the
|
|
89
|
+
// request (and its warnings about it) is not used. A request with no
|
|
90
|
+
// attributable address passes rather than share a bucket.
|
|
91
|
+
validate: false,
|
|
92
|
+
keyGenerator: (req) => this.clientKeyFor(req) ?? "",
|
|
93
|
+
skip: (req) => this.clientKeyFor(req) === undefined,
|
|
77
94
|
message: {
|
|
78
95
|
error: {
|
|
79
96
|
code: "RATE_LIMIT_EXCEEDED",
|
|
@@ -125,6 +142,20 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
125
142
|
}
|
|
126
143
|
this.frameworkInitialized = true;
|
|
127
144
|
}
|
|
145
|
+
/** The address a request is counted against; `undefined` when there is none. */
|
|
146
|
+
clientKeyFor(req) {
|
|
147
|
+
const key = resolveClientAddress({
|
|
148
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
149
|
+
forwardedFor: req.headers["x-forwarded-for"],
|
|
150
|
+
trustProxy: this.config.rateLimit.trustProxy,
|
|
151
|
+
trustedProxies: this.config.rateLimit.trustedProxies,
|
|
152
|
+
});
|
|
153
|
+
if (key === undefined && !this.warnedNoClientAddress) {
|
|
154
|
+
this.warnedNoClientAddress = true;
|
|
155
|
+
logger.alwaysStderr("[ExpressAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
|
|
156
|
+
}
|
|
157
|
+
return key;
|
|
158
|
+
}
|
|
128
159
|
/**
|
|
129
160
|
* Override initialize to ensure async framework setup
|
|
130
161
|
*/
|
|
@@ -149,7 +180,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
149
180
|
*/
|
|
150
181
|
registerFrameworkRoute(route) {
|
|
151
182
|
const method = route.method.toLowerCase();
|
|
152
|
-
this.app[method](route.path, async (req, res,
|
|
183
|
+
this.app[method](route.path, async (req, res, _next) => {
|
|
153
184
|
const requestId = req.headers["x-request-id"];
|
|
154
185
|
const startTime = Date.now();
|
|
155
186
|
// Create server context
|
|
@@ -161,12 +192,24 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
161
192
|
query: req.query,
|
|
162
193
|
params: req.params,
|
|
163
194
|
body: req.body,
|
|
195
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
164
196
|
});
|
|
165
197
|
// Copy response headers from middleware (stored in res.locals by middleware)
|
|
166
198
|
if (res.locals.responseHeaders) {
|
|
167
199
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
168
200
|
}
|
|
169
201
|
this.restoreCarriedContext(ctx, res.locals.carriedContext);
|
|
202
|
+
// The handler learns of a client that went away, and a request over
|
|
203
|
+
// `config.timeout` is answered 504 and its work aborted. Express used
|
|
204
|
+
// to apply that timeout only to its own listen().
|
|
205
|
+
const abort = this.attachRequestAbort(ctx, {
|
|
206
|
+
onClientClose: (listener) => res.on("close", () => {
|
|
207
|
+
if (!res.writableFinished) {
|
|
208
|
+
listener();
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
applyTimeout: !route.streaming?.enabled,
|
|
212
|
+
});
|
|
170
213
|
// Emit request event
|
|
171
214
|
this.emit("request", {
|
|
172
215
|
requestId,
|
|
@@ -180,7 +223,9 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
180
223
|
return this.handleStreamingResponse(res, ctx, route);
|
|
181
224
|
}
|
|
182
225
|
// Execute handler
|
|
183
|
-
const result =
|
|
226
|
+
const result = abort.deadline
|
|
227
|
+
? await Promise.race([route.handler(ctx), abort.deadline])
|
|
228
|
+
: await route.handler(ctx);
|
|
184
229
|
const duration = Date.now() - startTime;
|
|
185
230
|
// A route that answers in its own wire format returns a web Response.
|
|
186
231
|
if (isWebResponse(result)) {
|
|
@@ -252,7 +297,31 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
252
297
|
});
|
|
253
298
|
}
|
|
254
299
|
catch (error) {
|
|
255
|
-
|
|
300
|
+
// The exception's text stays in the log under this request id; the
|
|
301
|
+
// client gets a fixed message for its category.
|
|
302
|
+
const shaped = shapeRouteError("ExpressAdapter", requestId, error);
|
|
303
|
+
this.emitRequestError({
|
|
304
|
+
requestId,
|
|
305
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
306
|
+
timestamp: new Date(),
|
|
307
|
+
});
|
|
308
|
+
this.emit("response", {
|
|
309
|
+
requestId,
|
|
310
|
+
statusCode: shaped.status,
|
|
311
|
+
duration: Date.now() - startTime,
|
|
312
|
+
timestamp: new Date(),
|
|
313
|
+
});
|
|
314
|
+
if (res.headersSent) {
|
|
315
|
+
res.end();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
res.status(shaped.status).json({
|
|
319
|
+
error: { code: shaped.code, message: shaped.message },
|
|
320
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
abort.release();
|
|
256
325
|
}
|
|
257
326
|
});
|
|
258
327
|
// Error handling middleware (should be registered once after all routes)
|
|
@@ -332,9 +401,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
332
401
|
res.end();
|
|
333
402
|
}
|
|
334
403
|
catch (error) {
|
|
404
|
+
const shaped = shapeRouteError("ExpressAdapter", ctx.requestId, error);
|
|
335
405
|
res.write(`event: error\n`);
|
|
336
406
|
res.write(`data: ${JSON.stringify({
|
|
337
|
-
error:
|
|
407
|
+
error: shaped.message,
|
|
408
|
+
code: shaped.code,
|
|
409
|
+
status: shaped.status,
|
|
410
|
+
requestId: ctx.requestId,
|
|
338
411
|
})}\n\n`);
|
|
339
412
|
res.end();
|
|
340
413
|
}
|
|
@@ -347,7 +420,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
347
420
|
for (const path of paths) {
|
|
348
421
|
this.app.use(path, async (req, res, next) => {
|
|
349
422
|
// Skip excluded paths
|
|
350
|
-
if (
|
|
423
|
+
if (pathMatchesAny(req.path, middleware.excludePaths)) {
|
|
351
424
|
return next();
|
|
352
425
|
}
|
|
353
426
|
// Initialize response headers storage in res.locals if not present
|
|
@@ -364,6 +437,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
364
437
|
query: req.query,
|
|
365
438
|
params: req.params,
|
|
366
439
|
body: req.body,
|
|
440
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
367
441
|
});
|
|
368
442
|
// Copy existing response headers to context
|
|
369
443
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
@@ -405,13 +479,14 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
405
479
|
}
|
|
406
480
|
this.lifecycleState = "starting";
|
|
407
481
|
const { port, host } = this.config;
|
|
408
|
-
const startupTimeout =
|
|
482
|
+
const startupTimeout = STARTUP_TIMEOUT_MS;
|
|
409
483
|
const startPromise = new Promise((resolve, reject) => {
|
|
410
484
|
try {
|
|
411
485
|
this.server = this.app.listen(port, host, () => {
|
|
412
486
|
this.isRunning = true;
|
|
413
487
|
this.startTime = new Date();
|
|
414
488
|
this.lifecycleState = "running";
|
|
489
|
+
this.warnIfSensitiveRoutesUnauthenticated();
|
|
415
490
|
// Track connections for graceful shutdown
|
|
416
491
|
this.server?.on("connection", (socket) => {
|
|
417
492
|
const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -13,6 +13,7 @@ import type { MiddlewareDefinition, RouteDefinition, ServerAdapterConfig } from
|
|
|
13
13
|
export declare class FastifyServerAdapter extends BaseServerAdapter {
|
|
14
14
|
private app;
|
|
15
15
|
private frameworkInitialized;
|
|
16
|
+
private warnedNoClientAddress;
|
|
16
17
|
private connectionIds;
|
|
17
18
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
18
19
|
/**
|
|
@@ -24,6 +25,8 @@ export declare class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
24
25
|
* Initialize Fastify framework with async imports
|
|
25
26
|
*/
|
|
26
27
|
private initializeFrameworkAsync;
|
|
28
|
+
/** The address a request is counted against; `undefined` when there is none. */
|
|
29
|
+
private clientKeyFor;
|
|
27
30
|
/**
|
|
28
31
|
* Parse body limit string to number (bytes)
|
|
29
32
|
*/
|