@juspay/neurolink 13.8.17 → 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 +78 -6
- 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/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 +206 -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 +71 -49
- 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,9 +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";
|
|
9
12
|
import { createBoundedBodyParser } from "../utils/expressBody.js";
|
|
10
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;
|
|
11
16
|
/**
|
|
12
17
|
* Express-specific server adapter
|
|
13
18
|
*/
|
|
@@ -16,6 +21,8 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
16
21
|
server;
|
|
17
22
|
frameworkInitialized = false;
|
|
18
23
|
sockets = new Set();
|
|
24
|
+
// The rate limiter counts nothing it cannot attribute, and says so once.
|
|
25
|
+
warnedNoClientAddress = false;
|
|
19
26
|
constructor(neurolink, config = {}) {
|
|
20
27
|
super(neurolink, config);
|
|
21
28
|
}
|
|
@@ -77,6 +84,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
77
84
|
const limiter = rateLimit({
|
|
78
85
|
windowMs,
|
|
79
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,
|
|
80
94
|
message: {
|
|
81
95
|
error: {
|
|
82
96
|
code: "RATE_LIMIT_EXCEEDED",
|
|
@@ -128,6 +142,20 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
128
142
|
}
|
|
129
143
|
this.frameworkInitialized = true;
|
|
130
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
|
+
}
|
|
131
159
|
/**
|
|
132
160
|
* Override initialize to ensure async framework setup
|
|
133
161
|
*/
|
|
@@ -152,7 +180,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
152
180
|
*/
|
|
153
181
|
registerFrameworkRoute(route) {
|
|
154
182
|
const method = route.method.toLowerCase();
|
|
155
|
-
this.app[method](route.path, async (req, res,
|
|
183
|
+
this.app[method](route.path, async (req, res, _next) => {
|
|
156
184
|
const requestId = req.headers["x-request-id"];
|
|
157
185
|
const startTime = Date.now();
|
|
158
186
|
// Create server context
|
|
@@ -164,12 +192,24 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
164
192
|
query: req.query,
|
|
165
193
|
params: req.params,
|
|
166
194
|
body: req.body,
|
|
195
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
167
196
|
});
|
|
168
197
|
// Copy response headers from middleware (stored in res.locals by middleware)
|
|
169
198
|
if (res.locals.responseHeaders) {
|
|
170
199
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
171
200
|
}
|
|
172
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
|
+
});
|
|
173
213
|
// Emit request event
|
|
174
214
|
this.emit("request", {
|
|
175
215
|
requestId,
|
|
@@ -183,7 +223,9 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
183
223
|
return this.handleStreamingResponse(res, ctx, route);
|
|
184
224
|
}
|
|
185
225
|
// Execute handler
|
|
186
|
-
const result =
|
|
226
|
+
const result = abort.deadline
|
|
227
|
+
? await Promise.race([route.handler(ctx), abort.deadline])
|
|
228
|
+
: await route.handler(ctx);
|
|
187
229
|
const duration = Date.now() - startTime;
|
|
188
230
|
// A route that answers in its own wire format returns a web Response.
|
|
189
231
|
if (isWebResponse(result)) {
|
|
@@ -255,7 +297,31 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
255
297
|
});
|
|
256
298
|
}
|
|
257
299
|
catch (error) {
|
|
258
|
-
|
|
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();
|
|
259
325
|
}
|
|
260
326
|
});
|
|
261
327
|
// Error handling middleware (should be registered once after all routes)
|
|
@@ -335,9 +401,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
335
401
|
res.end();
|
|
336
402
|
}
|
|
337
403
|
catch (error) {
|
|
404
|
+
const shaped = shapeRouteError("ExpressAdapter", ctx.requestId, error);
|
|
338
405
|
res.write(`event: error\n`);
|
|
339
406
|
res.write(`data: ${JSON.stringify({
|
|
340
|
-
error:
|
|
407
|
+
error: shaped.message,
|
|
408
|
+
code: shaped.code,
|
|
409
|
+
status: shaped.status,
|
|
410
|
+
requestId: ctx.requestId,
|
|
341
411
|
})}\n\n`);
|
|
342
412
|
res.end();
|
|
343
413
|
}
|
|
@@ -350,7 +420,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
350
420
|
for (const path of paths) {
|
|
351
421
|
this.app.use(path, async (req, res, next) => {
|
|
352
422
|
// Skip excluded paths
|
|
353
|
-
if (
|
|
423
|
+
if (pathMatchesAny(req.path, middleware.excludePaths)) {
|
|
354
424
|
return next();
|
|
355
425
|
}
|
|
356
426
|
// Initialize response headers storage in res.locals if not present
|
|
@@ -367,6 +437,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
367
437
|
query: req.query,
|
|
368
438
|
params: req.params,
|
|
369
439
|
body: req.body,
|
|
440
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
370
441
|
});
|
|
371
442
|
// Copy existing response headers to context
|
|
372
443
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
@@ -408,13 +479,14 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
408
479
|
}
|
|
409
480
|
this.lifecycleState = "starting";
|
|
410
481
|
const { port, host } = this.config;
|
|
411
|
-
const startupTimeout =
|
|
482
|
+
const startupTimeout = STARTUP_TIMEOUT_MS;
|
|
412
483
|
const startPromise = new Promise((resolve, reject) => {
|
|
413
484
|
try {
|
|
414
485
|
this.server = this.app.listen(port, host, () => {
|
|
415
486
|
this.isRunning = true;
|
|
416
487
|
this.startTime = new Date();
|
|
417
488
|
this.lifecycleState = "running";
|
|
489
|
+
this.warnIfSensitiveRoutesUnauthenticated();
|
|
418
490
|
// Track connections for graceful shutdown
|
|
419
491
|
this.server?.on("connection", (socket) => {
|
|
420
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
|
*/
|