@juspay/neurolink 13.8.14 → 13.8.15
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/dist/browser/neurolink.min.js +398 -398
- package/dist/cli/utils/serverUtils.d.ts +4 -0
- package/dist/cli/utils/serverUtils.js +5 -1
- package/dist/server/abstract/baseServerAdapter.d.ts +30 -1
- package/dist/server/abstract/baseServerAdapter.js +71 -0
- package/dist/server/adapters/expressAdapter.js +5 -0
- package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
- package/dist/server/adapters/fastifyAdapter.js +16 -3
- package/dist/server/adapters/honoAdapter.d.ts +6 -0
- package/dist/server/adapters/honoAdapter.js +67 -16
- package/dist/server/adapters/koaAdapter.js +5 -0
- package/dist/server/middleware/auth.js +3 -0
- package/dist/server/routes/agentRoutes.d.ts +42 -1
- package/dist/server/routes/agentRoutes.js +140 -31
- package/dist/server/routes/index.d.ts +9 -0
- package/dist/server/routes/index.js +15 -1
- package/dist/server/utils/carriedContextMark.d.ts +28 -0
- package/dist/server/utils/carriedContextMark.js +32 -0
- package/dist/types/server.d.ts +140 -2
- package/docs-site/static/search-index.json +6 -4
- package/package.json +2 -1
|
@@ -16,6 +16,10 @@ export declare function readServerApiKeys(): string[];
|
|
|
16
16
|
* Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
|
|
17
17
|
* Health probes stay open so orchestrators can reach them, and the dev
|
|
18
18
|
* playground header cannot bypass a key the operator configured.
|
|
19
|
+
*
|
|
20
|
+
* Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
|
|
21
|
+
* the agent routes know it names no person and do not warn when a request
|
|
22
|
+
* names its own userId.
|
|
19
23
|
*/
|
|
20
24
|
export declare function createServerApiKeyMiddleware(apiKeys: string[], basePath: string): MiddlewareDefinition;
|
|
21
25
|
/**
|
|
@@ -41,6 +41,10 @@ const matchesKey = (key, token) => key.length === token.length && timingSafeEqua
|
|
|
41
41
|
* Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
|
|
42
42
|
* Health probes stay open so orchestrators can reach them, and the dev
|
|
43
43
|
* playground header cannot bypass a key the operator configured.
|
|
44
|
+
*
|
|
45
|
+
* Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
|
|
46
|
+
* the agent routes know it names no person and do not warn when a request
|
|
47
|
+
* names its own userId.
|
|
44
48
|
*/
|
|
45
49
|
export function createServerApiKeyMiddleware(apiKeys, basePath) {
|
|
46
50
|
const keys = apiKeys.map((key) => Buffer.from(key, "utf8"));
|
|
@@ -53,7 +57,7 @@ export function createServerApiKeyMiddleware(apiKeys, basePath) {
|
|
|
53
57
|
validate: async (token) => {
|
|
54
58
|
const candidate = Buffer.from(token, "utf8");
|
|
55
59
|
return keys.some((key) => matchesKey(key, candidate))
|
|
56
|
-
? { id: "api-key" }
|
|
60
|
+
? { id: "api-key", shared: true }
|
|
57
61
|
: null;
|
|
58
62
|
},
|
|
59
63
|
skipPaths: [`${basePath.replace(/\/+$/, "")}/health`],
|
|
@@ -7,7 +7,7 @@ import { EventEmitter } from "events";
|
|
|
7
7
|
import type { ExternalServerManager } from "../../mcp/externalServerManager.js";
|
|
8
8
|
import type { MCPToolRegistry } from "../../mcp/toolRegistry.js";
|
|
9
9
|
import type { NeuroLink } from "../../neurolink.js";
|
|
10
|
-
import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
|
|
10
|
+
import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerCarriedContext, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
|
|
11
11
|
/**
|
|
12
12
|
* Abstract base class for server adapters
|
|
13
13
|
* Provides common functionality and defines the interface for framework-specific implementations
|
|
@@ -28,6 +28,7 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
|
|
|
28
28
|
protected lifecycleState: ServerLifecycleState;
|
|
29
29
|
protected activeConnections: Map<string, TrackedConnection>;
|
|
30
30
|
protected readonly shutdownConfig: RequiredShutdownConfig;
|
|
31
|
+
private carryRequestedByRoutes;
|
|
31
32
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
32
33
|
/**
|
|
33
34
|
* Initialize the underlying server framework
|
|
@@ -155,6 +156,34 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
|
|
|
155
156
|
* Only registers health routes if disableBuiltInHealth is false (default)
|
|
156
157
|
*/
|
|
157
158
|
protected registerBuiltInRoutes(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Ask the adapter to carry middleware state (`ctx.user` and the rest) to later
|
|
161
|
+
* middleware and the route handler, as the `carryMiddlewareState` option does.
|
|
162
|
+
* `registerAllRoutes()` calls it when given `perUserAuth`, which cannot work
|
|
163
|
+
* without the user in the handler. It takes effect for requests that start
|
|
164
|
+
* after the call, so call it before the server starts, as registering routes
|
|
165
|
+
* is.
|
|
166
|
+
*/
|
|
167
|
+
enableMiddlewareStateCarry(): void;
|
|
168
|
+
/** Whether this adapter carries middleware state to the handler. */
|
|
169
|
+
protected carriesMiddlewareState(): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* What middleware has set on its context that later middleware and the route
|
|
172
|
+
* handler must see. Each step gets a freshly built context, so unless this is
|
|
173
|
+
* parked in the framework's per-request storage an authenticated `ctx.user`
|
|
174
|
+
* set by middleware is gone before the handler runs. `undefined` when the
|
|
175
|
+
* adapter does not carry middleware state, so nothing is parked.
|
|
176
|
+
*/
|
|
177
|
+
protected captureCarriedContext(ctx: ServerContext): ServerCarriedContext | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* Put what `captureCarriedContext` saved onto a freshly built context, and
|
|
180
|
+
* mark the context as one that received the carry. The agent routes read the
|
|
181
|
+
* mark: a request that never used to reach its handler with `ctx.user` must
|
|
182
|
+
* not start being identified by it unless the server opts in. Does nothing
|
|
183
|
+
* when the adapter does not carry middleware state: the context stays the
|
|
184
|
+
* fresh one it always was, and unmarked.
|
|
185
|
+
*/
|
|
186
|
+
protected restoreCarriedContext(ctx: ServerContext, carried: ServerCarriedContext | undefined): void;
|
|
158
187
|
/**
|
|
159
188
|
* Generate unique request ID
|
|
160
189
|
*/
|
|
@@ -7,6 +7,7 @@ import { EventEmitter } from "events";
|
|
|
7
7
|
import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js";
|
|
8
8
|
import { withTimeout } from "../../utils/errorHandling.js";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { markContextCarried } from "../utils/carriedContextMark.js";
|
|
10
11
|
import { tryImport } from "../../utils/tryImport.js";
|
|
11
12
|
import { DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ShutdownTimeoutError, } from "../errors.js";
|
|
12
13
|
/**
|
|
@@ -33,6 +34,9 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
33
34
|
lifecycleState = "uninitialized";
|
|
34
35
|
activeConnections = new Map();
|
|
35
36
|
shutdownConfig;
|
|
37
|
+
// Set by enableMiddlewareStateCarry(); the carryMiddlewareState option is the
|
|
38
|
+
// other way in.
|
|
39
|
+
carryRequestedByRoutes = false;
|
|
36
40
|
constructor(neurolink, config = {}) {
|
|
37
41
|
super();
|
|
38
42
|
this.neurolink = neurolink;
|
|
@@ -101,6 +105,7 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
101
105
|
enableSwagger: config.enableSwagger ?? false,
|
|
102
106
|
disableBuiltInHealth: config.disableBuiltInHealth ?? false,
|
|
103
107
|
shutdown: this.shutdownConfig,
|
|
108
|
+
carryMiddlewareState: config.carryMiddlewareState ?? false,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
/**
|
|
@@ -454,6 +459,72 @@ export class BaseServerAdapter extends EventEmitter {
|
|
|
454
459
|
});
|
|
455
460
|
}
|
|
456
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Ask the adapter to carry middleware state (`ctx.user` and the rest) to later
|
|
464
|
+
* middleware and the route handler, as the `carryMiddlewareState` option does.
|
|
465
|
+
* `registerAllRoutes()` calls it when given `perUserAuth`, which cannot work
|
|
466
|
+
* without the user in the handler. It takes effect for requests that start
|
|
467
|
+
* after the call, so call it before the server starts, as registering routes
|
|
468
|
+
* is.
|
|
469
|
+
*/
|
|
470
|
+
enableMiddlewareStateCarry() {
|
|
471
|
+
this.carryRequestedByRoutes = true;
|
|
472
|
+
}
|
|
473
|
+
/** Whether this adapter carries middleware state to the handler. */
|
|
474
|
+
carriesMiddlewareState() {
|
|
475
|
+
return this.config.carryMiddlewareState || this.carryRequestedByRoutes;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* What middleware has set on its context that later middleware and the route
|
|
479
|
+
* handler must see. Each step gets a freshly built context, so unless this is
|
|
480
|
+
* parked in the framework's per-request storage an authenticated `ctx.user`
|
|
481
|
+
* set by middleware is gone before the handler runs. `undefined` when the
|
|
482
|
+
* adapter does not carry middleware state, so nothing is parked.
|
|
483
|
+
*/
|
|
484
|
+
captureCarriedContext(ctx) {
|
|
485
|
+
if (!this.carriesMiddlewareState()) {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
user: ctx.user,
|
|
490
|
+
session: ctx.session,
|
|
491
|
+
metadata: ctx.metadata,
|
|
492
|
+
abortSignal: ctx.abortSignal,
|
|
493
|
+
abortController: ctx.abortController,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Put what `captureCarriedContext` saved onto a freshly built context, and
|
|
498
|
+
* mark the context as one that received the carry. The agent routes read the
|
|
499
|
+
* mark: a request that never used to reach its handler with `ctx.user` must
|
|
500
|
+
* not start being identified by it unless the server opts in. Does nothing
|
|
501
|
+
* when the adapter does not carry middleware state: the context stays the
|
|
502
|
+
* fresh one it always was, and unmarked.
|
|
503
|
+
*/
|
|
504
|
+
restoreCarriedContext(ctx, carried) {
|
|
505
|
+
if (!this.carriesMiddlewareState()) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
markContextCarried(ctx);
|
|
509
|
+
if (!carried) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (carried.user) {
|
|
513
|
+
ctx.user = carried.user;
|
|
514
|
+
}
|
|
515
|
+
if (carried.session) {
|
|
516
|
+
ctx.session = carried.session;
|
|
517
|
+
}
|
|
518
|
+
if (carried.metadata) {
|
|
519
|
+
ctx.metadata = { ...ctx.metadata, ...carried.metadata };
|
|
520
|
+
}
|
|
521
|
+
if (carried.abortSignal) {
|
|
522
|
+
ctx.abortSignal = carried.abortSignal;
|
|
523
|
+
}
|
|
524
|
+
if (carried.abortController) {
|
|
525
|
+
ctx.abortController = carried.abortController;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
457
528
|
/**
|
|
458
529
|
* Generate unique request ID
|
|
459
530
|
*/
|
|
@@ -166,6 +166,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
166
166
|
if (res.locals.responseHeaders) {
|
|
167
167
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
168
168
|
}
|
|
169
|
+
this.restoreCarriedContext(ctx, res.locals.carriedContext);
|
|
169
170
|
// Emit request event
|
|
170
171
|
this.emit("request", {
|
|
171
172
|
requestId,
|
|
@@ -366,9 +367,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
|
|
|
366
367
|
});
|
|
367
368
|
// Copy existing response headers to context
|
|
368
369
|
ctx.responseHeaders = { ...res.locals.responseHeaders };
|
|
370
|
+
this.restoreCarriedContext(ctx, res.locals.carriedContext);
|
|
369
371
|
// Execute middleware
|
|
370
372
|
try {
|
|
371
373
|
await middleware.handler(ctx, async () => {
|
|
374
|
+
// What this middleware set (ctx.user included) is for the next
|
|
375
|
+
// middleware and the route handler, which build their own contexts.
|
|
376
|
+
res.locals.carriedContext = this.captureCarriedContext(ctx);
|
|
372
377
|
// After middleware execution, merge response headers back to res.locals
|
|
373
378
|
if (ctx.responseHeaders) {
|
|
374
379
|
Object.assign(res.locals.responseHeaders, ctx.responseHeaders);
|
|
@@ -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 connectionIds;
|
|
16
17
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
17
18
|
/**
|
|
18
19
|
* Initialize Fastify framework
|
|
@@ -16,6 +16,11 @@ import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/
|
|
|
16
16
|
export class FastifyServerAdapter extends BaseServerAdapter {
|
|
17
17
|
app = null;
|
|
18
18
|
frameworkInitialized = false;
|
|
19
|
+
// The shutdown drain's record for each in-flight request. Fastify takes
|
|
20
|
+
// `request.id` from the caller's X-Request-ID, so that id can repeat across
|
|
21
|
+
// overlapping requests: a record named after it would be shared, and the
|
|
22
|
+
// first request to finish would untrack the other.
|
|
23
|
+
connectionIds = new WeakMap();
|
|
19
24
|
constructor(neurolink, config = {}) {
|
|
20
25
|
super(neurolink, config);
|
|
21
26
|
}
|
|
@@ -239,6 +244,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
239
244
|
if (reqWithHeaders.responseHeaders) {
|
|
240
245
|
ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
|
|
241
246
|
}
|
|
247
|
+
this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
|
|
242
248
|
// Emit request event
|
|
243
249
|
this.emit("request", {
|
|
244
250
|
requestId,
|
|
@@ -399,8 +405,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
399
405
|
});
|
|
400
406
|
// Copy existing response headers to context
|
|
401
407
|
ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
|
|
408
|
+
this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
|
|
402
409
|
// Execute middleware
|
|
403
410
|
await middleware.handler(ctx, async () => {
|
|
411
|
+
// What this middleware set (ctx.user included) is for the next
|
|
412
|
+
// middleware and the route handler, which build their own contexts.
|
|
413
|
+
reqWithHeaders.carriedContext = this.captureCarriedContext(ctx);
|
|
404
414
|
// After middleware execution, merge response headers back to request
|
|
405
415
|
if (ctx.responseHeaders) {
|
|
406
416
|
Object.assign(reqWithHeaders.responseHeaders, ctx.responseHeaders);
|
|
@@ -431,12 +441,15 @@ export class FastifyServerAdapter extends BaseServerAdapter {
|
|
|
431
441
|
const startupTimeout = this.config.timeout || 30000;
|
|
432
442
|
// Track connections via Fastify hooks (must be registered before listen)
|
|
433
443
|
app.addHook("onRequest", async (request) => {
|
|
434
|
-
const connectionId = `conn-${
|
|
444
|
+
const connectionId = `conn-${this.generateRequestId()}`;
|
|
445
|
+
this.connectionIds.set(request, connectionId);
|
|
435
446
|
this.trackConnection(connectionId, request.raw.socket, request.id);
|
|
436
447
|
});
|
|
437
448
|
app.addHook("onResponse", async (request) => {
|
|
438
|
-
const connectionId =
|
|
439
|
-
|
|
449
|
+
const connectionId = this.connectionIds.get(request);
|
|
450
|
+
if (connectionId) {
|
|
451
|
+
this.untrackConnection(connectionId);
|
|
452
|
+
}
|
|
440
453
|
});
|
|
441
454
|
try {
|
|
442
455
|
await withTimeout(app.listen({ port, host }), startupTimeout, new ServerStartError(`Fastify server startup timed out after ${startupTimeout}ms`, undefined, port, host));
|
|
@@ -16,6 +16,7 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
16
16
|
private rateLimitStore;
|
|
17
17
|
private rateLimitCleanupInterval?;
|
|
18
18
|
private requestContextStore;
|
|
19
|
+
private requestIds;
|
|
19
20
|
private HTTPExceptionCtor;
|
|
20
21
|
private streamSSEFn;
|
|
21
22
|
private parseFormBody;
|
|
@@ -52,6 +53,11 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
52
53
|
* Register middleware with Hono
|
|
53
54
|
*/
|
|
54
55
|
protected registerFrameworkMiddleware(middleware: MiddlewareDefinition): void;
|
|
56
|
+
/**
|
|
57
|
+
* The id of this request: the caller's `X-Request-ID` when sent, otherwise
|
|
58
|
+
* one generated on first use, and the same one on every later use.
|
|
59
|
+
*/
|
|
60
|
+
private requestIdFor;
|
|
55
61
|
/**
|
|
56
62
|
* Start the Hono server
|
|
57
63
|
*/
|
|
@@ -6,6 +6,7 @@
|
|
|
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 { markContextCarried } from "../utils/carriedContextMark.js";
|
|
9
10
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
11
|
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
|
|
11
12
|
/**
|
|
@@ -17,8 +18,17 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
17
18
|
server;
|
|
18
19
|
rateLimitStore = new Map();
|
|
19
20
|
rateLimitCleanupInterval;
|
|
20
|
-
//
|
|
21
|
-
|
|
21
|
+
// Context shared between middleware and the route handler. Keyed by the request
|
|
22
|
+
// object, never by the request id: the id can be the
|
|
23
|
+
// caller's own X-Request-ID, so two overlapping requests that send the same one
|
|
24
|
+
// would share a context, and the second would run with the first one's
|
|
25
|
+
// identity, headers and body. A WeakMap also frees an entry whose handler
|
|
26
|
+
// never ran.
|
|
27
|
+
requestContextStore = new WeakMap();
|
|
28
|
+
// One id per request, for logs and events: the caller's X-Request-ID when sent,
|
|
29
|
+
// otherwise generated once, so every step reports the same id. With no header
|
|
30
|
+
// every step used to invent its own.
|
|
31
|
+
requestIds = new WeakMap();
|
|
22
32
|
// Populated by initializeFramework's dynamic imports; used by methods that
|
|
23
33
|
// run after initialization (route registration, streaming) and so cannot
|
|
24
34
|
// rely on initializeFramework's own local closure variables.
|
|
@@ -50,7 +60,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
50
60
|
headers[name] = value;
|
|
51
61
|
});
|
|
52
62
|
const minCtx = {
|
|
53
|
-
requestId:
|
|
63
|
+
requestId: this.requestIdFor(c),
|
|
54
64
|
method: c.req.method,
|
|
55
65
|
path: c.req.path,
|
|
56
66
|
headers,
|
|
@@ -162,7 +172,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
162
172
|
}
|
|
163
173
|
// Global error handler
|
|
164
174
|
this.app.onError((error, c) => {
|
|
165
|
-
const requestId =
|
|
175
|
+
const requestId = this.requestIdFor(c);
|
|
166
176
|
this.logRequestError("HonoAdapter", requestId, error);
|
|
167
177
|
this.emitRequestError({
|
|
168
178
|
requestId,
|
|
@@ -217,8 +227,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
217
227
|
registerFrameworkRoute(route) {
|
|
218
228
|
const method = route.method.toLowerCase();
|
|
219
229
|
this.app[method](route.path, async (c) => {
|
|
220
|
-
const requestId =
|
|
221
|
-
|
|
230
|
+
const requestId = this.requestIdFor(c);
|
|
231
|
+
// Not derived from the request id, which a caller may repeat: two
|
|
232
|
+
// overlapping requests would then share one record and the first to
|
|
233
|
+
// finish would untrack the other.
|
|
234
|
+
const connectionId = `conn-${this.generateRequestId()}`;
|
|
222
235
|
const startTime = Date.now();
|
|
223
236
|
// Track connection for graceful shutdown
|
|
224
237
|
this.trackConnection(connectionId, undefined, requestId);
|
|
@@ -226,12 +239,30 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
226
239
|
// Extract path parameters
|
|
227
240
|
const params = c.req.param();
|
|
228
241
|
// Reuse existing context from middleware if available, otherwise create new one
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
const existingCtx = this.requestContextStore.get(c.req.raw);
|
|
243
|
+
// Middleware ran before the router matched this route, so the shared
|
|
244
|
+
// context carries no path parameters yet.
|
|
245
|
+
if (existingCtx) {
|
|
246
|
+
existingCtx.params = params;
|
|
247
|
+
// The one request the handler already got the middleware's context
|
|
248
|
+
// for before this adapter carried it on every request: the caller
|
|
249
|
+
// sent its own X-Request-ID, which the old store was keyed by. That
|
|
250
|
+
// context stays unmarked, so the agent routes keep trusting its
|
|
251
|
+
// ctx.user there. A context found for any other request exists only
|
|
252
|
+
// because the server carries middleware state, is new to the
|
|
253
|
+
// handler, and is marked.
|
|
254
|
+
if (!c.req.header("X-Request-ID")) {
|
|
255
|
+
markContextCarried(existingCtx);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
let ctx = existingCtx;
|
|
259
|
+
if (!ctx) {
|
|
260
|
+
const created = await this.createRequestContext(c, requestId, params);
|
|
261
|
+
if (isWebResponse(created)) {
|
|
262
|
+
return created;
|
|
263
|
+
}
|
|
264
|
+
ctx = created;
|
|
233
265
|
}
|
|
234
|
-
const ctx = created;
|
|
235
266
|
// Emit request event
|
|
236
267
|
this.emit("request", {
|
|
237
268
|
requestId,
|
|
@@ -325,7 +356,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
325
356
|
// Untrack connection when request completes
|
|
326
357
|
this.untrackConnection(connectionId);
|
|
327
358
|
// Clean up context store to prevent memory leaks
|
|
328
|
-
this.requestContextStore.delete(
|
|
359
|
+
this.requestContextStore.delete(c.req.raw);
|
|
329
360
|
}
|
|
330
361
|
});
|
|
331
362
|
}
|
|
@@ -457,9 +488,9 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
457
488
|
// Extract path parameters
|
|
458
489
|
const params = c.req.param();
|
|
459
490
|
// Get or generate request ID for context lookup
|
|
460
|
-
const requestId =
|
|
491
|
+
const requestId = this.requestIdFor(c);
|
|
461
492
|
// Reuse existing context from previous middleware if available
|
|
462
|
-
let ctx = this.requestContextStore.get(
|
|
493
|
+
let ctx = this.requestContextStore.get(c.req.raw);
|
|
463
494
|
if (!ctx) {
|
|
464
495
|
// Create new context for the first middleware
|
|
465
496
|
const created = await this.createRequestContext(c, requestId, params);
|
|
@@ -467,14 +498,34 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
467
498
|
return created;
|
|
468
499
|
}
|
|
469
500
|
ctx = created;
|
|
470
|
-
//
|
|
471
|
-
|
|
501
|
+
// Share the context between the middleware steps and the route
|
|
502
|
+
// handler when the server carries middleware state, or when the
|
|
503
|
+
// caller sent its own X-Request-ID: the one case the old store, keyed
|
|
504
|
+
// by that header, ever found a context again. Otherwise every step
|
|
505
|
+
// and the handler build their own, and storing this one would only
|
|
506
|
+
// leave it behind.
|
|
507
|
+
if (this.carriesMiddlewareState() || c.req.header("X-Request-ID")) {
|
|
508
|
+
this.requestContextStore.set(c.req.raw, ctx);
|
|
509
|
+
}
|
|
472
510
|
}
|
|
473
511
|
// Execute middleware
|
|
474
512
|
return middleware.handler(ctx, next);
|
|
475
513
|
});
|
|
476
514
|
}
|
|
477
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The id of this request: the caller's `X-Request-ID` when sent, otherwise
|
|
518
|
+
* one generated on first use, and the same one on every later use.
|
|
519
|
+
*/
|
|
520
|
+
requestIdFor(c) {
|
|
521
|
+
const known = this.requestIds.get(c.req.raw);
|
|
522
|
+
if (known) {
|
|
523
|
+
return known;
|
|
524
|
+
}
|
|
525
|
+
const id = c.req.header("X-Request-ID") || this.generateRequestId();
|
|
526
|
+
this.requestIds.set(c.req.raw, id);
|
|
527
|
+
return id;
|
|
528
|
+
}
|
|
478
529
|
/**
|
|
479
530
|
* Start the Hono server
|
|
480
531
|
*/
|
|
@@ -261,6 +261,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
261
261
|
if (ctx.state.responseHeaders) {
|
|
262
262
|
serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
|
|
263
263
|
}
|
|
264
|
+
this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
|
|
264
265
|
// Emit request event
|
|
265
266
|
this.emit("request", {
|
|
266
267
|
requestId,
|
|
@@ -419,8 +420,12 @@ export class KoaServerAdapter extends BaseServerAdapter {
|
|
|
419
420
|
});
|
|
420
421
|
// Copy existing response headers to context
|
|
421
422
|
serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
|
|
423
|
+
this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
|
|
422
424
|
// Execute middleware
|
|
423
425
|
await middleware.handler(serverCtx, async () => {
|
|
426
|
+
// What this middleware set (ctx.user included) is for the next
|
|
427
|
+
// middleware and the route handler, which build their own contexts.
|
|
428
|
+
ctx.state.carriedContext = this.captureCarriedContext(serverCtx);
|
|
424
429
|
// After middleware execution, merge response headers back to ctx.state
|
|
425
430
|
if (serverCtx.responseHeaders) {
|
|
426
431
|
Object.assign(ctx.state.responseHeaders, serverCtx.responseHeaders);
|
|
@@ -35,6 +35,7 @@ export function isDevPlayground(headers) {
|
|
|
35
35
|
export const DEV_PLAYGROUND_USER = {
|
|
36
36
|
id: "playground",
|
|
37
37
|
roles: ["developer"],
|
|
38
|
+
shared: true,
|
|
38
39
|
};
|
|
39
40
|
/**
|
|
40
41
|
* Authentication result
|
|
@@ -76,6 +77,7 @@ export function createAuthMiddleware(config) {
|
|
|
76
77
|
id: DEV_PLAYGROUND_USER.id,
|
|
77
78
|
email: DEV_PLAYGROUND_USER.email,
|
|
78
79
|
roles: DEV_PLAYGROUND_USER.roles,
|
|
80
|
+
shared: DEV_PLAYGROUND_USER.shared,
|
|
79
81
|
};
|
|
80
82
|
return next();
|
|
81
83
|
}
|
|
@@ -95,6 +97,7 @@ export function createAuthMiddleware(config) {
|
|
|
95
97
|
id: result.id,
|
|
96
98
|
email: result.email,
|
|
97
99
|
roles: result.roles,
|
|
100
|
+
shared: result.shared,
|
|
98
101
|
};
|
|
99
102
|
// Store additional metadata
|
|
100
103
|
if (result.metadata) {
|
|
@@ -6,11 +6,52 @@ import type { RouteGroup, ServerAgentRouteOptions } from "../../types/index.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Create agent routes
|
|
8
8
|
*
|
|
9
|
+
* `options.perUserAuth` decides where a request's identity comes from; see
|
|
10
|
+
* `ServerAgentRouteOptions`. Authentication middleware that sets `ctx.user`
|
|
11
|
+
* does not change that on its own: one shared API key sets the same user for
|
|
12
|
+
* every caller, so trusting it as a person would give every client the same
|
|
13
|
+
* userId and no conversation memory. The converse is the operator's to act on:
|
|
14
|
+
* a server whose authentication identifies individual people must set
|
|
15
|
+
* `perUserAuth`, or any authenticated caller can name another userId and
|
|
16
|
+
* sessionId in the body.
|
|
17
|
+
*
|
|
18
|
+
* Unset, identity resolves as it did before the adapters carried `ctx.user` to
|
|
19
|
+
* the handler: the body's `userId` and `sessionId`, with one exception. On
|
|
20
|
+
* Hono, a request whose client sent its own `X-Request-ID` header reached the
|
|
21
|
+
* handler with the middleware's context, so an authenticated `ctx.user` (and
|
|
22
|
+
* `ctx.session`) won over the body there, and still does. A context the
|
|
23
|
+
* built-in adapters did not hand the carry to also keeps that rule: a custom
|
|
24
|
+
* adapter that sets `ctx.user` itself, or a handler called directly with a
|
|
25
|
+
* context the caller built. Only `perUserAuth` (with the server carrying
|
|
26
|
+
* middleware state) makes these routes take the user from `ctx.user`, and
|
|
27
|
+
* ignore the body's `userId`, on every built-in adapter and for every request.
|
|
28
|
+
*
|
|
29
|
+
* `perUserAuth` needs the user in the handler, which an adapter hands over on
|
|
30
|
+
* every request only when the server carries middleware state.
|
|
31
|
+
* `registerAllRoutes()` turns that on for the server when it is given
|
|
32
|
+
* `perUserAuth`. A caller that registers these routes itself with `perUserAuth`
|
|
33
|
+
* must create the server with `carryMiddlewareState: true`. Without it, a
|
|
34
|
+
* request has a user only on Hono when the client sent its own `X-Request-ID`
|
|
35
|
+
* (Hono then hands the handler the middleware's context regardless); every
|
|
36
|
+
* other request (Hono without that header, Express, Fastify, Koa) runs as
|
|
37
|
+
* anonymous, with no `userId` and no session.
|
|
38
|
+
*
|
|
39
|
+
* Without `perUserAuth`, and when the server carries middleware state, the
|
|
40
|
+
* routes log one warning per instance the first time a request whose body names
|
|
41
|
+
* a different `userId` than its `ctx.user` is served from the body, unless that
|
|
42
|
+
* user is marked `shared: true` (an identity for every holder of one
|
|
43
|
+
* credential, which the CLI's API-key middleware sets). Such a request is
|
|
44
|
+
* served from the body; the warning only tells the operator that per-person
|
|
45
|
+
* authentication is running without the flag that makes it hold. With the
|
|
46
|
+
* carry off the handler has no `ctx.user` on those requests, so there is
|
|
47
|
+
* nothing to compare and no warning. A request where `ctx.user` already won has
|
|
48
|
+
* nothing to warn about.
|
|
49
|
+
*
|
|
9
50
|
* @param options - Limits for `input.images` / `input.files`:
|
|
10
51
|
* `allowedInputRoots` (default: none, every local-path attachment is
|
|
11
52
|
* refused), `maxInputBytes` per attachment (default 50 MiB),
|
|
12
53
|
* `maxInputAttachments` per request (default 20, images and files together)
|
|
13
54
|
* and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
|
|
14
|
-
* images and files).
|
|
55
|
+
* images and files); and `perUserAuth` (described above).
|
|
15
56
|
*/
|
|
16
57
|
export declare function createAgentRoutes(basePath?: string, options?: ServerAgentRouteOptions): RouteGroup;
|