@jini-ai/http-kit 0.2.1
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/LICENSE +201 -0
- package/README.md +107 -0
- package/dist/active-context.d.ts +99 -0
- package/dist/active-context.d.ts.map +1 -0
- package/dist/active-context.js +77 -0
- package/dist/active-context.js.map +1 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +92 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agents.d.ts +55 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +23 -0
- package/dist/agents.js.map +1 -0
- package/dist/api-security-middleware.d.ts +146 -0
- package/dist/api-security-middleware.d.ts.map +1 -0
- package/dist/api-security-middleware.js +276 -0
- package/dist/api-security-middleware.js.map +1 -0
- package/dist/attachments.d.ts +317 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +664 -0
- package/dist/attachments.js.map +1 -0
- package/dist/cancel-owned-runs.d.ts +44 -0
- package/dist/cancel-owned-runs.d.ts.map +1 -0
- package/dist/cancel-owned-runs.js +35 -0
- package/dist/cancel-owned-runs.js.map +1 -0
- package/dist/compat.d.ts +22 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +17 -0
- package/dist/compat.js.map +1 -0
- package/dist/component-catalog.d.ts +74 -0
- package/dist/component-catalog.d.ts.map +1 -0
- package/dist/component-catalog.js +74 -0
- package/dist/component-catalog.js.map +1 -0
- package/dist/connectors.d.ts +165 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +474 -0
- package/dist/connectors.js.map +1 -0
- package/dist/daemon-status.d.ts +72 -0
- package/dist/daemon-status.d.ts.map +1 -0
- package/dist/daemon-status.js +60 -0
- package/dist/daemon-status.js.map +1 -0
- package/dist/db-ops.d.ts +116 -0
- package/dist/db-ops.d.ts.map +1 -0
- package/dist/db-ops.js +191 -0
- package/dist/db-ops.js.map +1 -0
- package/dist/delegated-tools.d.ts +46 -0
- package/dist/delegated-tools.d.ts.map +1 -0
- package/dist/delegated-tools.js +155 -0
- package/dist/delegated-tools.js.map +1 -0
- package/dist/express/run-stream.d.ts +15 -0
- package/dist/express/run-stream.d.ts.map +1 -0
- package/dist/express/run-stream.js +11 -0
- package/dist/express/run-stream.js.map +1 -0
- package/dist/express-index.d.ts +32 -0
- package/dist/express-index.d.ts.map +1 -0
- package/dist/express-index.js +14 -0
- package/dist/express-index.js.map +1 -0
- package/dist/fastify/adapter.d.ts +31 -0
- package/dist/fastify/adapter.d.ts.map +1 -0
- package/dist/fastify/adapter.js +64 -0
- package/dist/fastify/adapter.js.map +1 -0
- package/dist/fastify/agents.d.ts +13 -0
- package/dist/fastify/agents.d.ts.map +1 -0
- package/dist/fastify/agents.js +7 -0
- package/dist/fastify/agents.js.map +1 -0
- package/dist/fastify/api-security-middleware.d.ts +64 -0
- package/dist/fastify/api-security-middleware.d.ts.map +1 -0
- package/dist/fastify/api-security-middleware.js +139 -0
- package/dist/fastify/api-security-middleware.js.map +1 -0
- package/dist/fastify/compat.d.ts +22 -0
- package/dist/fastify/compat.d.ts.map +1 -0
- package/dist/fastify/compat.js +16 -0
- package/dist/fastify/compat.js.map +1 -0
- package/dist/fastify/daemon-status.d.ts +22 -0
- package/dist/fastify/daemon-status.d.ts.map +1 -0
- package/dist/fastify/daemon-status.js +9 -0
- package/dist/fastify/daemon-status.js.map +1 -0
- package/dist/fastify/host-tools.d.ts +13 -0
- package/dist/fastify/host-tools.d.ts.map +1 -0
- package/dist/fastify/host-tools.js +8 -0
- package/dist/fastify/host-tools.js.map +1 -0
- package/dist/fastify/index.d.ts +36 -0
- package/dist/fastify/index.d.ts.map +1 -0
- package/dist/fastify/index.js +18 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/local-daemon-request.d.ts +43 -0
- package/dist/fastify/local-daemon-request.d.ts.map +1 -0
- package/dist/fastify/local-daemon-request.js +155 -0
- package/dist/fastify/local-daemon-request.js.map +1 -0
- package/dist/fastify/origin.d.ts +21 -0
- package/dist/fastify/origin.d.ts.map +1 -0
- package/dist/fastify/origin.js +14 -0
- package/dist/fastify/origin.js.map +1 -0
- package/dist/fastify/request.d.ts +20 -0
- package/dist/fastify/request.d.ts.map +1 -0
- package/dist/fastify/request.js +25 -0
- package/dist/fastify/request.js.map +1 -0
- package/dist/fastify/response.d.ts +20 -0
- package/dist/fastify/response.d.ts.map +1 -0
- package/dist/fastify/response.js +41 -0
- package/dist/fastify/response.js.map +1 -0
- package/dist/fastify/route-registration-guard.d.ts +70 -0
- package/dist/fastify/route-registration-guard.d.ts.map +1 -0
- package/dist/fastify/route-registration-guard.js +69 -0
- package/dist/fastify/route-registration-guard.js.map +1 -0
- package/dist/fastify/run-stream.d.ts +18 -0
- package/dist/fastify/run-stream.d.ts.map +1 -0
- package/dist/fastify/run-stream.js +10 -0
- package/dist/fastify/run-stream.js.map +1 -0
- package/dist/fastify/runs.d.ts +17 -0
- package/dist/fastify/runs.d.ts.map +1 -0
- package/dist/fastify/runs.js +33 -0
- package/dist/fastify/runs.js.map +1 -0
- package/dist/frontend-control.d.ts +131 -0
- package/dist/frontend-control.d.ts.map +1 -0
- package/dist/frontend-control.js +77 -0
- package/dist/frontend-control.js.map +1 -0
- package/dist/frontend-sessions.d.ts +100 -0
- package/dist/frontend-sessions.d.ts.map +1 -0
- package/dist/frontend-sessions.js +180 -0
- package/dist/frontend-sessions.js.map +1 -0
- package/dist/health.d.ts +91 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +53 -0
- package/dist/health.js.map +1 -0
- package/dist/host-tools.d.ts +174 -0
- package/dist/host-tools.d.ts.map +1 -0
- package/dist/host-tools.js +338 -0
- package/dist/host-tools.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/local-daemon-request.d.ts +44 -0
- package/dist/local-daemon-request.d.ts.map +1 -0
- package/dist/local-daemon-request.js +145 -0
- package/dist/local-daemon-request.js.map +1 -0
- package/dist/media.d.ts +131 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +352 -0
- package/dist/media.js.map +1 -0
- package/dist/memory.d.ts +230 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +331 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-proxy.d.ts +45 -0
- package/dist/model-proxy.d.ts.map +1 -0
- package/dist/model-proxy.js +619 -0
- package/dist/model-proxy.js.map +1 -0
- package/dist/origin-validation.d.ts +64 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +226 -0
- package/dist/origin-validation.js.map +1 -0
- package/dist/origin.d.ts +32 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +14 -0
- package/dist/origin.js.map +1 -0
- package/dist/pack-http.d.ts +30 -0
- package/dist/pack-http.d.ts.map +1 -0
- package/dist/pack-http.js +12 -0
- package/dist/pack-http.js.map +1 -0
- package/dist/raw-sse.d.ts +44 -0
- package/dist/raw-sse.d.ts.map +1 -0
- package/dist/raw-sse.js +93 -0
- package/dist/raw-sse.js.map +1 -0
- package/dist/remote-run-events.d.ts +66 -0
- package/dist/remote-run-events.d.ts.map +1 -0
- package/dist/remote-run-events.js +230 -0
- package/dist/remote-run-events.js.map +1 -0
- package/dist/request.d.ts +19 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +25 -0
- package/dist/request.js.map +1 -0
- package/dist/research.d.ts +42 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +316 -0
- package/dist/research.js.map +1 -0
- package/dist/response.d.ts +14 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +45 -0
- package/dist/response.js.map +1 -0
- package/dist/route-manifest.d.ts +31 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +103 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/route-registration-guard.d.ts +65 -0
- package/dist/route-registration-guard.d.ts.map +1 -0
- package/dist/route-registration-guard.js +71 -0
- package/dist/route-registration-guard.js.map +1 -0
- package/dist/routines.d.ts +114 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +320 -0
- package/dist/routines.js.map +1 -0
- package/dist/run-stream.d.ts +60 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +108 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs.d.ts +79 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +232 -0
- package/dist/runs.js.map +1 -0
- package/dist/sse.d.ts +139 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +157 -0
- package/dist/sse.js.map +1 -0
- package/dist/terminals.d.ts +125 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +349 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tool-catalog.d.ts +68 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +68 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-root.d.ts +69 -0
- package/dist/workspace-root.d.ts.map +1 -0
- package/dist/workspace-root.js +63 -0
- package/dist/workspace-root.js.map +1 -0
- package/dist/xai.d.ts +86 -0
- package/dist/xai.d.ts.map +1 -0
- package/dist/xai.js +668 -0
- package/dist/xai.js.map +1 -0
- package/package.json +58 -0
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module agents
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/agents` — lists the agents a host can expose to a client,
|
|
5
|
+
* including host-probed availability/model metadata when supplied.
|
|
6
|
+
* `POST /api/agents/rescan` asks the host to invalidate its discovery cache
|
|
7
|
+
* and probe again.
|
|
8
|
+
*
|
|
9
|
+
* `listAgents` is injected (matching `daemon-status.ts`/`active-context.ts`'s
|
|
10
|
+
* DI convention) rather than this module importing `@jini-ai/agent-runtime`
|
|
11
|
+
* directly — a host typically already has that package's `AGENT_DEFS` array
|
|
12
|
+
* in scope and just needs to project it, and this keeps `@jini-ai/http-kit` from
|
|
13
|
+
* taking on a dependency on subprocess discovery. The host owns probing,
|
|
14
|
+
* timeouts, caching, PATH/env policy, and the projection of spawn-only
|
|
15
|
+
* metadata; this transport only serializes the safe summary.
|
|
16
|
+
*/
|
|
17
|
+
import type { Express } from 'express';
|
|
18
|
+
import { type AdapterContext } from './adapter.js';
|
|
19
|
+
export interface AgentModelSummary {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Client-safe agent discovery data. Optional probe fields preserve the
|
|
25
|
+
* original static-registry contract for hosts that only expose `{id, name}`.
|
|
26
|
+
* Spawn internals (`bin`, resolved path, argv builders, env) never cross HTTP.
|
|
27
|
+
*/
|
|
28
|
+
export interface AgentSummary {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly available?: boolean;
|
|
32
|
+
readonly version?: string | null;
|
|
33
|
+
readonly authStatus?: 'ok' | 'missing' | 'unknown';
|
|
34
|
+
readonly models?: readonly AgentModelSummary[];
|
|
35
|
+
readonly reasoningOptions?: readonly AgentModelSummary[];
|
|
36
|
+
readonly modelsSource?: 'live' | 'fallback';
|
|
37
|
+
readonly supportsCustomModel?: boolean;
|
|
38
|
+
readonly diagnostic?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AgentsHttpDeps {
|
|
41
|
+
/** Returns the host's cached or freshly resolved client-safe agent inventory. */
|
|
42
|
+
readonly listAgents: () => Promise<readonly AgentSummary[]> | readonly AgentSummary[];
|
|
43
|
+
/** Forces host-owned discovery to run again. Falls back to `listAgents` when omitted. */
|
|
44
|
+
readonly rescanAgents?: () => Promise<readonly AgentSummary[]> | readonly AgentSummary[];
|
|
45
|
+
}
|
|
46
|
+
export interface AgentListResponse {
|
|
47
|
+
readonly agents: readonly AgentSummary[];
|
|
48
|
+
}
|
|
49
|
+
/** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
|
|
50
|
+
export declare const agentListRoute: import("./types.js").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
|
|
51
|
+
/** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
|
|
52
|
+
export declare const agentRescanRoute: import("./types.js").JsonRouteSpec<void, AgentListResponse, AgentsHttpDeps>;
|
|
53
|
+
/** Mounts the read and explicit-rescan agent discovery routes. */
|
|
54
|
+
export declare function registerAgentRoutes(app: Express, deps: AgentsHttpDeps, adapter: AdapterContext): void;
|
|
55
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAGpF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5C,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC;IACtF,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,GAAG,SAAS,YAAY,EAAE,CAAC;CAC1F;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAED,sIAAsI;AACtI,eAAO,MAAM,cAAc,6EAKzB,CAAC;AAEH,mGAAmG;AACnG,eAAO,MAAM,gBAAgB,6EAM3B,CAAC;AAEH,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAGrG"}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineJsonRoute, mountJsonRoute } from './adapter.js';
|
|
2
|
+
import { ok } from './types.js';
|
|
3
|
+
/** `GET /api/agents` — read-only, no side effects; matches `runListRoute`/`runStatusRoute`'s posture of not requiring same-origin. */
|
|
4
|
+
export const agentListRoute = defineJsonRoute({
|
|
5
|
+
method: 'get',
|
|
6
|
+
path: '/api/agents',
|
|
7
|
+
parse: () => ok(undefined),
|
|
8
|
+
handle: async (_input, deps) => ok({ agents: await deps.listAgents() }),
|
|
9
|
+
});
|
|
10
|
+
/** `POST /api/agents/rescan` — explicit state refresh, protected by the local same-origin gate. */
|
|
11
|
+
export const agentRescanRoute = defineJsonRoute({
|
|
12
|
+
method: 'post',
|
|
13
|
+
path: '/api/agents/rescan',
|
|
14
|
+
requireSameOrigin: true,
|
|
15
|
+
parse: () => ok(undefined),
|
|
16
|
+
handle: async (_input, deps) => ok({ agents: await (deps.rescanAgents ?? deps.listAgents)() }),
|
|
17
|
+
});
|
|
18
|
+
/** Mounts the read and explicit-rescan agent discovery routes. */
|
|
19
|
+
export function registerAgentRoutes(app, deps, adapter) {
|
|
20
|
+
mountJsonRoute(app, agentListRoute, deps, adapter);
|
|
21
|
+
mountJsonRoute(app, agentRescanRoute, deps, adapter);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAuB,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAoChC,sIAAsI;AACtI,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAA0C;IACrF,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;CACxE,CAAC,CAAC;AAEH,mGAAmG;AACnG,MAAM,CAAC,MAAM,gBAAgB,GAAG,eAAe,CAA0C;IACvF,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,oBAAoB;IAC1B,iBAAiB,EAAE,IAAI;IACvB,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;CAC/F,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,UAAU,mBAAmB,CAAC,GAAY,EAAE,IAAoB,EAAE,OAAuB;IAC7F,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACnD,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Express, NextFunction, Request, Response } from 'express';
|
|
2
|
+
import { type ApiTokenAuthEnvConfig } from '@jini-ai/core';
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the token from an `Authorization: Bearer <token>` header value. The scheme is
|
|
5
|
+
* case-insensitive per RFC 7235 §2.1; the token itself is not.
|
|
6
|
+
*
|
|
7
|
+
* Shared by every bearer gate in this package so the accepted header grammar is defined exactly
|
|
8
|
+
* once — three separate copies of this regex is three chances for them to disagree about what
|
|
9
|
+
* counts as a well-formed header.
|
|
10
|
+
*
|
|
11
|
+
* @param header - The raw `Authorization` header value, or `undefined` when absent.
|
|
12
|
+
* @returns The token, or `null` when the header is absent or not a well-formed bearer header.
|
|
13
|
+
* @complexity O(n) in the header length.
|
|
14
|
+
* @overallScore 100/100
|
|
15
|
+
*/
|
|
16
|
+
export declare function bearerTokenFromHeader(header: string | undefined): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Constant-time token comparison. Length is compared first and NOT in constant time — that leaks
|
|
19
|
+
* only the expected token's length, which for a generated secret is fixed and not itself a secret,
|
|
20
|
+
* never any of its bytes. `timingSafeEqual` throws on a length mismatch, so the early return is
|
|
21
|
+
* required rather than merely an optimization.
|
|
22
|
+
*
|
|
23
|
+
* Every bearer gate in this package routes through here instead of `===`. A plain string compare
|
|
24
|
+
* short-circuits on the first differing byte, which makes the comparison's duration a function of
|
|
25
|
+
* how many leading bytes the presented token got right — enough, over many requests, to recover a
|
|
26
|
+
* token one byte at a time.
|
|
27
|
+
*
|
|
28
|
+
* @param presented - The token from the request's `Authorization` header.
|
|
29
|
+
* @param expected - The configured token to match against.
|
|
30
|
+
* @returns `true` only on an exact match.
|
|
31
|
+
* @complexity O(n) in the token length, with no data-dependent early exit.
|
|
32
|
+
* @overallScore 100/100
|
|
33
|
+
*/
|
|
34
|
+
export declare function timingSafeTokenMatch(presented: string, expected: string): boolean;
|
|
35
|
+
export interface ApiBearerAuthMiddlewareDeps {
|
|
36
|
+
/** Env var names for the token/disable flags. Defaults to `JINI_API_TOKEN` / `JINI_DISABLE_API_AUTH`. */
|
|
37
|
+
tokenConfig?: ApiTokenAuthEnvConfig;
|
|
38
|
+
/** Defaults to `process.env`. Threaded through so tests never have to mutate real process env. */
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
/**
|
|
41
|
+
* Whether an unproxied loopback peer may skip the bearer check. Defaults to `true` — the
|
|
42
|
+
* affordance a desktop UI or local CLI talking to its own daemon relies on.
|
|
43
|
+
*
|
|
44
|
+
* Set to `false` for any deployment where a process other than a trusted local client can open a
|
|
45
|
+
* loopback connection to this daemon, which includes every same-host reverse-proxy deployment
|
|
46
|
+
* (see the forwarded-header note at the check itself) and any multi-tenant or shared host. There
|
|
47
|
+
* is no way to tell those peers apart by socket address alone, so this is the host's call to
|
|
48
|
+
* make, not one this middleware can infer. {@link requireStrictBearerToken} is the same posture
|
|
49
|
+
* for a daemon that should never have had the exemption in the first place.
|
|
50
|
+
*/
|
|
51
|
+
trustLoopbackPeers?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Registers a bearer-token gate on every `/api/*` route, active only when
|
|
55
|
+
* {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
|
|
56
|
+
* When active: open-probe paths and loopback peers skip the check unconditionally; every other
|
|
57
|
+
* request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
|
|
58
|
+
* request is rejected with 401 before reaching any route handler.
|
|
59
|
+
*
|
|
60
|
+
* @param app - The Express app to register the gate on.
|
|
61
|
+
* @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
|
|
62
|
+
* entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
|
|
63
|
+
* @returns Nothing. Registers zero middleware (a deliberate no-op, not a bug) when no token is configured.
|
|
64
|
+
* @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
|
|
65
|
+
* @overallScore 100/100
|
|
66
|
+
*/
|
|
67
|
+
export declare function registerApiBearerAuthMiddleware(app: Express, deps?: ApiBearerAuthMiddlewareDeps): void;
|
|
68
|
+
export interface StrictBearerTokenDeps {
|
|
69
|
+
/**
|
|
70
|
+
* Env var carrying the expected token. Required with no default: a gate this strict must name the
|
|
71
|
+
* secret it enforces, and this package has no business guessing a host's env-var name.
|
|
72
|
+
*/
|
|
73
|
+
readonly tokenEnvVar: string;
|
|
74
|
+
/** Defaults to `process.env`. Threaded through so tests never have to mutate real process env. */
|
|
75
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
76
|
+
/**
|
|
77
|
+
* Exact request paths this gate does not apply to. Defaults to none — this is a gate-everything
|
|
78
|
+
* primitive, and each exemption must be opted into explicitly at the mount site with a stated
|
|
79
|
+
* reason. Matched by exact equality, never by prefix, so a longer path that merely starts with an
|
|
80
|
+
* exempt one stays gated.
|
|
81
|
+
*/
|
|
82
|
+
readonly exemptPaths?: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds a bearer gate with **no loopback exemption and no disable flag** — for a daemon whose
|
|
86
|
+
* threat model is "another process running as the same OS user", not "a remote attacker".
|
|
87
|
+
*
|
|
88
|
+
* This is deliberately not {@link registerApiBearerAuthMiddleware}. That gate short-circuits for any
|
|
89
|
+
* loopback peer before it ever reads the `Authorization` header, which is the right affordance for a
|
|
90
|
+
* desktop UI or local CLI talking to its own daemon, and a no-op against the threat here: a
|
|
91
|
+
* `127.0.0.1` bind keeps remote hosts out but does nothing about a co-resident process, and a
|
|
92
|
+
* sidecar daemon that can start real agent runs and execute real tools is exactly the kind of thing
|
|
93
|
+
* such a process would want to reach. This gate treats a request from `127.0.0.1` like any other.
|
|
94
|
+
*
|
|
95
|
+
* The posture matches `remote-run-events.ts`'s own token gate, which made the same call for the same
|
|
96
|
+
* reason on a single route; this generalizes it to a whole `/api` surface.
|
|
97
|
+
*
|
|
98
|
+
* Fail-closed contract:
|
|
99
|
+
* - token env var unset/empty -> **503**, never a silent pass-through. A misconfigured daemon
|
|
100
|
+
* refuses to serve rather than serving unauthenticated callers.
|
|
101
|
+
* - missing / malformed / wrong token -> **401**.
|
|
102
|
+
* - exact match -> `next()`.
|
|
103
|
+
*
|
|
104
|
+
* The env var is read on every request rather than captured at factory time: a host that mints a
|
|
105
|
+
* per-boot token may not have done so yet when its module graph is first evaluated, and re-reading
|
|
106
|
+
* costs one property lookup.
|
|
107
|
+
*
|
|
108
|
+
* @param deps - See {@link StrictBearerTokenDeps}. `tokenEnvVar` is required.
|
|
109
|
+
* @returns Express middleware. Mount it **unprefixed** — `app.use(handler)` — before any route
|
|
110
|
+
* registrar and before the JSON body parser, so an unauthenticated caller's body is never parsed.
|
|
111
|
+
* @complexity O(1) per request plus O(n) in the token length for the constant-time comparison.
|
|
112
|
+
* @overallScore 95/100 — Medium: mounting under a prefix (`app.use('/api', handler)`) silently breaks
|
|
113
|
+
* `exemptPaths`. Express strips the mount prefix from `req.path`, so an entry written as a full path
|
|
114
|
+
* (`/api/delegated-tool-calls`) stops matching and the gate rejects a caller the mount site meant to
|
|
115
|
+
* exempt. Nothing type-checks that, and the failure is a 401 on a route that is supposed to be open
|
|
116
|
+
* rather than the reverse — noisy rather than dangerous, but confusing to diagnose. Documented rather
|
|
117
|
+
* than defended against in code: normalizing `req.baseUrl + req.path` here would silently accept both
|
|
118
|
+
* mount styles and remove the caller's ability to reason about which paths its entries refer to.
|
|
119
|
+
*/
|
|
120
|
+
export declare function requireStrictBearerToken(deps: StrictBearerTokenDeps): (req: Request, res: Response, next: NextFunction) => void;
|
|
121
|
+
export interface ApiOriginGuardMiddlewareDeps {
|
|
122
|
+
/** The host this daemon is bound to — compared against a request's `Host`/`Origin` headers. */
|
|
123
|
+
host: string;
|
|
124
|
+
/** Extra allow-listed origins (e.g. a reverse-proxy's public origin). Defaults to none. */
|
|
125
|
+
extraAllowedOrigins?: readonly string[];
|
|
126
|
+
/** Returns the daemon's resolved listen port, or a falsy value before it has resolved. */
|
|
127
|
+
getResolvedPort: () => number | null | undefined;
|
|
128
|
+
/** Defaults to `process.env`. Threaded through so `JINI_WEB_PORT` is testable without mutating real process env. */
|
|
129
|
+
env?: NodeJS.ProcessEnv;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
|
|
133
|
+
* `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
|
|
134
|
+
* allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
|
|
135
|
+
* (typically a sandboxed iframe) is always rejected — see this module's doc for why the origin
|
|
136
|
+
* daemon's safe-GET exemption for that case was dropped.
|
|
137
|
+
*
|
|
138
|
+
* @param app - The Express app to register the gate on.
|
|
139
|
+
* @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
|
|
140
|
+
* @returns Nothing. Unlike the bearer-token gate, this always registers its middleware — there is
|
|
141
|
+
* no "disabled" state.
|
|
142
|
+
* @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
|
|
143
|
+
* @overallScore 100/100
|
|
144
|
+
*/
|
|
145
|
+
export declare function registerApiOriginGuardMiddleware(app: Express, deps: ApiOriginGuardMiddlewareDeps): void;
|
|
146
|
+
//# sourceMappingURL=api-security-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-security-middleware.d.ts","sourceRoot":"","sources":["../src/api-security-middleware.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAgBvB;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKjF;AAED,MAAM,WAAW,2BAA2B;IAC1C,yGAAyG;IACzG,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAuBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE,2BAAgC,GAAG,IAAI,CA6C1G;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,IAKf,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CA2B1G;AAED,MAAM,WAAW,4BAA4B;IAC3C,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,0FAA0F;IAC1F,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjD,oHAAoH;IACpH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,4BAA4B,GAAG,IAAI,CAmCvG"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module api-security-middleware
|
|
3
|
+
*
|
|
4
|
+
* The `/api` request gates a locally-bound daemon needs before any route handler runs:
|
|
5
|
+
* bearer-token authentication (optional, active only when a token is configured), cross-origin
|
|
6
|
+
* rejection (always active), and — for a daemon whose threat model is a co-resident process rather
|
|
7
|
+
* than a remote one — a strict bearer gate with no loopback exemption and no disable flag
|
|
8
|
+
* ({@link requireStrictBearerToken}). The first two were genericized from an origin daemon's inline
|
|
9
|
+
* `startServer` middleware pair — see `source-map.md` for the exact drop-list. All are plain Express
|
|
10
|
+
* middleware factories with configuration injected, so none reads a hardcoded env var name or an
|
|
11
|
+
* OD-specific request shape.
|
|
12
|
+
*
|
|
13
|
+
* The two token gates differ in exactly one load-bearing way and are not interchangeable: the
|
|
14
|
+
* optional one lets any loopback peer through unauthenticated, the strict one does not. Picking the
|
|
15
|
+
* wrong one is a silent no-op rather than a visible error, so see each function's own doc before
|
|
16
|
+
* choosing.
|
|
17
|
+
*
|
|
18
|
+
* **Dropped, not carried over** (see `source-map.md`'s transformation table for the full
|
|
19
|
+
* reasoning): the project-preview-scope GET exemption, the zero-config browser-extension
|
|
20
|
+
* ("clipper") bypass, the live-artifacts-preview bypass, and the `Origin: null` safe-GET
|
|
21
|
+
* allow-list regex — all four name or exist solely for OD product routes with no meaning in the
|
|
22
|
+
* generic engine. `Origin: null` is therefore always rejected here, not conditionally allowed.
|
|
23
|
+
*/
|
|
24
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
25
|
+
import { apiTokenFromEnv, isApiTokenMiddlewareEnabled, } from '@jini-ai/core';
|
|
26
|
+
import { isLoopbackPeerAddress } from './local-daemon-request.js';
|
|
27
|
+
import { allowedBrowserPorts, isAllowedBrowserOrigin } from './origin-validation.js';
|
|
28
|
+
/** Health/readiness/version probes stay reachable without a bearer token so monitoring never needs one. Both the mount-relative and `/api`-prefixed forms are listed because this middleware is always registered via `app.use('/api', ...)`, under which Express strips the `/api` prefix from `req.path` for a request to `/api/health` — the prefixed form is kept for parity with the origin module's own set rather than dropped as dead, in case a future caller mounts this middleware unprefixed. */
|
|
29
|
+
const OPEN_PROBE_PATHS = new Set([
|
|
30
|
+
'/health',
|
|
31
|
+
'/api/health',
|
|
32
|
+
'/ready',
|
|
33
|
+
'/api/ready',
|
|
34
|
+
'/version',
|
|
35
|
+
'/api/version',
|
|
36
|
+
]);
|
|
37
|
+
const BEARER_TOKEN_PATTERN = /^Bearer\s+(\S+)\s*$/i;
|
|
38
|
+
/**
|
|
39
|
+
* Extracts the token from an `Authorization: Bearer <token>` header value. The scheme is
|
|
40
|
+
* case-insensitive per RFC 7235 §2.1; the token itself is not.
|
|
41
|
+
*
|
|
42
|
+
* Shared by every bearer gate in this package so the accepted header grammar is defined exactly
|
|
43
|
+
* once — three separate copies of this regex is three chances for them to disagree about what
|
|
44
|
+
* counts as a well-formed header.
|
|
45
|
+
*
|
|
46
|
+
* @param header - The raw `Authorization` header value, or `undefined` when absent.
|
|
47
|
+
* @returns The token, or `null` when the header is absent or not a well-formed bearer header.
|
|
48
|
+
* @complexity O(n) in the header length.
|
|
49
|
+
* @overallScore 100/100
|
|
50
|
+
*/
|
|
51
|
+
export function bearerTokenFromHeader(header) {
|
|
52
|
+
return BEARER_TOKEN_PATTERN.exec(header ?? '')?.[1] ?? null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Constant-time token comparison. Length is compared first and NOT in constant time — that leaks
|
|
56
|
+
* only the expected token's length, which for a generated secret is fixed and not itself a secret,
|
|
57
|
+
* never any of its bytes. `timingSafeEqual` throws on a length mismatch, so the early return is
|
|
58
|
+
* required rather than merely an optimization.
|
|
59
|
+
*
|
|
60
|
+
* Every bearer gate in this package routes through here instead of `===`. A plain string compare
|
|
61
|
+
* short-circuits on the first differing byte, which makes the comparison's duration a function of
|
|
62
|
+
* how many leading bytes the presented token got right — enough, over many requests, to recover a
|
|
63
|
+
* token one byte at a time.
|
|
64
|
+
*
|
|
65
|
+
* @param presented - The token from the request's `Authorization` header.
|
|
66
|
+
* @param expected - The configured token to match against.
|
|
67
|
+
* @returns `true` only on an exact match.
|
|
68
|
+
* @complexity O(n) in the token length, with no data-dependent early exit.
|
|
69
|
+
* @overallScore 100/100
|
|
70
|
+
*/
|
|
71
|
+
export function timingSafeTokenMatch(presented, expected) {
|
|
72
|
+
const presentedBytes = Buffer.from(presented, 'utf8');
|
|
73
|
+
const expectedBytes = Buffer.from(expected, 'utf8');
|
|
74
|
+
if (presentedBytes.length !== expectedBytes.length)
|
|
75
|
+
return false;
|
|
76
|
+
return timingSafeEqual(presentedBytes, expectedBytes);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Headers a reverse proxy adds when it forwards a request on behalf of some other client. Their
|
|
80
|
+
* *values* are deliberately never read — this package does not trust them to identify anyone (that
|
|
81
|
+
* is the `X-Forwarded-For` spoofing trap the loopback check was written to avoid). Only their
|
|
82
|
+
* presence is used, and only to withhold a privilege: a request carrying one was relayed by a
|
|
83
|
+
* proxy, so it is not the local desktop/CLI peer the loopback exemption exists for.
|
|
84
|
+
*/
|
|
85
|
+
const PROXY_FORWARDING_HEADERS = ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'forwarded'];
|
|
86
|
+
function looksProxied(req) {
|
|
87
|
+
return PROXY_FORWARDING_HEADERS.some((header) => {
|
|
88
|
+
const value = req.headers[header];
|
|
89
|
+
return typeof value === 'string' ? value.length > 0 : Array.isArray(value) && value.length > 0;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const DEFAULT_TOKEN_CONFIG = {
|
|
93
|
+
tokenEnvVar: 'JINI_API_TOKEN',
|
|
94
|
+
disableEnvVar: 'JINI_DISABLE_API_AUTH',
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Registers a bearer-token gate on every `/api/*` route, active only when
|
|
98
|
+
* {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
|
|
99
|
+
* When active: open-probe paths and loopback peers skip the check unconditionally; every other
|
|
100
|
+
* request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
|
|
101
|
+
* request is rejected with 401 before reaching any route handler.
|
|
102
|
+
*
|
|
103
|
+
* @param app - The Express app to register the gate on.
|
|
104
|
+
* @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
|
|
105
|
+
* entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
|
|
106
|
+
* @returns Nothing. Registers zero middleware (a deliberate no-op, not a bug) when no token is configured.
|
|
107
|
+
* @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
|
|
108
|
+
* @overallScore 100/100
|
|
109
|
+
*/
|
|
110
|
+
export function registerApiBearerAuthMiddleware(app, deps = {}) {
|
|
111
|
+
const tokenConfig = deps.tokenConfig ?? DEFAULT_TOKEN_CONFIG;
|
|
112
|
+
const env = deps.env ?? process.env;
|
|
113
|
+
if (!isApiTokenMiddlewareEnabled(tokenConfig, env))
|
|
114
|
+
return;
|
|
115
|
+
const apiToken = apiTokenFromEnv(tokenConfig, env);
|
|
116
|
+
const trustLoopbackPeers = deps.trustLoopbackPeers ?? true;
|
|
117
|
+
app.use('/api', (req, res, next) => {
|
|
118
|
+
if (OPEN_PROBE_PATHS.has(req.path)) {
|
|
119
|
+
next();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Loopback short-circuit: the desktop UI / local CLI never carry a bearer, and the peer address
|
|
123
|
+
// is read from the socket rather than `X-Forwarded-For` so a spoofed forwarding header can
|
|
124
|
+
// never *grant* the exemption.
|
|
125
|
+
//
|
|
126
|
+
// That alone is not enough. A reverse proxy running on the same host as the daemon — nginx or
|
|
127
|
+
// caddy on the same box, proxying to a bind on 127.0.0.1, an extremely common deployment —
|
|
128
|
+
// terminates the client's connection itself and opens its own from loopback, so every
|
|
129
|
+
// externally-originated request through it arrives with a loopback socket address. Trusting the
|
|
130
|
+
// socket address alone would hand the exemption to the entire public internet.
|
|
131
|
+
//
|
|
132
|
+
// So the exemption is withheld from any request carrying a forwarding header: those are added
|
|
133
|
+
// by the proxy, not by the local desktop/CLI clients this exemption exists for. The header's
|
|
134
|
+
// value is still never read — presence only ever *removes* the exemption, so a caller who
|
|
135
|
+
// forges one gains nothing and simply has to present the bearer like anyone else. A proxy
|
|
136
|
+
// deployment that strips its own forwarding headers is not covered by this and must set
|
|
137
|
+
// `trustLoopbackPeers: false`, which is also the correct setting for any host where an
|
|
138
|
+
// untrusted co-resident process can reach the port.
|
|
139
|
+
if (trustLoopbackPeers && isLoopbackPeerAddress(req.socket?.remoteAddress) && !looksProxied(req)) {
|
|
140
|
+
next();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const presented = bearerTokenFromHeader(req.get('authorization'));
|
|
144
|
+
if (presented === null || !timingSafeTokenMatch(presented, apiToken)) {
|
|
145
|
+
res.status(401).json({
|
|
146
|
+
error: {
|
|
147
|
+
code: 'API_TOKEN_REQUIRED',
|
|
148
|
+
message: `Authorization: Bearer <${tokenConfig.tokenEnvVar}> required`,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
next();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Builds a bearer gate with **no loopback exemption and no disable flag** — for a daemon whose
|
|
158
|
+
* threat model is "another process running as the same OS user", not "a remote attacker".
|
|
159
|
+
*
|
|
160
|
+
* This is deliberately not {@link registerApiBearerAuthMiddleware}. That gate short-circuits for any
|
|
161
|
+
* loopback peer before it ever reads the `Authorization` header, which is the right affordance for a
|
|
162
|
+
* desktop UI or local CLI talking to its own daemon, and a no-op against the threat here: a
|
|
163
|
+
* `127.0.0.1` bind keeps remote hosts out but does nothing about a co-resident process, and a
|
|
164
|
+
* sidecar daemon that can start real agent runs and execute real tools is exactly the kind of thing
|
|
165
|
+
* such a process would want to reach. This gate treats a request from `127.0.0.1` like any other.
|
|
166
|
+
*
|
|
167
|
+
* The posture matches `remote-run-events.ts`'s own token gate, which made the same call for the same
|
|
168
|
+
* reason on a single route; this generalizes it to a whole `/api` surface.
|
|
169
|
+
*
|
|
170
|
+
* Fail-closed contract:
|
|
171
|
+
* - token env var unset/empty -> **503**, never a silent pass-through. A misconfigured daemon
|
|
172
|
+
* refuses to serve rather than serving unauthenticated callers.
|
|
173
|
+
* - missing / malformed / wrong token -> **401**.
|
|
174
|
+
* - exact match -> `next()`.
|
|
175
|
+
*
|
|
176
|
+
* The env var is read on every request rather than captured at factory time: a host that mints a
|
|
177
|
+
* per-boot token may not have done so yet when its module graph is first evaluated, and re-reading
|
|
178
|
+
* costs one property lookup.
|
|
179
|
+
*
|
|
180
|
+
* @param deps - See {@link StrictBearerTokenDeps}. `tokenEnvVar` is required.
|
|
181
|
+
* @returns Express middleware. Mount it **unprefixed** — `app.use(handler)` — before any route
|
|
182
|
+
* registrar and before the JSON body parser, so an unauthenticated caller's body is never parsed.
|
|
183
|
+
* @complexity O(1) per request plus O(n) in the token length for the constant-time comparison.
|
|
184
|
+
* @overallScore 95/100 — Medium: mounting under a prefix (`app.use('/api', handler)`) silently breaks
|
|
185
|
+
* `exemptPaths`. Express strips the mount prefix from `req.path`, so an entry written as a full path
|
|
186
|
+
* (`/api/delegated-tool-calls`) stops matching and the gate rejects a caller the mount site meant to
|
|
187
|
+
* exempt. Nothing type-checks that, and the failure is a 401 on a route that is supposed to be open
|
|
188
|
+
* rather than the reverse — noisy rather than dangerous, but confusing to diagnose. Documented rather
|
|
189
|
+
* than defended against in code: normalizing `req.baseUrl + req.path` here would silently accept both
|
|
190
|
+
* mount styles and remove the caller's ability to reason about which paths its entries refer to.
|
|
191
|
+
*/
|
|
192
|
+
export function requireStrictBearerToken(deps) {
|
|
193
|
+
const { tokenEnvVar } = deps;
|
|
194
|
+
const env = deps.env ?? process.env;
|
|
195
|
+
const exempt = new Set(deps.exemptPaths ?? []);
|
|
196
|
+
return function requireStrictBearerTokenMiddleware(req, res, next) {
|
|
197
|
+
if (exempt.has(req.path)) {
|
|
198
|
+
next();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const expected = env[tokenEnvVar];
|
|
202
|
+
if (typeof expected !== 'string' || expected.length === 0) {
|
|
203
|
+
res.status(503).json({
|
|
204
|
+
error: {
|
|
205
|
+
code: 'API_TOKEN_NOT_CONFIGURED',
|
|
206
|
+
message: `${tokenEnvVar} is not set — this daemon refuses to serve unauthenticated callers`,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Deliberately no loopback/peer-address exemption — see this function's doc.
|
|
212
|
+
const presented = bearerTokenFromHeader(req.get('authorization'));
|
|
213
|
+
if (presented === null || !timingSafeTokenMatch(presented, expected)) {
|
|
214
|
+
res.status(401).json({
|
|
215
|
+
error: { code: 'API_TOKEN_REQUIRED', message: `Authorization: Bearer <${tokenEnvVar}> required` },
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
next();
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Chrome may strip the port from the `Origin` header on same-origin GET requests. Used only as a
|
|
224
|
+
* narrow fallback for safe, idempotent GET requests once the exact-match check below has already
|
|
225
|
+
* failed — mutating routes always require an exact origin/host match.
|
|
226
|
+
*/
|
|
227
|
+
function isPortlessLoopbackOrigin(origin) {
|
|
228
|
+
return /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])$/.test(origin);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
|
|
232
|
+
* `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
|
|
233
|
+
* allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
|
|
234
|
+
* (typically a sandboxed iframe) is always rejected — see this module's doc for why the origin
|
|
235
|
+
* daemon's safe-GET exemption for that case was dropped.
|
|
236
|
+
*
|
|
237
|
+
* @param app - The Express app to register the gate on.
|
|
238
|
+
* @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
|
|
239
|
+
* @returns Nothing. Unlike the bearer-token gate, this always registers its middleware — there is
|
|
240
|
+
* no "disabled" state.
|
|
241
|
+
* @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
|
|
242
|
+
* @overallScore 100/100
|
|
243
|
+
*/
|
|
244
|
+
export function registerApiOriginGuardMiddleware(app, deps) {
|
|
245
|
+
const { host, getResolvedPort } = deps;
|
|
246
|
+
const extraAllowedOrigins = deps.extraAllowedOrigins ?? [];
|
|
247
|
+
const env = deps.env ?? process.env;
|
|
248
|
+
app.use('/api', (req, res, next) => {
|
|
249
|
+
const origin = req.headers.origin;
|
|
250
|
+
if (origin == null || origin === '') {
|
|
251
|
+
next();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (origin === 'null') {
|
|
255
|
+
res.status(403).json({ error: 'Origin: null not allowed for this route' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Fail-closed: block every browser origin until the daemon's real listen port is known, so a
|
|
259
|
+
// request arriving in the brief window before `.listen()` resolves can never be compared
|
|
260
|
+
// against a wrong or default port.
|
|
261
|
+
const resolvedPort = getResolvedPort();
|
|
262
|
+
if (!resolvedPort) {
|
|
263
|
+
res.status(403).json({ error: 'Server initializing' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const ports = allowedBrowserPorts(resolvedPort, env);
|
|
267
|
+
if (!isAllowedBrowserOrigin(origin, req.headers.host, ports, host, [...extraAllowedOrigins])) {
|
|
268
|
+
if (req.method !== 'GET' || !isPortlessLoopbackOrigin(String(origin))) {
|
|
269
|
+
res.status(403).json({ error: 'Cross-origin requests are not allowed' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
next();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=api-security-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-security-middleware.js","sourceRoot":"","sources":["../src/api-security-middleware.ts"],"names":[],"mappings":"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"}
|