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:
+12
-61
@@ -32,7 +32,10 @@ import {
|
||||
ytExtraArgs,
|
||||
ytCookieMethod,
|
||||
setupCookieRoutes,
|
||||
getCookieFilePath,
|
||||
} from "./cookies.js";
|
||||
import * as config from "./config.js";
|
||||
import { resolveApiKey } from "./config.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const app = express();
|
||||
@@ -48,67 +51,14 @@ const configDir = path.join(DATA_DIR, "config");
|
||||
await fs.mkdir(historyDir, { recursive: true }).catch(() => {});
|
||||
await fs.mkdir(configDir, { recursive: true }).catch(() => {});
|
||||
|
||||
// ── Server-side API key (shared across all clients) ───────────────────────
|
||||
// Priority: GEMINI_API_KEY env var → StartOS config → .env file
|
||||
//
|
||||
// The StartOS config path is watched for changes — when a user updates the
|
||||
// key via the "Set Gemini API Key" action, the new value is picked up
|
||||
// without a service restart. Env var takes priority and pins the value.
|
||||
const envPath = path.join(DATA_DIR, ".env");
|
||||
const startosConfigPath = path.join(configDir, "startos-config.json");
|
||||
const envApiKey = process.env.GEMINI_API_KEY || "";
|
||||
let serverApiKey = envApiKey;
|
||||
|
||||
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})`);
|
||||
}
|
||||
}
|
||||
|
||||
await refreshServerApiKey("startup");
|
||||
|
||||
// Poll the StartOS config file every few seconds and refresh the in-memory
|
||||
// API key when it changes — so a key updated via the "Set Gemini API Key"
|
||||
// action is picked up without a service restart. Polling is more reliable
|
||||
// than fs.watch (FSEvents on macOS, inotify edge cases on Linux, atomic-
|
||||
// write rename behavior in the SDK file model). The cost is a single stat
|
||||
// every 3 s, which is negligible.
|
||||
const CONFIG_POLL_MS = parseInt(process.env.RECAP_CONFIG_POLL_MS || "3000", 10);
|
||||
setInterval(() => {
|
||||
refreshServerApiKey("config poll").catch(() => {});
|
||||
}, CONFIG_POLL_MS);
|
||||
// API key + live reload moved to ./config.js
|
||||
await config.initConfig({ dataDir: DATA_DIR });
|
||||
const envPath = config.getEnvPath();
|
||||
|
||||
// Cookies state + helpers + routes moved to ./cookies.js
|
||||
await initCookies({ dataDir: DATA_DIR, envPath });
|
||||
|
||||
function resolveApiKey(clientKey) {
|
||||
// Client can send "USE_SERVER_KEY" or empty to use the server's stored key
|
||||
if (!clientKey || clientKey === "USE_SERVER_KEY") return serverApiKey;
|
||||
return clientKey;
|
||||
}
|
||||
// resolveApiKey moved to ./config.js
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "100mb" }));
|
||||
@@ -330,17 +280,18 @@ app.use("/assets", express.static(path.join(__dirname, "..", "assets")));
|
||||
app.get("/api/health", async (req, res) => {
|
||||
const info = await checkYtdlp();
|
||||
// Check cookies.txt freshness
|
||||
let cookieInfo = { method: ytCookieMethod() };
|
||||
if (ytCookiesFileExists) {
|
||||
const cookieMethod = ytCookieMethod();
|
||||
let cookieInfo = { method: cookieMethod };
|
||||
if (cookieMethod === "cookies.txt") {
|
||||
try {
|
||||
const stat = await fs.stat(ytCookiesFilePath);
|
||||
const stat = await fs.stat(getCookieFilePath());
|
||||
const ageMs = Date.now() - stat.mtimeMs;
|
||||
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
||||
cookieInfo.fileAgeDays = ageDays;
|
||||
cookieInfo.fileExpiring = ageDays > 12; // cookies typically expire after ~14 days
|
||||
} catch {}
|
||||
}
|
||||
res.json({ ok: true, ytdlp: info.installed, hasServerKey: !!serverApiKey, cookies: cookieInfo, ...info });
|
||||
res.json({ ok: true, ytdlp: info.installed, hasServerKey: !!config.serverApiKey, cookies: cookieInfo, ...info });
|
||||
});
|
||||
|
||||
// ── Status endpoints ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user