@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.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +107 -0
  3. package/dist/active-context.d.ts +99 -0
  4. package/dist/active-context.d.ts.map +1 -0
  5. package/dist/active-context.js +77 -0
  6. package/dist/active-context.js.map +1 -0
  7. package/dist/adapter.d.ts +36 -0
  8. package/dist/adapter.d.ts.map +1 -0
  9. package/dist/adapter.js +92 -0
  10. package/dist/adapter.js.map +1 -0
  11. package/dist/agents.d.ts +55 -0
  12. package/dist/agents.d.ts.map +1 -0
  13. package/dist/agents.js +23 -0
  14. package/dist/agents.js.map +1 -0
  15. package/dist/api-security-middleware.d.ts +146 -0
  16. package/dist/api-security-middleware.d.ts.map +1 -0
  17. package/dist/api-security-middleware.js +276 -0
  18. package/dist/api-security-middleware.js.map +1 -0
  19. package/dist/attachments.d.ts +317 -0
  20. package/dist/attachments.d.ts.map +1 -0
  21. package/dist/attachments.js +664 -0
  22. package/dist/attachments.js.map +1 -0
  23. package/dist/cancel-owned-runs.d.ts +44 -0
  24. package/dist/cancel-owned-runs.d.ts.map +1 -0
  25. package/dist/cancel-owned-runs.js +35 -0
  26. package/dist/cancel-owned-runs.js.map +1 -0
  27. package/dist/compat.d.ts +22 -0
  28. package/dist/compat.d.ts.map +1 -0
  29. package/dist/compat.js +17 -0
  30. package/dist/compat.js.map +1 -0
  31. package/dist/component-catalog.d.ts +74 -0
  32. package/dist/component-catalog.d.ts.map +1 -0
  33. package/dist/component-catalog.js +74 -0
  34. package/dist/component-catalog.js.map +1 -0
  35. package/dist/connectors.d.ts +165 -0
  36. package/dist/connectors.d.ts.map +1 -0
  37. package/dist/connectors.js +474 -0
  38. package/dist/connectors.js.map +1 -0
  39. package/dist/daemon-status.d.ts +72 -0
  40. package/dist/daemon-status.d.ts.map +1 -0
  41. package/dist/daemon-status.js +60 -0
  42. package/dist/daemon-status.js.map +1 -0
  43. package/dist/db-ops.d.ts +116 -0
  44. package/dist/db-ops.d.ts.map +1 -0
  45. package/dist/db-ops.js +191 -0
  46. package/dist/db-ops.js.map +1 -0
  47. package/dist/delegated-tools.d.ts +46 -0
  48. package/dist/delegated-tools.d.ts.map +1 -0
  49. package/dist/delegated-tools.js +155 -0
  50. package/dist/delegated-tools.js.map +1 -0
  51. package/dist/express/run-stream.d.ts +15 -0
  52. package/dist/express/run-stream.d.ts.map +1 -0
  53. package/dist/express/run-stream.js +11 -0
  54. package/dist/express/run-stream.js.map +1 -0
  55. package/dist/express-index.d.ts +32 -0
  56. package/dist/express-index.d.ts.map +1 -0
  57. package/dist/express-index.js +14 -0
  58. package/dist/express-index.js.map +1 -0
  59. package/dist/fastify/adapter.d.ts +31 -0
  60. package/dist/fastify/adapter.d.ts.map +1 -0
  61. package/dist/fastify/adapter.js +64 -0
  62. package/dist/fastify/adapter.js.map +1 -0
  63. package/dist/fastify/agents.d.ts +13 -0
  64. package/dist/fastify/agents.d.ts.map +1 -0
  65. package/dist/fastify/agents.js +7 -0
  66. package/dist/fastify/agents.js.map +1 -0
  67. package/dist/fastify/api-security-middleware.d.ts +64 -0
  68. package/dist/fastify/api-security-middleware.d.ts.map +1 -0
  69. package/dist/fastify/api-security-middleware.js +139 -0
  70. package/dist/fastify/api-security-middleware.js.map +1 -0
  71. package/dist/fastify/compat.d.ts +22 -0
  72. package/dist/fastify/compat.d.ts.map +1 -0
  73. package/dist/fastify/compat.js +16 -0
  74. package/dist/fastify/compat.js.map +1 -0
  75. package/dist/fastify/daemon-status.d.ts +22 -0
  76. package/dist/fastify/daemon-status.d.ts.map +1 -0
  77. package/dist/fastify/daemon-status.js +9 -0
  78. package/dist/fastify/daemon-status.js.map +1 -0
  79. package/dist/fastify/host-tools.d.ts +13 -0
  80. package/dist/fastify/host-tools.d.ts.map +1 -0
  81. package/dist/fastify/host-tools.js +8 -0
  82. package/dist/fastify/host-tools.js.map +1 -0
  83. package/dist/fastify/index.d.ts +36 -0
  84. package/dist/fastify/index.d.ts.map +1 -0
  85. package/dist/fastify/index.js +18 -0
  86. package/dist/fastify/index.js.map +1 -0
  87. package/dist/fastify/local-daemon-request.d.ts +43 -0
  88. package/dist/fastify/local-daemon-request.d.ts.map +1 -0
  89. package/dist/fastify/local-daemon-request.js +155 -0
  90. package/dist/fastify/local-daemon-request.js.map +1 -0
  91. package/dist/fastify/origin.d.ts +21 -0
  92. package/dist/fastify/origin.d.ts.map +1 -0
  93. package/dist/fastify/origin.js +14 -0
  94. package/dist/fastify/origin.js.map +1 -0
  95. package/dist/fastify/request.d.ts +20 -0
  96. package/dist/fastify/request.d.ts.map +1 -0
  97. package/dist/fastify/request.js +25 -0
  98. package/dist/fastify/request.js.map +1 -0
  99. package/dist/fastify/response.d.ts +20 -0
  100. package/dist/fastify/response.d.ts.map +1 -0
  101. package/dist/fastify/response.js +41 -0
  102. package/dist/fastify/response.js.map +1 -0
  103. package/dist/fastify/route-registration-guard.d.ts +70 -0
  104. package/dist/fastify/route-registration-guard.d.ts.map +1 -0
  105. package/dist/fastify/route-registration-guard.js +69 -0
  106. package/dist/fastify/route-registration-guard.js.map +1 -0
  107. package/dist/fastify/run-stream.d.ts +18 -0
  108. package/dist/fastify/run-stream.d.ts.map +1 -0
  109. package/dist/fastify/run-stream.js +10 -0
  110. package/dist/fastify/run-stream.js.map +1 -0
  111. package/dist/fastify/runs.d.ts +17 -0
  112. package/dist/fastify/runs.d.ts.map +1 -0
  113. package/dist/fastify/runs.js +33 -0
  114. package/dist/fastify/runs.js.map +1 -0
  115. package/dist/frontend-control.d.ts +131 -0
  116. package/dist/frontend-control.d.ts.map +1 -0
  117. package/dist/frontend-control.js +77 -0
  118. package/dist/frontend-control.js.map +1 -0
  119. package/dist/frontend-sessions.d.ts +100 -0
  120. package/dist/frontend-sessions.d.ts.map +1 -0
  121. package/dist/frontend-sessions.js +180 -0
  122. package/dist/frontend-sessions.js.map +1 -0
  123. package/dist/health.d.ts +91 -0
  124. package/dist/health.d.ts.map +1 -0
  125. package/dist/health.js +53 -0
  126. package/dist/health.js.map +1 -0
  127. package/dist/host-tools.d.ts +174 -0
  128. package/dist/host-tools.d.ts.map +1 -0
  129. package/dist/host-tools.js +338 -0
  130. package/dist/host-tools.js.map +1 -0
  131. package/dist/index.d.ts +86 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +40 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/local-daemon-request.d.ts +44 -0
  136. package/dist/local-daemon-request.d.ts.map +1 -0
  137. package/dist/local-daemon-request.js +145 -0
  138. package/dist/local-daemon-request.js.map +1 -0
  139. package/dist/media.d.ts +131 -0
  140. package/dist/media.d.ts.map +1 -0
  141. package/dist/media.js +352 -0
  142. package/dist/media.js.map +1 -0
  143. package/dist/memory.d.ts +230 -0
  144. package/dist/memory.d.ts.map +1 -0
  145. package/dist/memory.js +331 -0
  146. package/dist/memory.js.map +1 -0
  147. package/dist/model-proxy.d.ts +45 -0
  148. package/dist/model-proxy.d.ts.map +1 -0
  149. package/dist/model-proxy.js +619 -0
  150. package/dist/model-proxy.js.map +1 -0
  151. package/dist/origin-validation.d.ts +64 -0
  152. package/dist/origin-validation.d.ts.map +1 -0
  153. package/dist/origin-validation.js +226 -0
  154. package/dist/origin-validation.js.map +1 -0
  155. package/dist/origin.d.ts +32 -0
  156. package/dist/origin.d.ts.map +1 -0
  157. package/dist/origin.js +14 -0
  158. package/dist/origin.js.map +1 -0
  159. package/dist/pack-http.d.ts +30 -0
  160. package/dist/pack-http.d.ts.map +1 -0
  161. package/dist/pack-http.js +12 -0
  162. package/dist/pack-http.js.map +1 -0
  163. package/dist/raw-sse.d.ts +44 -0
  164. package/dist/raw-sse.d.ts.map +1 -0
  165. package/dist/raw-sse.js +93 -0
  166. package/dist/raw-sse.js.map +1 -0
  167. package/dist/remote-run-events.d.ts +66 -0
  168. package/dist/remote-run-events.d.ts.map +1 -0
  169. package/dist/remote-run-events.js +230 -0
  170. package/dist/remote-run-events.js.map +1 -0
  171. package/dist/request.d.ts +19 -0
  172. package/dist/request.d.ts.map +1 -0
  173. package/dist/request.js +25 -0
  174. package/dist/request.js.map +1 -0
  175. package/dist/research.d.ts +42 -0
  176. package/dist/research.d.ts.map +1 -0
  177. package/dist/research.js +316 -0
  178. package/dist/research.js.map +1 -0
  179. package/dist/response.d.ts +14 -0
  180. package/dist/response.d.ts.map +1 -0
  181. package/dist/response.js +45 -0
  182. package/dist/response.js.map +1 -0
  183. package/dist/route-manifest.d.ts +31 -0
  184. package/dist/route-manifest.d.ts.map +1 -0
  185. package/dist/route-manifest.js +103 -0
  186. package/dist/route-manifest.js.map +1 -0
  187. package/dist/route-registration-guard.d.ts +65 -0
  188. package/dist/route-registration-guard.d.ts.map +1 -0
  189. package/dist/route-registration-guard.js +71 -0
  190. package/dist/route-registration-guard.js.map +1 -0
  191. package/dist/routines.d.ts +114 -0
  192. package/dist/routines.d.ts.map +1 -0
  193. package/dist/routines.js +320 -0
  194. package/dist/routines.js.map +1 -0
  195. package/dist/run-stream.d.ts +60 -0
  196. package/dist/run-stream.d.ts.map +1 -0
  197. package/dist/run-stream.js +108 -0
  198. package/dist/run-stream.js.map +1 -0
  199. package/dist/runs.d.ts +79 -0
  200. package/dist/runs.d.ts.map +1 -0
  201. package/dist/runs.js +232 -0
  202. package/dist/runs.js.map +1 -0
  203. package/dist/sse.d.ts +139 -0
  204. package/dist/sse.d.ts.map +1 -0
  205. package/dist/sse.js +157 -0
  206. package/dist/sse.js.map +1 -0
  207. package/dist/terminals.d.ts +125 -0
  208. package/dist/terminals.d.ts.map +1 -0
  209. package/dist/terminals.js +349 -0
  210. package/dist/terminals.js.map +1 -0
  211. package/dist/tool-catalog.d.ts +68 -0
  212. package/dist/tool-catalog.d.ts.map +1 -0
  213. package/dist/tool-catalog.js +68 -0
  214. package/dist/tool-catalog.js.map +1 -0
  215. package/dist/types.d.ts +58 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +5 -0
  218. package/dist/types.js.map +1 -0
  219. package/dist/workspace-root.d.ts +69 -0
  220. package/dist/workspace-root.d.ts.map +1 -0
  221. package/dist/workspace-root.js +63 -0
  222. package/dist/workspace-root.js.map +1 -0
  223. package/dist/xai.d.ts +86 -0
  224. package/dist/xai.d.ts.map +1 -0
  225. package/dist/xai.js +668 -0
  226. package/dist/xai.js.map +1 -0
  227. 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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"}
@@ -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"}