@jini-ai/http-kit 0.3.7 → 0.3.8

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.
Files changed (37) hide show
  1. package/dist/delegated-tools.d.ts +22 -0
  2. package/dist/delegated-tools.js +35 -6
  3. package/package.json +5 -5
  4. package/dist/express/run-stream.d.ts +0 -15
  5. package/dist/express/run-stream.js +0 -11
  6. package/dist/express-index.d.ts +0 -32
  7. package/dist/express-index.js +0 -14
  8. package/dist/fastify/adapter.d.ts +0 -31
  9. package/dist/fastify/adapter.js +0 -64
  10. package/dist/fastify/agents.d.ts +0 -13
  11. package/dist/fastify/agents.js +0 -7
  12. package/dist/fastify/api-security-middleware.d.ts +0 -64
  13. package/dist/fastify/api-security-middleware.js +0 -139
  14. package/dist/fastify/compat.d.ts +0 -22
  15. package/dist/fastify/compat.js +0 -16
  16. package/dist/fastify/daemon-status.d.ts +0 -22
  17. package/dist/fastify/daemon-status.js +0 -9
  18. package/dist/fastify/host-tools.d.ts +0 -13
  19. package/dist/fastify/host-tools.js +0 -8
  20. package/dist/fastify/index.d.ts +0 -36
  21. package/dist/fastify/index.js +0 -18
  22. package/dist/fastify/local-daemon-request.d.ts +0 -43
  23. package/dist/fastify/local-daemon-request.js +0 -155
  24. package/dist/fastify/origin.d.ts +0 -21
  25. package/dist/fastify/origin.js +0 -14
  26. package/dist/fastify/request.d.ts +0 -20
  27. package/dist/fastify/request.js +0 -25
  28. package/dist/fastify/response.d.ts +0 -20
  29. package/dist/fastify/response.js +0 -41
  30. package/dist/fastify/route-registration-guard.d.ts +0 -70
  31. package/dist/fastify/route-registration-guard.js +0 -69
  32. package/dist/fastify/run-stream.d.ts +0 -18
  33. package/dist/fastify/run-stream.js +0 -10
  34. package/dist/fastify/runs.d.ts +0 -17
  35. package/dist/fastify/runs.js +0 -33
  36. package/dist/run-stream.d.ts +0 -60
  37. package/dist/run-stream.js +0 -108
@@ -36,6 +36,13 @@ export interface DelegatedToolsInternalErrorContext {
36
36
  readonly toolId: string;
37
37
  readonly correlationId: string;
38
38
  readonly error: unknown;
39
+ /**
40
+ * The settled status when the failure is a `ToolExecutionResult` this route will not show as-is
41
+ * (`timed-out`, `cancelled`, or a `failed` the host did not mark model-safe); absent when `error`
42
+ * is a thrown exception. Lets a host word a timeout differently from a crash without parsing
43
+ * `error`, which for a settled result is only `result.error ?? result.status`.
44
+ */
45
+ readonly status?: 'timed-out' | 'cancelled' | 'failed';
39
46
  }
40
47
  export interface DelegatedToolsHttpDeps {
41
48
  readonly lifecycle: RunLifecycle;
@@ -67,6 +74,21 @@ export interface DelegatedToolsHttpDeps {
67
74
  * redaction of its own on this path.
68
75
  */
69
76
  readonly isModelSafeToolFailure?: (result: ToolExecutionResult) => boolean;
77
+ /**
78
+ * Host opt-in: the model-safe text to send in place of `'an internal error occurred'` for every
79
+ * failure this route would otherwise answer with the SEC-005-redacted `500 INTERNAL_ERROR` — a
80
+ * thrown executor (e.g. an unknown tool id), a throwing `resolvePrincipal`, `timed-out`,
81
+ * `cancelled`, and a `failed` result `isModelSafeToolFailure` did not vouch for. Without it, those
82
+ * reach the model as a bare "INTERNAL_ERROR" it can neither act on nor report.
83
+ *
84
+ * The response keeps `500 INTERNAL_ERROR` and its `requestId`; only `message` changes, and
85
+ * `onInternalError` still receives the raw failure first. Returning `undefined` or `''` keeps the
86
+ * generic message for that failure, and so does a describer that throws — a broken describer can
87
+ * never turn a 500 into a leak or a crash. Omitted (the default), nothing changes. The host owns
88
+ * redaction here exactly as for `isModelSafeToolFailure`: this route sends the returned text
89
+ * verbatim.
90
+ */
91
+ readonly describeInternalError?: (context: DelegatedToolsInternalErrorContext) => string | undefined;
70
92
  }
71
93
  /**
72
94
  * Refusal text for a `requireReadOnly` call naming a tool that is not registered read-only —
@@ -168,11 +168,39 @@ function defaultInternalErrorSink(context) {
168
168
  // eslint-disable-next-line no-console
169
169
  console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
170
170
  }
171
- function reportInternalError(deps, source, error, runId, toolId) {
171
+ /**
172
+ * The host's {@link DelegatedToolsHttpDeps.describeInternalError} text for `context`, or `undefined`
173
+ * when there is no describer, it declines (`undefined`/`''`), or it throws.
174
+ *
175
+ * @complexity O(1) plus the describer's own cost.
176
+ */
177
+ function describeSafely(deps, context) {
178
+ if (deps.describeInternalError === undefined)
179
+ return undefined;
180
+ try {
181
+ const text = deps.describeInternalError(context);
182
+ return typeof text === 'string' && text.length > 0 ? text : undefined;
183
+ }
184
+ catch (error) {
185
+ // eslint-disable-next-line no-console
186
+ console.error(`[@jini-ai/http-kit] describeInternalError threw (correlationId=${context.correlationId})`, error);
187
+ return undefined;
188
+ }
189
+ }
190
+ function reportInternalError(deps, source, error, runId, toolId, status) {
172
191
  const correlationId = randomUUID();
192
+ const context = {
193
+ source,
194
+ runId,
195
+ toolId,
196
+ correlationId,
197
+ error,
198
+ ...(status === undefined ? {} : { status }),
199
+ };
173
200
  const sink = deps.onInternalError ?? defaultInternalErrorSink;
174
- sink({ source, runId, toolId, correlationId, error });
175
- return createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId });
201
+ sink(context);
202
+ const message = describeSafely(deps, context) ?? 'an internal error occurred';
203
+ return createApiError('INTERNAL_ERROR', message, { requestId: correlationId });
176
204
  }
177
205
  function isRecord(value) {
178
206
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -212,7 +240,8 @@ function parseDelegatedToolExecute(input) {
212
240
  * union, kept consistent here rather than reinvented: `completed` → `200 {result}`,
213
241
  * `denied`/`confirmation-denied` → `403 TOOL_OPERATION_DENIED`, `timed-out`/`cancelled` → a
214
242
  * SEC-005-redacted `500 INTERNAL_ERROR` (the real status/error goes to `onInternalError`, never the
215
- * wire).
243
+ * wire — unless the host supplies `deps.describeInternalError`, whose text then replaces the generic
244
+ * message on every redacted 500 below).
216
245
  *
217
246
  * `failed` splits in two, on `result.errorKind` (`@jini-ai/daemon`'s `ToolExecutor` sets it from
218
247
  * whether the handler threw `@jini-ai/core`'s `ToolInputError`): `'validation'` means the CALLER's
@@ -235,7 +264,7 @@ function toolExecutionResultToApiResult(deps, runId, toolId, result) {
235
264
  return err(createApiError('TOOL_OPERATION_DENIED', 'this operation was denied during confirmation'));
236
265
  case 'timed-out':
237
266
  case 'cancelled':
238
- return err(reportInternalError(deps, 'delegated-tool-execute', result.status, runId, toolId));
267
+ return err(reportInternalError(deps, 'delegated-tool-execute', result.status, runId, toolId, result.status));
239
268
  case 'failed':
240
269
  if (result.errorKind === 'validation') {
241
270
  return err(createApiError('BAD_REQUEST', result.error ?? 'invalid tool input'));
@@ -243,7 +272,7 @@ function toolExecutionResultToApiResult(deps, runId, toolId, result) {
243
272
  if (result.error && deps.isModelSafeToolFailure?.(result) === true) {
244
273
  return err(createApiError('TOOL_EXECUTION_FAILED', result.error));
245
274
  }
246
- return err(reportInternalError(deps, 'delegated-tool-execute', result.error ?? result.status, runId, toolId));
275
+ return err(reportInternalError(deps, 'delegated-tool-execute', result.error ?? result.status, runId, toolId, 'failed'));
247
276
  }
248
277
  }
249
278
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/http-kit",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Composable Express route packs and JSON-route transport for a @jini-ai/core daemon composition: request parsing, response serialization, same-origin guard, and a route-pack registrar.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,15 +40,15 @@
40
40
  "dependencies": {
41
41
  "express": "^4.21.0",
42
42
  "@jini-ai/core": "0.3.1",
43
+ "@jini-ai/protocol": "0.3.1",
43
44
  "@jini-ai/platform": "0.3.0",
44
- "@jini-ai/agent-runtime": "0.3.9",
45
- "@jini-ai/protocol": "0.3.0",
46
- "@jini-ai/daemon": "0.3.7"
45
+ "@jini-ai/daemon": "0.3.8",
46
+ "@jini-ai/agent-runtime": "0.3.10"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/express": "^4.17.21",
50
50
  "@vitest/coverage-v8": "^2.1.9",
51
- "@jini-ai/chat": "0.3.8"
51
+ "@jini-ai/chat": "0.3.11"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsc -p tsconfig.json",
@@ -1,15 +0,0 @@
1
- /**
2
- * @module express/run-stream
3
- *
4
- * Express-specific mounting glue for the shared AG-UI SSE run-stream handler (`../run-stream.js`).
5
- * Express's `Request`/`Response` already *are* Node's raw `http.IncomingMessage`/
6
- * `http.ServerResponse` (Express's `Response` extends `http.ServerResponse` directly), so this
7
- * file's only job is resolving `req.params.runId` and handing the request/response straight
8
- * through — a few lines, not a reimplementation, per the design decision that motivated building
9
- * the SSE primitive once instead of duplicating it per transport.
10
- */
11
- import type { Express } from 'express';
12
- import { type RunStreamDeps } from '../run-stream.js';
13
- /** Mounts the AG-UI SSE run-stream route on `app`. A pack's `http(app, services)` calls this directly. */
14
- export declare function registerRunStreamRoute(app: Express, deps: RunStreamDeps): void;
15
- //# sourceMappingURL=run-stream.d.ts.map
@@ -1,11 +0,0 @@
1
- import { handleRunStreamRequest, RUN_STREAM_ROUTE_PATH } from '../run-stream.js';
2
- /** Mounts the AG-UI SSE run-stream route on `app`. A pack's `http(app, services)` calls this directly. */
3
- export function registerRunStreamRoute(app, deps) {
4
- app.get(RUN_STREAM_ROUTE_PATH, async (req, res) => {
5
- // `:runId` is a required path segment of RUN_STREAM_ROUTE_PATH — this handler is only ever
6
- // reached via a URL that already matched it, so the param is always present at runtime even
7
- // though @types/express types every param as possibly `undefined` in general.
8
- await handleRunStreamRequest(req, res, req.params.runId, deps);
9
- });
10
- }
11
- //# sourceMappingURL=run-stream.js.map
@@ -1,32 +0,0 @@
1
- /**
2
- * @module @jini/http/express
3
- *
4
- * The Express-native transport namespace: re-exports this package's existing flat Express
5
- * implementation (request parsing, response serialization, the mounting Adapter, the `/api`
6
- * security middleware, the route-registration guard, compat error helpers, the daemon-status
7
- * routes, and the runs/agents/host-tools route packs) under one namespace, mirroring
8
- * `./fastify/index.js`'s shape so a transport-switchable caller (`@jini/node-host`'s
9
- * `createLocalNodeDaemon`) can import either namespace symmetrically. Nothing here is a new
10
- * implementation — every export is the same flat file every other consumer of this package
11
- * already imports directly from the root barrel; this module exists only so `httpExpress.*` and
12
- * `httpFastify.*` read the same at call sites that need to pick a transport explicitly.
13
- */
14
- export type { AdapterContext } from './adapter.js';
15
- export { defineJsonRoute, mountJsonRoute } from './adapter.js';
16
- export type { ApiBearerAuthMiddlewareDeps, ApiOriginGuardMiddlewareDeps } from './api-security-middleware.js';
17
- export { registerApiBearerAuthMiddleware, registerApiOriginGuardMiddleware } from './api-security-middleware.js';
18
- export { createCompatApiError, createCompatApiErrorResponse, sendApiError as sendCompatApiError, } from './compat.js';
19
- export type { DaemonShutdownResponse, DaemonStatusDeps, DaemonStatusResponse, } from './daemon-status.js';
20
- export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
21
- export { isLoopbackHostname, isLoopbackPeerAddress, localOriginFromHeader, normalizeLocalAuthority, requireLocalDaemonRequest, validateLocalDaemonRequest, } from './local-daemon-request.js';
22
- export type { OriginContext } from './origin.js';
23
- export { guardSameOrigin } from './origin.js';
24
- export { rawInput, validationError } from './request.js';
25
- export { registerRunStreamRoute } from './express/run-stream.js';
26
- export { sendApiError, sendJson, statusForError } from './response.js';
27
- export type { InstallRouteRegistrationGuardOptions, RouteRegistration } from './route-registration-guard.js';
28
- export { getRouteRegistrationInventory, guardedRouteKey, installRouteRegistrationGuard, } from './route-registration-guard.js';
29
- export { registerRunRoutes } from './runs.js';
30
- export { registerAgentRoutes } from './agents.js';
31
- export { registerHostToolsRoutes } from './host-tools.js';
32
- //# sourceMappingURL=express-index.d.ts.map
@@ -1,14 +0,0 @@
1
- export { defineJsonRoute, mountJsonRoute } from './adapter.js';
2
- export { registerApiBearerAuthMiddleware, registerApiOriginGuardMiddleware } from './api-security-middleware.js';
3
- export { createCompatApiError, createCompatApiErrorResponse, sendApiError as sendCompatApiError, } from './compat.js';
4
- export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
5
- export { isLoopbackHostname, isLoopbackPeerAddress, localOriginFromHeader, normalizeLocalAuthority, requireLocalDaemonRequest, validateLocalDaemonRequest, } from './local-daemon-request.js';
6
- export { guardSameOrigin } from './origin.js';
7
- export { rawInput, validationError } from './request.js';
8
- export { registerRunStreamRoute } from './express/run-stream.js';
9
- export { sendApiError, sendJson, statusForError } from './response.js';
10
- export { getRouteRegistrationInventory, guardedRouteKey, installRouteRegistrationGuard, } from './route-registration-guard.js';
11
- export { registerRunRoutes } from './runs.js';
12
- export { registerAgentRoutes } from './agents.js';
13
- export { registerHostToolsRoutes } from './host-tools.js';
14
- //# sourceMappingURL=express-index.js.map
@@ -1,31 +0,0 @@
1
- /**
2
- * The module's top orchestration layer: wires request parsing, the same-origin guard, a route's
3
- * `handle`, and response serialization into a single Fastify route handler. This is the only file
4
- * in this subtree that knows about Fastify's `request`/`reply` on the mounting side. Same job as
5
- * `../express/adapter.ts`'s `mountJsonRoute`, registered through Fastify's native
6
- * `fastify.route({ method, url, handler })` instead of Express's `app[method](path, handler)`.
7
- */
8
- import type { FastifyInstance } from 'fastify';
9
- import { type OriginContext } from './origin.js';
10
- import type { JsonRouteSpec } from '../types.js';
11
- /** Server startup state a mounted route needs to evaluate its same-origin guard. */
12
- export interface AdapterContext extends OriginContext {
13
- }
14
- /**
15
- * Identity function that pins a route spec's generic parameters at the definition site so
16
- * callers do not have to repeat them. The returned spec is consumed by `mountJsonRoute` (live)
17
- * and by tests (direct invocation of `route.parse` / `route.handle`).
18
- */
19
- export declare function defineJsonRoute<Input, Output, Deps>(spec: JsonRouteSpec<Input, Output, Deps>): JsonRouteSpec<Input, Output, Deps>;
20
- /**
21
- * Mounts one JsonRouteSpec on a Fastify instance. The Adapter is the only code here that knows
22
- * about request/reply; the route's parse and handle functions operate on `RouteInputContext` and
23
- * `Deps` respectively, so they are unit testable without Fastify.
24
- *
25
- * `exposeHeadRoute: false` is passed explicitly so a `GET` spec does not also register Fastify's
26
- * automatic `HEAD` sibling route — the Express version never registers one either, and a spec's
27
- * `handle` is not written to answer a bodyless `HEAD` request correctly (e.g. `daemonStatusRoute`
28
- * always returns a JSON body).
29
- */
30
- export declare function mountJsonRoute<Input, Output, Deps>(app: FastifyInstance, spec: JsonRouteSpec<Input, Output, Deps>, deps: Deps, adapter: AdapterContext): void;
31
- //# sourceMappingURL=adapter.d.ts.map
@@ -1,64 +0,0 @@
1
- import { createApiError } from '@jini/protocol';
2
- import { rawInput } from './request.js';
3
- import { sendApiError, sendJson, statusForError } from './response.js';
4
- import { guardSameOrigin } from './origin.js';
5
- /**
6
- * Identity function that pins a route spec's generic parameters at the definition site so
7
- * callers do not have to repeat them. The returned spec is consumed by `mountJsonRoute` (live)
8
- * and by tests (direct invocation of `route.parse` / `route.handle`).
9
- */
10
- export function defineJsonRoute(spec) {
11
- return spec;
12
- }
13
- /** Maps the shared, lower-case `HttpMethod` union to the upper-case method literal Fastify's `route()` expects. */
14
- const FASTIFY_METHOD = {
15
- get: 'GET',
16
- post: 'POST',
17
- put: 'PUT',
18
- delete: 'DELETE',
19
- patch: 'PATCH',
20
- };
21
- /**
22
- * Mounts one JsonRouteSpec on a Fastify instance. The Adapter is the only code here that knows
23
- * about request/reply; the route's parse and handle functions operate on `RouteInputContext` and
24
- * `Deps` respectively, so they are unit testable without Fastify.
25
- *
26
- * `exposeHeadRoute: false` is passed explicitly so a `GET` spec does not also register Fastify's
27
- * automatic `HEAD` sibling route — the Express version never registers one either, and a spec's
28
- * `handle` is not written to answer a bodyless `HEAD` request correctly (e.g. `daemonStatusRoute`
29
- * always returns a JSON body).
30
- */
31
- export function mountJsonRoute(app, spec, deps, adapter) {
32
- app.route({
33
- method: FASTIFY_METHOD[spec.method],
34
- url: spec.path,
35
- exposeHeadRoute: false,
36
- handler: async (req, reply) => {
37
- try {
38
- if (spec.requireSameOrigin) {
39
- const origin = guardSameOrigin(req, adapter);
40
- if (!origin.ok) {
41
- sendApiError(reply, statusForError(origin.error), origin.error);
42
- return;
43
- }
44
- }
45
- const parsed = spec.parse(rawInput(req));
46
- if (!parsed.ok) {
47
- sendApiError(reply, statusForError(parsed.error), parsed.error);
48
- return;
49
- }
50
- const result = await spec.handle(parsed.value, deps);
51
- if (!result.ok) {
52
- sendApiError(reply, statusForError(result.error), result.error);
53
- return;
54
- }
55
- sendJson(reply, spec.successStatus ?? 200, result.value);
56
- }
57
- catch (e) {
58
- const message = e instanceof Error ? e.message : String(e);
59
- sendApiError(reply, 500, createApiError('INTERNAL_ERROR', message));
60
- }
61
- },
62
- });
63
- }
64
- //# sourceMappingURL=adapter.js.map
@@ -1,13 +0,0 @@
1
- /**
2
- * @module fastify/agents
3
- *
4
- * Fastify-mounting glue for `../agents.js`'s `agentListRoute` — the exact same `JsonRouteSpec`
5
- * the Express mounting in `../agents.js` uses, just mounted through this subtree's own
6
- * `mountJsonRoute` instead.
7
- */
8
- import type { FastifyInstance } from 'fastify';
9
- import { type AgentsHttpDeps } from '../agents.js';
10
- import { type AdapterContext } from './adapter.js';
11
- /** Mounts `GET /api/agents` on `app` — the Fastify-native equivalent of `../agents.js`'s `registerAgentRoutes`. */
12
- export declare function registerAgentRoutes(app: FastifyInstance, deps: AgentsHttpDeps, adapter: AdapterContext): void;
13
- //# sourceMappingURL=agents.d.ts.map
@@ -1,7 +0,0 @@
1
- import { agentListRoute } from '../agents.js';
2
- import { mountJsonRoute } from './adapter.js';
3
- /** Mounts `GET /api/agents` on `app` — the Fastify-native equivalent of `../agents.js`'s `registerAgentRoutes`. */
4
- export function registerAgentRoutes(app, deps, adapter) {
5
- mountJsonRoute(app, agentListRoute, deps, adapter);
6
- }
7
- //# sourceMappingURL=agents.js.map
@@ -1,64 +0,0 @@
1
- /**
2
- * @module fastify/api-security-middleware
3
- *
4
- * The two `/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) and cross-origin
6
- * rejection (always active). Same job and decision logic as `../express/api-security-middleware.ts`
7
- * (see that file's own doc for the full drop-list this was genericized from), reimplemented on
8
- * Fastify's native `onRequest` lifecycle hook (`app.addHook('onRequest', ...)`) in place of
9
- * Express's `app.use('/api', ...)` path-scoped middleware. Fastify has no first-class "scope this
10
- * hook to a URL prefix" primitive at the plain-instance level (that requires nesting the routes
11
- * themselves inside a `.register()` plugin with a `prefix`, which `mountPackHttp`'s pack-agnostic
12
- * mounting does not do), so both hooks below scope themselves manually by checking the request's
13
- * own path against the `/api` prefix before applying either gate — the observable behavior is
14
- * identical to the Express version's `app.use('/api', ...)` scoping.
15
- */
16
- import type { FastifyInstance } from 'fastify';
17
- import { type ApiTokenAuthEnvConfig } from '@jini/core';
18
- export interface ApiBearerAuthMiddlewareDeps {
19
- /** Env var names for the token/disable flags. Defaults to `JINI_API_TOKEN` / `JINI_DISABLE_API_AUTH`. */
20
- tokenConfig?: ApiTokenAuthEnvConfig;
21
- /** Defaults to `process.env`. Threaded through so tests never have to mutate real process env. */
22
- env?: NodeJS.ProcessEnv;
23
- }
24
- /**
25
- * Registers a bearer-token gate on every `/api/*` route, active only when
26
- * {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
27
- * When active: open-probe paths and loopback peers skip the check unconditionally; every other
28
- * request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
29
- * request is rejected with 401 before reaching any route handler.
30
- *
31
- * @param app - The Fastify instance to register the gate on.
32
- * @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
33
- * entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
34
- * @returns Nothing. Registers zero hooks (a deliberate no-op, not a bug) when no token is configured.
35
- * @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
36
- * @overallScore 100/100
37
- */
38
- export declare function registerApiBearerAuthMiddleware(app: FastifyInstance, deps?: ApiBearerAuthMiddlewareDeps): void;
39
- export interface ApiOriginGuardMiddlewareDeps {
40
- /** The host this daemon is bound to — compared against a request's `Host`/`Origin` headers. */
41
- host: string;
42
- /** Extra allow-listed origins (e.g. a reverse-proxy's public origin). Defaults to none. */
43
- extraAllowedOrigins?: readonly string[];
44
- /** Returns the daemon's resolved listen port, or a falsy value before it has resolved. */
45
- getResolvedPort: () => number | null | undefined;
46
- /** Defaults to `process.env`. Threaded through so `JINI_WEB_PORT` is testable without mutating real process env. */
47
- env?: NodeJS.ProcessEnv;
48
- }
49
- /**
50
- * Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
51
- * `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
52
- * allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
53
- * (typically a sandboxed iframe) is always rejected — see the Express version's doc for why the
54
- * origin daemon's safe-GET exemption for that case was dropped.
55
- *
56
- * @param app - The Fastify instance to register the gate on.
57
- * @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
58
- * @returns Nothing. Unlike the bearer-token gate, this always registers its hook — there is
59
- * no "disabled" state.
60
- * @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
61
- * @overallScore 100/100
62
- */
63
- export declare function registerApiOriginGuardMiddleware(app: FastifyInstance, deps: ApiOriginGuardMiddlewareDeps): void;
64
- //# sourceMappingURL=api-security-middleware.d.ts.map
@@ -1,139 +0,0 @@
1
- import { apiTokenFromEnv, isApiTokenMiddlewareEnabled, } from '@jini/core';
2
- import { isLoopbackPeerAddress } from './local-daemon-request.js';
3
- import { allowedBrowserPorts, isAllowedBrowserOrigin } from '../origin-validation.js';
4
- /** Health/readiness/version probes stay reachable without a bearer token so monitoring never needs one. Both the mount-relative and `/api`-prefixed forms are kept for parity with the Express version's set (only the `/api`-prefixed forms are ever reachable here, since both hooks below already require the path to start with `/api` before this set is even consulted). */
5
- const OPEN_PROBE_PATHS = new Set([
6
- '/health',
7
- '/api/health',
8
- '/ready',
9
- '/api/ready',
10
- '/version',
11
- '/api/version',
12
- ]);
13
- const BEARER_TOKEN_PATTERN = /^Bearer\s+(\S+)\s*$/i;
14
- const DEFAULT_TOKEN_CONFIG = {
15
- tokenEnvVar: 'JINI_API_TOKEN',
16
- disableEnvVar: 'JINI_DISABLE_API_AUTH',
17
- };
18
- /**
19
- * The request path with no query string — Fastify's `request.url` (unlike Express's
20
- * `request.path`) always includes the query string, so this strips it to match Express's
21
- * comparison semantics. `req.url` is typed as a required `string` on `FastifyRequest`
22
- * (never `undefined`), and `String.prototype.split` on a string separator always returns an
23
- * array with at least one element, so the `[0]` index is provably defined — the non-null
24
- * assertion documents that guarantee instead of masking it behind an unreachable `?? ''` fallback
25
- * that could never be exercised by a real request.
26
- */
27
- function requestPath(req) {
28
- return req.url.split('?')[0];
29
- }
30
- /**
31
- * Registers a bearer-token gate on every `/api/*` route, active only when
32
- * {@link isApiTokenMiddlewareEnabled} says a token is configured and auth hasn't been disabled.
33
- * When active: open-probe paths and loopback peers skip the check unconditionally; every other
34
- * request must send `Authorization: Bearer <token>` matching the configured token exactly, or the
35
- * request is rejected with 401 before reaching any route handler.
36
- *
37
- * @param app - The Fastify instance to register the gate on.
38
- * @param deps - See {@link ApiBearerAuthMiddlewareDeps}. Both fields are optional; omitting `deps`
39
- * entirely reads `JINI_API_TOKEN`/`JINI_DISABLE_API_AUTH` from real `process.env`.
40
- * @returns Nothing. Registers zero hooks (a deliberate no-op, not a bug) when no token is configured.
41
- * @complexity Setup is O(1). Each gated request is O(1) (one Set lookup, one regex match).
42
- * @overallScore 100/100
43
- */
44
- export function registerApiBearerAuthMiddleware(app, deps = {}) {
45
- const tokenConfig = deps.tokenConfig ?? DEFAULT_TOKEN_CONFIG;
46
- const env = deps.env ?? process.env;
47
- if (!isApiTokenMiddlewareEnabled(tokenConfig, env))
48
- return;
49
- const apiToken = apiTokenFromEnv(tokenConfig, env);
50
- app.addHook('onRequest', (req, reply, done) => {
51
- const path = requestPath(req);
52
- if (!path.startsWith('/api')) {
53
- done();
54
- return;
55
- }
56
- if (OPEN_PROBE_PATHS.has(path)) {
57
- done();
58
- return;
59
- }
60
- // Loopback short-circuit: the desktop UI / local CLI never carry a bearer, and a reverse
61
- // proxy in front of a non-loopback bind must always forward the real bearer itself — so this
62
- // is intentionally not fooled by a proxied `X-Forwarded-For` header.
63
- if (isLoopbackPeerAddress(req.socket?.remoteAddress)) {
64
- done();
65
- return;
66
- }
67
- const match = BEARER_TOKEN_PATTERN.exec(req.headers.authorization ?? '');
68
- if (!match || match[1] !== apiToken) {
69
- reply.code(401).send({
70
- error: {
71
- code: 'API_TOKEN_REQUIRED',
72
- message: `Authorization: Bearer <${tokenConfig.tokenEnvVar}> required`,
73
- },
74
- });
75
- return;
76
- }
77
- done();
78
- });
79
- }
80
- /**
81
- * Chrome may strip the port from the `Origin` header on same-origin GET requests. Used only as a
82
- * narrow fallback for safe, idempotent GET requests once the exact-match check below has already
83
- * failed — mutating routes always require an exact origin/host match.
84
- */
85
- function isPortlessLoopbackOrigin(origin) {
86
- return /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])$/.test(origin);
87
- }
88
- /**
89
- * Registers an unconditional cross-origin gate on every `/api/*` route: non-browser clients (no
90
- * `Origin` header) and requests whose `Origin` resolves to a loopback, private-LAN, or explicitly
91
- * allow-listed origin are let through; everything else is rejected with 403. `Origin: null`
92
- * (typically a sandboxed iframe) is always rejected — see the Express version's doc for why the
93
- * origin daemon's safe-GET exemption for that case was dropped.
94
- *
95
- * @param app - The Fastify instance to register the gate on.
96
- * @param deps - See {@link ApiOriginGuardMiddlewareDeps}.
97
- * @returns Nothing. Unlike the bearer-token gate, this always registers its hook — there is
98
- * no "disabled" state.
99
- * @complexity Setup is O(1). Each gated request is O(p) in the number of allowed ports (typically 1-2).
100
- * @overallScore 100/100
101
- */
102
- export function registerApiOriginGuardMiddleware(app, deps) {
103
- const { host, getResolvedPort } = deps;
104
- const extraAllowedOrigins = deps.extraAllowedOrigins ?? [];
105
- const env = deps.env ?? process.env;
106
- app.addHook('onRequest', (req, reply, done) => {
107
- const path = requestPath(req);
108
- if (!path.startsWith('/api')) {
109
- done();
110
- return;
111
- }
112
- const origin = req.headers.origin;
113
- if (origin == null || origin === '') {
114
- done();
115
- return;
116
- }
117
- if (origin === 'null') {
118
- reply.code(403).send({ error: 'Origin: null not allowed for this route' });
119
- return;
120
- }
121
- // Fail-closed: block every browser origin until the daemon's real listen port is known, so a
122
- // request arriving in the brief window before `.listen()` resolves can never be compared
123
- // against a wrong or default port.
124
- const resolvedPort = getResolvedPort();
125
- if (!resolvedPort) {
126
- reply.code(403).send({ error: 'Server initializing' });
127
- return;
128
- }
129
- const ports = allowedBrowserPorts(resolvedPort, env);
130
- if (!isAllowedBrowserOrigin(origin, req.headers.host, ports, host, [...extraAllowedOrigins])) {
131
- if (req.method !== 'GET' || !isPortlessLoopbackOrigin(String(origin))) {
132
- reply.code(403).send({ error: 'Cross-origin requests are not allowed' });
133
- return;
134
- }
135
- }
136
- done();
137
- });
138
- }
139
- //# sourceMappingURL=api-security-middleware.js.map
@@ -1,22 +0,0 @@
1
- /**
2
- * Legacy-shaped error helpers: build/send an `ApiError` from separate `code`/`message`/`init`
3
- * arguments, matching call sites that predate `JsonRouteSpec`/`mountJsonRoute` (e.g. a host
4
- * application's own hand-mounted routes). No internal dependencies. `sendApiError` here is
5
- * deliberately the same name as `response.ts`'s `ApiError`-object-taking `sendApiError` — they
6
- * are different signatures for the same "send an error" job at two call-site generations; the
7
- * package's `fastify/index.ts` barrel re-exports this one as `sendCompatApiError` to keep both
8
- * addressable without a collision. Deliberately duplicated from `../express/compat.ts` — see
9
- * `./response.ts`'s top-of-module doc for why this subtree does not import from `express/`.
10
- */
11
- import type { ApiError, ApiErrorCode, ApiErrorResponse } from '@jini/protocol';
12
- import type { FastifyReply } from 'fastify';
13
- /** Builds an `ApiError` from separate `code`/`message`/`init` arguments (legacy call shape). */
14
- export declare function createCompatApiError(code: ApiErrorCode, message: string, init?: Omit<ApiError, 'code' | 'message'>): ApiError;
15
- /** Wraps `createCompatApiError`'s result in the standard `{ error }` envelope. */
16
- export declare function createCompatApiErrorResponse(code: ApiErrorCode, message: string, init?: Omit<ApiError, 'code' | 'message'>): ApiErrorResponse;
17
- /**
18
- * Writes an `ApiError` response built from separate `code`/`message`/`init` arguments (legacy
19
- * call shape) with the given status code. Exported from `fastify/index.ts` as `sendCompatApiError`.
20
- */
21
- export declare function sendApiError(reply: FastifyReply, status: number, code: ApiErrorCode, message: string, init?: Omit<ApiError, 'code' | 'message'>): FastifyReply;
22
- //# sourceMappingURL=compat.d.ts.map
@@ -1,16 +0,0 @@
1
- /** Builds an `ApiError` from separate `code`/`message`/`init` arguments (legacy call shape). */
2
- export function createCompatApiError(code, message, init = {}) {
3
- return { code, message, ...init };
4
- }
5
- /** Wraps `createCompatApiError`'s result in the standard `{ error }` envelope. */
6
- export function createCompatApiErrorResponse(code, message, init = {}) {
7
- return { error: createCompatApiError(code, message, init) };
8
- }
9
- /**
10
- * Writes an `ApiError` response built from separate `code`/`message`/`init` arguments (legacy
11
- * call shape) with the given status code. Exported from `fastify/index.ts` as `sendCompatApiError`.
12
- */
13
- export function sendApiError(reply, status, code, message, init = {}) {
14
- return reply.code(status).send(createCompatApiErrorResponse(code, message, init));
15
- }
16
- //# sourceMappingURL=compat.js.map
@@ -1,22 +0,0 @@
1
- /**
2
- * @module fastify/daemon-status
3
- *
4
- * Mounts the generic daemon status + shutdown routes through the Fastify Adapter. The route
5
- * SPECS themselves (`daemonStatusRoute`/`daemonShutdownRoute` — pure `defineJsonRoute`-shaped
6
- * data plus framework-agnostic `parse`/`handle` functions, see `../daemon-status.ts`'s own doc
7
- * for their full provenance) are intentionally NOT duplicated here: they never reference Express
8
- * at runtime (`../daemon-status.ts` only imports `type { Express }` for its own
9
- * `registerDaemonStatusRoutes`'s parameter — a type-only import that TypeScript's
10
- * `verbatimModuleSyntax` erases from the compiled output entirely, so importing the specs from
11
- * that module does not pull the `express` package into this subtree at runtime). This file's own
12
- * job is only the Fastify-specific mounting wrapper, calling `./adapter.js`'s Fastify
13
- * `mountJsonRoute` instead of the Express one.
14
- */
15
- import type { FastifyInstance } from 'fastify';
16
- import { daemonShutdownRoute, daemonStatusRoute, type DaemonShutdownResponse, type DaemonStatusDeps, type DaemonStatusResponse } from '../daemon-status.js';
17
- import { type AdapterContext } from './adapter.js';
18
- export { daemonShutdownRoute, daemonStatusRoute };
19
- export type { DaemonShutdownResponse, DaemonStatusDeps, DaemonStatusResponse };
20
- /** Mounts both daemon-status routes on `app`. A pack's `http(app, services)` calls this directly. */
21
- export declare function registerDaemonStatusRoutes(app: FastifyInstance, deps: DaemonStatusDeps, adapter: AdapterContext): void;
22
- //# sourceMappingURL=daemon-status.d.ts.map
@@ -1,9 +0,0 @@
1
- import { daemonShutdownRoute, daemonStatusRoute, } from '../daemon-status.js';
2
- import { mountJsonRoute } from './adapter.js';
3
- export { daemonShutdownRoute, daemonStatusRoute };
4
- /** Mounts both daemon-status routes on `app`. A pack's `http(app, services)` calls this directly. */
5
- export function registerDaemonStatusRoutes(app, deps, adapter) {
6
- mountJsonRoute(app, daemonStatusRoute, deps, adapter);
7
- mountJsonRoute(app, daemonShutdownRoute, deps, adapter);
8
- }
9
- //# sourceMappingURL=daemon-status.js.map
@@ -1,13 +0,0 @@
1
- /**
2
- * @module fastify/host-tools
3
- *
4
- * Fastify-mounting glue for `../host-tools.js`'s `hostEditorsRoute`/`openResourceInEditorRoute` —
5
- * the exact same `JsonRouteSpec` objects the Express mounting in `../host-tools.js` uses, just
6
- * mounted through this subtree's own `mountJsonRoute` instead.
7
- */
8
- import type { FastifyInstance } from 'fastify';
9
- import { type HostToolsOpenInDeps } from '../host-tools.js';
10
- import { type AdapterContext } from './adapter.js';
11
- /** Mounts `GET /api/editors` and `POST /api/resources/:resourceRef/open-in` on `app` — the Fastify-native equivalent of `../host-tools.js`'s `registerHostToolsRoutes`. */
12
- export declare function registerHostToolsRoutes(app: FastifyInstance, adapter: AdapterContext, openInDeps?: HostToolsOpenInDeps): void;
13
- //# sourceMappingURL=host-tools.d.ts.map
@@ -1,8 +0,0 @@
1
- import { hostEditorsRoute, openResourceInEditorRoute } from '../host-tools.js';
2
- import { mountJsonRoute } from './adapter.js';
3
- /** Mounts `GET /api/editors` and `POST /api/resources/:resourceRef/open-in` on `app` — the Fastify-native equivalent of `../host-tools.js`'s `registerHostToolsRoutes`. */
4
- export function registerHostToolsRoutes(app, adapter, openInDeps = {}) {
5
- mountJsonRoute(app, hostEditorsRoute, {}, adapter);
6
- mountJsonRoute(app, openResourceInEditorRoute, openInDeps, adapter);
7
- }
8
- //# sourceMappingURL=host-tools.js.map