feat(bootstrap): zero-config auto-generated secrets on first run

Resolves root cause of #252 (Electron black screen) and #249 (OAuth fail)
for users running with zero configuration (no .env needed).

New: scripts/bootstrap-env.mjs
- Auto-generates JWT_SECRET (64 bytes), STORAGE_ENCRYPTION_KEY (32 bytes),
  API_KEY_SECRET (32 bytes) if missing or empty
- Persists to {DATA_DIR}/server.env — survives restarts, Docker volume
  remounts, and upgrades without changing secrets
- Reads .env from CWD (user overrides), then merges process.env (highest prio)
- Logs friendly warnings for missing optional OAuth secrets

Updated: run-standalone.mjs + run-next.mjs
- Call bootstrapEnv() before spawning server — covers npm + Docker paths

Updated: electron/main.js (synchronous inline — CJS cannot await import ESM)
- Reads userData/server.env, generates missing secrets with crypto.randomBytes()
- Persists back to server.env, sets OMNIROUTE_BOOTSTRAPPED=true

New: BootstrapBanner.tsx + page.tsx update
- Dismissable amber banner on dashboard home when running in zero-config mode
- Shows where server.env is located and how to customize secrets
This commit is contained in:
diegosouzapw
2026-03-10 15:15:07 -03:00
parent fd749d1e0b
commit af46f87eed
10 changed files with 327 additions and 32 deletions

View File

@@ -383,33 +383,67 @@ function startNextServer() {
return;
}
// ── Auto-generate required env vars for Electron ──
// In packaged Electron, there is no .env file — JWT_SECRET and
// STORAGE_ENCRYPTION_KEY must be generated once and persisted.
const envFilePath = path.join(app.getPath("userData"), "electron-env.json");
let persistedEnv = {};
try {
if (fs.existsSync(envFilePath)) {
persistedEnv = JSON.parse(fs.readFileSync(envFilePath, "utf8"));
// ── Zero-config bootstrap: auto-generate required secrets ─────────────────
// Electron uses CJS — cannot dynamically import ESM bootstrap-env.mjs.
// This mirrors bootstrap-env.mjs logic synchronously:
// 1. Read persisted secrets from userData/server.env
// 2. Generate missing secrets with crypto.randomBytes()
// 3. Persist back to userData/server.env for future restarts
const crypto = require("crypto");
const userDataDir = app.getPath("userData");
const serverEnvPath = path.join(userDataDir, "server.env");
// Parse a simple KEY=VALUE file
function parseEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const env = {};
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const t = line.trim();
if (!t || t.startsWith("#")) continue;
const eq = t.indexOf("=");
if (eq < 1) continue;
env[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
}
} catch {
/* ignore read errors */
return env;
}
if (!persistedEnv.JWT_SECRET) {
persistedEnv.JWT_SECRET = require("crypto").randomBytes(64).toString("hex");
}
if (!persistedEnv.STORAGE_ENCRYPTION_KEY) {
persistedEnv.STORAGE_ENCRYPTION_KEY = require("crypto").randomBytes(32).toString("hex");
}
if (!persistedEnv.STORAGE_ENCRYPTION_KEY_VERSION) {
persistedEnv.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
}
const persisted = parseEnvFile(serverEnvPath);
const serverEnv = { ...process.env, ...persisted };
let changed = false;
try {
fs.writeFileSync(envFilePath, JSON.stringify(persistedEnv, null, 2));
} catch {
/* ignore write errors */
if (!serverEnv.JWT_SECRET) {
serverEnv.JWT_SECRET = persisted.JWT_SECRET = crypto.randomBytes(64).toString("hex");
changed = true;
console.log("[Electron] ✨ JWT_SECRET auto-generated");
}
if (!serverEnv.STORAGE_ENCRYPTION_KEY) {
serverEnv.STORAGE_ENCRYPTION_KEY = persisted.STORAGE_ENCRYPTION_KEY = crypto
.randomBytes(32)
.toString("hex");
serverEnv.STORAGE_ENCRYPTION_KEY_VERSION = persisted.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
changed = true;
console.log("[Electron] ✨ STORAGE_ENCRYPTION_KEY auto-generated");
}
if (!serverEnv.API_KEY_SECRET) {
serverEnv.API_KEY_SECRET = persisted.API_KEY_SECRET = crypto.randomBytes(32).toString("hex");
changed = true;
console.log("[Electron] ✨ API_KEY_SECRET auto-generated");
}
if (changed) {
serverEnv.OMNIROUTE_BOOTSTRAPPED = "true";
try {
fs.mkdirSync(userDataDir, { recursive: true });
const lines = [
"# Auto-generated by OmniRoute bootstrap",
"",
...Object.entries(persisted).map(([k, v]) => `${k}=${v}`),
"",
];
fs.writeFileSync(serverEnvPath, lines.join("\n"), "utf8");
console.log("[Electron] 📁 Secrets persisted to:", serverEnvPath);
} catch (e) {
console.warn("[Electron] Could not persist secrets:", e.message);
}
}
console.log("[Electron] Starting Next.js server on port", serverPort);
@@ -419,8 +453,7 @@ function startNextServer() {
nextServer = spawn("node", [serverScript], {
cwd: NEXT_SERVER_PATH,
env: {
...process.env,
...persistedEnv,
...serverEnv,
PORT: String(serverPort),
NODE_ENV: "production",
},