Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
5.2 kB · 118 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119/** * Dedicated Jazz storage-server process (production topology). * * Exactly one long-lived process owns the on-disk Jazz database. This script * is that process. It: * * 1. starts the local Jazz server on a stable loopback endpoint (explicit * port via JAZZ_SERVER_PORT, or 127.0.0.1 with a recorded port when * omitted), * 2. generates 256-bit backend/admin secrets (never the upstream dev * helper's 32-bit defaults) and records the endpoint + backend secret * in a 0600 discovery file next to the database so sibling processes can * attach as clients, * 3. stays alive until SIGTERM/SIGINT, then shuts down in order: remove * the discovery file first (no new attacher binds to a dying server), * then stop the server (releases the RocksDB lock; the database itself * is preserved because an explicit dataDir is always passed). * * Usage: * THOUGHTSTREAM_ROOT=/path/to/project tsx scripts/serve-jazz-storage.ts * * Optional environment: * JAZZ_SERVER_PORT explicit port (default: ephemeral, recorded in the * discovery file) * JAZZ_APP_ID app id (default thoughtstream-local) * * Secrets are never logged. The discovery file is the only credential * artifact and is 0600, owned by the running user, and removed on shutdown. */import { randomBytes } from "node:crypto";import { chmod, mkdir, rm, writeFile } from "node:fs/promises";import path from "node:path";
import { startLocalJazzServer, type LocalJazzServerHandle } from "jazz-tools/dev";
import { thoughtstreamApp } from "../src/jazz/schema.js";
const projectRoot = process.env.THOUGHTSTREAM_ROOT;if (!projectRoot) { console.error("THOUGHTSTREAM_ROOT is required (the project root whose .thoughtstream/state this server owns)"); process.exit(2);}const dataPath = path.join(projectRoot, ".thoughtstream", "state", "jazz.sqlite");const discoveryPath = `${dataPath}.server.json`;const port = process.env.JAZZ_SERVER_PORT ? Number(process.env.JAZZ_SERVER_PORT) : undefined;if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535)) { console.error("JAZZ_SERVER_PORT must be an integer in 1..65535"); process.exit(2);}
function strongSecret(prefix: string): string { return `${prefix}-${randomBytes(32).toString("hex")}`;}
function configuredSecret(name: string, prefix: string): string { const value = process.env[name]; if (value && new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(value)) return value; if (!value && process.env.VITEST === "true") return strongSecret(prefix); throw new Error(`Provision a valid ${name} before starting storage`);}
let server: LocalJazzServerHandle | undefined;let shuttingDown = false;// Set only after THIS process successfully wrote the discovery file; a// failed startup must never remove a file it does not own (e.g. when a live// sibling owner's guard or the RocksDB lock rejects this process).let ownsDiscovery = false;
async function writeDiscovery(url: string, backendSecret: string): Promise<void> { await mkdir(path.dirname(dataPath), { recursive: true }); await writeFile(discoveryPath, JSON.stringify({ url, backendSecret }), { mode: 0o600 }); await chmod(discoveryPath, 0o600); ownsDiscovery = true;}
async function shutdown(): Promise<void> { if (shuttingDown) return; shuttingDown = true; // Ordered: discovery file first so no new client attaches to a server that // is about to stop; then the server itself (idempotent stop). Only the // process that published the file may remove it. if (ownsDiscovery) { await rm(discoveryPath, { force: true }).catch(() => undefined); } await server?.stop().catch(() => undefined); process.exit(0);}
process.on("SIGTERM", () => void shutdown());process.on("SIGINT", () => void shutdown());
try { await mkdir(path.dirname(dataPath), { recursive: true }); server = await startLocalJazzServer({ appId: process.env.JAZZ_APP_ID ?? "thoughtstream-local", dataDir: dataPath, schema: thoughtstreamApp, allowLocalFirstAuth: false, port, // 256-bit secrets generated here; the upstream dev defaults (32 bits) // are never used because both are always passed explicitly. backendSecret: configuredSecret("JAZZ_BACKEND_SECRET", "thoughtstream-backend"), adminSecret: configuredSecret("JAZZ_ADMIN_SECRET", "thoughtstream-admin"), }); await writeDiscovery(server.url, server.backendSecret); // Credential-dark readiness line: endpoint and paths only, never secrets. console.log(`jazz storage server ready: url=${server.url} dataDir=${dataPath} pid=${process.pid}`);} catch (error) { console.error(`jazz storage server failed to start: ${error instanceof Error ? error.message : String(error)}`); // Do NOT touch the discovery file here: a startup failure means this // process never owned it (either a live sibling owner or the RocksDB lock // rejected us). Removing it would sever live clients from a healthy owner. await server?.stop().catch(() => undefined); process.exit(1);}
// Keep the event loop alive until a signal arrives.setInterval(() => undefined, 60_000);