// Persistent per-install identifier. Generated once on first boot and // stashed at `/install-id` (typically /data/install-id on // StartOS). Survives container restarts and Recap upgrades; lost on a // full uninstall + reinstall. // // What it's for: the upcoming relay backend will use this ID as the // owner of comped/paid relay credits. Without a stable client identity // the relay can't tell whether a request belongs to a credited install // or a fresh one. Direct install-ID auth is the v1 choice (see the // project roadmap discussion) — simple, sufficient for low-count free // credits, can be hardened later with license-server-minted JWTs. // // What it is NOT: a license key. The license system (./license.js) is // completely separate — license keys are user-facing strings that // authorize Pro features, while install-IDs are opaque per-install // UUIDs the relay backend uses for credit accounting. import fs from "fs/promises"; import path from "path"; import { randomUUID } from "crypto"; let cachedId = null; // Initialize on boot. Reads the existing ID off disk; if there's no // file, generates a fresh UUIDv4 and writes it. Subsequent calls to // getInstallId() return the cached value without touching disk. // // `dataDir` must be writable — on StartOS that's /data (the persistent // volume), on local dev it's the project root. export async function initInstallId({ dataDir }) { if (!dataDir) throw new Error("initInstallId: dataDir is required"); const filePath = path.join(dataDir, "install-id"); try { const raw = await fs.readFile(filePath, "utf8"); const trimmed = raw.trim(); if (isValidInstallId(trimmed)) { cachedId = trimmed; console.log(`[install-id] loaded ${redact(cachedId)} from ${filePath}`); return cachedId; } console.warn( `[install-id] file at ${filePath} contained an invalid value — regenerating` ); } catch (err) { if (err.code !== "ENOENT") { console.warn(`[install-id] read failed (${err.code}): ${err.message}`); } } // No valid file — mint a new one. UUIDv4 is plenty: 122 bits of // randomness, no collision risk across realistic install counts, and // it's opaque enough to share over the wire without leaking system // info (unlike e.g. a machine-id). const fresh = randomUUID(); await fs.writeFile(filePath, fresh + "\n", { mode: 0o600 }); cachedId = fresh; console.log(`[install-id] generated ${redact(cachedId)} → ${filePath}`); return cachedId; } export function getInstallId() { return cachedId; } // Loose UUID shape check — accepts any reasonable UUID-ish string. // Avoids requiring v4 specifically in case operators want to seed // non-standard IDs. function isValidInstallId(s) { return typeof s === "string" && /^[0-9a-f-]{32,40}$/i.test(s); } // Log-safe display: first 8 + last 4 chars only. function redact(id) { if (!id || id.length < 12) return "(short)"; return `${id.slice(0, 8)}…${id.slice(-4)}`; }