@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module frontend-control
|
|
3
|
+
*
|
|
4
|
+
* Assembles the three halves of agent-driven frontend control into one thing a host can wire up as
|
|
5
|
+
* a config entry rather than a checklist.
|
|
6
|
+
*
|
|
7
|
+
* The parts already existed and none of them composed: `@jini-ai/daemon`'s `FrontendSessionRegistry`
|
|
8
|
+
* addresses a run to a surface, this package's own frontend-session routes carry invocations to a
|
|
9
|
+
* browser and answers back, and `createFrontendCapabilityRegistrations` projects a manifest into
|
|
10
|
+
* gated tools. A host had to construct all three, keep them consistent, and remember to bind a run
|
|
11
|
+
* when it started. Every one of those is a chance to get it subtly wrong, and one of them —
|
|
12
|
+
* binding — is a security decision.
|
|
13
|
+
*
|
|
14
|
+
* **Moved here from `@jini-ai/server` (2026-07-31).** It never needed that package: its real
|
|
15
|
+
* dependencies are `core`, `daemon`, `express` and this module's own siblings, all of which
|
|
16
|
+
* `http-kit` already has. The only thing pinning it there was a *type-only* import of
|
|
17
|
+
* `LocalNodeHttpExtension` — so a helper any Express host could use sat behind a package that also
|
|
18
|
+
* pulls in `@jini-ai/sidecar` and `better-sqlite3`. The reference implementation hit exactly this
|
|
19
|
+
* and hand-reimplemented the composition instead. `@jini-ai/server` re-exports it, so existing
|
|
20
|
+
* imports keep working.
|
|
21
|
+
*
|
|
22
|
+
* **This facade never returns the registry**, and that is its most important property. The
|
|
23
|
+
* registry's `invoke` executes a capability on a real user's screen with no policy check, no
|
|
24
|
+
* confirmation, no timeout, and no audit record — it is safe only because the sole thing that can
|
|
25
|
+
* reach it is a `ToolHandler` that `ToolExecutor` already gated. Handing a host the registry so it
|
|
26
|
+
* could "wire something custom" would hand it a bypass, and a bypass that exists gets used. The
|
|
27
|
+
* host gets an HTTP extension, a set of registrations, and a bind hook; none of them can execute a
|
|
28
|
+
* capability directly.
|
|
29
|
+
*
|
|
30
|
+
* **The bind token is not a field on the run DTO.** A run is a neutral kernel noun and a browser
|
|
31
|
+
* surface is not; putting `frontendBindToken` on `RunCreateRequest` would push a browser concept
|
|
32
|
+
* into a protocol every consumer shares, to serve one transport. Instead the host supplies
|
|
33
|
+
* {@link CreateFrontendControlOptions.resolveBindToken}, which reads the token out of whatever
|
|
34
|
+
* envelope that host already uses — `contextRef` is an opaque host blob precisely so hosts can put
|
|
35
|
+
* their own metadata in it. The engine stays neutral and the host keeps one place to look.
|
|
36
|
+
*/
|
|
37
|
+
import type { Express } from 'express';
|
|
38
|
+
import type { ToolPolicy, ToolRegistration } from '@jini-ai/core';
|
|
39
|
+
import { type FrontendCapabilitySpec } from '@jini-ai/daemon';
|
|
40
|
+
import type { AdapterContext } from './adapter.js';
|
|
41
|
+
import type { RunCreateRequest, RunStartHandler } from './runs.js';
|
|
42
|
+
/**
|
|
43
|
+
* The context {@link FrontendControl.httpExtension} actually reads — just the adapter.
|
|
44
|
+
*
|
|
45
|
+
* Declared structurally here rather than importing `@jini-ai/server`'s `LocalNodeHttpExtension`,
|
|
46
|
+
* which is what used to keep this module in that package. A function taking this narrower context
|
|
47
|
+
* stays assignable to one taking the wider `{adapter, lifecycle, dataDir}`, so passing the result
|
|
48
|
+
* to `createLocalNodeDaemon`'s `httpExtensions` still typechecks — while a host that builds its own
|
|
49
|
+
* Express app (and so has no `dataDir` or `LocalNodeHttpExtensionContext` to hand over) can now use
|
|
50
|
+
* this facade too, instead of reimplementing it. The reference implementation had to do exactly
|
|
51
|
+
* that.
|
|
52
|
+
*/
|
|
53
|
+
export type FrontendHttpExtension = (app: Express, context: {
|
|
54
|
+
readonly adapter: AdapterContext;
|
|
55
|
+
}) => void;
|
|
56
|
+
export interface FrontendBindErrorContext {
|
|
57
|
+
readonly runId: string;
|
|
58
|
+
readonly error: unknown;
|
|
59
|
+
}
|
|
60
|
+
export interface CreateFrontendControlOptions {
|
|
61
|
+
/**
|
|
62
|
+
* The capabilities to expose. `@jini-ai/agentic`'s `PAGE_CAPABILITIES` and `@jini-ai/chat/core`'s
|
|
63
|
+
* `CHAT_CAPABILITIES` satisfy this structurally — the engine never imports that vocabulary
|
|
64
|
+
* (see `frontend-capability-tools.ts`'s module doc for why the edge points one way).
|
|
65
|
+
*/
|
|
66
|
+
readonly capabilities: readonly FrontendCapabilitySpec[];
|
|
67
|
+
/**
|
|
68
|
+
* Reads this run's bind token out of the host's own run request, or returns `undefined` when the
|
|
69
|
+
* run has no originating surface.
|
|
70
|
+
*
|
|
71
|
+
* Required, and deliberately not defaulted. A default would have to guess where a host keeps the
|
|
72
|
+
* token, and guessing wrong fails *open* in the confusing direction: the run starts, the agent
|
|
73
|
+
* gets its tools, and every page call fails later with "no frontend is bound" — a symptom that
|
|
74
|
+
* looks like a browser problem and is actually a config one.
|
|
75
|
+
*
|
|
76
|
+
* Returning `undefined` is normal, not an error. A run started from a CLI has no surface, and
|
|
77
|
+
* capabilities whose `surface` is `'server'` do not need one.
|
|
78
|
+
*/
|
|
79
|
+
readonly resolveBindToken: (request: RunCreateRequest) => string | undefined;
|
|
80
|
+
/** @default `@jini-ai/daemon`'s `denyAllFrontendCapabilityPolicy` — a host grants access explicitly. */
|
|
81
|
+
readonly policy?: ToolPolicy;
|
|
82
|
+
/** @default `@jini-ai/daemon`'s `DEFAULT_FRONTEND_CAPABILITY_TIMEOUT_MS`. */
|
|
83
|
+
readonly timeoutMs?: number;
|
|
84
|
+
/** Applied to every registration. Frontend output is untrusted page text; bound it. */
|
|
85
|
+
readonly maxOutputBytes?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Host-owned sink for a bind failure. Defaults to `console.error`.
|
|
88
|
+
*
|
|
89
|
+
* A failed bind does not fail the run. The run is still legitimate — the agent simply cannot
|
|
90
|
+
* drive the page, and every capability call it makes will say so by name. Killing a run because
|
|
91
|
+
* one optional channel could not be established would turn a degraded session into no session.
|
|
92
|
+
*/
|
|
93
|
+
readonly onBindError?: (context: FrontendBindErrorContext) => void;
|
|
94
|
+
}
|
|
95
|
+
export interface FrontendControl {
|
|
96
|
+
/**
|
|
97
|
+
* Mounts the frontend-session stream and response routes. Pass in `createLocalNodeDaemon`'s
|
|
98
|
+
* `httpExtensions`, or call it directly with `(app, { adapter })` from a host that owns its own
|
|
99
|
+
* Express app — see {@link FrontendHttpExtension}.
|
|
100
|
+
*/
|
|
101
|
+
readonly httpExtension: FrontendHttpExtension;
|
|
102
|
+
/** One gated tool per capability. Pass in `toolRegistrations`. */
|
|
103
|
+
readonly toolRegistrations: readonly ToolRegistration[];
|
|
104
|
+
/**
|
|
105
|
+
* Binds each starting run to the surface that originated it, and releases the binding when the
|
|
106
|
+
* run reaches a terminal state so a long-lived tab does not accumulate dead bindings.
|
|
107
|
+
*
|
|
108
|
+
* Compose it with whatever the host already does on run start — this hook only binds, it never
|
|
109
|
+
* executes anything:
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* onRunStarted: (context) => {
|
|
113
|
+
* frontend.bindOnStarted(context);
|
|
114
|
+
* void startMyAgent(context);
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
readonly bindOnStarted: RunStartHandler;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Builds the frontend-control bundle.
|
|
122
|
+
*
|
|
123
|
+
* @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
|
|
124
|
+
* @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
|
|
125
|
+
* @throws Never at construction. A duplicate capability id throws later, when the registrations are
|
|
126
|
+
* registered, naming the id (`ToolRegistry.register`'s own error).
|
|
127
|
+
*
|
|
128
|
+
* @complexity O(n) in `capabilities`.
|
|
129
|
+
*/
|
|
130
|
+
export declare function createFrontendControl(options: CreateFrontendControlOptions): FrontendControl;
|
|
131
|
+
//# sourceMappingURL=frontend-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-control.d.ts","sourceRoot":"","sources":["../src/frontend-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAmB,eAAe,EAAE,MAAM,WAAW,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;CAAE,KAAK,IAAI,CAAC;AAE1G,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACzD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,CAAC;IAC7E,wGAAwG;IACxG,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAC;CACpE;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,kEAAkE;IAClE,QAAQ,CAAC,iBAAiB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACxD;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC;CACzC;AAOD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,eAAe,CA8D5F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createFrontendCapabilityRegistrations, createFrontendSessionRegistry, } from '@jini-ai/daemon';
|
|
2
|
+
import { registerFrontendSessionRoutes } from './frontend-sessions.js';
|
|
3
|
+
function defaultBindErrorSink(context) {
|
|
4
|
+
// eslint-disable-next-line no-console
|
|
5
|
+
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, context.error);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Builds the frontend-control bundle.
|
|
9
|
+
*
|
|
10
|
+
* @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
|
|
11
|
+
* @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
|
|
12
|
+
* @throws Never at construction. A duplicate capability id throws later, when the registrations are
|
|
13
|
+
* registered, naming the id (`ToolRegistry.register`'s own error).
|
|
14
|
+
*
|
|
15
|
+
* @complexity O(n) in `capabilities`.
|
|
16
|
+
*/
|
|
17
|
+
export function createFrontendControl(options) {
|
|
18
|
+
// Closed over, never exposed. See the module doc: reaching this is reaching an ungated `invoke`.
|
|
19
|
+
const registry = createFrontendSessionRegistry();
|
|
20
|
+
const onBindError = options.onBindError ?? defaultBindErrorSink;
|
|
21
|
+
const toolRegistrations = createFrontendCapabilityRegistrations({
|
|
22
|
+
registry,
|
|
23
|
+
capabilities: options.capabilities,
|
|
24
|
+
...(options.policy !== undefined ? { policy: options.policy } : {}),
|
|
25
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
26
|
+
...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
|
|
27
|
+
});
|
|
28
|
+
const httpExtension = (app, context) => {
|
|
29
|
+
registerFrontendSessionRoutes(app, { registry }, context.adapter);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The error boundary the `onBindError` contract actually needs. A `RunStartHandler` throwing is
|
|
33
|
+
* not a no-op: `@jini-ai/http-kit`'s run-start route catches it, marks the run `failed` and answers
|
|
34
|
+
* 500 — so anything inside this hook that can throw can produce exactly the killed run that
|
|
35
|
+
* contract promises never to cause. `resolveBindToken` is host-supplied and reads an opaque host
|
|
36
|
+
* blob (a `JSON.parse` away from throwing), and the sink itself is host-supplied too. A sink that
|
|
37
|
+
* throws leaves nowhere left to report, so that one is swallowed rather than escalated.
|
|
38
|
+
*/
|
|
39
|
+
const reportBindError = (context) => {
|
|
40
|
+
try {
|
|
41
|
+
onBindError(context);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Deliberately terminal: the reporting channel is what failed.
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const bindOnStarted = ({ request, run, lifecycle }) => {
|
|
48
|
+
let bindToken;
|
|
49
|
+
try {
|
|
50
|
+
bindToken = options.resolveBindToken(request);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
reportBindError({ runId: run.id, error });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (bindToken === undefined)
|
|
57
|
+
return;
|
|
58
|
+
let release;
|
|
59
|
+
try {
|
|
60
|
+
release = registry.bindRunByToken(run.id, bindToken);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
reportBindError({ runId: run.id, error });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Release on terminal so a tab that outlives many runs does not accumulate bindings for runs
|
|
67
|
+
// that ended. `waitForTerminal` rejecting is itself a reason to release, not to keep waiting,
|
|
68
|
+
// so both paths run it — and the whole chain is swallowed, because a bookkeeping failure must
|
|
69
|
+
// never surface as an unhandled rejection that takes the process down.
|
|
70
|
+
void lifecycle
|
|
71
|
+
.waitForTerminal(run.id)
|
|
72
|
+
.then(release, release)
|
|
73
|
+
.catch(() => undefined);
|
|
74
|
+
};
|
|
75
|
+
return { httpExtension, toolRegistrations, bindOnStarted };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=frontend-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-control.js","sourceRoot":"","sources":["../src/frontend-control.ts"],"names":[],"mappings":"AAsCA,OAAO,EACL,qCAAqC,EACrC,6BAA6B,GAE9B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AAmFvE,SAAS,oBAAoB,CAAC,OAAiC;IAC7D,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,yCAAyC,OAAO,CAAC,KAAK,yBAAyB,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAChH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAqC;IACzE,iGAAiG;IACjG,MAAM,QAAQ,GAAG,6BAA6B,EAAE,CAAC;IACjD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAEhE,MAAM,iBAAiB,GAAG,qCAAqC,CAAC;QAC9D,QAAQ;QACR,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,GAAG,CAAC,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5F,CAAC,CAAC;IAEH,MAAM,aAAa,GAA0B,CAAC,GAAY,EAAE,OAAO,EAAE,EAAE;QACrE,6BAA6B,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC,CAAC;IAEF;;;;;;;OAOG;IACH,MAAM,eAAe,GAAG,CAAC,OAAiC,EAAQ,EAAE;QAClE,IAAI,CAAC;YACH,WAAW,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;QACjE,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,aAAa,GAAoB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAmB,EAAE,EAAE;QACtF,IAAI,SAA6B,CAAC;QAClC,IAAI,CAAC;YACH,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,eAAe,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO;QAEpC,IAAI,OAAmB,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,eAAe,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QAED,6FAA6F;QAC7F,8FAA8F;QAC9F,8FAA8F;QAC9F,uEAAuE;QACvE,KAAK,SAAS;aACX,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;aACvB,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC;aACtB,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import type { FrontendSessionRegistry } from '@jini-ai/daemon';
|
|
3
|
+
import { type AdapterContext } from './adapter.js';
|
|
4
|
+
export declare const FRONTEND_SESSION_STREAM_ROUTE_PATH = "/api/frontend-sessions/stream";
|
|
5
|
+
export declare const FRONTEND_SESSION_RESPONSE_ROUTE_PATH = "/api/frontend-sessions/:sessionId/responses";
|
|
6
|
+
/** First event on the stream, telling the surface the id it must answer under. */
|
|
7
|
+
export interface FrontendSessionAttachedEvent {
|
|
8
|
+
readonly type: 'attached';
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
/**
|
|
11
|
+
* Secret this surface presents to bind a run to itself. Delivered here and nowhere else: this
|
|
12
|
+
* stream is the one channel already proven to belong to the surface that opened it.
|
|
13
|
+
*
|
|
14
|
+
* Never put this in a URL, a query string, or a log. It is separate from `sessionId` precisely
|
|
15
|
+
* because that id already travels in a request path (`…/:sessionId/responses`) and therefore
|
|
16
|
+
* leaks into access logs and proxies by design — see `@jini-ai/daemon`'s `FrontendSessionHandle`.
|
|
17
|
+
*/
|
|
18
|
+
readonly bindToken: string;
|
|
19
|
+
}
|
|
20
|
+
/** One capability call for the surface to execute. */
|
|
21
|
+
export interface FrontendSessionInvocationEvent {
|
|
22
|
+
readonly type: 'invocation';
|
|
23
|
+
readonly invocationId: string;
|
|
24
|
+
readonly capabilityId: string;
|
|
25
|
+
readonly input: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Sent instead of `attached` when the request itself was unusable; the stream then closes. */
|
|
28
|
+
export interface FrontendSessionErrorEvent {
|
|
29
|
+
readonly type: 'error';
|
|
30
|
+
readonly message: string;
|
|
31
|
+
}
|
|
32
|
+
export type FrontendSessionStreamEvent = FrontendSessionAttachedEvent | FrontendSessionInvocationEvent | FrontendSessionErrorEvent;
|
|
33
|
+
/**
|
|
34
|
+
* A surface's answer, as a discriminated union rather than a flat record with two optional fields.
|
|
35
|
+
*
|
|
36
|
+
* The parser already refuses a failure with no message, so `message?: string` alongside
|
|
37
|
+
* `ok: boolean` would let the type describe a state the parser guarantees cannot exist — and force
|
|
38
|
+
* a `?? 'no message'` fallback at the use site that no input could ever reach. Encoding the
|
|
39
|
+
* guarantee here removes the branch instead of leaving it to be covered by a test that would have
|
|
40
|
+
* to fabricate an impossible input.
|
|
41
|
+
*/
|
|
42
|
+
export type FrontendSessionResponseRequest = {
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
readonly invocationId: string;
|
|
45
|
+
readonly ok: true;
|
|
46
|
+
readonly output?: unknown;
|
|
47
|
+
} | {
|
|
48
|
+
readonly sessionId: string;
|
|
49
|
+
readonly invocationId: string;
|
|
50
|
+
readonly ok: false;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
};
|
|
53
|
+
export interface FrontendSessionResponseBody {
|
|
54
|
+
/** `false` when the invocation was unknown or already settled — a duplicate answer, not an error. */
|
|
55
|
+
readonly settled: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface FrontendSessionsHttpDeps {
|
|
58
|
+
readonly registry: FrontendSessionRegistry;
|
|
59
|
+
/**
|
|
60
|
+
* Mints session ids. Defaults to `randomUUID`. A surface never supplies its own: an id the
|
|
61
|
+
* caller chose could collide with, or deliberately impersonate, a session already attached.
|
|
62
|
+
*/
|
|
63
|
+
readonly newSessionId?: () => string;
|
|
64
|
+
/** Host-owned sink for failures that must not escape an async Express handler. Defaults to `console.error`. */
|
|
65
|
+
readonly onInternalError?: (context: {
|
|
66
|
+
readonly source: string;
|
|
67
|
+
readonly error: unknown;
|
|
68
|
+
}) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reads the repeatable `?capability=` query parameter.
|
|
72
|
+
*
|
|
73
|
+
* Express gives a single value as a string and repeats as an array; anything else (a nested object
|
|
74
|
+
* from a bracketed query string) is rejected rather than coerced.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseCapabilityQuery(raw: unknown): readonly string[] | null;
|
|
77
|
+
/**
|
|
78
|
+
* Handles one surface's stream from open to close.
|
|
79
|
+
*
|
|
80
|
+
* SSE commits `text/event-stream` headers before anything else can be known, so a bad request is
|
|
81
|
+
* reported as one `{type:'error'}` data event and then closed — there is no status-code channel
|
|
82
|
+
* left. Same precedent as `run-stream.ts`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function handleFrontendSessionStream(req: Request, res: Response, deps: FrontendSessionsHttpDeps): void;
|
|
85
|
+
/**
|
|
86
|
+
* `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
|
|
87
|
+
* handler awaiting it. Always `200`; `{settled:false}` reports a duplicate or unknown answer,
|
|
88
|
+
* which is an expected outcome rather than a transport error.
|
|
89
|
+
*/
|
|
90
|
+
export declare const frontendSessionResponseRoute: import("./types.js").JsonRouteSpec<FrontendSessionResponseRequest, FrontendSessionResponseBody, FrontendSessionsHttpDeps>;
|
|
91
|
+
/**
|
|
92
|
+
* Mounts both frontend-session routes on `app`.
|
|
93
|
+
*
|
|
94
|
+
* The stream route bypasses `mountJsonRoute` (raw `app.get`, for the SSE response shape), so it
|
|
95
|
+
* does not get `requireSameOrigin` for free the way `frontendSessionResponseRoute` does — the
|
|
96
|
+
* guard is applied here directly, before the SSE channel is opened, so a cross-origin request
|
|
97
|
+
* never gets a stream (and, notably, never mints a session id or `bindToken` either).
|
|
98
|
+
*/
|
|
99
|
+
export declare function registerFrontendSessionRoutes(app: Express, deps: FrontendSessionsHttpDeps, adapter: AdapterContext): void;
|
|
100
|
+
//# sourceMappingURL=frontend-sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-sessions.d.ts","sourceRoot":"","sources":["../src/frontend-sessions.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAyB,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AACtF,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAOpF,eAAO,MAAM,kCAAkC,kCAAkC,CAAC;AAClF,eAAO,MAAM,oCAAoC,gDAAgD,CAAC;AAElG,kFAAkF;AAClF,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,sDAAsD;AACtD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,+FAA+F;AAC/F,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,0BAA0B,GAClC,4BAA4B,GAC5B,8BAA8B,GAC9B,yBAAyB,CAAC;AAE9B;;;;;;;;GAQG;AACH,MAAM,MAAM,8BAA8B,GACtC;IACA,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B,GACC;IACA,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEJ,MAAM,WAAW,2BAA2B;IAC1C,qGAAqG;IACrG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;IACrC,+GAA+G;IAC/G,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpG;AAcD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,CAQ3E;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,wBAAwB,GAC7B,IAAI,CAgCN;AAiCD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,2HAmBvC,CAAC;AAEH;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,wBAAwB,EAC9B,OAAO,EAAE,cAAc,GACtB,IAAI,CAmBN"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module frontend-sessions
|
|
3
|
+
*
|
|
4
|
+
* The transport half of `@jini-ai/daemon`'s `FrontendSessionRegistry`: how a browser surface reaches
|
|
5
|
+
* the daemon, and how the daemon reaches back into it.
|
|
6
|
+
*
|
|
7
|
+
* Two routes, because the connection *is* the session:
|
|
8
|
+
*
|
|
9
|
+
* - `GET /api/frontend-sessions/stream` opens an SSE channel. The daemon mints the session id,
|
|
10
|
+
* attaches the surface, sends `{type:'attached', sessionId}` as the first event, and pushes one
|
|
11
|
+
* `{type:'invocation', ...}` per capability call. When the connection closes for any reason —
|
|
12
|
+
* navigation, a crashed tab, a pulled network cable — the surface is detached and every
|
|
13
|
+
* invocation still awaiting it is failed.
|
|
14
|
+
* - `POST /api/frontend-sessions/:sessionId/responses` carries the surface's answer back.
|
|
15
|
+
*
|
|
16
|
+
* There is deliberately **no separate register/unregister pair**. A surface whose lifetime is a
|
|
17
|
+
* `POST` to create and a `DELETE` to destroy leaks a session every time a tab dies without running
|
|
18
|
+
* its unload handler, and those leaked sessions keep claiming capabilities they can no longer
|
|
19
|
+
* serve. Tying the lifetime to the stream makes the failure mode impossible instead of unlikely.
|
|
20
|
+
*
|
|
21
|
+
* **The response route does not execute anything.** It settles a promise that a `ToolHandler` is
|
|
22
|
+
* already awaiting, so the call it answers has already passed `ToolExecutor`'s authorization,
|
|
23
|
+
* confirmation, timeout, cancellation, truncation, and audit. An answer for an unknown or
|
|
24
|
+
* already-settled invocation is reported as `{settled:false}` rather than being an error: a stream
|
|
25
|
+
* reconnect or a retried POST is expected, not exceptional.
|
|
26
|
+
*
|
|
27
|
+
* Binding a run to a session is NOT exposed here. Which surface owns a run is a decision only the
|
|
28
|
+
* composition root can make (it is the thing that starts runs), so it calls `registry.bindRun`
|
|
29
|
+
* directly — see `@jini-ai/daemon`'s own module doc for why attach and bind are separate.
|
|
30
|
+
*/
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { createApiError } from '@jini-ai/protocol';
|
|
33
|
+
import { defineJsonRoute, mountJsonRoute } from './adapter.js';
|
|
34
|
+
import { guardSameOrigin } from './origin.js';
|
|
35
|
+
import { createSseResponse } from './raw-sse.js';
|
|
36
|
+
import { validationError } from './request.js';
|
|
37
|
+
import { sendApiError, statusForError } from './response.js';
|
|
38
|
+
import { err, ok } from './types.js';
|
|
39
|
+
export const FRONTEND_SESSION_STREAM_ROUTE_PATH = '/api/frontend-sessions/stream';
|
|
40
|
+
export const FRONTEND_SESSION_RESPONSE_ROUTE_PATH = '/api/frontend-sessions/:sessionId/responses';
|
|
41
|
+
function reportInternalError(deps, source, error) {
|
|
42
|
+
try {
|
|
43
|
+
if (deps.onInternalError)
|
|
44
|
+
deps.onInternalError({ source, error });
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
else
|
|
47
|
+
console.error(`[@jini-ai/http-kit] internal error (frontend-sessions:${source})`, error);
|
|
48
|
+
}
|
|
49
|
+
catch (sinkError) {
|
|
50
|
+
// A diagnostic sink must never turn a contained failure into an unhandled rejection of its own.
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(`[@jini-ai/http-kit] internal error sink failed (frontend-sessions:${source})`, sinkError);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads the repeatable `?capability=` query parameter.
|
|
57
|
+
*
|
|
58
|
+
* Express gives a single value as a string and repeats as an array; anything else (a nested object
|
|
59
|
+
* from a bracketed query string) is rejected rather than coerced.
|
|
60
|
+
*/
|
|
61
|
+
export function parseCapabilityQuery(raw) {
|
|
62
|
+
const values = raw === undefined ? [] : (Array.isArray(raw) ? raw : [raw]);
|
|
63
|
+
const capabilities = [];
|
|
64
|
+
for (const value of values) {
|
|
65
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
capabilities.push(value);
|
|
68
|
+
}
|
|
69
|
+
return capabilities;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handles one surface's stream from open to close.
|
|
73
|
+
*
|
|
74
|
+
* SSE commits `text/event-stream` headers before anything else can be known, so a bad request is
|
|
75
|
+
* reported as one `{type:'error'}` data event and then closed — there is no status-code channel
|
|
76
|
+
* left. Same precedent as `run-stream.ts`.
|
|
77
|
+
*/
|
|
78
|
+
export function handleFrontendSessionStream(req, res, deps) {
|
|
79
|
+
const capabilities = parseCapabilityQuery(req.query['capability']);
|
|
80
|
+
// Assigned below, after the connection exists — `attach` needs something to deliver through, and
|
|
81
|
+
// `onClose` has to be armed before then so a connection that dies during setup still detaches.
|
|
82
|
+
let handle;
|
|
83
|
+
const connection = createSseResponse(req, res, {
|
|
84
|
+
// Detach on close rather than on an explicit teardown call: this fires for a closed tab and a
|
|
85
|
+
// dropped connection too, which an unregister route would not.
|
|
86
|
+
onClose: () => handle?.detach(),
|
|
87
|
+
});
|
|
88
|
+
if (capabilities === null) {
|
|
89
|
+
connection.send({
|
|
90
|
+
type: 'error',
|
|
91
|
+
message: 'each "capability" query parameter must be a non-empty string',
|
|
92
|
+
});
|
|
93
|
+
connection.close();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const sessionId = (deps.newSessionId ?? randomUUID)();
|
|
97
|
+
handle = deps.registry.attach({ sessionId, capabilities }, (invocation) => connection.send({ type: 'invocation', ...invocation }));
|
|
98
|
+
connection.send({
|
|
99
|
+
type: 'attached',
|
|
100
|
+
sessionId,
|
|
101
|
+
bindToken: handle.bindToken,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function isRecord(value) {
|
|
105
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
function parseFrontendSessionResponse(input) {
|
|
108
|
+
const sessionId = input.params['sessionId'];
|
|
109
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
110
|
+
return err(validationError('sessionId must be a non-empty string', [{ path: 'sessionId', message: 'required non-empty string' }]));
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(input.body))
|
|
113
|
+
return err(validationError('body must be a JSON object'));
|
|
114
|
+
const invocationId = input.body['invocationId'];
|
|
115
|
+
if (typeof invocationId !== 'string' || invocationId.trim().length === 0) {
|
|
116
|
+
return err(validationError('invocationId must be a non-empty string', [{ path: 'invocationId', message: 'required non-empty string' }]));
|
|
117
|
+
}
|
|
118
|
+
const okFlag = input.body['ok'];
|
|
119
|
+
if (typeof okFlag !== 'boolean') {
|
|
120
|
+
return err(validationError('ok must be a boolean', [{ path: 'ok', message: 'required boolean' }]));
|
|
121
|
+
}
|
|
122
|
+
if (!okFlag) {
|
|
123
|
+
const message = input.body['message'];
|
|
124
|
+
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
125
|
+
// A failure with no reason gives the agent nothing to act on, which is how a caller ends up
|
|
126
|
+
// retrying the same refusal forever.
|
|
127
|
+
return err(validationError('message must be a non-empty string when ok is false', [{ path: 'message', message: 'required when ok is false' }]));
|
|
128
|
+
}
|
|
129
|
+
return ok({ sessionId, invocationId, ok: false, message });
|
|
130
|
+
}
|
|
131
|
+
return ok({ sessionId, invocationId, ok: true, output: input.body['output'] });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
|
|
135
|
+
* handler awaiting it. Always `200`; `{settled:false}` reports a duplicate or unknown answer,
|
|
136
|
+
* which is an expected outcome rather than a transport error.
|
|
137
|
+
*/
|
|
138
|
+
export const frontendSessionResponseRoute = defineJsonRoute({
|
|
139
|
+
method: 'post',
|
|
140
|
+
path: FRONTEND_SESSION_RESPONSE_ROUTE_PATH,
|
|
141
|
+
requireSameOrigin: true,
|
|
142
|
+
parse: parseFrontendSessionResponse,
|
|
143
|
+
handle: (input, deps) => {
|
|
144
|
+
const settled = deps.registry.settle(input.sessionId, input.invocationId, input.ok
|
|
145
|
+
? { ok: true, output: input.output }
|
|
146
|
+
: { ok: false, message: input.message });
|
|
147
|
+
return ok({ settled });
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* Mounts both frontend-session routes on `app`.
|
|
152
|
+
*
|
|
153
|
+
* The stream route bypasses `mountJsonRoute` (raw `app.get`, for the SSE response shape), so it
|
|
154
|
+
* does not get `requireSameOrigin` for free the way `frontendSessionResponseRoute` does — the
|
|
155
|
+
* guard is applied here directly, before the SSE channel is opened, so a cross-origin request
|
|
156
|
+
* never gets a stream (and, notably, never mints a session id or `bindToken` either).
|
|
157
|
+
*/
|
|
158
|
+
export function registerFrontendSessionRoutes(app, deps, adapter) {
|
|
159
|
+
app.get(FRONTEND_SESSION_STREAM_ROUTE_PATH, (req, res) => {
|
|
160
|
+
const origin = guardSameOrigin(req, adapter);
|
|
161
|
+
if (!origin.ok) {
|
|
162
|
+
sendApiError(res, statusForError(origin.error), origin.error);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
handleFrontendSessionStream(req, res, deps);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
reportInternalError(deps, 'stream', error);
|
|
170
|
+
if (!res.headersSent) {
|
|
171
|
+
res.status(500).json({ error: createApiError('INTERNAL_ERROR', 'an internal error occurred') });
|
|
172
|
+
}
|
|
173
|
+
else if (!res.writableEnded) {
|
|
174
|
+
res.end();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
mountJsonRoute(app, frontendSessionResponseRoute, deps, adapter);
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=frontend-sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-sessions.js","sourceRoot":"","sources":["../src/frontend-sessions.ts"],"names":[],"mappings":"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"}
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module health
|
|
3
|
+
*
|
|
4
|
+
* `GET /health`, `GET /api/health` (plain liveness — always `200 {ok: true, version}` once the
|
|
5
|
+
* process is up, no dependency checks), `GET /ready`, `GET /api/ready` (real readiness —
|
|
6
|
+
* delegates to an injected `checkReadiness`, `503` when it reports `ok: false`), and `GET
|
|
7
|
+
* /version`, `GET /api/version` (echoes the running build's version). Both the mount-relative
|
|
8
|
+
* and `/api`-prefixed form of each probe are registered because `api-security-middleware.ts`'s
|
|
9
|
+
* `OPEN_PROBE_PATHS` already anticipated exactly these six paths as always-open (no bearer token,
|
|
10
|
+
* no origin check) — this module is what finally makes that anticipated set of paths real routes
|
|
11
|
+
* instead of an open gate over nothing.
|
|
12
|
+
*
|
|
13
|
+
* **Mount before `express.json()`/the bearer-auth/origin-guard middleware** (see
|
|
14
|
+
* `@jini-ai/server`'s `create-local-node-daemon.ts` wiring): these are simple parameterless GETs
|
|
15
|
+
* with no request body, so nothing here needs JSON body-parsing, and a monitoring probe should
|
|
16
|
+
* never need a bearer token or same-origin `Origin` header just to confirm the process is up —
|
|
17
|
+
* the same reasoning `api-security-middleware.ts`'s own doc already gives for exempting these
|
|
18
|
+
* paths.
|
|
19
|
+
*
|
|
20
|
+
* **`daemon-status.ts#daemonStatusRoute`'s own doc previously called `GET /api/daemon/status` "a
|
|
21
|
+
* health-check" — this module is the real, purpose-built answer to that now.** `/api/daemon/status`
|
|
22
|
+
* still exists and still reports version/host/port/data-dir/shutdown state (useful operator
|
|
23
|
+
* detail), but a liveness/readiness monitor should poll `/health`/`/ready` instead: those are
|
|
24
|
+
* unauthenticated by design (see above), `/api/daemon/status` is not gated same-origin either
|
|
25
|
+
* today, but was never *documented* as the liveness contract — see that module's updated doc
|
|
26
|
+
* comment.
|
|
27
|
+
*
|
|
28
|
+
* **OD-parity, verified live 2026-07-22** by booting a real Open Design daemon (`apps/daemon/bin/
|
|
29
|
+
* od.mjs --no-open`, from a full clone of `nexu-io/open-design`) and curling its real
|
|
30
|
+
* `/api/health`, `/api/ready`, `/api/version` handlers (`apps/daemon/src/server.ts:2624-2642`).
|
|
31
|
+
* Live responses observed: `GET /api/health` → `200 {"ok":true,"version":"0.15.1"}`; `GET
|
|
32
|
+
* /api/ready` → `200 {"ok":true,"ready":true,"version":"0.15.1"}` (the handler's readiness check
|
|
33
|
+
* is a single `!daemonShuttingDown` boolean, not a pluggable checks map — `server.ts:2629-2637`);
|
|
34
|
+
* `GET /api/version` → `200 {"version":{"version":"0.15.1","channel":"development",
|
|
35
|
+
* "packaged":false,"platform":"darwin","arch":"x64"}}` (the full `AppVersionInfo` shape from
|
|
36
|
+
* `app-version.ts:12-18`, nested under the `version` key — not a bare string). Confirmed OD
|
|
37
|
+
* registers no bare (non-`/api`-prefixed) `/health`/`/ready`/`/version` routes at all (`GET
|
|
38
|
+
* /health` 404s live) — this module's mount-relative variants are a deliberate Jini generalization
|
|
39
|
+
* for hosts that don't prefix their API under `/api`, not an OD-observed route. Also confirmed no
|
|
40
|
+
* origin/CORS headers appear on any of the three live OD responses — `requireLocalDaemonRequest`
|
|
41
|
+
* (`http/local-daemon-request.ts`), which is what stamps `Vary: Origin`/`Access-Control-*`
|
|
42
|
+
* headers on other `/api/daemon/*` routes like shutdown, is never applied to health/ready/version,
|
|
43
|
+
* matching this module's `requireSameOrigin`-unset routes.
|
|
44
|
+
*
|
|
45
|
+
* Two real gaps that live comparison caught and this module now fixes: `livenessRoute` and
|
|
46
|
+
* `readinessRoute` used to omit `version` entirely, and `readinessRoute`'s success body had no
|
|
47
|
+
* `ready` field — both now present, sourced from the same `deps.getVersion()` already wired for
|
|
48
|
+
* `/version`. One gap intentionally left open: `/version`'s response here stays a flat `{version:
|
|
49
|
+
* string}` (via the same string-returning `getVersion` convention `daemon-status.ts` already
|
|
50
|
+
* uses) rather than OD's nested `AppVersionInfo` object — the channel/packaged/platform/arch
|
|
51
|
+
* fields aren't wired anywhere in Jini today (`@jini-ai/server`'s `create-local-node-daemon.ts`
|
|
52
|
+
* only ever supplies a plain version string), and inventing that plumbing is a host-level decision
|
|
53
|
+
* out of this file's scope, not something to paper over with an unused type change here.
|
|
54
|
+
*/
|
|
55
|
+
import type { Express } from 'express';
|
|
56
|
+
import { type AdapterContext } from './adapter.js';
|
|
57
|
+
export interface HealthReadinessResult {
|
|
58
|
+
readonly ok: boolean;
|
|
59
|
+
readonly checks: Record<string, boolean>;
|
|
60
|
+
}
|
|
61
|
+
export interface HealthHttpDeps {
|
|
62
|
+
/** Resolves the running build's version string. May be sync or async, matching `daemon-status.ts#DaemonStatusDeps.getVersion`. */
|
|
63
|
+
readonly getVersion: () => Promise<string> | string;
|
|
64
|
+
/** Host-owned readiness probe (e.g. "is the sqlite handle healthy and is the process not mid-shutdown"). Defaults to always-ready (`{ok: true, checks: {}}`) — a host with nothing worth checking gets a working route with no wiring required. */
|
|
65
|
+
readonly checkReadiness?: () => Promise<HealthReadinessResult>;
|
|
66
|
+
}
|
|
67
|
+
export interface LivenessResponse {
|
|
68
|
+
readonly ok: true;
|
|
69
|
+
/** The running build's version string. Matches OD's real `/api/health` — see module doc. */
|
|
70
|
+
readonly version: string;
|
|
71
|
+
}
|
|
72
|
+
export interface ReadinessResponse {
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
/** Mirrors `ok` — OD's real `/api/ready` carries both fields; see module doc. */
|
|
75
|
+
readonly ready: true;
|
|
76
|
+
/** The running build's version string. Matches OD's real `/api/ready` — see module doc. */
|
|
77
|
+
readonly version: string;
|
|
78
|
+
readonly checks: Record<string, boolean>;
|
|
79
|
+
}
|
|
80
|
+
export interface VersionResponse {
|
|
81
|
+
readonly version: string;
|
|
82
|
+
}
|
|
83
|
+
export declare const healthRoute: import("./types.js").JsonRouteSpec<void, LivenessResponse, HealthHttpDeps>;
|
|
84
|
+
export declare const apiHealthRoute: import("./types.js").JsonRouteSpec<void, LivenessResponse, HealthHttpDeps>;
|
|
85
|
+
export declare const readyRoute: import("./types.js").JsonRouteSpec<void, ReadinessResponse, HealthHttpDeps>;
|
|
86
|
+
export declare const apiReadyRoute: import("./types.js").JsonRouteSpec<void, ReadinessResponse, HealthHttpDeps>;
|
|
87
|
+
export declare const versionInfoRoute: import("./types.js").JsonRouteSpec<void, VersionResponse, HealthHttpDeps>;
|
|
88
|
+
export declare const apiVersionInfoRoute: import("./types.js").JsonRouteSpec<void, VersionResponse, HealthHttpDeps>;
|
|
89
|
+
/** Mounts all six health/readiness/version routes on `app`. Call this before installing `express.json()`/the bearer-auth/origin-guard middleware — see module doc. */
|
|
90
|
+
export declare function registerHealthRoutes(app: Express, deps: HealthHttpDeps, adapter: AdapterContext): void;
|
|
91
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAGpF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,kIAAkI;IAClI,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IACpD,mPAAmP;IACnP,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAChE;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAoCD,eAAO,MAAM,WAAW,4EAA2B,CAAC;AACpD,eAAO,MAAM,cAAc,4EAA+B,CAAC;AAC3D,eAAO,MAAM,UAAU,6EAA2B,CAAC;AACnD,eAAO,MAAM,aAAa,6EAA+B,CAAC;AAC1D,eAAO,MAAM,gBAAgB,2EAA2B,CAAC;AACzD,eAAO,MAAM,mBAAmB,2EAA+B,CAAC;AAEhE,sKAAsK;AACtK,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAOtG"}
|