@ceralive/cerastream 2026.6.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @ceralive/cerastream (TypeScript bindings)
2
+
3
+ Type-safe control-IPC **schema + client surface** for the cerastream Rust
4
+ streaming engine. This package is the **public contract** (ADR-0002 Decision 13):
5
+ a CI-published npm package — **not** a sibling `link:` dep — consumable by Bun with
6
+ zero native dependencies.
7
+
8
+ - JSON-RPC 2.0 envelope + `hello` handshake schemas
9
+ - The eight v1 control methods (params + result Zod schemas + inferred types)
10
+ - The seven server-push event payloads (discriminated union)
11
+ - Two-tier error codes (Tier 1 RPC + Tier 2 runtime, 1:1 with CeraUI)
12
+ - A unified engine config schema (= `start` params)
13
+ - A `CerastreamClient` interface + `connect()` factory (Bun-native UDS transport)
14
+
15
+ > **Schema + client.** The Zod schemas, types, and constants are the **frozen**
16
+ > wire contract; `connect()` drives that contract over a Bun-native NDJSON/UDS
17
+ > transport (no native deps). The schema shape is unchanged from the v0.1.0-stub
18
+ > freeze — this release adds the runtime client behind it (plan Task 31).
19
+
20
+ See [`API.md`](./API.md) for the full exported surface, ADR-0002
21
+ ([`../../docs/adr/ADR-0002-control-ipc.md`](../../docs/adr/ADR-0002-control-ipc.md))
22
+ for the protocol decision, and `schema.md`
23
+ ([`../../docs/adr/schema.md`](../../docs/adr/schema.md)) for the v1 message
24
+ inventory.
25
+
26
+ ## Install / develop
27
+
28
+ ```bash
29
+ bun install
30
+ bun run typecheck # tsc --noEmit
31
+ bun test # schema round-trip + stub-honesty + count + skew guards
32
+ bun run build # emit dist/
33
+ ```
34
+
35
+ ## Usage (schema today, client after Task 31)
36
+
37
+ The schemas are usable now — validate any control message on either side of the
38
+ wire:
39
+
40
+ ```ts
41
+ import { startParamsSchema, eventParamsSchema } from "@ceralive/cerastream";
42
+
43
+ const params = startParamsSchema.parse({
44
+ pipeline: "h264_camlink_1080p",
45
+ srt: { host: "relay.example.com", port: 8890, latency_ms: 2000 },
46
+ bitrate: { min_bitrate: 500, max_bitrate: 6000 }, // balancer defaults to "adaptive"
47
+ });
48
+
49
+ const event = eventParamsSchema.parse(incomingNotification.params); // typed by `type`
50
+ ```
51
+
52
+ Drive the engine through the control plane:
53
+
54
+ ```ts
55
+ import { connect } from "@ceralive/cerastream";
56
+
57
+ const client = await connect({ autoReconnect: true }); // hello handshake runs here
58
+ console.log(client.hello.engine_version);
59
+
60
+ const sub = await client.subscribeEvents({ topics: ["status", "bitrate"] }, (ev) => {
61
+ if (ev.type === "bitrate") console.log("bitrate", ev.current_bitrate);
62
+ });
63
+
64
+ await client.start({
65
+ pipeline: "h264_camlink_1080p",
66
+ srt: { host: "relay.example.com", port: 8890, latency_ms: 2000 },
67
+ bitrate: { min_bitrate: 500, max_bitrate: 6000 },
68
+ });
69
+ await client.setBitrate({ max_bitrate: 4500 });
70
+ await client.switchInput({ input_id: "video0" });
71
+ await client.stop();
72
+
73
+ sub.close();
74
+ await client.close(); // closes the connection; never respawns the engine (ADR-0005)
75
+ ```
76
+
77
+ `connect()` honors `socketPath` (default: the resolved `/run/cerastream/control.sock`),
78
+ `requestTimeoutMs`, and `autoReconnect` (capped backoff + re-handshake + re-subscribe).
79
+ The engine is a systemd-owned service — the client connects, it never spawns it.
80
+
81
+ ## Versioning — SemVer on top of CalVer
82
+
83
+ Two version axes, kept separate: the **wire-schema version** (`PROTOCOL_VERSION` +
84
+ `SCHEMA_VERSION`) is pinned cross-language with the Rust engine and is additive-only
85
+ within `cerastream-ipc/1`; the **npm package version** (`package.json`) is CalVer
86
+ (`YYYY.MINOR.PATCH`) for the library release line, with SemVer intent applied to the
87
+ exported TypeScript surface. Release candidates use `-rc.N` (npm `next` dist-tag).
88
+ Full policy + the tag-driven publish pipeline: [`CHANGELOG.md`](./CHANGELOG.md).
@@ -0,0 +1,58 @@
1
+ import { type HelloResult } from "./envelope.js";
2
+ import { type EventParams } from "./events.js";
3
+ import { type ListDevicesParams, type ListDevicesResult, type PreviewSessionParams, type PreviewSessionResult, type ReloadConfigParams, type ReloadConfigResult, type SetBitrateParams, type SetBitrateResult, type StartParams, type StartResult, type StopParams, type StopResult, type SubscribeEventsParams, type SubscribeEventsResult, type SwitchInputParams, type SwitchInputResult } from "./messages.js";
4
+ /** Options for {@link connect}. All optional — `connect({})` is valid. */
5
+ export interface ConnectOptions {
6
+ /** Control socket path override. Defaults to the resolved /run/cerastream/control.sock. */
7
+ socketPath?: string;
8
+ /** Client name/version reported in the mandatory `hello` handshake. */
9
+ client?: string;
10
+ /** Per-request timeout (ms). Defaults to 10000. */
11
+ requestTimeoutMs?: number;
12
+ /** Auto-reconnect with capped backoff + re-subscribe on a dropped socket (ADR-0002 §6). */
13
+ autoReconnect?: boolean;
14
+ /** Initial reconnect backoff (ms). Defaults to 200. */
15
+ reconnectInitialDelayMs?: number;
16
+ /** Maximum reconnect backoff (ms). Defaults to 5000. */
17
+ reconnectMaxDelayMs?: number;
18
+ }
19
+ /** Handler invoked for each server-pushed `event` after `subscribeEvents`. */
20
+ export type EventHandler = (event: EventParams) => void;
21
+ /** Subscription handle returned by {@link CerastreamClient.subscribeEvents}. */
22
+ export interface Subscription {
23
+ /** The topics actually subscribed to. */
24
+ readonly result: SubscribeEventsResult;
25
+ /** Stop receiving events for this subscription. Idempotent. */
26
+ close(): void;
27
+ }
28
+ /**
29
+ * Bidirectional JSON-RPC 2.0 control client for cerastream. One method per v1
30
+ * request plus the handshake. Server-push events arrive via `subscribeEvents`.
31
+ */
32
+ export interface CerastreamClient {
33
+ /** Negotiated handshake result (protocol + schema/engine versions). */
34
+ readonly hello: HelloResult;
35
+ /** Start the streaming pipeline (encode → bond → SRT). */
36
+ start(params: StartParams): Promise<StartResult>;
37
+ /** Stop the streaming pipeline. Idempotent. Stops the pipeline, NOT the OS process. */
38
+ stop(params?: StopParams): Promise<StopResult>;
39
+ /** Hot-reload engine config (replaces SIGHUP). Returns the applied config. */
40
+ reloadConfig(params: ReloadConfigParams): Promise<ReloadConfigResult>;
41
+ /** Hot-adjust max bitrate while streaming. Returns the post-clamp applied value. */
42
+ setBitrate(params: SetBitrateParams): Promise<SetBitrateResult>;
43
+ /** Switch the active capture source (manual) or hand selection to failover (auto). */
44
+ switchInput(params: SwitchInputParams): Promise<SwitchInputResult>;
45
+ /** Enumerate capture devices (GstDeviceMonitor, de-duped by device path). */
46
+ listDevices(params?: ListDevicesParams): Promise<ListDevicesResult>;
47
+ /** Subscribe to the live event stream; `handler` fires for each pushed event. */
48
+ subscribeEvents(params: SubscribeEventsParams, handler: EventHandler): Promise<Subscription>;
49
+ /** Control a local preview session (WebCodecs binary tier / WebRTC signaling). */
50
+ previewSession(params: PreviewSessionParams): Promise<PreviewSessionResult>;
51
+ /** Close the control connection. Never respawns the engine (ADR-0005). */
52
+ close(): Promise<void>;
53
+ }
54
+ /**
55
+ * Connect to the cerastream control socket, run the mandatory `hello`
56
+ * handshake, and return a {@link CerastreamClient}.
57
+ */
58
+ export declare function connect(options?: ConnectOptions): Promise<CerastreamClient>;
package/dist/client.js ADDED
@@ -0,0 +1,274 @@
1
+ import { CONTROL_SOCKET_PATH, PROTOCOL_VERSION } from "./constants.js";
2
+ import { helloResultSchema, rpcErrorSchema, rpcResponseSchema, } from "./envelope.js";
3
+ import { CerastreamConnectionError, CerastreamRpcError, CerastreamTimeoutError, } from "./errors.js";
4
+ import { eventParamsSchema } from "./events.js";
5
+ import { controlSocketPath } from "./paths.js";
6
+ import { requestSchemas, } from "./messages.js";
7
+ import { LineSocket } from "./transport.js";
8
+ const DEFAULTS = {
9
+ requestTimeoutMs: 10_000,
10
+ reconnectInitialDelayMs: 200,
11
+ reconnectMaxDelayMs: 5_000,
12
+ };
13
+ class ClientImpl {
14
+ hello;
15
+ socket;
16
+ nextId = 1;
17
+ pending = new Map();
18
+ subscriptions = new Set();
19
+ intentionalClose = false;
20
+ reconnecting = false;
21
+ socketPath;
22
+ clientName;
23
+ requestTimeoutMs;
24
+ autoReconnect;
25
+ reconnectInitialDelayMs;
26
+ reconnectMaxDelayMs;
27
+ constructor(options) {
28
+ this.socketPath =
29
+ options.socketPath ?? safeControlSocketPath();
30
+ this.clientName = options.client ?? "@ceralive/cerastream";
31
+ this.requestTimeoutMs =
32
+ options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs;
33
+ this.autoReconnect = options.autoReconnect ?? false;
34
+ this.reconnectInitialDelayMs =
35
+ options.reconnectInitialDelayMs ?? DEFAULTS.reconnectInitialDelayMs;
36
+ this.reconnectMaxDelayMs =
37
+ options.reconnectMaxDelayMs ?? DEFAULTS.reconnectMaxDelayMs;
38
+ }
39
+ async init() {
40
+ await this.openSocket();
41
+ this.hello = await this.handshake();
42
+ }
43
+ async openSocket() {
44
+ const socket = new LineSocket(this.socketPath);
45
+ try {
46
+ await socket.open({
47
+ onLine: (line) => this.onLine(line),
48
+ onClose: (err) => this.onClose(err),
49
+ });
50
+ }
51
+ catch (err) {
52
+ throw new CerastreamConnectionError(`failed to connect to cerastream control socket at ${this.socketPath}`, err);
53
+ }
54
+ this.socket = socket;
55
+ }
56
+ async handshake() {
57
+ const raw = await this.rawRequest("hello", {
58
+ protocol: PROTOCOL_VERSION,
59
+ client: this.clientName,
60
+ });
61
+ return helloResultSchema.parse(raw);
62
+ }
63
+ start(params) {
64
+ return this.call("start", params);
65
+ }
66
+ stop(params) {
67
+ return this.call("stop", params);
68
+ }
69
+ reloadConfig(params) {
70
+ return this.call("reload-config", params);
71
+ }
72
+ setBitrate(params) {
73
+ return this.call("set-bitrate", params);
74
+ }
75
+ switchInput(params) {
76
+ return this.call("switch-input", params);
77
+ }
78
+ listDevices(params) {
79
+ return this.call("list-devices", params);
80
+ }
81
+ previewSession(params) {
82
+ return this.call("preview-session", params);
83
+ }
84
+ async subscribeEvents(params, handler) {
85
+ const result = await this.call("subscribe-events", params);
86
+ const sub = {
87
+ result,
88
+ handler,
89
+ topics: result.subscribed,
90
+ close: () => {
91
+ this.subscriptions.delete(sub);
92
+ },
93
+ };
94
+ this.subscriptions.add(sub);
95
+ return sub;
96
+ }
97
+ async close() {
98
+ this.intentionalClose = true;
99
+ for (const sub of this.subscriptions)
100
+ sub.close();
101
+ const conn = new CerastreamConnectionError("client closed");
102
+ this.rejectAllPending(conn);
103
+ this.socket?.close();
104
+ this.socket = undefined;
105
+ }
106
+ // Typed request: validate params, send, validate the result against the
107
+ // frozen Zod contract (TS → engine and engine → TS over the same schema).
108
+ async call(method, params) {
109
+ const pair = requestSchemas[method];
110
+ const parsedParams = pair.params.parse(params);
111
+ const raw = await this.rawRequest(method, parsedParams);
112
+ return pair.result.parse(raw);
113
+ }
114
+ rawRequest(method, params) {
115
+ const socket = this.socket;
116
+ if (!socket) {
117
+ return Promise.reject(new CerastreamConnectionError("control connection is not open"));
118
+ }
119
+ const id = this.nextId++;
120
+ const envelope = {
121
+ jsonrpc: "2.0",
122
+ id,
123
+ method,
124
+ };
125
+ if (params !== undefined)
126
+ envelope.params = params;
127
+ return new Promise((resolve, reject) => {
128
+ const timer = setTimeout(() => {
129
+ this.pending.delete(id);
130
+ reject(new CerastreamTimeoutError(method, this.requestTimeoutMs));
131
+ }, this.requestTimeoutMs);
132
+ this.pending.set(id, { method, resolve, reject, timer });
133
+ try {
134
+ socket.send(JSON.stringify(envelope));
135
+ }
136
+ catch (err) {
137
+ clearTimeout(timer);
138
+ this.pending.delete(id);
139
+ reject(new CerastreamConnectionError("failed to write request", err));
140
+ }
141
+ });
142
+ }
143
+ onLine(line) {
144
+ let msg;
145
+ try {
146
+ msg = JSON.parse(line);
147
+ }
148
+ catch {
149
+ return; // a non-JSON line is not addressable to any request; drop it.
150
+ }
151
+ if (!isObject(msg))
152
+ return;
153
+ if (msg.method === "event") {
154
+ this.dispatchEvent(msg.params);
155
+ return;
156
+ }
157
+ if (!("id" in msg))
158
+ return;
159
+ const id = msg.id;
160
+ if ("error" in msg) {
161
+ this.settleError(msg);
162
+ return;
163
+ }
164
+ if ("result" in msg && id != null) {
165
+ const pending = this.pending.get(id);
166
+ if (!pending)
167
+ return;
168
+ this.pending.delete(id);
169
+ clearTimeout(pending.timer);
170
+ const parsed = rpcResponseSchema.safeParse(msg);
171
+ if (parsed.success)
172
+ pending.resolve(parsed.data.result);
173
+ else
174
+ pending.resolve(msg.result);
175
+ }
176
+ }
177
+ settleError(msg) {
178
+ const parsed = rpcErrorSchema.safeParse(msg);
179
+ if (!parsed.success)
180
+ return;
181
+ const { id, error } = parsed.data;
182
+ const rpcErr = new CerastreamRpcError(error.code, error.message, error.data?.code, id);
183
+ if (id == null)
184
+ return; // parse error with null id — no request to settle.
185
+ const pending = this.pending.get(id);
186
+ if (!pending)
187
+ return;
188
+ this.pending.delete(id);
189
+ clearTimeout(pending.timer);
190
+ pending.reject(rpcErr);
191
+ }
192
+ dispatchEvent(params) {
193
+ const parsed = eventParamsSchema.safeParse(params);
194
+ if (!parsed.success)
195
+ return;
196
+ const event = parsed.data;
197
+ for (const sub of this.subscriptions) {
198
+ if (sub.topics.includes(event.type))
199
+ sub.handler(event);
200
+ }
201
+ }
202
+ onClose(err) {
203
+ this.socket = undefined;
204
+ if (this.intentionalClose)
205
+ return;
206
+ const conn = new CerastreamConnectionError("control connection lost", err);
207
+ this.rejectAllPending(conn);
208
+ if (this.autoReconnect)
209
+ void this.reconnectLoop();
210
+ }
211
+ rejectAllPending(err) {
212
+ for (const [, pending] of this.pending) {
213
+ clearTimeout(pending.timer);
214
+ pending.reject(err);
215
+ }
216
+ this.pending.clear();
217
+ }
218
+ async reconnectLoop() {
219
+ if (this.reconnecting)
220
+ return;
221
+ this.reconnecting = true;
222
+ let delay = this.reconnectInitialDelayMs;
223
+ while (!this.intentionalClose) {
224
+ await sleep(delay);
225
+ if (this.intentionalClose)
226
+ break;
227
+ try {
228
+ await this.openSocket();
229
+ this.hello = await this.handshake();
230
+ await this.resubscribe();
231
+ this.reconnecting = false;
232
+ return;
233
+ }
234
+ catch {
235
+ this.socket?.close();
236
+ this.socket = undefined;
237
+ delay = Math.min(delay * 2, this.reconnectMaxDelayMs);
238
+ }
239
+ }
240
+ this.reconnecting = false;
241
+ }
242
+ async resubscribe() {
243
+ const topics = new Set();
244
+ for (const sub of this.subscriptions)
245
+ for (const t of sub.topics)
246
+ topics.add(t);
247
+ if (topics.size === 0)
248
+ return;
249
+ await this.rawRequest("subscribe-events", { topics: [...topics] });
250
+ }
251
+ }
252
+ function safeControlSocketPath() {
253
+ try {
254
+ return controlSocketPath();
255
+ }
256
+ catch {
257
+ return CONTROL_SOCKET_PATH;
258
+ }
259
+ }
260
+ function isObject(value) {
261
+ return typeof value === "object" && value !== null;
262
+ }
263
+ function sleep(ms) {
264
+ return new Promise((resolve) => setTimeout(resolve, ms));
265
+ }
266
+ /**
267
+ * Connect to the cerastream control socket, run the mandatory `hello`
268
+ * handshake, and return a {@link CerastreamClient}.
269
+ */
270
+ export async function connect(options) {
271
+ const client = new ClientImpl(options ?? {});
272
+ await client.init();
273
+ return client;
274
+ }
@@ -0,0 +1,16 @@
1
+ import { type CerastreamConfig, type PartialCerastreamConfig } from "./types.js";
2
+ /** Default on-device config path (systemd `StateDirectory=cerastream`). */
3
+ export declare const DEFAULT_CONFIG_PATH = "/var/lib/cerastream/config.json";
4
+ /** Validate and pretty-print a config to its canonical JSON form. */
5
+ export declare function serializeCerastreamConfig(config: PartialCerastreamConfig): string;
6
+ /** Validate `config` and write it to disk as canonical JSON. Returns the path. */
7
+ export declare function writeCerastreamConfig(config: PartialCerastreamConfig, path?: string): string;
8
+ /** Read + validate a config from disk. Throws on a missing file or bad shape. */
9
+ export declare function readCerastreamConfig(path?: string): CerastreamConfig;
10
+ /**
11
+ * Coerce a loose/legacy config object into a valid {@link CerastreamConfig},
12
+ * filling defaults for any omitted bitrate/SRT/balancer field before validation.
13
+ * Use it to migrate a stored profile to the current schema; it throws if the
14
+ * required `pipeline` + `srt.host`/`srt.port` cannot be recovered.
15
+ */
16
+ export declare function migrateCerastreamConfig(input: unknown): CerastreamConfig;
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { DEFAULT_BALANCER, DEFAULT_MAX_BITRATE, DEFAULT_MIN_BITRATE, DEFAULT_SRT_LATENCY, } from "./constants.js";
3
+ import { cerastreamConfigSchema, } from "./types.js";
4
+ // Persisted-config helpers. The unified engine config IS the `start` params
5
+ // (types.cerastreamConfigSchema), so a config written here is exactly what
6
+ // `client.start()` accepts — wire shape and stored profile cannot drift.
7
+ /** Default on-device config path (systemd `StateDirectory=cerastream`). */
8
+ export const DEFAULT_CONFIG_PATH = "/var/lib/cerastream/config.json";
9
+ /** Validate and pretty-print a config to its canonical JSON form. */
10
+ export function serializeCerastreamConfig(config) {
11
+ const parsed = cerastreamConfigSchema.parse(config);
12
+ return `${JSON.stringify(parsed, null, 2)}\n`;
13
+ }
14
+ /** Validate `config` and write it to disk as canonical JSON. Returns the path. */
15
+ export function writeCerastreamConfig(config, path = DEFAULT_CONFIG_PATH) {
16
+ writeFileSync(path, serializeCerastreamConfig(config));
17
+ return path;
18
+ }
19
+ /** Read + validate a config from disk. Throws on a missing file or bad shape. */
20
+ export function readCerastreamConfig(path = DEFAULT_CONFIG_PATH) {
21
+ return cerastreamConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
22
+ }
23
+ /**
24
+ * Coerce a loose/legacy config object into a valid {@link CerastreamConfig},
25
+ * filling defaults for any omitted bitrate/SRT/balancer field before validation.
26
+ * Use it to migrate a stored profile to the current schema; it throws if the
27
+ * required `pipeline` + `srt.host`/`srt.port` cannot be recovered.
28
+ */
29
+ export function migrateCerastreamConfig(input) {
30
+ if (typeof input !== "object" || input === null) {
31
+ throw new TypeError("migrateCerastreamConfig: expected a config object");
32
+ }
33
+ const raw = input;
34
+ const srt = asRecord(raw.srt);
35
+ const bitrate = asRecord(raw.bitrate);
36
+ const candidate = {
37
+ ...raw,
38
+ srt: {
39
+ ...srt,
40
+ latency_ms: srt.latency_ms ?? DEFAULT_SRT_LATENCY,
41
+ },
42
+ bitrate: {
43
+ min_bitrate: bitrate.min_bitrate ?? DEFAULT_MIN_BITRATE,
44
+ max_bitrate: bitrate.max_bitrate ?? DEFAULT_MAX_BITRATE,
45
+ balancer: bitrate.balancer ?? DEFAULT_BALANCER,
46
+ },
47
+ };
48
+ return cerastreamConfigSchema.parse(candidate);
49
+ }
50
+ function asRecord(value) {
51
+ return typeof value === "object" && value !== null
52
+ ? value
53
+ : {};
54
+ }
@@ -0,0 +1,31 @@
1
+ /** JSON-RPC handshake protocol major. Bumped only on a breaking schema change. */
2
+ export declare const PROTOCOL_VERSION: "cerastream-ipc/1";
3
+ /**
4
+ * Wire-schema contract version, pinned cross-language with the Rust engine
5
+ * (`crates/cerastream-ipc/src/constants.rs`) and the conformance fixtures. It
6
+ * versions the JSON message shapes — NOT the npm release line, which is the CalVer
7
+ * in `package.json` (see CHANGELOG "Versioning"). The schema shape is frozen and
8
+ * additive-only within protocol major `cerastream-ipc/1` (ADR-0002 §4); this value
9
+ * only moves when the wire schema itself does, in lockstep across both languages.
10
+ */
11
+ export declare const SCHEMA_VERSION: "0.1.0-stub";
12
+ /** Runtime dir holding both control + preview sockets. systemd `RuntimeDirectory=cerastream`. */
13
+ export declare const DEFAULT_IPC_DIR: "/run/cerastream";
14
+ /** Env override for the IPC dir (tests/dev). Defaults to {@link DEFAULT_IPC_DIR}. */
15
+ export declare const IPC_DIR_ENV: "CERASTREAM_IPC_DIR";
16
+ /** Control-plane socket basename (JSON-RPC 2.0 / NDJSON). */
17
+ export declare const CONTROL_SOCKET_NAME: "control.sock";
18
+ /** Preview socket basename (length-prefixed binary frames). */
19
+ export declare const PREVIEW_SOCKET_NAME: "preview.sock";
20
+ /** Default control-plane socket path. */
21
+ export declare const CONTROL_SOCKET_PATH: "/run/cerastream/control.sock";
22
+ /** Default binary preview socket path. */
23
+ export declare const PREVIEW_SOCKET_PATH: "/run/cerastream/preview.sock";
24
+ /** Per-line framing cap (ADR-0002 §1): an oversized line is a fatal protocol error. */
25
+ export declare const MAX_LINE_BYTES: number;
26
+ /** Engine binary name (systemd-owned; CeraUI never spawns it — ADR-0005). */
27
+ export declare const CERASTREAM_BIN: "cerastream";
28
+ export declare const DEFAULT_MIN_BITRATE = 300;
29
+ export declare const DEFAULT_MAX_BITRATE = 6000;
30
+ export declare const DEFAULT_SRT_LATENCY = 2000;
31
+ export declare const DEFAULT_BALANCER: "adaptive";
@@ -0,0 +1,35 @@
1
+ // Protocol, socket, and version constants for the cerastream control IPC.
2
+ // These are CONTRACT values (ADR-0002 / schema.md), not implementation logic —
3
+ // they are the stable wire/runtime literals every consumer derives from.
4
+ /** JSON-RPC handshake protocol major. Bumped only on a breaking schema change. */
5
+ export const PROTOCOL_VERSION = "cerastream-ipc/1";
6
+ /**
7
+ * Wire-schema contract version, pinned cross-language with the Rust engine
8
+ * (`crates/cerastream-ipc/src/constants.rs`) and the conformance fixtures. It
9
+ * versions the JSON message shapes — NOT the npm release line, which is the CalVer
10
+ * in `package.json` (see CHANGELOG "Versioning"). The schema shape is frozen and
11
+ * additive-only within protocol major `cerastream-ipc/1` (ADR-0002 §4); this value
12
+ * only moves when the wire schema itself does, in lockstep across both languages.
13
+ */
14
+ export const SCHEMA_VERSION = "0.1.0-stub";
15
+ /** Runtime dir holding both control + preview sockets. systemd `RuntimeDirectory=cerastream`. */
16
+ export const DEFAULT_IPC_DIR = "/run/cerastream";
17
+ /** Env override for the IPC dir (tests/dev). Defaults to {@link DEFAULT_IPC_DIR}. */
18
+ export const IPC_DIR_ENV = "CERASTREAM_IPC_DIR";
19
+ /** Control-plane socket basename (JSON-RPC 2.0 / NDJSON). */
20
+ export const CONTROL_SOCKET_NAME = "control.sock";
21
+ /** Preview socket basename (length-prefixed binary frames). */
22
+ export const PREVIEW_SOCKET_NAME = "preview.sock";
23
+ /** Default control-plane socket path. */
24
+ export const CONTROL_SOCKET_PATH = "/run/cerastream/control.sock";
25
+ /** Default binary preview socket path. */
26
+ export const PREVIEW_SOCKET_PATH = "/run/cerastream/preview.sock";
27
+ /** Per-line framing cap (ADR-0002 §1): an oversized line is a fatal protocol error. */
28
+ export const MAX_LINE_BYTES = 1024 * 1024; // 1 MiB
29
+ /** Engine binary name (systemd-owned; CeraUI never spawns it — ADR-0005). */
30
+ export const CERASTREAM_BIN = "cerastream";
31
+ // ---- config defaults (mirror ceracoder, the engine being replaced) ----
32
+ export const DEFAULT_MIN_BITRATE = 300; // kbps
33
+ export const DEFAULT_MAX_BITRATE = 6000; // kbps
34
+ export const DEFAULT_SRT_LATENCY = 2000; // ms
35
+ export const DEFAULT_BALANCER = "adaptive";
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ export declare const jsonrpcVersion: z.ZodLiteral<"2.0">;
3
+ export declare const requestId: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
4
+ export type RequestId = z.infer<typeof requestId>;
5
+ export declare const rpcRequestSchema: z.ZodObject<{
6
+ jsonrpc: z.ZodLiteral<"2.0">;
7
+ id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
8
+ method: z.ZodString;
9
+ params: z.ZodOptional<z.ZodUnknown>;
10
+ }, z.core.$strip>;
11
+ export type RpcRequest = z.infer<typeof rpcRequestSchema>;
12
+ export declare const rpcResponseSchema: z.ZodObject<{
13
+ jsonrpc: z.ZodLiteral<"2.0">;
14
+ id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
15
+ result: z.ZodUnknown;
16
+ }, z.core.$strip>;
17
+ export type RpcResponse = z.infer<typeof rpcResponseSchema>;
18
+ export declare const rpcErrorSchema: z.ZodObject<{
19
+ jsonrpc: z.ZodLiteral<"2.0">;
20
+ id: z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
21
+ error: z.ZodObject<{
22
+ code: z.ZodNumber;
23
+ message: z.ZodString;
24
+ data: z.ZodOptional<z.ZodObject<{
25
+ code: z.ZodString;
26
+ }, z.core.$loose>>;
27
+ }, z.core.$strip>;
28
+ }, z.core.$strip>;
29
+ export type RpcError = z.infer<typeof rpcErrorSchema>;
30
+ export declare const rpcEventSchema: z.ZodObject<{
31
+ jsonrpc: z.ZodLiteral<"2.0">;
32
+ method: z.ZodLiteral<"event">;
33
+ params: z.ZodObject<{
34
+ type: z.ZodString;
35
+ seq: z.ZodNumber;
36
+ }, z.core.$loose>;
37
+ }, z.core.$strip>;
38
+ export type RpcEvent = z.infer<typeof rpcEventSchema>;
39
+ export declare const helloParamsSchema: z.ZodObject<{
40
+ protocol: z.ZodLiteral<"cerastream-ipc/1">;
41
+ client: z.ZodString;
42
+ }, z.core.$strip>;
43
+ export type HelloParams = z.infer<typeof helloParamsSchema>;
44
+ export declare const helloResultSchema: z.ZodObject<{
45
+ protocol: z.ZodLiteral<"cerastream-ipc/1">;
46
+ schema_version: z.ZodString;
47
+ engine_version: z.ZodString;
48
+ }, z.core.$strip>;
49
+ export type HelloResult = z.infer<typeof helloResultSchema>;
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { PROTOCOL_VERSION } from "./constants.js";
3
+ // JSON-RPC 2.0 envelope, newline-delimited over the control UDS (ADR-0002 §2,
4
+ // schema.md "Envelope"). The JSON shape on the wire is the contract — not any
5
+ // Rust serde type.
6
+ export const jsonrpcVersion = z.literal("2.0");
7
+ export const requestId = z.union([z.number().int(), z.string()]);
8
+ // ---- request: client → server ----
9
+ export const rpcRequestSchema = z.object({
10
+ jsonrpc: jsonrpcVersion,
11
+ id: requestId,
12
+ method: z.string(),
13
+ params: z.unknown().optional(),
14
+ });
15
+ // ---- success response: server → client ----
16
+ export const rpcResponseSchema = z.object({
17
+ jsonrpc: jsonrpcVersion,
18
+ id: requestId,
19
+ result: z.unknown(),
20
+ });
21
+ // ---- error response: server → client ----
22
+ export const rpcErrorSchema = z.object({
23
+ jsonrpc: jsonrpcVersion,
24
+ id: requestId.nullable(),
25
+ error: z.object({
26
+ code: z.number().int(), // JSON-RPC numeric (see errors.ts RPC_ERROR_NUMERIC)
27
+ message: z.string(),
28
+ // stable string code under data.code (errors.ts rpcErrorCodeSchema)
29
+ data: z.object({ code: z.string() }).passthrough().optional(),
30
+ }),
31
+ });
32
+ // ---- event: server → client, no id ----
33
+ export const rpcEventSchema = z.object({
34
+ jsonrpc: jsonrpcVersion,
35
+ method: z.literal("event"),
36
+ params: z
37
+ .object({
38
+ type: z.string(), // event type (see events.ts)
39
+ seq: z.number().int().nonnegative(), // per-type monotonic counter
40
+ })
41
+ .passthrough(),
42
+ });
43
+ // ---- handshake: hello (mandatory first call, ADR-0002 §4) ----
44
+ export const helloParamsSchema = z.object({
45
+ protocol: z.literal(PROTOCOL_VERSION),
46
+ client: z.string(), // e.g. "ceraui-backend/2026.6.0"
47
+ });
48
+ export const helloResultSchema = z.object({
49
+ protocol: z.literal(PROTOCOL_VERSION),
50
+ schema_version: z.string(), // @ceralive/cerastream semver
51
+ engine_version: z.string(), // cerastream CalVer build
52
+ });