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",