@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
|
@@ -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
|
|
@@ -7,6 +7,9 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
9
9
|
import { markContextCarried } from "../utils/carriedContextMark.js";
|
|
10
|
+
import { resolveClientAddress } from "../utils/clientAddress.js";
|
|
11
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
12
|
+
import { pathMatchesAny } from "../utils/pathMatch.js";
|
|
10
13
|
import { isErrorResponse } from "../utils/validation.js";
|
|
11
14
|
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
|
|
12
15
|
/**
|
|
@@ -32,9 +35,13 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
32
35
|
// Populated by initializeFramework's dynamic imports; used by methods that
|
|
33
36
|
// run after initialization (route registration, streaming) and so cannot
|
|
34
37
|
// rely on initializeFramework's own local closure variables.
|
|
35
|
-
HTTPExceptionCtor;
|
|
36
38
|
streamSSEFn;
|
|
37
39
|
parseFormBody;
|
|
40
|
+
// The runtime's view of the TCP peer (Node, Bun or Deno); undefined where the
|
|
41
|
+
// runtime offers none, and then requests carry no address.
|
|
42
|
+
getConnInfoFn;
|
|
43
|
+
// The rate limiter counts nothing it cannot attribute, and says so once.
|
|
44
|
+
warnedNoClientAddress = false;
|
|
38
45
|
constructor(neurolink, config = {}) {
|
|
39
46
|
super(neurolink, config);
|
|
40
47
|
}
|
|
@@ -44,12 +51,8 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
44
51
|
createRateLimiter() {
|
|
45
52
|
return async (c, next) => {
|
|
46
53
|
// Skip rate limiting for excluded paths
|
|
47
|
-
if (this.config.rateLimit.skipPaths) {
|
|
48
|
-
|
|
49
|
-
if (c.req.path.startsWith(skipPath)) {
|
|
50
|
-
return next();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
54
|
+
if (pathMatchesAny(c.req.path, this.config.rateLimit.skipPaths)) {
|
|
55
|
+
return next();
|
|
53
56
|
}
|
|
54
57
|
// Use custom keyGenerator if provided, otherwise default to IP-based key
|
|
55
58
|
let key;
|
|
@@ -71,14 +74,29 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
71
74
|
externalServerManager: this.externalServerManager,
|
|
72
75
|
timestamp: Date.now(),
|
|
73
76
|
metadata: {},
|
|
77
|
+
remoteAddress: this.remoteAddressFor(c),
|
|
74
78
|
};
|
|
75
79
|
key = this.config.rateLimit.keyGenerator(minCtx);
|
|
76
80
|
}
|
|
77
81
|
else {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
// The TCP peer, or behind a declared proxy the client it reports. A
|
|
83
|
+
// header alone used to be the key, so one shared "unknown" bucket
|
|
84
|
+
// served every direct client and a rotating X-Forwarded-For escaped
|
|
85
|
+
// the limit; a request with no attributable address is now let
|
|
86
|
+
// through rather than pooled with every other such request.
|
|
87
|
+
key = resolveClientAddress({
|
|
88
|
+
remoteAddress: this.remoteAddressFor(c),
|
|
89
|
+
forwardedFor: c.req.header("x-forwarded-for"),
|
|
90
|
+
trustProxy: this.config.rateLimit.trustProxy,
|
|
91
|
+
trustedProxies: this.config.rateLimit.trustedProxies,
|
|
92
|
+
});
|
|
93
|
+
if (key === undefined) {
|
|
94
|
+
if (!this.warnedNoClientAddress) {
|
|
95
|
+
this.warnedNoClientAddress = true;
|
|
96
|
+
logger.alwaysStderr("[HonoAdapter] The runtime reports no peer address for requests, so the rate limiter cannot count them; configure rateLimit.keyGenerator to limit by another key.");
|
|
97
|
+
}
|
|
98
|
+
return next();
|
|
99
|
+
}
|
|
82
100
|
}
|
|
83
101
|
const now = Date.now();
|
|
84
102
|
const windowMs = this.config.rateLimit.windowMs;
|
|
@@ -137,9 +155,9 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
137
155
|
this.importFrameworkDependency("hono/timeout", "hono"),
|
|
138
156
|
this.importFrameworkDependency("hono/utils/body", "hono"),
|
|
139
157
|
]);
|
|
140
|
-
this.HTTPExceptionCtor = HTTPException;
|
|
141
158
|
this.streamSSEFn = streamSSE;
|
|
142
159
|
this.parseFormBody = parseBody;
|
|
160
|
+
this.getConnInfoFn = await this.loadConnInfo();
|
|
143
161
|
this.app = new Hono();
|
|
144
162
|
// Add secure headers
|
|
145
163
|
this.app.use("*", secureHeaders());
|
|
@@ -235,6 +253,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
235
253
|
const startTime = Date.now();
|
|
236
254
|
// Track connection for graceful shutdown
|
|
237
255
|
this.trackConnection(connectionId, undefined, requestId);
|
|
256
|
+
let abort;
|
|
238
257
|
try {
|
|
239
258
|
// Extract path parameters
|
|
240
259
|
const params = c.req.param();
|
|
@@ -263,6 +282,15 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
263
282
|
}
|
|
264
283
|
ctx = created;
|
|
265
284
|
}
|
|
285
|
+
// The handler learns of a client that went away through the request's
|
|
286
|
+
// own signal, and of the request timeout through the same controller
|
|
287
|
+
// (Hono's timeout middleware answers 504; this stops the work).
|
|
288
|
+
abort = this.attachRequestAbort(ctx, {
|
|
289
|
+
onClientClose: (listener) => c.req.raw.signal.addEventListener("abort", listener, {
|
|
290
|
+
once: true,
|
|
291
|
+
}),
|
|
292
|
+
applyTimeout: !route.streaming?.enabled,
|
|
293
|
+
});
|
|
266
294
|
// Emit request event
|
|
267
295
|
this.emit("request", {
|
|
268
296
|
requestId,
|
|
@@ -344,15 +372,23 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
344
372
|
});
|
|
345
373
|
}
|
|
346
374
|
catch (error) {
|
|
347
|
-
|
|
348
|
-
|
|
375
|
+
// The exception's text stays in the log under this request id; the
|
|
376
|
+
// client gets a fixed message for its category. It used to get the
|
|
377
|
+
// text itself, environment variable names and upstream bodies included.
|
|
378
|
+
const shaped = shapeRouteError("HonoAdapter", requestId, error);
|
|
379
|
+
this.emit("response", {
|
|
349
380
|
requestId,
|
|
350
|
-
|
|
351
|
-
|
|
381
|
+
statusCode: shaped.status,
|
|
382
|
+
duration: Date.now() - startTime,
|
|
383
|
+
timestamp: new Date(),
|
|
352
384
|
});
|
|
353
|
-
|
|
385
|
+
return c.json({
|
|
386
|
+
error: { code: shaped.code, message: shaped.message },
|
|
387
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
388
|
+
}, shaped.status);
|
|
354
389
|
}
|
|
355
390
|
finally {
|
|
391
|
+
abort?.release();
|
|
356
392
|
// Untrack connection when request completes
|
|
357
393
|
this.untrackConnection(connectionId);
|
|
358
394
|
// Clean up context store to prevent memory leaks
|
|
@@ -417,13 +453,14 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
417
453
|
});
|
|
418
454
|
}
|
|
419
455
|
catch (error) {
|
|
456
|
+
const shaped = shapeRouteError("HonoAdapter", ctx.requestId, error);
|
|
420
457
|
await stream.writeSSE({
|
|
421
458
|
data: JSON.stringify({
|
|
422
459
|
type: "error",
|
|
423
460
|
error: {
|
|
424
|
-
code:
|
|
425
|
-
message:
|
|
426
|
-
status:
|
|
461
|
+
code: shaped.code,
|
|
462
|
+
message: shaped.message,
|
|
463
|
+
status: shaped.status,
|
|
427
464
|
},
|
|
428
465
|
timestamp: Date.now(),
|
|
429
466
|
}),
|
|
@@ -482,7 +519,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
482
519
|
for (const path of paths) {
|
|
483
520
|
this.app.use(path, async (c, next) => {
|
|
484
521
|
// Skip excluded paths
|
|
485
|
-
if (
|
|
522
|
+
if (pathMatchesAny(c.req.path, middleware.excludePaths)) {
|
|
486
523
|
return next();
|
|
487
524
|
}
|
|
488
525
|
// Extract path parameters
|
|
@@ -566,6 +603,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
566
603
|
this.startTime = new Date();
|
|
567
604
|
this.lifecycleState = "running";
|
|
568
605
|
logger.info(`[HonoAdapter] Server started on ${host}:${port}`);
|
|
606
|
+
this.warnIfSensitiveRoutesUnauthenticated();
|
|
569
607
|
this.emit("started", {
|
|
570
608
|
port,
|
|
571
609
|
host,
|
|
@@ -684,6 +722,45 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
684
722
|
});
|
|
685
723
|
return headers;
|
|
686
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* The runtime's connection-info helper: `@hono/node-server`'s on Node, the
|
|
727
|
+
* Bun or Deno adapter's there. Absent on a runtime that offers none.
|
|
728
|
+
*/
|
|
729
|
+
async loadConnInfo() {
|
|
730
|
+
try {
|
|
731
|
+
if (typeof Bun !== "undefined") {
|
|
732
|
+
const bun = await import("hono/bun");
|
|
733
|
+
return bun.getConnInfo;
|
|
734
|
+
}
|
|
735
|
+
if (typeof Deno !== "undefined") {
|
|
736
|
+
const deno = await import("hono/deno");
|
|
737
|
+
return deno.getConnInfo;
|
|
738
|
+
}
|
|
739
|
+
const node = await import("@hono/node-server/conninfo");
|
|
740
|
+
return node.getConnInfo;
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
logger.debug("[HonoAdapter] No connection-info helper for this runtime", {
|
|
744
|
+
error: error instanceof Error ? error.message : String(error),
|
|
745
|
+
});
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/** The TCP peer of this request, when the runtime reports one. */
|
|
750
|
+
remoteAddressFor(c) {
|
|
751
|
+
if (!this.getConnInfoFn) {
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const address = this.getConnInfoFn(c).remote.address;
|
|
756
|
+
return typeof address === "string" && address.length > 0
|
|
757
|
+
? address
|
|
758
|
+
: undefined;
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
687
764
|
extractQuery(c) {
|
|
688
765
|
const query = {};
|
|
689
766
|
const url = new URL(c.req.url);
|
|
@@ -726,6 +803,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
726
803
|
query: this.extractQuery(c),
|
|
727
804
|
params,
|
|
728
805
|
body,
|
|
806
|
+
remoteAddress: this.remoteAddressFor(c),
|
|
729
807
|
});
|
|
730
808
|
}
|
|
731
809
|
async extractBody(c) {
|
|
@@ -18,6 +18,7 @@ export declare class KoaServerAdapter extends BaseServerAdapter {
|
|
|
18
18
|
private rateLimitStore;
|
|
19
19
|
private rateLimitCleanupInterval?;
|
|
20
20
|
private sockets;
|
|
21
|
+
private warnedNoClientAddress;
|
|
21
22
|
private registeredMiddleware;
|
|
22
23
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
23
24
|
/**
|