export interface LocalServiceRecord { id: string; createdAt: string; [key: string]: unknown; } export interface LocalServicesState { database: Map; cache: Map; mail: LocalServiceRecord[]; sms: LocalServiceRecord[]; webhooks: LocalServiceRecord[]; storage: Map; queue: LocalServiceRecord[]; cron: LocalServiceRecord[]; auth: Map; metrics: LocalServiceRecord[]; } export function createLocalServicesState(): LocalServicesState { return { database: new Map(), cache: new Map(), mail: [], sms: [], webhooks: [], storage: new Map(), queue: [], cron: [], auth: new Map(), metrics: [], }; } const json = (value: unknown, status = 200, origin = "https://localhost:3000") => Response.json(value, { status, headers: { "cache-control": "no-store", "access-control-allow-origin": origin, "x-content-type-options": "nosniff", }, }); async function boundedJson( request: Request, maximum = 256 * 1024, ): Promise> { const text = await request.text(); if (new TextEncoder().encode(text).byteLength > maximum) throw new RangeError("payload-too-large"); const value = JSON.parse(text) as unknown; if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("object-required"); return value as Record; } function record(value: Record): LocalServiceRecord { return { id: crypto.randomUUID(), createdAt: new Date().toISOString(), ...value }; } export function createLocalServicesHandler( state = createLocalServicesState(), options: { origin?: string } = {}, ) { const origin = options.origin ?? "https://localhost:3000"; return async (request: Request): Promise => { const url = new URL(request.url); const path = url.pathname; if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: { "access-control-allow-origin": origin, "access-control-allow-methods": "GET,POST,PUT,DELETE", "access-control-allow-headers": "content-type", }, }); if (path === "/healthz" || path === "/readyz") return json({ status: "up", service: "wrnexus-local-services" }, 200, origin); if (path === "/" || path === "/__services") return new Response( `WRNexus Local Services

WRNexus Local Services

Use the JSON endpoints to inspect or inject local development events.

`, { headers: { "content-type": "text/html; charset=utf-8", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'", }, }, ); try { if (path === "/database") { if (request.method === "GET") return json(Object.fromEntries(state.database), 200, origin); const body = await boundedJson(request); if (typeof body.key !== "string") return json({ error: "key is required" }, 400); state.database.set(body.key, body.value); return json({ key: body.key, value: body.value }, 201, origin); } if (path === "/cache") { if (request.method === "GET") { const now = Date.now(); for (const [key, value] of state.cache) if (value.expiresAt && value.expiresAt <= now) state.cache.delete(key); return json(Object.fromEntries(state.cache), 200, origin); } const body = await boundedJson(request); if (typeof body.key !== "string") return json({ error: "key is required" }, 400); const ttlMs = typeof body.ttlMs === "number" ? Math.max(0, body.ttlMs) : undefined; state.cache.set(body.key, { value: body.value, ...(ttlMs ? { expiresAt: Date.now() + ttlMs } : {}), }); return json({ stored: true }, 201, origin); } for (const [name, values] of [ ["mail", state.mail], ["sms", state.sms], ["webhooks", state.webhooks], ["queue", state.queue], ["cron", state.cron], ["metrics", state.metrics], ] as const) if (path === `/${name}`) { if (request.method === "GET") return json(values, 200, origin); const value = record(await boundedJson(request)); values.unshift(value); if (values.length > 500) values.length = 500; return json(value, 202, origin); } if (path === "/storage") { if (request.method === "GET") return json( [...state.storage.entries()].map(([key, value]) => ({ key, bytes: value.byteLength })), 200, origin, ); const key = url.searchParams.get("key"); if (!key || key.includes("..") || key.length > 256) return json({ error: "safe key is required" }, 400); const bytes = new Uint8Array(await request.arrayBuffer()); if (bytes.byteLength > 10 * 1024 * 1024) return json({ error: "object too large" }, 413); state.storage.set(key, bytes); return json({ key, bytes: bytes.byteLength }, 201, origin); } if (path.startsWith("/storage/") && request.method === "GET") { const key = decodeURIComponent(path.slice(9)); const value = state.storage.get(key); return value ? new Response(Uint8Array.from(value), { headers: { "content-type": "application/octet-stream", "content-disposition": "attachment", "x-content-type-options": "nosniff", }, }) : json({ error: "not found" }, 404); } if (path === "/auth") { if (request.method === "GET") return json([...state.auth.values()], 200, origin); const value = record(await boundedJson(request)); if (typeof value.email !== "string") return json({ error: "email is required" }, 400); state.auth.set(value.id, value); return json({ user: value, accessToken: `local_${value.id}` }, 201, origin); } return json({ error: "not found" }, 404); } catch (error) { if (error instanceof RangeError) return json({ error: error.message }, 413); return json({ error: error instanceof Error ? error.message : "invalid request" }, 400); } }; } export interface LocalCertificate { cert: string; key: string; certFile: string; keyFile: string; reused: boolean; } /** Generate and cache a localhost-only development certificate without requiring OpenSSL. */ export async function ensureLocalCertificate(appRoot: string): Promise { const directory = join(resolve(appRoot), ".wrnexus", "certificates"); const certFile = join(directory, "localhost.pem"); const keyFile = join(directory, "localhost-key.pem"); if (existsSync(certFile) && existsSync(keyFile)) { try { const cert = readFileSync(certFile, "utf8"); const key = readFileSync(keyFile, "utf8"); const certificate = new X509Certificate(cert); if (Date.parse(certificate.validTo) > Date.now() + 7 * 24 * 60 * 60 * 1000) { return { cert, key, certFile, keyFile, reused: true }; } } catch { // Replace invalid or expired development material below. } } const now = new Date(); const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); const generated = await generate([{ name: "commonName", value: "localhost" }], { algorithm: "sha256", keyType: "ec", curve: "P-256", notBeforeDate: new Date(now.getTime() - 60_000), notAfterDate: expires, extensions: [ { name: "basicConstraints", cA: false, critical: true }, { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true }, { name: "extKeyUsage", serverAuth: true }, { name: "subjectAltName", altNames: [ { type: 2, value: "localhost" }, { type: 2, value: "*.localhost" }, { type: 7, ip: "127.0.0.1" }, { type: 7, ip: "::1" }, ], }, ], }); mkdirSync(directory, { recursive: true }); writeFileSync(certFile, generated.cert, { encoding: "utf8", mode: 0o600 }); writeFileSync(keyFile, generated.private, { encoding: "utf8", mode: 0o600 }); try { chmodSync(certFile, 0o600); chmodSync(keyFile, 0o600); } catch { // Windows ACLs are inherited from the private workspace directory. } return { cert: generated.cert, key: generated.private, certFile, keyFile, reused: false, }; } export async function startLocalServices( options: { appRoot?: string; port?: number; hostname?: string; https?: boolean; origin?: string; certificate?: LocalCertificate; } = {}, ) { const port = options.port ?? 3099; const hostname = options.hostname ?? "127.0.0.1"; const secure = options.https !== false; const tls = secure ? (options.certificate ?? (await ensureLocalCertificate(options.appRoot ?? "."))) : undefined; const server = Bun.serve({ port, hostname, fetch: createLocalServicesHandler(undefined, { origin: options.origin }), ...(tls ? { tls: { cert: tls.cert, key: tls.key } } : {}), }); console.log( ` ▸ Local services: ${secure ? "https" : "http"}://${hostname}:${server.port}/__services`, ); if (tls) console.log(` certificate: ${tls.certFile} (trust locally to remove warnings)`); console.log(" database cache mail sms webhooks storage queue cron auth metrics"); return server; } import { X509Certificate } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { generate } from "selfsigned";