Files
recap-relay/server/config.js
T
2026-05-11 20:03:27 -05:00

99 lines
3.1 KiB
JavaScript

// Live-reloading config layer. Mirrors Recap's config.js pattern: read
// /data/config/relay-config.json on every access (filesystem watcher
// pulls in StartOS-action changes without a daemon restart), parse,
// and expose typed accessors.
//
// All defaults match the schema in startos/file-models/config.json.ts.
import fs from "fs/promises";
import path from "path";
let dataDir = "/data";
let cached = { mtimeMs: 0, snapshot: defaultConfig() };
function defaultConfig() {
return {
relay_gemini_api_key: "",
relay_parakeet_base_url: "",
relay_gemma_base_url: "",
relay_keysat_base_url: "https://keysat.xyz",
relay_admin_username: "",
relay_admin_password_hash: "",
relay_admin_password_salt: "",
relay_admin_session_secret: "",
relay_tier_quotas_json: JSON.stringify({
core: { lifetime: 5, monthly: null, geminiCapMonthly: null },
pro: { lifetime: null, monthly: 50, geminiCapMonthly: 25 },
max: { lifetime: null, monthly: null, geminiCapMonthly: 50 },
}),
};
}
function configPath() {
return path.join(dataDir, "config", "relay-config.json");
}
export async function initConfig({ dataDir: dd }) {
if (dd) dataDir = dd;
await fs.mkdir(path.dirname(configPath()), { recursive: true }).catch(() => {});
// Prime the cache so the first request doesn't pay for a file-read.
await getConfigSnapshot();
}
// Reads the on-disk config and merges with defaults. Cheap — single
// stat + read per call, but the result is cached until the file mtime
// changes so repeat callers within one request don't re-read.
export async function getConfigSnapshot() {
const p = configPath();
let stat;
try {
stat = await fs.stat(p);
} catch {
return cached.snapshot;
}
if (stat.mtimeMs === cached.mtimeMs) return cached.snapshot;
try {
const raw = await fs.readFile(p, "utf8");
const parsed = JSON.parse(raw);
cached = {
mtimeMs: stat.mtimeMs,
snapshot: { ...defaultConfig(), ...parsed },
};
} catch (err) {
console.warn(`[config] failed to parse ${p}: ${err?.message}`);
}
return cached.snapshot;
}
// Parsed view of relay_tier_quotas_json, with safe fallbacks if the
// blob is missing or malformed.
export async function getTierQuotas() {
const cfg = await getConfigSnapshot();
try {
const parsed = JSON.parse(cfg.relay_tier_quotas_json);
return {
core: {
lifetime: parsed?.core?.lifetime ?? 5,
monthly: parsed?.core?.monthly ?? null,
geminiCapMonthly: parsed?.core?.geminiCapMonthly ?? null,
},
pro: {
lifetime: parsed?.pro?.lifetime ?? null,
monthly: parsed?.pro?.monthly ?? 50,
geminiCapMonthly: parsed?.pro?.geminiCapMonthly ?? 25,
},
max: {
lifetime: parsed?.max?.lifetime ?? null,
monthly: parsed?.max?.monthly ?? null,
geminiCapMonthly: parsed?.max?.geminiCapMonthly ?? 50,
},
};
} catch {
return {
core: { lifetime: 5, monthly: null, geminiCapMonthly: null },
pro: { lifetime: null, monthly: 50, geminiCapMonthly: 25 },
max: { lifetime: null, monthly: null, geminiCapMonthly: 50 },
};
}
}