import * as fs from "node:fs"; /** Mesh engine config, stored at ~/.pi/xfer/mesh.config.json. */ export interface MeshConfig { endpoint: string; namespace?: string; token?: string; /** * Envoy/runner pool this host's runners join (RivetKit `envoy.poolName`). * Pins this Node's actor to its own host instead of the engine's shared * `default` pool. Absent = today's behavior (default pool). */ pool?: string; } /** Single actor type hosting one mesh node per pi instance. */ export const XFER_INSTANCE_ACTOR = "xfer-instance"; /** Queue carrying inline handoff payloads between mesh nodes. */ export const HANDOFF_QUEUE = "handoff"; /** * Parse + validate a mesh config object. Accepts namespace/token inline in the * endpoint via URL auth (`https://ns:token@host`) when the explicit fields are * absent. Throws with a human-readable message when malformed. */ export function parseMeshConfig(raw: unknown): MeshConfig { if (typeof raw !== "object" || raw === null) { throw new Error("mesh config must be a JSON object"); } const obj = raw as Record; if (typeof obj.endpoint !== "string" || obj.endpoint.trim() === "") { throw new Error('mesh config needs a non-empty "endpoint"'); } const config: MeshConfig = { endpoint: obj.endpoint.trim() }; if (typeof obj.namespace === "string" && obj.namespace !== "") config.namespace = obj.namespace; if (typeof obj.token === "string" && obj.token !== "") config.token = obj.token; if (obj.pool !== undefined) { if (typeof obj.pool !== "string" || obj.pool.trim() === "") { throw new Error('mesh config "pool" must be a non-empty string'); } config.pool = obj.pool.trim(); } return withUrlAuth(config); } /** Fill namespace/token from URL auth syntax when not set explicitly. */ /** * Fill namespace/token from URL auth syntax when not set explicitly. Both * spellings at once is almost always a stale placeholder in one of them, so it * is refused instead of silently preferring the explicit fields. */ function withUrlAuth(config: MeshConfig): MeshConfig { try { const url = new URL(config.endpoint); if (url.username || url.password) { if (config.namespace || config.token) { throw new Error( "mesh config sets credentials twice — namespace/token fields AND URL userinfo (https://ns:token@host). " + "Keep exactly one; a stale placeholder in either would otherwise win silently", ); } config.namespace = decodeURIComponent(url.username); config.token = decodeURIComponent(url.password); url.username = ""; url.password = ""; config.endpoint = url.toString(); } } catch (err) { if (err instanceof Error && err.message.includes("credentials twice")) throw err; } return config; } /** Read + parse the mesh config file; throws when missing or malformed. */ export function loadMeshConfig(configPath: string): MeshConfig { let text: string; try { text = fs.readFileSync(configPath, "utf-8"); } catch { throw new Error(`mesh not configured — create ${configPath} ({ endpoint, namespace?, token?, pool? })`); } let raw: unknown; try { raw = JSON.parse(text); } catch (err) { throw new Error(`mesh config is not valid JSON: ${configPath} — ${err instanceof Error ? err.message : String(err)}`); } return parseMeshConfig(raw); } /** * Serialize an actor key the way the engine does (`/`-joined with escaping) so * REST list results can be matched against an expected key. Mirrors rivetkit's * serializeActorKey for the shapes mesh keys use (non-empty string parts). */ export function serializeMeshKey(key: string[]): string { if (key.length === 0) return "/"; return key .map((part) => part === "" ? "\\0" : part.replace(/\\/g, "\\\\").replace(/\//g, "\\/"), ) .join("/"); } export function deserializeMeshKey(serialized: string | undefined): string[] { if (serialized === undefined || serialized === null || serialized === "/" || serialized === "") return []; const parts: string[] = []; let current = ""; let escaped = false; for (const ch of serialized) { if (escaped) { current += ch === "0" ? "" : ch; escaped = false; } else if (ch === "\\") { escaped = true; } else if (ch === "/") { parts.push(current); current = ""; } else { current += ch; } } parts.push(current); return parts; }