@juspay/neurolink 13.8.17 → 13.8.19
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 +5 -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/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- 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/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- 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/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- 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/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -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
|
*/
|
|
@@ -7,8 +7,13 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { withTimeout } from "../../utils/errorHandling.js";
|
|
9
9
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
10
|
+
import { resolveClientAddress } from "../utils/clientAddress.js";
|
|
11
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
12
|
+
import { pathMatchesAny, stripQuery } from "../utils/pathMatch.js";
|
|
10
13
|
import { isErrorResponse } from "../utils/validation.js";
|
|
11
14
|
import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
|
|
15
|
+
/** How long `listen()` may take; `config.timeout` is the per-request budget. */
|
|
16
|
+
const STARTUP_TIMEOUT_MS = 30_000;
|
|
12
17
|
/**
|
|
13
18
|
* Fastify-specific server adapter
|
|
14
19
|
* Provides high-performance HTTP server with schema validation
|
|
@@ -16,6 +21,8 @@ import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/
|
|
|
16
21
|
export class FastifyServerAdapter extends BaseServerAdapter {
|
|
17
22
|
app = null;
|
|
18
23
|
frameworkInitialized = false;
|
|
24
|
+
// The rate limiter counts nothing it cannot attribute, and says so once.
|
|
25
|
+
warnedNoClientAddress = false;
|
|
19
26
|
// The shutdown drain's record for each in-flight request. Fastify takes
|
|
20
27
|
// `request.id` from the caller's X-Request-ID, so that id can repeat across
|
|
21
28
|
// overlapping requests: a record named after it would be shared, and the
|
|
@@ -87,15 +94,19 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
87
94
|
await this.app.register(rateLimitModule.default, {
|
|
88
95
|
max: this.config.rateLimit.maxRequests,
|
|
89
96
|
timeWindow: windowMs,
|
|
97
|
+
// The key is resolved here, from the TCP peer or, behind a declared
|
|
98
|
+
// proxy, the client it reports. A request with no attributable
|
|
99
|
+
// address passes rather than share a bucket.
|
|
100
|
+
keyGenerator: (request) => this.clientKeyFor(request) ?? "",
|
|
101
|
+
allowList: (request) => this.clientKeyFor(request) === undefined,
|
|
102
|
+
// The plugin throws what this builds, so the error handler below
|
|
103
|
+
// needs `statusCode` to answer 429; without it every exceeded limit
|
|
104
|
+
// was a 500.
|
|
90
105
|
errorResponseBuilder: (_request, context) => ({
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
metadata: {
|
|
96
|
-
retryAfter: Math.ceil(context.ttl / 1000),
|
|
97
|
-
timestamp: new Date().toISOString(),
|
|
98
|
-
},
|
|
106
|
+
statusCode: 429,
|
|
107
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
108
|
+
message: this.config.rateLimit.message,
|
|
109
|
+
retryAfter: Math.ceil(context.ttl / 1000),
|
|
99
110
|
}),
|
|
100
111
|
});
|
|
101
112
|
// Add hook to set Retry-After header for 429 responses
|
|
@@ -131,7 +142,25 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
131
142
|
});
|
|
132
143
|
return;
|
|
133
144
|
}
|
|
134
|
-
|
|
145
|
+
// An exceeded rate limit, thrown by @fastify/rate-limit as the object
|
|
146
|
+
// errorResponseBuilder built.
|
|
147
|
+
if (error.statusCode === 429) {
|
|
148
|
+
reply.status(429).send({
|
|
149
|
+
error: {
|
|
150
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
151
|
+
message: this.config.rateLimit.message,
|
|
152
|
+
},
|
|
153
|
+
metadata: {
|
|
154
|
+
requestId,
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
retryAfter: error.retryAfter,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Fastify's own client errors (an unparsable body, an unsupported
|
|
162
|
+
// media type) carry a message the client may see; a 5xx's does not.
|
|
163
|
+
if (error.statusCode && error.statusCode < 500) {
|
|
135
164
|
reply.status(error.statusCode).send({
|
|
136
165
|
error: {
|
|
137
166
|
code: `HTTP_${error.statusCode}`,
|
|
@@ -170,6 +199,21 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
170
199
|
});
|
|
171
200
|
this.frameworkInitialized = true;
|
|
172
201
|
}
|
|
202
|
+
/** The address a request is counted against; `undefined` when there is none. */
|
|
203
|
+
clientKeyFor(request) {
|
|
204
|
+
const forwarded = request.headers["x-forwarded-for"];
|
|
205
|
+
const key = resolveClientAddress({
|
|
206
|
+
remoteAddress: request.socket?.remoteAddress ?? request.ip,
|
|
207
|
+
forwardedFor: Array.isArray(forwarded) ? forwarded.join(",") : forwarded,
|
|
208
|
+
trustProxy: this.config.rateLimit.trustProxy,
|
|
209
|
+
trustedProxies: this.config.rateLimit.trustedProxies,
|
|
210
|
+
});
|
|
211
|
+
if (key === undefined && !this.warnedNoClientAddress) {
|
|
212
|
+
this.warnedNoClientAddress = true;
|
|
213
|
+
logger.alwaysStderr("[FastifyAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
|
|
214
|
+
}
|
|
215
|
+
return key;
|
|
216
|
+
}
|
|
173
217
|
/**
|
|
174
218
|
* Parse body limit string to number (bytes)
|
|
175
219
|
*/
|
|
@@ -233,11 +277,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
233
277
|
const ctx = this.createContext({
|
|
234
278
|
requestId,
|
|
235
279
|
method: request.method,
|
|
236
|
-
path: request.url
|
|
280
|
+
path: stripQuery(request.url),
|
|
237
281
|
headers: request.headers,
|
|
238
282
|
query: request.query,
|
|
239
283
|
params: request.params,
|
|
240
284
|
body: request.body,
|
|
285
|
+
remoteAddress: request.socket?.remoteAddress ?? request.ip,
|
|
241
286
|
});
|
|
242
287
|
// Copy response headers from middleware (stored in request by middleware)
|
|
243
288
|
const reqWithHeaders = request;
|
|
@@ -245,6 +290,17 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
245
290
|
ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
|
|
246
291
|
}
|
|
247
292
|
this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
|
|
293
|
+
// The handler learns of a client that went away, and a request over
|
|
294
|
+
// `config.timeout` is answered 504 and its work aborted. Fastify used
|
|
295
|
+
// to apply that timeout only to its own listen().
|
|
296
|
+
const abort = this.attachRequestAbort(ctx, {
|
|
297
|
+
onClientClose: (listener) => request.raw.on("close", () => {
|
|
298
|
+
if (!reply.raw.writableFinished) {
|
|
299
|
+
listener();
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
applyTimeout: !route.streaming?.enabled,
|
|
303
|
+
});
|
|
248
304
|
// Emit request event
|
|
249
305
|
this.emit("request", {
|
|
250
306
|
requestId,
|
|
@@ -252,38 +308,66 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
252
308
|
path: ctx.path,
|
|
253
309
|
timestamp: new Date(),
|
|
254
310
|
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Execute handler
|
|
260
|
-
const result = await route.handler(ctx);
|
|
261
|
-
const duration = Date.now() - startTime;
|
|
262
|
-
// A route that answers in its own wire format returns a web Response.
|
|
263
|
-
if (isWebResponse(result)) {
|
|
264
|
-
const raw = await readWebResponse(result);
|
|
265
|
-
this.emit("response", {
|
|
266
|
-
requestId,
|
|
267
|
-
statusCode: raw.status,
|
|
268
|
-
duration,
|
|
269
|
-
timestamp: new Date(),
|
|
270
|
-
});
|
|
271
|
-
for (const [key, value] of Object.entries({
|
|
272
|
-
...ctx.responseHeaders,
|
|
273
|
-
...raw.headers,
|
|
274
|
-
})) {
|
|
275
|
-
reply.header(key, value);
|
|
311
|
+
try {
|
|
312
|
+
// Handle streaming if configured
|
|
313
|
+
if (route.streaming?.enabled) {
|
|
314
|
+
return await this.handleStreamingResponse(reply, ctx, route);
|
|
276
315
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
316
|
+
// Execute handler
|
|
317
|
+
const result = abort.deadline
|
|
318
|
+
? await Promise.race([route.handler(ctx), abort.deadline])
|
|
319
|
+
: await route.handler(ctx);
|
|
320
|
+
const duration = Date.now() - startTime;
|
|
321
|
+
// A route that answers in its own wire format returns a web Response.
|
|
322
|
+
if (isWebResponse(result)) {
|
|
323
|
+
const raw = await readWebResponse(result);
|
|
324
|
+
this.emit("response", {
|
|
325
|
+
requestId,
|
|
326
|
+
statusCode: raw.status,
|
|
327
|
+
duration,
|
|
328
|
+
timestamp: new Date(),
|
|
329
|
+
});
|
|
330
|
+
for (const [key, value] of Object.entries({
|
|
331
|
+
...ctx.responseHeaders,
|
|
332
|
+
...raw.headers,
|
|
333
|
+
})) {
|
|
334
|
+
reply.header(key, value);
|
|
335
|
+
}
|
|
336
|
+
reply.status(raw.status);
|
|
337
|
+
return reply.send(raw.body);
|
|
338
|
+
}
|
|
339
|
+
// Check if result is an error response
|
|
340
|
+
if (isErrorResponse(result)) {
|
|
341
|
+
const statusCode = result.httpStatus ?? 500;
|
|
342
|
+
// Emit response event with error status
|
|
343
|
+
this.emit("response", {
|
|
344
|
+
requestId,
|
|
345
|
+
statusCode,
|
|
346
|
+
duration,
|
|
347
|
+
timestamp: new Date(),
|
|
348
|
+
});
|
|
349
|
+
// Apply response headers from middleware and handler
|
|
350
|
+
if (ctx.responseHeaders) {
|
|
351
|
+
for (const [key, value] of Object.entries(ctx.responseHeaders)) {
|
|
352
|
+
reply.header(key, value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Return error response with proper status code
|
|
356
|
+
reply.status(statusCode);
|
|
357
|
+
return {
|
|
358
|
+
error: result.error,
|
|
359
|
+
metadata: {
|
|
360
|
+
...result.metadata,
|
|
361
|
+
requestId,
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
363
|
+
duration,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Emit response event
|
|
284
368
|
this.emit("response", {
|
|
285
369
|
requestId,
|
|
286
|
-
statusCode,
|
|
370
|
+
statusCode: 200,
|
|
287
371
|
duration,
|
|
288
372
|
timestamp: new Date(),
|
|
289
373
|
});
|
|
@@ -293,40 +377,43 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
293
377
|
reply.header(key, value);
|
|
294
378
|
}
|
|
295
379
|
}
|
|
296
|
-
// Return
|
|
297
|
-
reply.status(statusCode);
|
|
380
|
+
// Return formatted response
|
|
298
381
|
return {
|
|
299
|
-
|
|
382
|
+
data: result,
|
|
300
383
|
metadata: {
|
|
301
|
-
...result.metadata,
|
|
302
384
|
requestId,
|
|
303
385
|
timestamp: new Date().toISOString(),
|
|
304
386
|
duration,
|
|
305
387
|
},
|
|
306
388
|
};
|
|
307
389
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
390
|
+
catch (error) {
|
|
391
|
+
// The exception's text stays in the log under this request id; the
|
|
392
|
+
// client gets a fixed message for its category.
|
|
393
|
+
const shaped = shapeRouteError("FastifyAdapter", requestId, error);
|
|
394
|
+
this.emitRequestError({
|
|
395
|
+
requestId,
|
|
396
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
397
|
+
timestamp: new Date(),
|
|
398
|
+
});
|
|
399
|
+
this.emit("response", {
|
|
400
|
+
requestId,
|
|
401
|
+
statusCode: shaped.status,
|
|
402
|
+
duration: Date.now() - startTime,
|
|
403
|
+
timestamp: new Date(),
|
|
404
|
+
});
|
|
405
|
+
if (reply.sent || reply.raw.headersSent) {
|
|
406
|
+
return;
|
|
319
407
|
}
|
|
408
|
+
reply.status(shaped.status);
|
|
409
|
+
return {
|
|
410
|
+
error: { code: shaped.code, message: shaped.message },
|
|
411
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
abort.release();
|
|
320
416
|
}
|
|
321
|
-
// Return formatted response
|
|
322
|
-
return {
|
|
323
|
-
data: result,
|
|
324
|
-
metadata: {
|
|
325
|
-
requestId,
|
|
326
|
-
timestamp: new Date().toISOString(),
|
|
327
|
-
duration,
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
417
|
},
|
|
331
418
|
});
|
|
332
419
|
}
|
|
@@ -363,9 +450,13 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
363
450
|
reply.raw.end();
|
|
364
451
|
}
|
|
365
452
|
catch (error) {
|
|
453
|
+
const shaped = shapeRouteError("FastifyAdapter", ctx.requestId, error);
|
|
366
454
|
reply.raw.write(`event: error\n`);
|
|
367
455
|
reply.raw.write(`data: ${JSON.stringify({
|
|
368
|
-
error:
|
|
456
|
+
error: shaped.message,
|
|
457
|
+
code: shaped.code,
|
|
458
|
+
status: shaped.status,
|
|
459
|
+
requestId: ctx.requestId,
|
|
369
460
|
})}\n\n`);
|
|
370
461
|
reply.raw.end();
|
|
371
462
|
}
|
|
@@ -378,14 +469,16 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
378
469
|
throw new Error("Fastify app not initialized. Call initialize() before registering middleware.");
|
|
379
470
|
}
|
|
380
471
|
this.app.addHook("preHandler", async (request, _reply) => {
|
|
472
|
+
// Matched on the path alone, at segment boundaries: `request.url`
|
|
473
|
+
// carries the query string, so `/api/health?x` used to miss an
|
|
474
|
+
// exemption for `/api/health` and `/api/healthz` used to take it.
|
|
475
|
+
const requestPath = stripQuery(request.url);
|
|
381
476
|
// Skip excluded paths
|
|
382
|
-
if (middleware.excludePaths
|
|
477
|
+
if (pathMatchesAny(requestPath, middleware.excludePaths)) {
|
|
383
478
|
return;
|
|
384
479
|
}
|
|
385
480
|
// Check if path matches
|
|
386
|
-
|
|
387
|
-
const matches = paths.some((p) => request.url.startsWith(p) || p === "*");
|
|
388
|
-
if (!matches) {
|
|
481
|
+
if (!pathMatchesAny(requestPath, middleware.paths || ["/"])) {
|
|
389
482
|
return;
|
|
390
483
|
}
|
|
391
484
|
// Initialize response headers storage if not present
|
|
@@ -397,11 +490,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
397
490
|
const ctx = this.createContext({
|
|
398
491
|
requestId: request.id,
|
|
399
492
|
method: request.method,
|
|
400
|
-
path:
|
|
493
|
+
path: requestPath,
|
|
401
494
|
headers: request.headers,
|
|
402
495
|
query: request.query,
|
|
403
496
|
params: request.params,
|
|
404
497
|
body: request.body,
|
|
498
|
+
remoteAddress: request.socket?.remoteAddress ?? request.ip,
|
|
405
499
|
});
|
|
406
500
|
// Copy existing response headers to context
|
|
407
501
|
ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
|
|
@@ -438,7 +532,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
438
532
|
const app = this.app;
|
|
439
533
|
this.lifecycleState = "starting";
|
|
440
534
|
const { port, host } = this.config;
|
|
441
|
-
const startupTimeout =
|
|
535
|
+
const startupTimeout = STARTUP_TIMEOUT_MS;
|
|
442
536
|
// Track connections via Fastify hooks (must be registered before listen)
|
|
443
537
|
app.addHook("onRequest", async (request) => {
|
|
444
538
|
const connectionId = `conn-${this.generateRequestId()}`;
|
|
@@ -457,6 +551,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
457
551
|
this.startTime = new Date();
|
|
458
552
|
this.lifecycleState = "running";
|
|
459
553
|
logger.info(`[FastifyAdapter] Server started on ${host}:${port}`);
|
|
554
|
+
this.warnIfSensitiveRoutesUnauthenticated();
|
|
460
555
|
this.emit("started", {
|
|
461
556
|
port,
|
|
462
557
|
host,
|
|
@@ -17,9 +17,10 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
17
17
|
private rateLimitCleanupInterval?;
|
|
18
18
|
private requestContextStore;
|
|
19
19
|
private requestIds;
|
|
20
|
-
private HTTPExceptionCtor;
|
|
21
20
|
private streamSSEFn;
|
|
22
21
|
private parseFormBody;
|
|
22
|
+
private getConnInfoFn?;
|
|
23
|
+
private warnedNoClientAddress;
|
|
23
24
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
24
25
|
/**
|
|
25
26
|
* Create rate limiter middleware for Hono
|
|
@@ -84,6 +85,13 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
84
85
|
getFrameworkInstance(): unknown;
|
|
85
86
|
getNativeServer(): unknown;
|
|
86
87
|
private extractHeaders;
|
|
88
|
+
/**
|
|
89
|
+
* The runtime's connection-info helper: `@hono/node-server`'s on Node, the
|
|
90
|
+
* Bun or Deno adapter's there. Absent on a runtime that offers none.
|
|
91
|
+
*/
|
|
92
|
+
private loadConnInfo;
|
|
93
|
+
/** The TCP peer of this request, when the runtime reports one. */
|
|
94
|
+
private remoteAddressFor;
|
|
87
95
|
private extractQuery;
|
|
88
96
|
/**
|
|
89
97
|
* A request's context, or the 413 that answers a JSON or form body over its
|