From af46f87eed86bd4aa42cf803c7a0834462d75ca1 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 10 Mar 2026 15:15:07 -0300 Subject: [PATCH] feat(bootstrap): zero-config auto-generated secrets on first run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 25 +++ docs/openapi.yaml | 2 +- electron/main.js | 83 ++++++--- package-lock.json | 4 +- package.json | 2 +- scripts/bootstrap-env.mjs | 174 ++++++++++++++++++ scripts/run-next.mjs | 6 +- scripts/run-standalone.mjs | 6 +- .../(dashboard)/dashboard/BootstrapBanner.tsx | 48 +++++ src/app/(dashboard)/dashboard/page.tsx | 9 +- 10 files changed, 327 insertions(+), 32 deletions(-) create mode 100644 scripts/bootstrap-env.mjs create mode 100644 src/app/(dashboard)/dashboard/BootstrapBanner.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 00215451..25649d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [2.2.5] โ€” 2026-03-10 + +> ### ๐Ÿ”ง Zero-Config Bootstrap ยท ๐Ÿ› Electron Black Screen Fix + +### Features + +- **Zero-config bootstrap (#252, #249)** โ€” OmniRoute now auto-generates required secrets on first run across all deployment modes (npm, Docker, Electron Desktop App): + - `JWT_SECRET` (64-byte hex) โ€” required for auth/sessions + - `STORAGE_ENCRYPTION_KEY` (32-byte hex) โ€” required for SQLite encryption + - `API_KEY_SECRET` (32-byte hex) โ€” required for API key signing + - Secrets are persisted to `{DATA_DIR}/server.env` and survive restarts, Docker volume remounts, and upgrades + - Friendly startup warnings if OAuth secrets (Antigravity, iFlow, Gemini) are not configured + - New **`scripts/bootstrap-env.mjs`** module โ€” single source of truth for zero-config initialization + +### Bug Fixes + +- **Electron black screen on macOS/Windows/Linux** โ€” The Next.js server was crashing silently because `JWT_SECRET` and `STORAGE_ENCRYPTION_KEY` are never present in desktop OS environments. Fixed by calling `bootstrapEnv()` before spawning `server.js`, with secrets persisted to Electron's `userData` directory. +- **Dashboard bootstrap banner** โ€” Added dismissable amber warning banner on the dashboard home when running in zero-config mode, showing where `server.env` is stored and how to customize secrets. + +### Note for Docker users + +Previously, `--env-file .env` was required to pass secrets to the container. Now OmniRoute will generate and persist them automatically in the mounted volume. Existing `DATA_DIR` secrets are always respected. + +--- + ## [2.2.4] โ€” 2026-03-10 > ### ๐Ÿ”ง CI Fixes diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0ec16a0a..22868c37 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 2.2.4 + version: 2.2.5 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/electron/main.js b/electron/main.js index d006e666..23878865 100644 --- a/electron/main.js +++ b/electron/main.js @@ -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", }, diff --git a/package-lock.json b/package-lock.json index 25b8dbac..c48e654a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "2.2.4", + "version": "2.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "2.2.4", + "version": "2.2.5", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 506a7317..a8dc07e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "2.2.4", + "version": "2.2.5", "description": "Smart AI Router with auto fallback โ€” route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { diff --git a/scripts/bootstrap-env.mjs b/scripts/bootstrap-env.mjs new file mode 100644 index 00000000..dacba006 --- /dev/null +++ b/scripts/bootstrap-env.mjs @@ -0,0 +1,174 @@ +#!/usr/bin/env node +/** + * OmniRoute โ€” Zero-Config Bootstrap + * + * Auto-generates required secrets (JWT_SECRET, STORAGE_ENCRYPTION_KEY) if + * missing or empty, persists them to {DATA_DIR}/server.env so they survive + * restarts, Docker volume remounts, and upgrades. + * + * Works across all deployment modes: + * - npm / CLI: called from run-standalone.mjs and run-next.mjs + * - Docker: same, secrets persisted in mounted volume + * - Electron: called from main.js startup, persisted in userData + * + * Priority (lowest โ†’ highest): + * 1. Auto-generated defaults + * 2. {DATA_DIR}/server.env (persisted on first boot) + * 3. .env in CWD (user overrides) + * 4. process.env (shell / Docker -e flags, highest priority) + */ + +import { createHash, randomBytes } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; + +// โ”€โ”€ OAuth secrets that are optional but warn if missing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const OPTIONAL_OAUTH_SECRETS = [ + { key: "ANTIGRAVITY_OAUTH_CLIENT_SECRET", label: "Antigravity OAuth" }, + { key: "IFLOW_OAUTH_CLIENT_SECRET", label: "iFlow OAuth" }, + { key: "GEMINI_OAUTH_CLIENT_SECRET", label: "Gemini OAuth" }, +]; + +// โ”€โ”€ Resolve DATA_DIR (mirrors dataPaths.ts logic) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function resolveDataDir(overridePath) { + if (overridePath) return resolve(overridePath); + + const configured = process.env.DATA_DIR?.trim(); + if (configured) return resolve(configured); + + if (process.platform === "win32") { + const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming"); + return join(appData, "omniroute"); + } + + const xdg = process.env.XDG_CONFIG_HOME?.trim(); + if (xdg) return join(resolve(xdg), "omniroute"); + + return join(homedir(), ".omniroute"); +} + +// โ”€โ”€ Parse a simple KEY=VALUE env file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function parseEnvFile(filePath) { + if (!existsSync(filePath)) return {}; + const env = {}; + const lines = readFileSync(filePath, "utf8").split(/\r?\n/); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx < 1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const val = trimmed.slice(eqIdx + 1).trim(); + env[key] = val; + } + return env; +} + +// โ”€โ”€ Write a simple KEY=VALUE env file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function writeEnvFile(filePath, env) { + const lines = [ + "# Auto-generated by OmniRoute bootstrap โ€” do not delete", + `# Created: ${new Date().toISOString()}`, + "", + ...Object.entries(env).map(([k, v]) => `${k}=${v}`), + "", + ]; + writeFileSync(filePath, lines.join("\n"), "utf8"); +} + +// โ”€โ”€ Main bootstrap function โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +/** + * @param {{ dataDirOverride?: string; quiet?: boolean }} options + * @returns {Record} merged env to pass to child process + */ +export function bootstrapEnv({ dataDirOverride, quiet = false } = {}) { + const log = quiet ? () => {} : (msg) => process.stderr.write(`[bootstrap] ${msg}\n`); + + const dataDir = resolveDataDir(dataDirOverride); + const serverEnvPath = join(dataDir, "server.env"); + const dotEnvPath = join(process.cwd(), ".env"); + + // โ”€โ”€ Layer 1: Load persisted server.env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let persisted = parseEnvFile(serverEnvPath); + + // โ”€โ”€ Layer 2: Load .env from CWD (user overrides, higher priority) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const dotEnv = parseEnvFile(dotEnvPath); + + // โ”€โ”€ Merge: persisted < .env < process.env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const merged = { ...persisted, ...dotEnv, ...process.env }; + + // โ”€โ”€ Auto-generate required secrets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let needsPersist = false; + + if (!merged.JWT_SECRET?.trim()) { + persisted.JWT_SECRET = randomBytes(64).toString("hex"); + merged.JWT_SECRET = persisted.JWT_SECRET; + needsPersist = true; + log("โœจ JWT_SECRET auto-generated (first run)"); + } + + if (!merged.STORAGE_ENCRYPTION_KEY?.trim()) { + persisted.STORAGE_ENCRYPTION_KEY = randomBytes(32).toString("hex"); + merged.STORAGE_ENCRYPTION_KEY = persisted.STORAGE_ENCRYPTION_KEY; + needsPersist = true; + log("โœจ STORAGE_ENCRYPTION_KEY auto-generated (first run)"); + } + + if (!merged.STORAGE_ENCRYPTION_KEY_VERSION?.trim()) { + persisted.STORAGE_ENCRYPTION_KEY_VERSION = "v1"; + merged.STORAGE_ENCRYPTION_KEY_VERSION = persisted.STORAGE_ENCRYPTION_KEY_VERSION; + needsPersist = true; + } + + if (!merged.API_KEY_SECRET?.trim()) { + persisted.API_KEY_SECRET = randomBytes(32).toString("hex"); + merged.API_KEY_SECRET = persisted.API_KEY_SECRET; + needsPersist = true; + log("โœจ API_KEY_SECRET auto-generated (first run)"); + } + + // โ”€โ”€ Persist new secrets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (needsPersist) { + try { + mkdirSync(dataDir, { recursive: true }); + // Only persist keys that we auto-generated (not .env or process.env vals) + writeEnvFile(serverEnvPath, persisted); + log(`๐Ÿ“ Secrets persisted to: ${serverEnvPath}`); + } catch (e) { + log(`โš ๏ธ Could not persist secrets to ${serverEnvPath}: ${e.message}`); + } + } + + // โ”€โ”€ Mark as bootstrapped โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (needsPersist) { + merged.OMNIROUTE_BOOTSTRAPPED = "true"; + } + + // โ”€โ”€ Warn about missing optional OAuth secrets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const missingOauth = OPTIONAL_OAUTH_SECRETS.filter(({ key }) => !merged[key]?.trim()); + if (missingOauth.length > 0) { + log("โ„น๏ธ The following OAuth integrations are not configured:"); + for (const { key, label } of missingOauth) { + log(` โ€ข ${label} (${key}) โ€” set in .env or ${serverEnvPath}`); + } + log(" These providers will not work until configured."); + } + + // โ”€โ”€ Warn about default password โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (merged.INITIAL_PASSWORD === "CHANGEME" || !merged.INITIAL_PASSWORD?.trim()) { + log("โš ๏ธ INITIAL_PASSWORD is not set โ€” using default 'CHANGEME'. Change it in Settings!"); + } + + return merged; +} + +// โ”€โ”€ CLI usage: node scripts/bootstrap-env.mjs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (process.argv[1] && process.argv[1].endsWith("bootstrap-env.mjs")) { + const env = bootstrapEnv(); + process.stderr.write(`[bootstrap] Done. DATA_DIR resolved to: ${resolveDataDir()}\n`); + process.stderr.write(`[bootstrap] JWT_SECRET length: ${env.JWT_SECRET?.length ?? 0}\n`); + process.stderr.write( + `[bootstrap] STORAGE_ENCRYPTION_KEY length: ${env.STORAGE_ENCRYPTION_KEY?.length ?? 0}\n` + ); +} diff --git a/scripts/run-next.mjs b/scripts/run-next.mjs index c56c8455..f289f21a 100644 --- a/scripts/run-next.mjs +++ b/scripts/run-next.mjs @@ -5,12 +5,16 @@ import { withRuntimePortEnv, spawnWithForwardedSignals, } from "./runtime-env.mjs"; +import { bootstrapEnv } from "./bootstrap-env.mjs"; const mode = process.argv[2] === "start" ? "start" : "dev"; const runtimePorts = resolveRuntimePorts(); const { dashboardPort } = runtimePorts; +// Auto-generate secrets on first run, merge .env + process.env +const env = bootstrapEnv(); + const args = ["./node_modules/next/dist/bin/next", mode, "--port", String(dashboardPort)]; if (mode === "dev") { args.splice(2, 0, "--webpack"); @@ -18,5 +22,5 @@ if (mode === "dev") { spawnWithForwardedSignals(process.execPath, args, { stdio: "inherit", - env: withRuntimePortEnv(process.env, runtimePorts), + env: withRuntimePortEnv(env, runtimePorts), }); diff --git a/scripts/run-standalone.mjs b/scripts/run-standalone.mjs index 1510cdc8..0bce2252 100644 --- a/scripts/run-standalone.mjs +++ b/scripts/run-standalone.mjs @@ -5,10 +5,14 @@ import { withRuntimePortEnv, spawnWithForwardedSignals, } from "./runtime-env.mjs"; +import { bootstrapEnv } from "./bootstrap-env.mjs"; const runtimePorts = resolveRuntimePorts(); +// Auto-generate secrets on first run, merge .env + process.env +const env = bootstrapEnv(); + spawnWithForwardedSignals("node", ["server.js"], { stdio: "inherit", - env: withRuntimePortEnv(process.env, runtimePorts), + env: withRuntimePortEnv(env, runtimePorts), }); diff --git a/src/app/(dashboard)/dashboard/BootstrapBanner.tsx b/src/app/(dashboard)/dashboard/BootstrapBanner.tsx new file mode 100644 index 00000000..87df3449 --- /dev/null +++ b/src/app/(dashboard)/dashboard/BootstrapBanner.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useState } from "react"; + +/** + * Shown when OmniRoute was started with auto-generated secrets (zero-config mode). + * The banner is dismissable and persists only for the current session. + */ +export default function BootstrapBanner() { + const [dismissed, setDismissed] = useState(false); + + if (dismissed) return null; + + // Determine default data dir hint based on platform hint from user-agent + const dataDir = + typeof navigator !== "undefined" && navigator.platform?.startsWith("Win") + ? "%APPDATA%\\omniroute\\server.env" + : "~/.omniroute/server.env"; + + return ( +
+ โš ๏ธ +
+

Running in zero-config mode

+

+ OmniRoute auto-generated secure encryption keys on first launch. They are persisted to{" "} + {dataDir}. No + action is required โ€” your data is encrypted and safe. To use custom keys, add{" "} + JWT_SECRET and{" "} + + STORAGE_ENCRYPTION_KEY + {" "} + to that file. +

+
+ +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 9b7ae9e9..b1b9c0c7 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -2,6 +2,7 @@ import { redirect } from "next/navigation"; import { getMachineId } from "@/shared/utils/machine"; import { getSettings } from "@/lib/localDb"; import HomePageClient from "./HomePageClient"; +import BootstrapBanner from "./BootstrapBanner"; // Must be dynamic โ€” depends on DB state (setupComplete) that changes at runtime export const dynamic = "force-dynamic"; @@ -12,5 +13,11 @@ export default async function DashboardPage() { redirect("/dashboard/onboarding"); } const machineId = await getMachineId(); - return ; + const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true"; + return ( + <> + {isBootstrapped && } + + + ); }