diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 82780967ed..4cef9b30a9 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -158,10 +158,10 @@ export default function HomePageClient({ machineId }) { 1. Create API key

Go to{" "} - - Settings + + Endpoint {" "} - → API Keys. Generate one key per environment. + → Registered Keys. Generate one key per environment.

diff --git a/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx b/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx index 22d71089f4..03674933ac 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx @@ -23,26 +23,51 @@ export default function SessionInfoCard() { const [loading, setLoading] = useState(true); useEffect(() => { - // Build session info from client-side data - const loginTime = sessionStorage.getItem("omniroute_login_time"); - const now = Date.now(); + let cancelled = false; - let sessionAge = "Unknown"; - if (loginTime) { - const elapsed = now - parseInt(loginTime, 10); - const hours = Math.floor(elapsed / 3600000); - const minutes = Math.floor((elapsed % 3600000) / 60000); - sessionAge = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + async function loadSession() { + // Build session info from client-side data + const loginTime = sessionStorage.getItem("omniroute_login_time"); + const now = Date.now(); + + let sessionAge = "Unknown"; + if (loginTime) { + const elapsed = now - parseInt(loginTime, 10); + const hours = Math.floor(elapsed / 3600000); + const minutes = Math.floor((elapsed % 3600000) / 60000); + sessionAge = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + } + + let authenticated = false; + try { + const res = await fetch("/api/auth/status", { + method: "GET", + cache: "no-store", + }); + if (res.ok) { + const data = await res.json(); + authenticated = data.authenticated === true; + } + } catch { + // Keep unauthenticated fallback on network errors. + } + + if (cancelled) return; + + setSession({ + authenticated, + loginTime: loginTime ? new Date(parseInt(loginTime, 10)).toLocaleString() : null, + sessionAge, + ipAddress: "—", // Server-side only + userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || "Unknown", + }); + setLoading(false); } - setSession({ - authenticated: !!document.cookie.includes("omniroute"), - loginTime: loginTime ? new Date(parseInt(loginTime, 10)).toLocaleString() : null, - sessionAge, - ipAddress: "—", // Server-side only - userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || "Unknown", - }); - setLoading(false); + loadSession(); + return () => { + cancelled = true; + }; }, []); const handleLogout = async () => { diff --git a/src/app/api/auth/status/route.ts b/src/app/api/auth/status/route.ts new file mode 100644 index 0000000000..d0c6cb3fa2 --- /dev/null +++ b/src/app/api/auth/status/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { jwtVerify } from "jose"; + +const SECRET = process.env.JWT_SECRET + ? new TextEncoder().encode(process.env.JWT_SECRET) + : null; + +export async function GET() { + try { + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + + if (!token || !SECRET) { + return NextResponse.json({ authenticated: false }); + } + + await jwtVerify(token, SECRET); + return NextResponse.json({ authenticated: true }); + } catch { + return NextResponse.json({ authenticated: false }); + } +} diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 176923a235..75b115caac 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -204,7 +204,7 @@ export default function DocsPage() {
  • 2. Create API key

    - Go to Settings → API Keys. Generate one key per environment. + Go to Endpoint → Registered Keys. Generate one key per environment.

  • diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 104161f32f..4ced00c1f4 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -56,6 +56,7 @@ export default function LoginPage() { }); if (res.ok) { + sessionStorage.setItem("omniroute_login_time", String(Date.now())); router.push("/dashboard"); router.refresh(); } else { diff --git a/src/lib/container.ts b/src/lib/container.ts index 86f722d09b..13a1a5feec 100644 --- a/src/lib/container.ts +++ b/src/lib/container.ts @@ -15,6 +15,18 @@ * @module lib/container */ +import { evaluateFirstAllowed, evaluateRequest, PolicyEngine } from "../domain/policyEngine"; +import { getDbInstance } from "./db/core"; +import { + decrypt, + decryptConnectionFields, + encrypt, + encryptConnectionFields, +} from "./db/encryption"; +import { getSettings } from "./localDb"; +import { getCircuitBreaker } from "../shared/utils/circuitBreaker"; +import { recordTelemetry, RequestTelemetry } from "../shared/utils/requestTelemetry"; + type Factory = () => T; class Container { @@ -76,41 +88,34 @@ class Container { export const container = new Container(); // ── Default registrations ── -// These lazy-load the actual modules so the container can be imported -// without triggering all side effects at module load time. +// Services are still lazily instantiated on first resolve(). container.register("settings", () => { - const { getSettings } = require("@/lib/localDb"); return { get: getSettings }; }); container.register("db", () => { - const { getDbInstance } = require("@/lib/db/core"); return getDbInstance(); }); container.register("encryption", () => { - const enc = require("@/lib/db/encryption"); return { - encrypt: enc.encrypt, - decrypt: enc.decrypt, - encryptConnectionFields: enc.encryptConnectionFields, - decryptConnectionFields: enc.decryptConnectionFields, + encrypt, + decrypt, + encryptConnectionFields, + decryptConnectionFields, }; }); container.register("policyEngine", () => { - const { evaluateRequest, evaluateFirstAllowed, PolicyEngine } = require("@/domain/policyEngine"); return { evaluateRequest, evaluateFirstAllowed, PolicyEngine }; }); container.register("circuitBreaker", () => { - const { getCircuitBreaker } = require("@/shared/utils/circuitBreaker"); return { get: getCircuitBreaker }; }); container.register("telemetry", () => { - const { RequestTelemetry, recordTelemetry } = require("@/shared/utils/requestTelemetry"); return { RequestTelemetry, recordTelemetry }; }); diff --git a/src/lib/db/core.ts b/src/lib/db/core.ts index 5ede90b951..ae766a3f9b 100644 --- a/src/lib/db/core.ts +++ b/src/lib/db/core.ts @@ -8,6 +8,7 @@ import Database from "better-sqlite3"; import path from "node:path"; import fs from "node:fs"; import { resolveDataDir, getLegacyDotDataDir } from "../dataPaths"; +import { runMigrations } from "./migrationRunner"; // ──────────────── Environment Detection ──────────────── @@ -355,21 +356,16 @@ export function getDbInstance() { // ── Versioned Migrations ── // Auto-seed 001 as applied (the inline SCHEMA_SQL already created these tables) // then run any new migrations (002+) - try { - const { runMigrations } = require("./migrationRunner"); - _db.exec(` - CREATE TABLE IF NOT EXISTS _omniroute_migrations ( - version TEXT PRIMARY KEY, - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - INSERT OR IGNORE INTO _omniroute_migrations (version, name) - VALUES ('001', 'initial_schema'); - `); - runMigrations(_db); - } catch (err: any) { - console.warn("[DB] Migration runner unavailable:", err.message); - } + _db.exec(` + CREATE TABLE IF NOT EXISTS _omniroute_migrations ( + version TEXT PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + INSERT OR IGNORE INTO _omniroute_migrations (version, name) + VALUES ('001', 'initial_schema'); + `); + runMigrations(_db); // Auto-migrate from db.json if exists if (JSON_DB_FILE && fs.existsSync(JSON_DB_FILE)) { diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index acce867458..d8fbe7a71b 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -11,8 +11,11 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import type Database from "better-sqlite3"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const MIGRATIONS_DIR = path.join(__dirname, "migrations"); /** diff --git a/src/lib/evals/scheduler.ts b/src/lib/evals/scheduler.ts index 6b4886698a..1e1aed1bfc 100644 --- a/src/lib/evals/scheduler.ts +++ b/src/lib/evals/scheduler.ts @@ -8,7 +8,7 @@ * @module lib/evals/scheduler */ -import { runSuite, listSuites, createScorecard } from "./evalRunner"; +import { runSuite, listSuites, createScorecard, getSuite } from "./evalRunner"; // ── Types ── @@ -159,7 +159,6 @@ async function executeScheduledRun(suiteId: string): Promise = {}; // We use the suite's cases to get the case IDs - const { getSuite } = require("./evalRunner"); const suite = getSuite(suiteId); if (!suite?.cases) return null; diff --git a/src/proxy.ts b/src/proxy.ts index 012ac49719..b56ce32aa2 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -3,7 +3,7 @@ import { jwtVerify } from "jose"; import { generateRequestId } from "./shared/utils/requestId"; import { getSettings } from "./lib/localDb"; import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth"; -import { checkBodySize } from "./shared/middleware/bodySizeGuard"; +import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard"; import { isDraining } from "./lib/gracefulShutdown"; // FASE-01: Fail-fast — no hardcoded fallback. Server must have JWT_SECRET configured. @@ -37,7 +37,7 @@ export async function proxy(request) { // ──────────────── Pre-flight: Reject oversized bodies ──────────────── if (pathname.startsWith("/api/") && request.method !== "GET" && request.method !== "OPTIONS") { - const bodySizeRejection = checkBodySize(request); + const bodySizeRejection = checkBodySize(request, getBodySizeLimit(pathname)); if (bodySizeRejection) return bodySizeRejection; } diff --git a/src/shared/middleware/bodySizeGuard.ts b/src/shared/middleware/bodySizeGuard.ts index 469eddd6c7..f028484f0c 100644 --- a/src/shared/middleware/bodySizeGuard.ts +++ b/src/shared/middleware/bodySizeGuard.ts @@ -19,12 +19,30 @@ const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024; /** Larger limit for backup/import routes: 100 MB */ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024; +/** Larger limit for audio transcription uploads: 100 MB */ +export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024; + /** Configured limit — reads from env or falls back to 10 MB */ export const MAX_BODY_BYTES = parseInt( process.env.MAX_BODY_SIZE_BYTES || String(DEFAULT_MAX_BODY_BYTES), 10 ); +type BodySizeRule = { prefix: string; limit: number }; + +const ROUTE_LIMITS: BodySizeRule[] = [ + { prefix: "/api/db-backups/import", limit: MAX_BODY_BYTES_IMPORT }, + { prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO }, +]; + +/** + * Resolve the body size limit for a request path. + */ +export function getBodySizeLimit(pathname: string): number { + const customRule = ROUTE_LIMITS.find((rule) => pathname.startsWith(rule.prefix)); + return customRule?.limit ?? MAX_BODY_BYTES; +} + /** * Check Content-Length header against the configured limit. * Returns a 413 Response if the body is too large, or null if OK. diff --git a/src/shared/utils/apiAuth.ts b/src/shared/utils/apiAuth.ts index 71e99b75c4..937315b523 100644 --- a/src/shared/utils/apiAuth.ts +++ b/src/shared/utils/apiAuth.ts @@ -20,6 +20,7 @@ const PUBLIC_API_ROUTES = [ // Auth flow — must be accessible to unauthenticated users "/api/auth/login", "/api/auth/logout", + "/api/auth/status", // Settings check — used by login page / onboarding "/api/settings/require-login",