@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
|
@@ -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
|
/**
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
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
|
+
import { resolveClientAddress } from "../utils/clientAddress.js";
|
|
10
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
11
|
+
import { pathMatchesAny } from "../utils/pathMatch.js";
|
|
9
12
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
13
|
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readRawBody, readWebResponse, } from "../utils/rawBody.js";
|
|
11
14
|
/**
|
|
@@ -20,6 +23,8 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
20
23
|
rateLimitStore = new Map();
|
|
21
24
|
rateLimitCleanupInterval;
|
|
22
25
|
sockets = new Set();
|
|
26
|
+
// The rate limiter counts nothing it cannot attribute, and says so once.
|
|
27
|
+
warnedNoClientAddress = false;
|
|
23
28
|
// Middleware from registerMiddleware(). It runs through one dispatcher that
|
|
24
29
|
// is mounted ahead of the router: app.use() after the router is mounted
|
|
25
30
|
// would only ever see requests no route matched.
|
|
@@ -72,12 +77,15 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
72
77
|
});
|
|
73
78
|
const adapterError = error instanceof ServerAdapterError ? error : undefined;
|
|
74
79
|
const statusCode = adapterError?.getHttpStatus() || err.status || err.statusCode || 500;
|
|
80
|
+
// A 4xx carries a message this server or Koa composed; any 5xx's
|
|
81
|
+
// text stays in the log.
|
|
82
|
+
const serverSide = statusCode >= 500;
|
|
75
83
|
ctx.status = statusCode;
|
|
76
84
|
ctx.body = {
|
|
77
85
|
error: {
|
|
78
86
|
code: adapterError?.code ??
|
|
79
|
-
(
|
|
80
|
-
message:
|
|
87
|
+
(serverSide ? "INTERNAL_ERROR" : `HTTP_${statusCode}`),
|
|
88
|
+
message: serverSide ? "An internal error occurred" : err.message,
|
|
81
89
|
},
|
|
82
90
|
metadata: {
|
|
83
91
|
requestId,
|
|
@@ -92,10 +100,13 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
92
100
|
const cors = corsModule.default;
|
|
93
101
|
this.app.use(cors({
|
|
94
102
|
origin: (ctx) => {
|
|
95
|
-
|
|
103
|
+
// The wildcard is sent as the literal "*": reflecting the
|
|
104
|
+
// request's Origin instead would grant any page a credentialed
|
|
105
|
+
// allowance the wildcard itself cannot carry.
|
|
96
106
|
if (this.config.cors.origins.includes("*")) {
|
|
97
|
-
return
|
|
107
|
+
return "*";
|
|
98
108
|
}
|
|
109
|
+
const origin = ctx.request.headers.origin ?? "";
|
|
99
110
|
return this.config.cors.origins.includes(origin) ? origin : "";
|
|
100
111
|
},
|
|
101
112
|
allowMethods: this.config.cors.methods?.join(","),
|
|
@@ -165,18 +176,38 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
165
176
|
createRateLimiter() {
|
|
166
177
|
return async (ctx, next) => {
|
|
167
178
|
// Skip rate limiting for excluded paths
|
|
168
|
-
if (this.config.rateLimit.skipPaths) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
if (pathMatchesAny(ctx.path, this.config.rateLimit.skipPaths)) {
|
|
180
|
+
await next();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// The TCP peer, or behind a declared proxy the client it reports. A
|
|
184
|
+
// header alone used to be the key, so a rotating X-Forwarded-For escaped
|
|
185
|
+
// the limit; a request with no attributable address is let through
|
|
186
|
+
// rather than pooled with every other such request.
|
|
187
|
+
const key = this.config.rateLimit.keyGenerator
|
|
188
|
+
? this.config.rateLimit.keyGenerator(this.createContext({
|
|
189
|
+
requestId: ctx.state.requestId,
|
|
190
|
+
method: ctx.method,
|
|
191
|
+
path: ctx.path,
|
|
192
|
+
headers: ctx.headers,
|
|
193
|
+
query: ctx.query,
|
|
194
|
+
params: {},
|
|
195
|
+
remoteAddress: ctx.req.socket?.remoteAddress,
|
|
196
|
+
}))
|
|
197
|
+
: resolveClientAddress({
|
|
198
|
+
remoteAddress: ctx.req.socket?.remoteAddress,
|
|
199
|
+
forwardedFor: ctx.get("x-forwarded-for") || undefined,
|
|
200
|
+
trustProxy: this.config.rateLimit.trustProxy,
|
|
201
|
+
trustedProxies: this.config.rateLimit.trustedProxies,
|
|
202
|
+
});
|
|
203
|
+
if (key === undefined) {
|
|
204
|
+
if (!this.warnedNoClientAddress) {
|
|
205
|
+
this.warnedNoClientAddress = true;
|
|
206
|
+
logger.alwaysStderr("[KoaAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
|
|
174
207
|
}
|
|
208
|
+
await next();
|
|
209
|
+
return;
|
|
175
210
|
}
|
|
176
|
-
const key = ctx.get("x-forwarded-for") ||
|
|
177
|
-
ctx.get("x-real-ip") ||
|
|
178
|
-
ctx.ip ||
|
|
179
|
-
"unknown";
|
|
180
211
|
const now = Date.now();
|
|
181
212
|
const windowMs = this.config.rateLimit.windowMs;
|
|
182
213
|
const maxRequests = this.config.rateLimit.maxRequests;
|
|
@@ -256,12 +287,24 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
256
287
|
query: ctx.query,
|
|
257
288
|
params: ctx.params,
|
|
258
289
|
body: ctx.request.body,
|
|
290
|
+
remoteAddress: ctx.req.socket?.remoteAddress,
|
|
259
291
|
});
|
|
260
292
|
// Copy response headers from middleware (stored in ctx.state by middleware)
|
|
261
293
|
if (ctx.state.responseHeaders) {
|
|
262
294
|
serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
|
|
263
295
|
}
|
|
264
296
|
this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
|
|
297
|
+
// The handler learns of a client that went away, and a request over
|
|
298
|
+
// `config.timeout` is answered 504 and its work aborted. Koa applied no
|
|
299
|
+
// request timeout at all.
|
|
300
|
+
const abort = this.attachRequestAbort(serverCtx, {
|
|
301
|
+
onClientClose: (listener) => ctx.req.on("close", () => {
|
|
302
|
+
if (!ctx.res.writableFinished) {
|
|
303
|
+
listener();
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
306
|
+
applyTimeout: !route.streaming?.enabled,
|
|
307
|
+
});
|
|
265
308
|
// Emit request event
|
|
266
309
|
this.emit("request", {
|
|
267
310
|
requestId,
|
|
@@ -269,39 +312,68 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
269
312
|
path: serverCtx.path,
|
|
270
313
|
timestamp: new Date(),
|
|
271
314
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Execute handler
|
|
277
|
-
const result = await route.handler(serverCtx);
|
|
278
|
-
const duration = Date.now() - startTime;
|
|
279
|
-
// A route that answers in its own wire format returns a web Response.
|
|
280
|
-
if (isWebResponse(result)) {
|
|
281
|
-
const raw = await readWebResponse(result);
|
|
282
|
-
this.emit("response", {
|
|
283
|
-
requestId,
|
|
284
|
-
statusCode: raw.status,
|
|
285
|
-
duration,
|
|
286
|
-
timestamp: new Date(),
|
|
287
|
-
});
|
|
288
|
-
for (const [key, value] of Object.entries({
|
|
289
|
-
...serverCtx.responseHeaders,
|
|
290
|
-
...raw.headers,
|
|
291
|
-
})) {
|
|
292
|
-
ctx.set(key, value);
|
|
315
|
+
try {
|
|
316
|
+
// Handle streaming if configured
|
|
317
|
+
if (route.streaming?.enabled) {
|
|
318
|
+
return await this.handleStreamingResponse(ctx, serverCtx, route);
|
|
293
319
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
320
|
+
// Execute handler
|
|
321
|
+
const result = abort.deadline
|
|
322
|
+
? await Promise.race([route.handler(serverCtx), abort.deadline])
|
|
323
|
+
: await route.handler(serverCtx);
|
|
324
|
+
const duration = Date.now() - startTime;
|
|
325
|
+
// A route that answers in its own wire format returns a web Response.
|
|
326
|
+
if (isWebResponse(result)) {
|
|
327
|
+
const raw = await readWebResponse(result);
|
|
328
|
+
this.emit("response", {
|
|
329
|
+
requestId,
|
|
330
|
+
statusCode: raw.status,
|
|
331
|
+
duration,
|
|
332
|
+
timestamp: new Date(),
|
|
333
|
+
});
|
|
334
|
+
for (const [key, value] of Object.entries({
|
|
335
|
+
...serverCtx.responseHeaders,
|
|
336
|
+
...raw.headers,
|
|
337
|
+
})) {
|
|
338
|
+
ctx.set(key, value);
|
|
339
|
+
}
|
|
340
|
+
ctx.status = raw.status;
|
|
341
|
+
ctx.body = raw.body;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Check if result is an error response
|
|
345
|
+
if (isErrorResponse(result)) {
|
|
346
|
+
const statusCode = result.httpStatus ?? 500;
|
|
347
|
+
// Emit response event with error status
|
|
348
|
+
this.emit("response", {
|
|
349
|
+
requestId,
|
|
350
|
+
statusCode,
|
|
351
|
+
duration,
|
|
352
|
+
timestamp: new Date(),
|
|
353
|
+
});
|
|
354
|
+
// Apply response headers from middleware and handler
|
|
355
|
+
if (serverCtx.responseHeaders) {
|
|
356
|
+
for (const [key, value] of Object.entries(serverCtx.responseHeaders)) {
|
|
357
|
+
ctx.set(key, value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Return error response with proper status code
|
|
361
|
+
ctx.status = statusCode;
|
|
362
|
+
ctx.body = {
|
|
363
|
+
error: result.error,
|
|
364
|
+
metadata: {
|
|
365
|
+
...result.metadata,
|
|
366
|
+
requestId,
|
|
367
|
+
timestamp: new Date().toISOString(),
|
|
368
|
+
duration,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Emit response event
|
|
302
374
|
this.emit("response", {
|
|
303
375
|
requestId,
|
|
304
|
-
statusCode,
|
|
376
|
+
statusCode: 200,
|
|
305
377
|
duration,
|
|
306
378
|
timestamp: new Date(),
|
|
307
379
|
});
|
|
@@ -311,41 +383,44 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
311
383
|
ctx.set(key, value);
|
|
312
384
|
}
|
|
313
385
|
}
|
|
314
|
-
// Return
|
|
315
|
-
ctx.status = statusCode;
|
|
386
|
+
// Return formatted response
|
|
316
387
|
ctx.body = {
|
|
317
|
-
|
|
388
|
+
data: result,
|
|
318
389
|
metadata: {
|
|
319
|
-
...result.metadata,
|
|
320
390
|
requestId,
|
|
321
391
|
timestamp: new Date().toISOString(),
|
|
322
392
|
duration,
|
|
323
393
|
},
|
|
324
394
|
};
|
|
325
|
-
return;
|
|
326
395
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
catch (error) {
|
|
397
|
+
// The exception's text stays in the log under this request id; the
|
|
398
|
+
// client gets a fixed message for its category.
|
|
399
|
+
const shaped = shapeRouteError("KoaAdapter", requestId, error);
|
|
400
|
+
this.emitRequestError({
|
|
401
|
+
requestId,
|
|
402
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
403
|
+
timestamp: new Date(),
|
|
404
|
+
});
|
|
405
|
+
this.emit("response", {
|
|
406
|
+
requestId,
|
|
407
|
+
statusCode: shaped.status,
|
|
408
|
+
duration: Date.now() - startTime,
|
|
409
|
+
timestamp: new Date(),
|
|
410
|
+
});
|
|
411
|
+
if (ctx.res.headersSent) {
|
|
412
|
+
ctx.res.end();
|
|
413
|
+
return;
|
|
338
414
|
}
|
|
415
|
+
ctx.status = shaped.status;
|
|
416
|
+
ctx.body = {
|
|
417
|
+
error: { code: shaped.code, message: shaped.message },
|
|
418
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
abort.release();
|
|
339
423
|
}
|
|
340
|
-
// Return formatted response
|
|
341
|
-
ctx.body = {
|
|
342
|
-
data: result,
|
|
343
|
-
metadata: {
|
|
344
|
-
requestId,
|
|
345
|
-
timestamp: new Date().toISOString(),
|
|
346
|
-
duration,
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
424
|
});
|
|
350
425
|
}
|
|
351
426
|
/**
|
|
@@ -382,9 +457,13 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
382
457
|
stream.end();
|
|
383
458
|
}
|
|
384
459
|
catch (error) {
|
|
460
|
+
const shaped = shapeRouteError("KoaAdapter", serverCtx.requestId, error);
|
|
385
461
|
stream.write(`event: error\n`);
|
|
386
462
|
stream.write(`data: ${JSON.stringify({
|
|
387
|
-
error:
|
|
463
|
+
error: shaped.message,
|
|
464
|
+
code: shaped.code,
|
|
465
|
+
status: shaped.status,
|
|
466
|
+
requestId: serverCtx.requestId,
|
|
388
467
|
})}\n\n`);
|
|
389
468
|
stream.end();
|
|
390
469
|
}
|
|
@@ -395,13 +474,11 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
395
474
|
registerFrameworkMiddleware(middleware) {
|
|
396
475
|
this.registeredMiddleware.push(async (ctx, next) => {
|
|
397
476
|
// Skip excluded paths
|
|
398
|
-
if (
|
|
477
|
+
if (pathMatchesAny(ctx.path, middleware.excludePaths)) {
|
|
399
478
|
return next();
|
|
400
479
|
}
|
|
401
480
|
// Check if path matches
|
|
402
|
-
|
|
403
|
-
const matches = paths.some((p) => ctx.path.startsWith(p) || p === "*");
|
|
404
|
-
if (!matches) {
|
|
481
|
+
if (!pathMatchesAny(ctx.path, middleware.paths || ["/"])) {
|
|
405
482
|
return next();
|
|
406
483
|
}
|
|
407
484
|
// Initialize response headers storage in ctx.state if not present
|
|
@@ -417,6 +494,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
417
494
|
query: ctx.query,
|
|
418
495
|
params: ctx.params,
|
|
419
496
|
body: ctx.request.body,
|
|
497
|
+
remoteAddress: ctx.req.socket?.remoteAddress,
|
|
420
498
|
});
|
|
421
499
|
// Copy existing response headers to context
|
|
422
500
|
serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
|
|
@@ -470,6 +548,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
470
548
|
this.isRunning = true;
|
|
471
549
|
this.startTime = new Date();
|
|
472
550
|
this.lifecycleState = "running";
|
|
551
|
+
this.warnIfSensitiveRoutesUnauthenticated();
|
|
473
552
|
// Track connections for graceful shutdown
|
|
474
553
|
this.server?.on("connection", (socket) => {
|
|
475
554
|
const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes
|
|
|
42
42
|
export { BaseDataStreamWriter, createDataStreamResponse, createDataStreamWriter, createNDJSONHeaders, createSSEHeaders, DataStreamResponse, formatSSEEvent, pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
|
|
43
43
|
export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
|
|
44
44
|
export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
|
|
45
|
+
export { isLoopbackAddress, resolveClientAddress, } from "./utils/clientAddress.js";
|
|
46
|
+
export { clientFacingError } from "./utils/clientError.js";
|
|
45
47
|
export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, TranscribeConfigSchema, TranscribeRequestSchema, TranscribeStreamConfigSchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
|
|
46
48
|
export { createAgentWebSocketHandler, WebSocketConnectionManager, WebSocketMessageRouter, } from "./websocket/index.js";
|
|
47
49
|
export { attachTranscribeWebSocket } from "./websocket/transcribeWebSocket.js";
|
package/dist/server/index.js
CHANGED
|
@@ -117,6 +117,10 @@ pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
|
|
|
117
117
|
// Export error constants
|
|
118
118
|
export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
|
|
119
119
|
export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
|
|
120
|
+
// The address a request is counted against (for a custom rate-limit
|
|
121
|
+
// keyGenerator behind a reverse proxy) and the loopback test the adapters use.
|
|
122
|
+
export { isLoopbackAddress, resolveClientAddress, } from "./utils/clientAddress.js";
|
|
123
|
+
export { clientFacingError } from "./utils/clientError.js";
|
|
120
124
|
// ============================================
|
|
121
125
|
// Validation Utilities
|
|
122
126
|
// ============================================
|
|
@@ -65,6 +65,7 @@ export function createAuthMiddleware(config) {
|
|
|
65
65
|
name: "authentication",
|
|
66
66
|
order: 10, // Run early but after request ID
|
|
67
67
|
excludePaths: skipPaths,
|
|
68
|
+
authenticates: true,
|
|
68
69
|
handler: async (ctx, next) => {
|
|
69
70
|
// The playground header is a development convenience, so it is honoured
|
|
70
71
|
// only when NODE_ENV is exactly "development". An unset NODE_ENV (the
|
|
@@ -286,12 +287,15 @@ export function createBearerAuthMiddleware(validate, options = {}) {
|
|
|
286
287
|
name: "bearer-auth",
|
|
287
288
|
order: 10,
|
|
288
289
|
excludePaths: skipPaths,
|
|
290
|
+
authenticates: true,
|
|
289
291
|
handler: async (ctx, next) => {
|
|
290
292
|
const authHeader = ctx.headers[headerName.toLowerCase()];
|
|
291
|
-
// Extract bearer token
|
|
293
|
+
// Extract bearer token. The token must open with a non-whitespace
|
|
294
|
+
// character, as in extractToken(): with `\s+(.+)` a header that failed
|
|
295
|
+
// to match was retried at every split, quadratic in its length.
|
|
292
296
|
let token = null;
|
|
293
297
|
if (authHeader) {
|
|
294
|
-
const match = authHeader.match(/^Bearer\s+(
|
|
298
|
+
const match = authHeader.match(/^Bearer\s+(\S.*)$/i);
|
|
295
299
|
token = match ? match[1] : null;
|
|
296
300
|
}
|
|
297
301
|
// Handle missing token
|
|
@@ -136,10 +136,15 @@ export function createCacheMiddleware(config) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
|
-
* Default cache key generator
|
|
139
|
+
* Default cache key generator. An authenticated user's id is part of the key,
|
|
140
|
+
* so one caller's cached response is never served to another; without a user
|
|
141
|
+
* the key is the request alone, as before.
|
|
140
142
|
*/
|
|
141
143
|
function defaultKeyGenerator(ctx) {
|
|
142
144
|
const parts = [ctx.method, ctx.path];
|
|
145
|
+
if (ctx.user?.id) {
|
|
146
|
+
parts.push(`user=${ctx.user.id}`);
|
|
147
|
+
}
|
|
143
148
|
// Include sorted query params
|
|
144
149
|
const queryKeys = Object.keys(ctx.query).sort();
|
|
145
150
|
if (queryKeys.length > 0) {
|