Module split: extract API key + live reload to server/config.js
• initConfig({ dataDir }) — boot-time setup; mkdir's configDir,
reads initial value, kicks off the
3 s poll loop
• serverApiKey — exported as a 'let' binding (ESM
live binding) so importers see the
current value
• resolveApiKey(clientKey) — picks per-request key (BYO vs
server)
• getEnvPath() — exposes /data/.env path so the
cookies module can read its own
legacy YT_COOKIES_FROM setting
Bug fix uncovered during this extraction: /api/health was directly
referencing ytCookiesFileExists / ytCookiesFilePath (module-scoped
vars I'd already moved to cookies.js). The route silently 500'd on the
first request after the cookies extraction. Now uses ytCookieMethod()
and getCookieFilePath() instead.
server/index.js: 2510 → 2461 lines.
Smoke tested: server boots; /api/health responds; updating
/data/config/startos-config.json flips hasServerKey from false → true
within 3 s ([config] server API key loaded log line confirms).
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// Server-side configuration: API key resolution + live reload of the
|
||||
// StartOS config file. Module owns its own state — `serverApiKey` is
|
||||
// exported as a `let` binding so importers see the current value
|
||||
// (ESM live bindings).
|
||||
//
|
||||
// Resolution priority:
|
||||
// 1. process.env.GEMINI_API_KEY (pins; never re-read after boot)
|
||||
// 2. /data/config/startos-config.json's gemini_api_key
|
||||
// 3. /data/.env's GEMINI_API_KEY
|
||||
//
|
||||
// Whenever the StartOS config file changes (via the "Set Gemini API
|
||||
// Key" action), the new value is picked up within CONFIG_POLL_MS — no
|
||||
// service restart required.
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
// ── Module state ────────────────────────────────────────────────────────────
|
||||
// Initialized by initConfig(). serverApiKey is exported as a live binding;
|
||||
// importers see the current value, not a snapshot.
|
||||
let envApiKey = "";
|
||||
let envPath = null;
|
||||
let configDir = null;
|
||||
let startosConfigPath = null;
|
||||
|
||||
export let serverApiKey = "";
|
||||
|
||||
// ── Init ────────────────────────────────────────────────────────────────────
|
||||
// Call once at boot. Sets up paths, reads the initial value, kicks off the
|
||||
// poll loop. Idempotent if you really want to call it twice (the interval
|
||||
// would just stack — don't).
|
||||
export async function initConfig({ dataDir }) {
|
||||
envPath = path.join(dataDir, ".env");
|
||||
configDir = path.join(dataDir, "config");
|
||||
startosConfigPath = path.join(configDir, "startos-config.json");
|
||||
envApiKey = process.env.GEMINI_API_KEY || "";
|
||||
serverApiKey = envApiKey;
|
||||
|
||||
await fs.mkdir(configDir, { recursive: true }).catch(() => {});
|
||||
await refreshServerApiKey("startup");
|
||||
|
||||
const pollMs = parseInt(process.env.RECAP_CONFIG_POLL_MS || "3000", 10);
|
||||
setInterval(() => {
|
||||
refreshServerApiKey("config poll").catch(() => {});
|
||||
}, pollMs);
|
||||
}
|
||||
|
||||
// ── Internals ───────────────────────────────────────────────────────────────
|
||||
async function readApiKeyFromConfig() {
|
||||
try {
|
||||
const content = await fs.readFile(startosConfigPath, "utf-8");
|
||||
const config = JSON.parse(content);
|
||||
return config.gemini_api_key || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function readApiKeyFromEnvFile() {
|
||||
try {
|
||||
const envContent = await fs.readFile(envPath, "utf-8");
|
||||
const match = envContent.match(/^GEMINI_API_KEY=(.+)$/m);
|
||||
if (match) return match[1].trim().replace(/^["']|["']$/g, "");
|
||||
} catch {}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function refreshServerApiKey(reason) {
|
||||
if (envApiKey) return; // env var pins the value
|
||||
const fromConfig = await readApiKeyFromConfig();
|
||||
const next = fromConfig || (await readApiKeyFromEnvFile()) || "";
|
||||
if (next !== serverApiKey) {
|
||||
serverApiKey = next;
|
||||
console.log(`[config] server API key ${next ? "loaded" : "cleared"} (${reason})`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public helpers ──────────────────────────────────────────────────────────
|
||||
// Resolves the per-request key — either the client's own (BYO) or the
|
||||
// server's stored key (when the client signals USE_SERVER_KEY or sends
|
||||
// nothing).
|
||||
export function resolveApiKey(clientKey) {
|
||||
if (!clientKey || clientKey === "USE_SERVER_KEY") return serverApiKey;
|
||||
return clientKey;
|
||||
}
|
||||
|
||||
// Where the .env file lives (some other modules read it for legacy
|
||||
// settings like YT_COOKIES_FROM).
|
||||
export function getEnvPath() {
|
||||
return envPath;
|
||||
}
|
||||
Reference in New Issue
Block a user