@cleocode/runtime 2026.5.130 → 2026.5.132

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.
@@ -0,0 +1,215 @@
1
+ // src/daemon/define-subsystem.ts
2
+ function defineSubsystem(definition) {
3
+ if (typeof definition.name !== "string" || definition.name.length === 0) {
4
+ throw new TypeError("defineSubsystem: `name` must be a non-empty string");
5
+ }
6
+ if (typeof definition.start !== "function") {
7
+ throw new TypeError(`defineSubsystem(${definition.name}): \`start\` must be a function`);
8
+ }
9
+ if (typeof definition.healthProbe !== "function") {
10
+ throw new TypeError(`defineSubsystem(${definition.name}): \`healthProbe\` must be a function`);
11
+ }
12
+ if (typeof definition.shutdown !== "function") {
13
+ throw new TypeError(`defineSubsystem(${definition.name}): \`shutdown\` must be a function`);
14
+ }
15
+ return Object.freeze({
16
+ name: definition.name,
17
+ start: definition.start,
18
+ healthProbe: definition.healthProbe,
19
+ shutdown: definition.shutdown
20
+ });
21
+ }
22
+
23
+ // src/daemon/logger.ts
24
+ import { getLogger } from "@cleocode/core";
25
+ function createSubsystemLogger(name) {
26
+ const log = getLogger("daemon").child({ daemonSubsystem: name });
27
+ return {
28
+ debug: (fields, msg) => log.debug(fields, msg),
29
+ info: (fields, msg) => log.info(fields, msg),
30
+ warn: (fields, msg) => log.warn(fields, msg),
31
+ error: (fields, msg) => log.error(fields, msg)
32
+ };
33
+ }
34
+
35
+ // src/daemon/registry.ts
36
+ import {
37
+ summarizeHealth
38
+ } from "@cleocode/contracts";
39
+ function toError(value) {
40
+ return value instanceof Error ? value : new Error(String(value));
41
+ }
42
+ var SubsystemRegistry = class {
43
+ #entries = [];
44
+ #hooks;
45
+ /**
46
+ * @param hooks - Optional lifecycle hooks fired around start/shutdown and on
47
+ * any lifecycle error.
48
+ */
49
+ constructor(hooks = {}) {
50
+ this.#hooks = hooks;
51
+ }
52
+ /**
53
+ * Register a subsystem. Registration order defines start order; shutdown runs
54
+ * in reverse.
55
+ *
56
+ * @typeParam TContext - The subsystem's start→shutdown context type.
57
+ * @param subsystem - A subsystem produced by `defineSubsystem`.
58
+ * @throws {Error} When a subsystem with the same `name` is already registered.
59
+ */
60
+ register(subsystem) {
61
+ if (this.#entries.some((entry) => entry.subsystem.name === subsystem.name)) {
62
+ throw new Error(`SubsystemRegistry: duplicate subsystem name '${subsystem.name}'`);
63
+ }
64
+ this.#entries.push({
65
+ subsystem,
66
+ started: false,
67
+ context: void 0
68
+ });
69
+ }
70
+ /** The names of all registered subsystems, in registration order. */
71
+ get names() {
72
+ return this.#entries.map((entry) => entry.subsystem.name);
73
+ }
74
+ /**
75
+ * Start every registered subsystem in registration order.
76
+ *
77
+ * For each subsystem: awaits `start()`, stores the resolved context, then
78
+ * fires `onStart`. If `start()` throws, fires `onError` with phase `start`
79
+ * and re-throws — a failed start is surfaced, never swallowed.
80
+ *
81
+ * @throws The first subsystem `start()` error (after firing `onError`).
82
+ */
83
+ async startAll() {
84
+ for (const entry of this.#entries) {
85
+ try {
86
+ entry.context = await entry.subsystem.start();
87
+ entry.started = true;
88
+ await this.#hooks.onStart?.(entry.subsystem.name);
89
+ } catch (cause) {
90
+ await this.#fireError(entry.subsystem.name, cause, "start");
91
+ throw toError(cause);
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Probe every registered subsystem and aggregate into a {@link HealthStatus}.
97
+ *
98
+ * A subsystem whose `healthProbe()` rejects does not abort the aggregate:
99
+ * `onError` fires with phase `healthProbe` and the subsystem is reported as a
100
+ * `stopped` row carrying the error in its `detail`, so the supervisor still
101
+ * receives a complete snapshot.
102
+ *
103
+ * @returns The aggregate health snapshot (projects onto `MonitorResponse`).
104
+ */
105
+ async aggregateHealth() {
106
+ const rows = [];
107
+ for (const entry of this.#entries) {
108
+ try {
109
+ rows.push(await entry.subsystem.healthProbe());
110
+ } catch (cause) {
111
+ await this.#fireError(entry.subsystem.name, cause, "healthProbe");
112
+ rows.push({
113
+ child_id: entry.subsystem.name,
114
+ pid: 0,
115
+ state: "stopped",
116
+ restart_count: 0,
117
+ detail: `healthProbe failed: ${toError(cause).message}`
118
+ });
119
+ }
120
+ }
121
+ return summarizeHealth(rows);
122
+ }
123
+ /**
124
+ * Shut down every started subsystem in reverse registration order (LIFO).
125
+ *
126
+ * Each subsystem's `shutdown(context)` is awaited and `onShutdown` fires. A
127
+ * throwing `shutdown` fires `onError` with phase `shutdown` but does NOT
128
+ * abort the teardown of the remaining subsystems — best-effort graceful
129
+ * shutdown of the whole daemon.
130
+ */
131
+ async shutdownAll() {
132
+ for (let i = this.#entries.length - 1; i >= 0; i -= 1) {
133
+ const entry = this.#entries[i];
134
+ if (entry === void 0 || !entry.started) continue;
135
+ try {
136
+ await entry.subsystem.shutdown(entry.context);
137
+ entry.started = false;
138
+ await this.#hooks.onShutdown?.(entry.subsystem.name);
139
+ } catch (cause) {
140
+ await this.#fireError(entry.subsystem.name, cause, "shutdown");
141
+ }
142
+ }
143
+ }
144
+ /** Fire the `onError` hook, swallowing any error the hook itself throws. */
145
+ async #fireError(name, cause, phase) {
146
+ try {
147
+ await this.#hooks.onError?.(name, toError(cause), phase);
148
+ } catch {
149
+ }
150
+ }
151
+ };
152
+
153
+ // src/daemon/supervisor-client.ts
154
+ import {
155
+ SUPERVISOR_IPC_PROTOCOL_VERSION,
156
+ SupervisorIpcRequestEnvelopeSchema,
157
+ SupervisorIpcResponseEnvelopeSchema
158
+ } from "@cleocode/contracts";
159
+ var MalformedIpcFrameError = class extends Error {
160
+ /** The raw NDJSON line that failed to decode. */
161
+ line;
162
+ /**
163
+ * @param line - The offending raw NDJSON line.
164
+ * @param cause - The underlying JSON or Zod error.
165
+ */
166
+ constructor(line, cause) {
167
+ super(`Malformed supervisor-ipc frame: ${cause instanceof Error ? cause.message : cause}`);
168
+ this.name = "MalformedIpcFrameError";
169
+ this.line = line;
170
+ this.cause = cause;
171
+ }
172
+ };
173
+ function nextCorrelationId(seq) {
174
+ return `c${seq}-${Math.random().toString(36).slice(2, 10)}`;
175
+ }
176
+ function createSupervisorIpcClient() {
177
+ let seq = 0;
178
+ return {
179
+ encodeRequest(request) {
180
+ seq += 1;
181
+ const id = nextCorrelationId(seq);
182
+ const envelope = {
183
+ protocol_version: SUPERVISOR_IPC_PROTOCOL_VERSION,
184
+ id,
185
+ direction: "request",
186
+ request
187
+ };
188
+ const parsed = SupervisorIpcRequestEnvelopeSchema.parse(envelope);
189
+ return { id, line: `${JSON.stringify(parsed)}
190
+ ` };
191
+ },
192
+ decodeResponseLine(line) {
193
+ let json;
194
+ try {
195
+ json = JSON.parse(line);
196
+ } catch (cause) {
197
+ throw new MalformedIpcFrameError(line, cause);
198
+ }
199
+ const result = SupervisorIpcResponseEnvelopeSchema.safeParse(json);
200
+ if (!result.success) {
201
+ throw new MalformedIpcFrameError(line, result.error);
202
+ }
203
+ return result.data;
204
+ }
205
+ };
206
+ }
207
+
208
+ export {
209
+ defineSubsystem,
210
+ createSubsystemLogger,
211
+ SubsystemRegistry,
212
+ MalformedIpcFrameError,
213
+ createSupervisorIpcClient
214
+ };
215
+ //# sourceMappingURL=chunk-WSQQ5DQL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/define-subsystem.ts","../src/daemon/logger.ts","../src/daemon/registry.ts","../src/daemon/supervisor-client.ts"],"sourcesContent":["/**\n * `defineSubsystem()` — declare a supervised daemon subsystem.\n *\n * The single factory R4-R7 adapters call to express a long-running concern\n * (Studio supervision, GC cron, web server, docs-viewer, runtime poller) as a\n * uniform {@link Subsystem}. The returned descriptor is frozen so the registry\n * can drive `start → healthProbe → shutdown` without the definition mutating\n * underfoot.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type { Subsystem, SubsystemDefinition } from '@cleocode/contracts';\n\n/**\n * Declare a daemon subsystem.\n *\n * Validates that the four required surfaces (`name`, `start`, `healthProbe`,\n * `shutdown`) are present and that `name` is a non-empty string, then returns a\n * frozen {@link Subsystem} ready to register with a `SubsystemRegistry`.\n *\n * @typeParam TContext - Opaque context threaded from `start` into `shutdown`\n * (e.g. a child handle or resolved config). Defaults to `void`.\n * @param definition - The subsystem descriptor.\n * @returns A frozen, registrable {@link Subsystem}.\n * @throws {TypeError} When `name` is empty or a required method is missing.\n *\n * @example\n * ```ts\n * const studio = defineSubsystem({\n * name: 'studio',\n * start: () => supervisor.start(),\n * healthProbe: () => ({ child_id: 'studio', pid: supervisor.pid ?? 0,\n * state: supervisor.status === 'running' ? 'running' : 'stopped',\n * restart_count: 0 }),\n * shutdown: () => supervisor.stop(),\n * });\n * ```\n */\nexport function defineSubsystem<TContext = void>(\n definition: SubsystemDefinition<TContext>,\n): Subsystem<TContext> {\n if (typeof definition.name !== 'string' || definition.name.length === 0) {\n throw new TypeError('defineSubsystem: `name` must be a non-empty string');\n }\n if (typeof definition.start !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`start\\` must be a function`);\n }\n if (typeof definition.healthProbe !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`healthProbe\\` must be a function`);\n }\n if (typeof definition.shutdown !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`shutdown\\` must be a function`);\n }\n\n return Object.freeze({\n name: definition.name,\n start: definition.start,\n healthProbe: definition.healthProbe,\n shutdown: definition.shutdown,\n });\n}\n","/**\n * Daemon subsystem log-routing primitive.\n *\n * `createSubsystemLogger(name)` is the **single** logging surface the\n * `@cleocode/runtime/daemon` submodule exposes. It routes through the canonical\n * pino factory `getLogger` (`packages/core/src/logger.ts`) — NOT raw\n * `createWriteStream` and NOT `console.*` — so every daemon log line lands in\n * the rotating, retention-managed CLEO log files with a consistent structured\n * shape and a per-subsystem `subsystem` field.\n *\n * It supersedes the **5 ad-hoc logging conventions** the legacy daemons grew,\n * which R4-R7 will migrate onto this primitive:\n *\n * 1. `sentient/daemon.ts` — `createWriteStream(sentient.log)` /\n * `createWriteStream(sentient.err)` + `process.stderr.write` lines.\n * 2. `gc/daemon.ts` — `createWriteStream(gc.log)` / `createWriteStream(gc.err)`.\n * 3. `web.ts` — `console.*`.\n * 4. `docs-viewer.ts` — ad-hoc stdout/stderr writes.\n * 5. the `runtime` services — bespoke per-service logging.\n *\n * ## Migration guidance for R4-R7 adapters\n *\n * Each adapter (Studio, GC, web, docs-viewer) replaces its bespoke sink with:\n *\n * ```ts\n * import { createSubsystemLogger } from '@cleocode/runtime/daemon';\n * const log = createSubsystemLogger('studio');\n * log.info({ pid }, 'Studio started'); // not process.stderr.write(...)\n * log.error({ err }, 'Studio crashed'); // not createWriteStream('sentient.err')\n * ```\n *\n * Do not import pino directly and do not open file streams — the canonical\n * factory owns rotation, retention, level, and the `projectHash` correlation\n * field. Routing through it is what makes the daemon's logs observable via the\n * same `cleo` log surface as the rest of the system.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11368 — log routing primitive (pino getLogger canonical sink)\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { getLogger } from '@cleocode/core';\n\n/**\n * A structured log payload — a plain object of serializable fields merged into\n * the emitted log record (e.g. `{ pid, err }`).\n */\nexport type SubsystemLogFields = Record<string, unknown>;\n\n/**\n * The narrow logging surface a daemon subsystem uses.\n *\n * Each method accepts an optional structured `fields` object followed by the\n * message, mirroring pino's `(obj, msg)` call shape. This is intentionally\n * smaller than the full pino `Logger` so the daemon submodule does not leak the\n * pino type across its public boundary while still routing through the\n * canonical pino sink.\n */\nexport interface SubsystemLogger {\n /** Log at `debug` level. */\n debug: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `info` level. */\n info: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `warn` level. */\n warn: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `error` level. */\n error: (fields: SubsystemLogFields, msg: string) => void;\n}\n\n/**\n * Create the single per-subsystem logger for the daemon submodule.\n *\n * Routes through the canonical pino factory `getLogger('daemon')` and namespaces\n * the result with a per-subsystem child binding (`{ subsystem: name }`), so all\n * daemon log lines share the `daemon` subsystem root while remaining\n * individually attributable. Never opens a file stream and never writes to\n * `console`.\n *\n * @param name - The logical subsystem name (e.g. `'studio'`, `'gc'`, `'web'`).\n * @returns A {@link SubsystemLogger} bound to `name`.\n *\n * @example\n * ```ts\n * const log = createSubsystemLogger('studio');\n * log.info({ pid: 4242, port: 3456 }, 'Studio started');\n * ```\n */\nexport function createSubsystemLogger(name: string): SubsystemLogger {\n // Bind under the canonical 'daemon' root, then a per-subsystem child so each\n // subsystem's lines carry `{ subsystem: 'daemon', daemonSubsystem: name }`.\n const log = getLogger('daemon').child({ daemonSubsystem: name });\n return {\n debug: (fields, msg) => log.debug(fields, msg),\n info: (fields, msg) => log.info(fields, msg),\n warn: (fields, msg) => log.warn(fields, msg),\n error: (fields, msg) => log.error(fields, msg),\n };\n}\n","/**\n * `SubsystemRegistry` — drives the uniform daemon subsystem lifecycle.\n *\n * Aggregates {@link Subsystem}s declared via `defineSubsystem` and runs them\n * through a uniform `start → healthProbe → shutdown` lifecycle, firing the\n * typed {@link DaemonLifecycleHooks} in order. Health probes are aggregated\n * into a {@link HealthStatus} that projects onto the FROZEN `supervisor-ipc`\n * `MonitorResponse` the Rust supervisor consumes (AC4).\n *\n * Shutdown runs in reverse registration order (LIFO) so dependants stop before\n * their dependencies — the conventional supervised-process teardown ordering.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport {\n type DaemonLifecycleHooks,\n type HealthStatus,\n type Subsystem,\n type SubsystemHealth,\n type SubsystemLifecyclePhase,\n summarizeHealth,\n} from '@cleocode/contracts';\n\n/**\n * A registered subsystem plus its runtime bookkeeping.\n *\n * The `context` is the value `start()` resolved to, threaded back into\n * `shutdown()`. It is type-erased to `unknown` inside the registry because the\n * registry holds subsystems of heterogeneous context types; each entry's\n * `subsystem.shutdown` is the only consumer and re-narrows it at the call site\n * via the subsystem's own typing — no `any`, and the erased value never escapes\n * the registry surface.\n */\ninterface RegisteredSubsystem {\n readonly subsystem: Subsystem<unknown>;\n started: boolean;\n context: unknown;\n}\n\n/** Normalize an unknown thrown value into an `Error`. */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/**\n * The registry that owns and drives the daemon's subsystems.\n *\n * Construct with optional {@link DaemonLifecycleHooks}; register subsystems,\n * then `startAll()` / `aggregateHealth()` / `shutdownAll()`.\n */\nexport class SubsystemRegistry {\n readonly #entries: RegisteredSubsystem[] = [];\n readonly #hooks: DaemonLifecycleHooks;\n\n /**\n * @param hooks - Optional lifecycle hooks fired around start/shutdown and on\n * any lifecycle error.\n */\n constructor(hooks: DaemonLifecycleHooks = {}) {\n this.#hooks = hooks;\n }\n\n /**\n * Register a subsystem. Registration order defines start order; shutdown runs\n * in reverse.\n *\n * @typeParam TContext - The subsystem's start→shutdown context type.\n * @param subsystem - A subsystem produced by `defineSubsystem`.\n * @throws {Error} When a subsystem with the same `name` is already registered.\n */\n register<TContext>(subsystem: Subsystem<TContext>): void {\n if (this.#entries.some((entry) => entry.subsystem.name === subsystem.name)) {\n throw new Error(`SubsystemRegistry: duplicate subsystem name '${subsystem.name}'`);\n }\n // Widen the context type to `unknown` for heterogeneous storage. The\n // subsystem's own `start`/`shutdown` remain individually well-typed; only\n // the registry's internal handle is erased, and it never leaks out.\n this.#entries.push({\n subsystem: subsystem as Subsystem<unknown>,\n started: false,\n context: undefined,\n });\n }\n\n /** The names of all registered subsystems, in registration order. */\n get names(): readonly string[] {\n return this.#entries.map((entry) => entry.subsystem.name);\n }\n\n /**\n * Start every registered subsystem in registration order.\n *\n * For each subsystem: awaits `start()`, stores the resolved context, then\n * fires `onStart`. If `start()` throws, fires `onError` with phase `start`\n * and re-throws — a failed start is surfaced, never swallowed.\n *\n * @throws The first subsystem `start()` error (after firing `onError`).\n */\n async startAll(): Promise<void> {\n for (const entry of this.#entries) {\n try {\n entry.context = await entry.subsystem.start();\n entry.started = true;\n await this.#hooks.onStart?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'start');\n throw toError(cause);\n }\n }\n }\n\n /**\n * Probe every registered subsystem and aggregate into a {@link HealthStatus}.\n *\n * A subsystem whose `healthProbe()` rejects does not abort the aggregate:\n * `onError` fires with phase `healthProbe` and the subsystem is reported as a\n * `stopped` row carrying the error in its `detail`, so the supervisor still\n * receives a complete snapshot.\n *\n * @returns The aggregate health snapshot (projects onto `MonitorResponse`).\n */\n async aggregateHealth(): Promise<HealthStatus> {\n const rows: SubsystemHealth[] = [];\n for (const entry of this.#entries) {\n try {\n rows.push(await entry.subsystem.healthProbe());\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'healthProbe');\n rows.push({\n child_id: entry.subsystem.name,\n pid: 0,\n state: 'stopped',\n restart_count: 0,\n detail: `healthProbe failed: ${toError(cause).message}`,\n });\n }\n }\n return summarizeHealth(rows);\n }\n\n /**\n * Shut down every started subsystem in reverse registration order (LIFO).\n *\n * Each subsystem's `shutdown(context)` is awaited and `onShutdown` fires. A\n * throwing `shutdown` fires `onError` with phase `shutdown` but does NOT\n * abort the teardown of the remaining subsystems — best-effort graceful\n * shutdown of the whole daemon.\n */\n async shutdownAll(): Promise<void> {\n for (let i = this.#entries.length - 1; i >= 0; i -= 1) {\n const entry = this.#entries[i];\n if (entry === undefined || !entry.started) continue;\n try {\n await entry.subsystem.shutdown(entry.context);\n entry.started = false;\n await this.#hooks.onShutdown?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'shutdown');\n // Continue tearing down the rest — do not abort on one failure.\n }\n }\n }\n\n /** Fire the `onError` hook, swallowing any error the hook itself throws. */\n async #fireError(name: string, cause: unknown, phase: SubsystemLifecyclePhase): Promise<void> {\n try {\n await this.#hooks.onError?.(name, toError(cause), phase);\n } catch {\n // A throwing error-hook must not mask the original failure.\n }\n }\n}\n","/**\n * Typed NDJSON client for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * The Rust `cleo-supervisor` is the server; a CLEO daemon process is a client\n * that sends {@link SupervisorIpcRequestEnvelope}s and receives\n * {@link SupervisorIpcResponseEnvelope}s, one newline-delimited JSON\n * (NDJSON) envelope per line. This module is a transport-agnostic codec: it\n * encodes outbound envelopes to NDJSON lines and decodes inbound lines through\n * the frozen Zod schemas. **Every inbound line is parsed** — a malformed or\n * schema-violating frame is rejected with a typed {@link MalformedIpcFrameError}\n * and never silently dropped (T11367 AC2).\n *\n * The codec does not own a socket — callers feed it raw lines from whatever\n * transport (Unix socket, named pipe, test pair) they own, keeping the daemon\n * submodule free of platform IO and trivially testable.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport {\n SUPERVISOR_IPC_PROTOCOL_VERSION,\n type SupervisorIpcRequest,\n type SupervisorIpcRequestEnvelope,\n SupervisorIpcRequestEnvelopeSchema,\n type SupervisorIpcResponseEnvelope,\n SupervisorIpcResponseEnvelopeSchema,\n} from '@cleocode/contracts';\n\n/**\n * A typed error raised when an inbound NDJSON line cannot be decoded into a\n * valid {@link SupervisorIpcResponseEnvelope}.\n *\n * Carries the offending raw `line` and the underlying `cause` (a JSON\n * `SyntaxError` or a Zod validation error) so callers can log + correlate.\n * Invalid frames are surfaced as this error — never silently dropped.\n */\nexport class MalformedIpcFrameError extends Error {\n /** The raw NDJSON line that failed to decode. */\n readonly line: string;\n\n /**\n * @param line - The offending raw NDJSON line.\n * @param cause - The underlying JSON or Zod error.\n */\n constructor(line: string, cause: unknown) {\n super(`Malformed supervisor-ipc frame: ${cause instanceof Error ? cause.message : cause}`);\n this.name = 'MalformedIpcFrameError';\n this.line = line;\n this.cause = cause;\n }\n}\n\n/**\n * A transport-agnostic codec for the FROZEN `supervisor-ipc` v1.0 wire format.\n *\n * Construct via {@link createSupervisorIpcClient}. The client is stateful only\n * in its monotonic correlation-id counter; it owns no socket.\n */\nexport interface SupervisorIpcClient {\n /**\n * Encode a request body into a complete, newline-terminated NDJSON envelope\n * line ready to write to the transport.\n *\n * Stamps the frozen protocol version and a fresh correlation `id`, validates\n * the result against {@link SupervisorIpcRequestEnvelopeSchema}, and returns\n * the serialized line (including the trailing `\\n`).\n *\n * @param request - The request body (validated by the envelope schema).\n * @returns `{ id, line }` — the correlation id and the NDJSON line to send.\n */\n encodeRequest: (request: SupervisorIpcRequest) => { id: string; line: string };\n\n /**\n * Decode a single inbound NDJSON line into a typed response envelope.\n *\n * Parses the line as JSON, then validates it against\n * {@link SupervisorIpcResponseEnvelopeSchema}. A malformed or\n * schema-violating line throws {@link MalformedIpcFrameError} — it is never\n * silently dropped.\n *\n * @param line - A single NDJSON line (without the trailing newline).\n * @returns The decoded {@link SupervisorIpcResponseEnvelope}.\n * @throws {MalformedIpcFrameError} When the line is not a valid response frame.\n */\n decodeResponseLine: (line: string) => SupervisorIpcResponseEnvelope;\n}\n\n/**\n * Generate a process-unique correlation id for an IPC request envelope.\n *\n * @param seq - The client's monotonic sequence number.\n * @returns A correlation id of the form `c<seq>-<random>`.\n */\nfunction nextCorrelationId(seq: number): string {\n return `c${seq}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a typed NDJSON codec for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * @returns A {@link SupervisorIpcClient}.\n *\n * @example\n * ```ts\n * const client = createSupervisorIpcClient();\n * const { id, line } = client.encodeRequest({ kind: 'health' });\n * socket.write(line);\n * socket.on('line', (raw) => {\n * const env = client.decodeResponseLine(raw); // throws MalformedIpcFrameError on bad frame\n * if (env.response.kind === 'health') { /* ... *\\/ }\n * });\n * ```\n */\nexport function createSupervisorIpcClient(): SupervisorIpcClient {\n let seq = 0;\n\n return {\n encodeRequest(request: SupervisorIpcRequest): { id: string; line: string } {\n seq += 1;\n const id = nextCorrelationId(seq);\n const envelope: SupervisorIpcRequestEnvelope = {\n protocol_version: SUPERVISOR_IPC_PROTOCOL_VERSION,\n id,\n direction: 'request',\n request,\n };\n // Validate before serialization so we never emit an off-contract frame.\n const parsed = SupervisorIpcRequestEnvelopeSchema.parse(envelope);\n return { id, line: `${JSON.stringify(parsed)}\\n` };\n },\n\n decodeResponseLine(line: string): SupervisorIpcResponseEnvelope {\n let json: unknown;\n try {\n json = JSON.parse(line);\n } catch (cause) {\n throw new MalformedIpcFrameError(line, cause);\n }\n const result = SupervisorIpcResponseEnvelopeSchema.safeParse(json);\n if (!result.success) {\n throw new MalformedIpcFrameError(line, result.error);\n }\n return result.data;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,305 @@
1
+ // src/gateway/http/listen.ts
2
+ import { createServer } from "http";
3
+ import { getLogger } from "@cleocode/core";
4
+
5
+ // src/gateway/http/server.ts
6
+ import { randomUUID } from "crypto";
7
+ function statusForResponse(response) {
8
+ if (response.success) return 200;
9
+ const code = response.error?.code ?? "";
10
+ if (code === "E_NOT_FOUND" || code.endsWith("_NOT_FOUND")) return 404;
11
+ if (code === "E_UNAUTHORIZED" || code === "E_AUTH_REQUIRED") return 401;
12
+ if (code === "E_FORBIDDEN") return 403;
13
+ if (code === "E_RATE_LIMITED" || code === "E_RATE_LIMIT_EXCEEDED") return 429;
14
+ if (code === "E_VALIDATION" || code.startsWith("E_VALIDATION") || code === "E_INVALID_INPUT" || code === "E_BAD_REQUEST") {
15
+ return 400;
16
+ }
17
+ if (code === "E_NOT_IMPLEMENTED" || code === "E_UNSUPPORTED") return 501;
18
+ return 500;
19
+ }
20
+ async function routeUnary(handler, req) {
21
+ const requestId = req.requestId ?? randomUUID();
22
+ const request = {
23
+ gateway: req.gateway,
24
+ domain: req.domain,
25
+ operation: req.operation,
26
+ params: req.params,
27
+ // Force the transport of origin — a client cannot impersonate cli/mcp/rpc.
28
+ source: "http",
29
+ requestId,
30
+ sessionId: req.sessionId
31
+ };
32
+ try {
33
+ const body = await handler.handle(request);
34
+ return { status: statusForResponse(body), body };
35
+ } catch (err) {
36
+ const message = err instanceof Error ? err.message : String(err);
37
+ const body = {
38
+ meta: {
39
+ gateway: req.gateway,
40
+ domain: req.domain,
41
+ operation: req.operation,
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
43
+ duration_ms: 0,
44
+ source: "http",
45
+ requestId
46
+ },
47
+ success: false,
48
+ error: { code: "E_HTTP_INTERNAL", message }
49
+ };
50
+ return { status: 500, body };
51
+ }
52
+ }
53
+
54
+ // src/gateway/http/listen.ts
55
+ var GATEWAYS = /* @__PURE__ */ new Set(["query", "mutate"]);
56
+ var MAX_BODY_BYTES = 1048576;
57
+ function parseHttpRoute(method, url) {
58
+ if (method !== "POST") {
59
+ return {
60
+ ok: false,
61
+ status: 405,
62
+ code: "E_HTTP_METHOD_NOT_ALLOWED",
63
+ message: `method ${method ?? "<none>"} not allowed; gateway accepts POST`
64
+ };
65
+ }
66
+ const pathname = new URL(url ?? "/", "http://localhost").pathname;
67
+ const segments = pathname.split("/").filter((s) => s.length > 0);
68
+ if (segments.length !== 3) {
69
+ return {
70
+ ok: false,
71
+ status: 404,
72
+ code: "E_HTTP_NOT_FOUND",
73
+ message: `expected POST /<gateway>/<domain>/<operation>, got '${pathname}'`
74
+ };
75
+ }
76
+ const [gateway, domain, operation] = segments;
77
+ if (!GATEWAYS.has(gateway)) {
78
+ return {
79
+ ok: false,
80
+ status: 404,
81
+ code: "E_HTTP_NOT_FOUND",
82
+ message: `unknown gateway '${gateway}'; expected one of query, mutate`
83
+ };
84
+ }
85
+ return { ok: true, gateway, domain, operation };
86
+ }
87
+ function readBody(req) {
88
+ return new Promise((resolve, reject) => {
89
+ const chunks = [];
90
+ let size = 0;
91
+ req.on("data", (chunk) => {
92
+ size += chunk.length;
93
+ if (size > MAX_BODY_BYTES) {
94
+ reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
95
+ req.destroy();
96
+ return;
97
+ }
98
+ chunks.push(chunk);
99
+ });
100
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
101
+ req.on("error", reject);
102
+ });
103
+ }
104
+ function writeEdgeError(res, status, code, message) {
105
+ const body = {
106
+ meta: {
107
+ gateway: "query",
108
+ domain: "",
109
+ operation: "",
110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
111
+ duration_ms: 0,
112
+ source: "http",
113
+ requestId: ""
114
+ },
115
+ success: false,
116
+ error: { code, message }
117
+ };
118
+ res.writeHead(status, { "Content-Type": "application/json" });
119
+ res.end(JSON.stringify(body));
120
+ }
121
+ async function handleHttpRequest(handler, req, res, log) {
122
+ const route = parseHttpRoute(req.method, req.url);
123
+ if (!route.ok) {
124
+ writeEdgeError(res, route.status, route.code, route.message);
125
+ return;
126
+ }
127
+ let params;
128
+ try {
129
+ const raw = await readBody(req);
130
+ if (raw.trim().length > 0) {
131
+ const parsed = JSON.parse(raw);
132
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
133
+ writeEdgeError(res, 400, "E_HTTP_BAD_REQUEST", "request body must be a JSON object");
134
+ return;
135
+ }
136
+ params = parsed;
137
+ }
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ writeEdgeError(res, 400, "E_HTTP_BAD_REQUEST", `invalid request body: ${message}`);
141
+ return;
142
+ }
143
+ const unary = {
144
+ gateway: route.gateway,
145
+ domain: route.domain,
146
+ operation: route.operation,
147
+ params
148
+ };
149
+ const result = await routeUnary(handler, unary);
150
+ res.writeHead(result.status, { "Content-Type": "application/json" });
151
+ res.end(JSON.stringify(result.body));
152
+ log.debug(
153
+ {
154
+ gateway: route.gateway,
155
+ domain: route.domain,
156
+ operation: route.operation,
157
+ status: result.status
158
+ },
159
+ "http request routed"
160
+ );
161
+ }
162
+ function startHttpServer(handler, opts) {
163
+ const log = getLogger("gateway-http");
164
+ const host = opts.host ?? "127.0.0.1";
165
+ const server = createServer((req, res) => {
166
+ handleHttpRequest(handler, req, res, log).catch((err) => {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ log.warn({ err }, "http request handling failed");
169
+ if (!res.headersSent) {
170
+ writeEdgeError(res, 500, "E_HTTP_INTERNAL", message);
171
+ } else if (!res.writableEnded) {
172
+ res.end();
173
+ }
174
+ });
175
+ });
176
+ return new Promise((resolve, reject) => {
177
+ server.once("error", reject);
178
+ server.listen(opts.port, host, () => {
179
+ server.removeListener("error", reject);
180
+ const address = server.address();
181
+ const boundPort = typeof address === "object" && address !== null ? address.port : opts.port;
182
+ log.info({ host, port: boundPort }, "http gateway server listening");
183
+ let closed = false;
184
+ const handle = {
185
+ server,
186
+ port: boundPort,
187
+ host,
188
+ close() {
189
+ if (closed) return Promise.resolve();
190
+ closed = true;
191
+ return new Promise((res) => {
192
+ server.close(() => res());
193
+ server.closeIdleConnections();
194
+ });
195
+ }
196
+ };
197
+ resolve(handle);
198
+ });
199
+ });
200
+ }
201
+
202
+ // src/gateway/http/sse.ts
203
+ var LF = "\n";
204
+ var RECORD_END = "\n\n";
205
+ function encodeSseFrame(frame) {
206
+ let out = "";
207
+ if (frame.event !== void 0) out += `event: ${frame.event}${LF}`;
208
+ if (frame.id !== void 0) out += `id: ${frame.id}${LF}`;
209
+ out += `data: ${JSON.stringify(frame.data)}${RECORD_END}`;
210
+ return out;
211
+ }
212
+ function encodeStreamEvent(frame) {
213
+ return encodeSseFrame({ id: String(frame.seq), data: frame });
214
+ }
215
+ var SSE_HEADERS = Object.freeze({
216
+ "Content-Type": "text/event-stream",
217
+ "Cache-Control": "no-cache",
218
+ Connection: "keep-alive",
219
+ "X-Accel-Buffering": "no"
220
+ });
221
+ function createSseStream(source, signal) {
222
+ const encoder = new TextEncoder();
223
+ return new ReadableStream({
224
+ start(controller) {
225
+ let closed = false;
226
+ let teardown;
227
+ let teardownDone = false;
228
+ let sourceRunning = false;
229
+ const onAbort = () => emitter.close();
230
+ const runTeardown = () => {
231
+ if (teardownDone) return;
232
+ teardownDone = true;
233
+ if (typeof teardown === "function") teardown();
234
+ };
235
+ const emitter = {
236
+ get closed() {
237
+ return closed;
238
+ },
239
+ send(frame) {
240
+ if (closed) return;
241
+ try {
242
+ controller.enqueue(encoder.encode(encodeSseFrame(frame)));
243
+ } catch {
244
+ emitter.close();
245
+ }
246
+ },
247
+ sendStreamEvent(frame) {
248
+ if (closed) return;
249
+ try {
250
+ controller.enqueue(encoder.encode(encodeStreamEvent(frame)));
251
+ } catch {
252
+ emitter.close();
253
+ }
254
+ },
255
+ sendRaw(record) {
256
+ if (closed) return;
257
+ try {
258
+ controller.enqueue(encoder.encode(record));
259
+ } catch {
260
+ emitter.close();
261
+ }
262
+ },
263
+ close() {
264
+ if (closed) return;
265
+ closed = true;
266
+ if (signal) signal.removeEventListener("abort", onAbort);
267
+ if (!sourceRunning) runTeardown();
268
+ try {
269
+ controller.close();
270
+ } catch {
271
+ }
272
+ }
273
+ };
274
+ if (signal) {
275
+ if (signal.aborted) {
276
+ closed = true;
277
+ teardownDone = true;
278
+ try {
279
+ controller.close();
280
+ } catch {
281
+ }
282
+ return;
283
+ }
284
+ signal.addEventListener("abort", onAbort);
285
+ }
286
+ sourceRunning = true;
287
+ const returned = source(emitter);
288
+ teardown = typeof returned === "function" ? returned : void 0;
289
+ sourceRunning = false;
290
+ if (closed) runTeardown();
291
+ }
292
+ });
293
+ }
294
+
295
+ export {
296
+ statusForResponse,
297
+ routeUnary,
298
+ parseHttpRoute,
299
+ startHttpServer,
300
+ encodeSseFrame,
301
+ encodeStreamEvent,
302
+ SSE_HEADERS,
303
+ createSseStream
304
+ };
305
+ //# sourceMappingURL=chunk-YZBIPGMH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gateway/http/listen.ts","../src/gateway/http/server.ts","../src/gateway/http/sse.ts"],"sourcesContent":["/**\n * `node:http` server binding for the framework-agnostic HTTP gateway adapter.\n *\n * The HTTP adapter ({@link routeUnary} + the SSE primitives) is deliberately\n * framework-agnostic — it owns NO port binding and NO `Request`/`Response`\n * lifecycle, so an embedder (a SvelteKit route, a test harness) can drive it\n * directly. The **daemon**, however, is itself the embedder: it must expose the\n * gateway to external HTTP clients over a real listening socket. This module is\n * that thin embedder — a minimal `node:http` server that parses the request URL\n * into a `(gateway, domain, operation)` triple, reads the JSON body, and routes\n * it through {@link routeUnary}. It deliberately stays in the runtime (no\n * SvelteKit / no `@cleocode/cleo` / no drizzle dependency) so the daemon can\n * host the gateway without reaching across a package boundary.\n *\n * Routing contract (intentionally minimal — the wire vocabulary is the same as\n * every other transport, only the framing differs):\n *\n * `POST /<gateway>/<domain>/<operation>` with a JSON body → operation params\n *\n * where `<gateway>` is `query` | `mutate`. A non-POST method, an unparseable\n * path, or a body that is not a JSON object is rejected at the wire edge with a\n * LAFS-shaped error envelope — the injected {@link GatewayHandler} is never\n * invoked with a malformed request.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11451\n * @epic T11254\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { DispatchResponse, Gateway } from '@cleocode/contracts/gateway';\nimport { getLogger } from '@cleocode/core';\nimport type { GatewayHandler } from '../index.js';\nimport { routeUnary } from './server.js';\nimport type { HttpUnaryRequest } from './types.js';\n\n/** The two CQRS gateways a path segment may name. */\nconst GATEWAYS: ReadonlySet<string> = new Set<Gateway>(['query', 'mutate']);\n\n/** Maximum accepted request-body size (1 MiB) — a crude DoS guard at the edge. */\nconst MAX_BODY_BYTES = 1_048_576;\n\n/**\n * Options for {@link startHttpServer}.\n *\n * Mirrors the connection-oriented {@link RpcServerOptions} shape: the caller\n * resolves the bind coordinates so the runtime stays free of any\n * `@cleocode/paths` dependency.\n */\nexport interface HttpServerOptions {\n /**\n * TCP port to bind. `0` selects an ephemeral port (the bound port is then\n * readable from {@link HttpServerHandle.port}).\n */\n port: number;\n /**\n * Host/interface to bind. Defaults to `127.0.0.1` (loopback only) — the\n * gateway is local-process-facing; exposing it on a public interface is an\n * explicit caller decision.\n */\n host?: string;\n}\n\n/**\n * A live HTTP server handle returned by {@link startHttpServer}.\n *\n * Exposes the bound {@link Server}, the actually-bound `port` (resolved even\n * when `0` was requested), and an idempotent {@link close}. The adapter never\n * calls `process.exit` — lifecycle stays with the caller (R3 contract).\n */\nexport interface HttpServerHandle {\n /** The bound `node:http` server. */\n server: Server;\n /** The actually-bound TCP port (resolved when `0` was requested). */\n port: number;\n /** Host/interface the server is bound to. */\n host: string;\n /** Close the server + all live connections; resolves once fully closed. */\n close(): Promise<void>;\n}\n\n/**\n * The result of parsing a request line into routing coordinates: either a\n * resolved unary triple or a rejection reason for the wire edge.\n */\ntype ParsedRoute =\n | { ok: true; gateway: Gateway; domain: string; operation: string }\n | { ok: false; status: number; code: string; message: string };\n\n/**\n * Parse `POST /<gateway>/<domain>/<operation>` into a routing triple.\n *\n * Rejects a non-POST method (`405`) and any path that does not resolve to a\n * known `(gateway, domain, operation)` triple (`404`). The gateway segment is\n * validated against the two CQRS gateways so a client cannot smuggle an\n * arbitrary value into the dispatch request.\n *\n * @param method - The HTTP request method.\n * @param url - The HTTP request URL (path + query).\n * @returns The parsed route, or a typed rejection for the wire edge.\n */\nexport function parseHttpRoute(method: string | undefined, url: string | undefined): ParsedRoute {\n if (method !== 'POST') {\n return {\n ok: false,\n status: 405,\n code: 'E_HTTP_METHOD_NOT_ALLOWED',\n message: `method ${method ?? '<none>'} not allowed; gateway accepts POST`,\n };\n }\n // Parse against a dummy origin so only the path is consumed (query ignored).\n const pathname = new URL(url ?? '/', 'http://localhost').pathname;\n const segments = pathname.split('/').filter((s) => s.length > 0);\n if (segments.length !== 3) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `expected POST /<gateway>/<domain>/<operation>, got '${pathname}'`,\n };\n }\n const [gateway, domain, operation] = segments;\n if (!GATEWAYS.has(gateway)) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `unknown gateway '${gateway}'; expected one of query, mutate`,\n };\n }\n return { ok: true, gateway: gateway as Gateway, domain, operation };\n}\n\n/**\n * Read the full request body as a string, enforcing {@link MAX_BODY_BYTES}.\n *\n * @param req - The inbound request stream.\n * @returns The decoded UTF-8 body.\n * @throws {Error} When the body exceeds the size cap (the connection is then\n * destroyed by the caller).\n */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\n/**\n * Write a LAFS-shaped error envelope to the response at the wire edge.\n *\n * Used for rejections that never reach the {@link GatewayHandler} (bad method,\n * unroutable path, malformed body), so a client always sees a well-formed LAFS\n * envelope rather than a bare HTTP error.\n *\n * @param res - The server response.\n * @param status - The HTTP status to set.\n * @param code - The machine-readable LAFS error code.\n * @param message - A human-readable message.\n */\nfunction writeEdgeError(res: ServerResponse, status: number, code: string, message: string): void {\n const body: DispatchResponse = {\n meta: {\n gateway: 'query',\n domain: '',\n operation: '',\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId: '',\n },\n success: false,\n error: { code, message },\n };\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Handle a single inbound HTTP request: parse the route, read + JSON-parse the\n * body, route it through the gateway handler, and serialize the LAFS response.\n *\n * All failure modes are rendered as LAFS envelopes — a bad route or malformed\n * body is rejected at the edge (handler never invoked); a thrown handler error\n * is already trapped inside {@link routeUnary} as a `500`. The function never\n * throws and never calls `process.exit`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The inbound request.\n * @param res - The outbound response.\n * @param log - The adapter's pino logger.\n */\nasync function handleHttpRequest(\n handler: GatewayHandler,\n req: IncomingMessage,\n res: ServerResponse,\n log: ReturnType<typeof getLogger>,\n): Promise<void> {\n const route = parseHttpRoute(req.method, req.url);\n if (!route.ok) {\n writeEdgeError(res, route.status, route.code, route.message);\n return;\n }\n\n let params: Record<string, unknown> | undefined;\n try {\n const raw = await readBody(req);\n if (raw.trim().length > 0) {\n const parsed: unknown = JSON.parse(raw);\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', 'request body must be a JSON object');\n return;\n }\n params = parsed as Record<string, unknown>;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', `invalid request body: ${message}`);\n return;\n }\n\n const unary: HttpUnaryRequest = {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n params,\n };\n const result = await routeUnary(handler, unary);\n res.writeHead(result.status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(result.body));\n log.debug(\n {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n status: result.status,\n },\n 'http request routed',\n );\n}\n\n/**\n * Start a `node:http` server that hosts the gateway over HTTP for external\n * clients.\n *\n * Binds {@link HttpServerOptions.port} on {@link HttpServerOptions.host}\n * (default loopback), routes every `POST /<gateway>/<domain>/<operation>`\n * through the injected {@link GatewayHandler} via {@link routeUnary}, and\n * resolves once listening. The returned {@link HttpServerHandle} exposes an\n * idempotent `close()` for deterministic teardown (daemon shutdown, tests). The\n * adapter never calls `process.exit`.\n *\n * The caller assembles the handler (via `createGatewayHandler` with its domain\n * handlers + middleware) and injects it here — this server never builds\n * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Bind coordinates.\n * @returns A promise resolving to the live {@link HttpServerHandle}.\n */\nexport function startHttpServer(\n handler: GatewayHandler,\n opts: HttpServerOptions,\n): Promise<HttpServerHandle> {\n const log = getLogger('gateway-http');\n const host = opts.host ?? '127.0.0.1';\n\n const server = createServer((req, res) => {\n handleHttpRequest(handler, req, res, log).catch((err: unknown) => {\n // handleHttpRequest traps its own failures; this guards the wire path.\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'http request handling failed');\n if (!res.headersSent) {\n writeEdgeError(res, 500, 'E_HTTP_INTERNAL', message);\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n\n return new Promise<HttpServerHandle>((resolve, reject) => {\n server.once('error', reject);\n server.listen(opts.port, host, () => {\n server.removeListener('error', reject);\n const address = server.address();\n const boundPort = typeof address === 'object' && address !== null ? address.port : opts.port;\n log.info({ host, port: boundPort }, 'http gateway server listening');\n\n let closed = false;\n const handle: HttpServerHandle = {\n server,\n port: boundPort,\n host,\n close(): Promise<void> {\n if (closed) return Promise.resolve();\n closed = true;\n return new Promise<void>((res) => {\n server.close(() => res());\n // Drop keep-alive connections so close() resolves promptly.\n server.closeIdleConnections();\n });\n },\n };\n resolve(handle);\n });\n });\n}\n","/**\n * HTTP transport adapter — serves gateway unary + SSE over HTTP.\n *\n * The fourth and final gateway transport (after CLI, MCP, RPC). Unlike the\n * connection-oriented RPC/MCP adapters, the HTTP adapter is framework-agnostic\n * by design: it does NOT bind a port or own a `Request`/`Response` lifecycle.\n * Instead it exposes two pure routing primitives the embedder (a SvelteKit\n * route handler, the daemon's HTTP server, a test harness) calls directly:\n *\n * - {@link routeUnary} — one POST → `source: 'http'` {@link DispatchRequest}\n * → injected {@link GatewayHandler} → `{ status, body }` (LAFS envelope).\n * - the SSE primitives in `./sse.js` (`createSseStream`) for streaming ops.\n *\n * This keeps the runtime free of any HTTP-server / SvelteKit / `@cleocode/cleo`\n * dependency: the embedder owns wire concerns (parsing the URL into a\n * `(gateway, domain, operation)` triple, setting headers, `process.exit`,\n * error-render at the edge), exactly mirroring how the CLI adapter wraps the\n * same handler. There is NO `process.exit` and NO error-render in the handlers\n * (R3 contract); the adapter never calls `process.exit` at all.\n *\n * The request `source` is forced to `'http'` regardless of what the embedder\n * passes, so a client cannot impersonate another transport.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { DispatchRequest, DispatchResponse } from '@cleocode/contracts/gateway';\nimport type { GatewayHandler } from '../index.js';\nimport type { HttpUnaryRequest, HttpUnaryResult } from './types.js';\n\n/**\n * Map a LAFS dispatch error code to an HTTP status code.\n *\n * The canonical machine-readable error code (`E_NOT_FOUND`, `E_VALIDATION_…`,\n * `E_FORBIDDEN`, …) is the source of truth; the numeric LAFS exit code is a\n * secondary hint. Unknown error codes fall back to `500`. A successful envelope\n * is always `200`.\n *\n * @param response - The dispatch response.\n * @returns The HTTP status the embedder should set.\n */\nexport function statusForResponse(response: DispatchResponse): number {\n if (response.success) return 200;\n const code = response.error?.code ?? '';\n if (code === 'E_NOT_FOUND' || code.endsWith('_NOT_FOUND')) return 404;\n if (code === 'E_UNAUTHORIZED' || code === 'E_AUTH_REQUIRED') return 401;\n if (code === 'E_FORBIDDEN') return 403;\n if (code === 'E_RATE_LIMITED' || code === 'E_RATE_LIMIT_EXCEEDED') return 429;\n if (\n code === 'E_VALIDATION' ||\n code.startsWith('E_VALIDATION') ||\n code === 'E_INVALID_INPUT' ||\n code === 'E_BAD_REQUEST'\n ) {\n return 400;\n }\n if (code === 'E_NOT_IMPLEMENTED' || code === 'E_UNSUPPORTED') return 501;\n return 500;\n}\n\n/**\n * Route a single HTTP unary request through the gateway handler.\n *\n * Builds a `source: 'http'` {@link DispatchRequest} from the embedder-resolved\n * `(gateway, domain, operation, params)` triple and delegates to the injected\n * {@link GatewayHandler}. A thrown handler error is trapped and rendered as a\n * `500` LAFS error envelope — never propagated as an unhandled rejection or\n * `process.exit`. The `source` is always forced to `'http'`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The embedder-resolved unary request coordinates + body params.\n * @returns The HTTP status + LAFS response body for the embedder to serialize.\n */\nexport async function routeUnary(\n handler: GatewayHandler,\n req: HttpUnaryRequest,\n): Promise<HttpUnaryResult> {\n const requestId = req.requestId ?? randomUUID();\n const request: DispatchRequest = {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n params: req.params,\n // Force the transport of origin — a client cannot impersonate cli/mcp/rpc.\n source: 'http',\n requestId,\n sessionId: req.sessionId,\n };\n\n try {\n const body = await handler.handle(request);\n return { status: statusForResponse(body), body };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const body: DispatchResponse = {\n meta: {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId,\n },\n success: false,\n error: { code: 'E_HTTP_INTERNAL', message },\n };\n return { status: 500, body };\n }\n}\n","/**\n * Server-Sent Events (SSE) wire primitives for the `@cleocode/runtime/gateway/http`\n * adapter.\n *\n * The HTTP transport is the only gateway transport that streams: long-running\n * and subscription operations emit a sequence of {@link GatewayStreamEvent}\n * frames terminated by a `done` or `error` frame. This module owns ONLY the SSE\n * wire encoding + the `ReadableStream` lifecycle plumbing; the actual frame\n * production (polling a DB, tailing a job, etc.) lives in the caller's async\n * source. Keeping the encode/stream concern here lets BOTH the gateway-routed\n * streaming ops AND the existing Studio SSE endpoints (which carry their own\n * domain event shapes) share one tested, leak-free stream builder.\n *\n * Two encoders are provided so an existing stream can adopt this plumbing\n * WITHOUT changing its observed bytes:\n * - {@link encodeStreamEvent} — the canonical `data: <json>\\n\\n` form for\n * {@link GatewayStreamEvent} frames (gateway-routed streaming ops).\n * - {@link encodeSseFrame} — a generic `event:`/`id:`/`data:` writer for any\n * payload, so endpoints with a bespoke wire (a named `event:` field, a\n * `data:`-only frame) keep an IDENTICAL byte stream while still routing their\n * lifecycle through the shared, abort-safe builder.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http/sse\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport type { GatewayStreamEvent } from '@cleocode/contracts/gateway';\n\n/** SSE field/record delimiter constants (the SSE wire is line-oriented). */\nconst LF = '\\n';\n/** A blank line terminates an SSE record (`\\n\\n`). */\nconst RECORD_END = '\\n\\n';\n\n/**\n * One generic SSE frame: an optional named `event`, an optional `id`, and a\n * `data` payload that is JSON-serialized.\n *\n * This is intentionally NOT {@link GatewayStreamEvent} — it is the raw wire\n * shape so a bespoke endpoint can reproduce its exact existing bytes (e.g. a\n * named `event: task-updated` line, or a `data:`-only frame with no `event`).\n */\nexport interface SseFrame {\n /** Optional SSE `event:` field name (omitted → a `data:`-only frame). */\n event?: string;\n /** Optional SSE `id:` field for client-side last-event-id resumption. */\n id?: string;\n /** The frame payload; JSON-serialized into the `data:` field. */\n data: unknown;\n}\n\n/**\n * Encode a generic {@link SseFrame} to its SSE wire bytes.\n *\n * Field order is `event:` (if present) → `id:` (if present) → `data:`, matching\n * the SSE spec record layout, then a blank line. When `event` is omitted the\n * frame is a `data:`-only record — byte-identical to a handwritten\n * `data: <json>\\n\\n`.\n *\n * @param frame - The frame to serialize.\n * @returns The SSE record as a string (UTF-8 caller-encoded).\n */\nexport function encodeSseFrame(frame: SseFrame): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}${LF}`;\n if (frame.id !== undefined) out += `id: ${frame.id}${LF}`;\n out += `data: ${JSON.stringify(frame.data)}${RECORD_END}`;\n return out;\n}\n\n/**\n * Encode a canonical {@link GatewayStreamEvent} as a `data:`-only SSE record.\n *\n * Gateway-routed streaming ops carry the discriminator + sequence inside the\n * JSON payload (`{ kind, seq, data, error, requestId }`), so no named SSE\n * `event:` field is used — the client demultiplexes on `kind`/`seq`. The\n * monotonic `seq` is also surfaced as the SSE `id:` so a reconnecting client can\n * send `Last-Event-ID`.\n *\n * @param frame - The gateway stream event to serialize.\n * @returns The SSE record as a string.\n */\nexport function encodeStreamEvent(frame: GatewayStreamEvent): string {\n return encodeSseFrame({ id: String(frame.seq), data: frame });\n}\n\n/** The standard SSE response headers (no caching, no proxy buffering). */\nexport const SSE_HEADERS: Readonly<Record<string, string>> = Object.freeze({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n});\n\n/**\n * A push-style SSE controller handed to an {@link SseSource} so it can emit\n * frames and close the stream without touching the underlying\n * `ReadableStreamDefaultController`.\n */\nexport interface SseEmitter {\n /** Emit one generic SSE frame; a no-op once the stream is closed. */\n send(frame: SseFrame): void;\n /** Emit one canonical {@link GatewayStreamEvent}; a no-op once closed. */\n sendStreamEvent(frame: GatewayStreamEvent): void;\n /**\n * Emit a pre-encoded SSE record verbatim; a no-op once closed. For endpoints\n * that own their own wire encoder (e.g. a bespoke `data: <json>\\n\\n`) and want\n * to keep emitting byte-identical records while still routing lifecycle\n * through this builder.\n */\n sendRaw(record: string): void;\n /** Whether the stream has been closed (client disconnect or `close()`). */\n readonly closed: boolean;\n /** Close the stream; idempotent. */\n close(): void;\n}\n\n/**\n * A teardown callback returned by an {@link SseSource}, invoked exactly once when\n * the stream ends (client disconnect, `emitter.close()`, or producer completion).\n */\nexport type SseTeardown = () => void;\n\n/**\n * A frame source for {@link createSseStream}. Receives an {@link SseEmitter} and\n * MAY return an {@link SseTeardown} callback (return nothing to skip teardown).\n *\n * The source is started SYNCHRONOUSLY inside `ReadableStream.start`, mirroring\n * the existing Studio endpoints (which schedule timers inside `start`). A source\n * may push frames immediately and/or schedule async work.\n */\nexport type SseSource = (emitter: SseEmitter) => SseTeardown | undefined;\n\n/**\n * Build a `text/event-stream` {@link ReadableStream} driven by an\n * {@link SseSource}.\n *\n * This is the shared, abort-safe stream builder every SSE endpoint routes\n * through. It guarantees:\n * - frames are dropped (never throw) after the stream closes;\n * - the source's teardown callback runs exactly once;\n * - an optional {@link AbortSignal} (the request's) closes the stream on client\n * disconnect, with its listener removed on teardown.\n *\n * The byte output is fully controlled by what the source emits — so an endpoint\n * adopting this builder reproduces its prior wire exactly by emitting the same\n * frames in the same order.\n *\n * @param source - The frame producer.\n * @param signal - Optional request abort signal that closes the stream on\n * client disconnect.\n * @returns A ReadableStream of UTF-8 SSE bytes for a `Response` body.\n */\nexport function createSseStream(\n source: SseSource,\n signal?: AbortSignal,\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n\n return new ReadableStream<Uint8Array>({\n start(controller): void {\n let closed = false;\n /** Set once the source returns; invoked exactly once when the stream ends. */\n let teardown: (() => void) | undefined;\n /** Guards against running the source teardown more than once. */\n let teardownDone = false;\n /** True while `source(emitter)` is still executing synchronously. */\n let sourceRunning = false;\n\n const onAbort = (): void => emitter.close();\n\n /** Invoke the source teardown at most once (no-op until the source returns). */\n const runTeardown = (): void => {\n if (teardownDone) return;\n teardownDone = true;\n if (typeof teardown === 'function') teardown();\n };\n\n const emitter: SseEmitter = {\n get closed(): boolean {\n return closed;\n },\n send(frame: SseFrame): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeSseFrame(frame)));\n } catch {\n emitter.close();\n }\n },\n sendStreamEvent(frame: GatewayStreamEvent): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeStreamEvent(frame)));\n } catch {\n emitter.close();\n }\n },\n sendRaw(record: string): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(record));\n } catch {\n emitter.close();\n }\n },\n close(): void {\n if (closed) return;\n closed = true;\n if (signal) signal.removeEventListener('abort', onAbort);\n // If the source closed synchronously mid-body, it has not yet returned\n // its teardown — defer it to the post-return reconciliation below.\n if (!sourceRunning) runTeardown();\n try {\n controller.close();\n } catch {\n // already closed by the runtime — nothing to do.\n }\n },\n };\n\n if (signal) {\n if (signal.aborted) {\n // Client already gone before we started — close immediately. The\n // source is never invoked, so there is no teardown to run.\n closed = true;\n teardownDone = true;\n try {\n controller.close();\n } catch {\n // already closed.\n }\n return;\n }\n signal.addEventListener('abort', onAbort);\n }\n\n sourceRunning = true;\n const returned = source(emitter);\n teardown = typeof returned === 'function' ? returned : undefined;\n sourceRunning = false;\n\n // The source may have called `emitter.close()` synchronously before\n // returning its teardown; run it now that the callback is in hand.\n if (closed) runTeardown();\n },\n });\n}\n"],"mappings":"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","names":[]}