diff --git a/src/lib/db/encryption.ts b/src/lib/db/encryption.ts index 1559c14bb5f..9bab679ae2d 100644 --- a/src/lib/db/encryption.ts +++ b/src/lib/db/encryption.ts @@ -26,6 +26,9 @@ */ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto"; +import { createLogger } from "@/shared/utils/logger"; + +const encryptionLog = createLogger("db:encryption"); const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 16; @@ -91,7 +94,11 @@ function getLegacyDynamicKey(): Buffer | null { const dynamicSalt = createHash("sha256").update(secret).digest().slice(0, 16); try { _legacyDynamicKey = scryptSync(secret, dynamicSalt, KEY_LENGTH); - } catch { + } catch (err) { + encryptionLog.error( + { err }, + "encryption.getLegacyDynamicKey: scryptSync failed — legacy decryptions will silently fail (tokens may stall migration)" + ); return null; } return _legacyDynamicKey; diff --git a/src/lib/machineToken.ts b/src/lib/machineToken.ts index e83da294953..d7c779a2d5d 100644 --- a/src/lib/machineToken.ts +++ b/src/lib/machineToken.ts @@ -1,11 +1,22 @@ import { createHash, createHmac } from "node:crypto"; +import { createLogger } from "@/shared/utils/logger"; + +const log = createLogger("auth:machine-token"); +let fallbackLogged = false; let machineIdSync: (original?: boolean) => string; try { // Use require() to bypass webpack static analysis that breaks the default export const mod = require("node-machine-id"); machineIdSync = mod.machineIdSync || mod.default?.machineIdSync; -} catch { +} catch (err) { + if (!fallbackLogged) { + fallbackLogged = true; + log.error( + { err }, + "machineToken: node-machine-id unavailable — HMAC salt falls back to empty string (security-relevant, all tokens become constant-keyed)" + ); + } machineIdSync = () => ""; } @@ -34,7 +45,11 @@ export function getMachineTokenSync(salt?: string): string { cachedSalt = activeSalt; } return token; - } catch { + } catch (err) { + log.error( + { err }, + "machineToken.getMachineTokenSync: deriveToken failed — returning empty string (security-relevant)" + ); return ""; } } @@ -47,7 +62,11 @@ export function getLegacyCliTokenSync(salt?: string): string { .update(machineId + activeSalt) .digest("hex") .substring(0, 32); - } catch { + } catch (err) { + log.error( + { err }, + "machineToken.getLegacyCliTokenSync: hash derivation failed — returning empty string (security-relevant)" + ); return ""; } } diff --git a/src/lib/resilience/anomalyHook.ts b/src/lib/resilience/anomalyHook.ts index 63df54142b3..3ac238b2780 100644 --- a/src/lib/resilience/anomalyHook.ts +++ b/src/lib/resilience/anomalyHook.ts @@ -9,7 +9,7 @@ * telemetry failures */ -import { isFeatureFlagEnabled } from "@/lib/featureFlags"; +import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; import { resolveSelfHealingSettings } from "./selfHealingSettings"; import { SelfHealingManager } from "./selfHealingManager"; import { createAnomalyDetector } from "./anomalyDetector"; diff --git a/src/lib/versionManager/processManager.ts b/src/lib/versionManager/processManager.ts index 4926a9deb25..4ac7dcfabb8 100644 --- a/src/lib/versionManager/processManager.ts +++ b/src/lib/versionManager/processManager.ts @@ -4,6 +4,9 @@ import fsSync from "fs"; import path from "path"; import os from "os"; import { setToolStatus, getVersionManagerTool } from "@/lib/db/versionManager"; +import { createLogger } from "@/shared/utils/logger"; + +const log = createLogger("version-manager:process-manager"); const DEFAULT_PORT = 8317; const GRACEFUL_TIMEOUT_MS = 5000; @@ -149,7 +152,11 @@ export async function getProcessInfo(pid: number): Promise<{ } } return { pid, alive: true }; - } catch { - return { pid, alive: true }; + } catch (err) { + log.error( + { err, pid, platform: process.platform }, + "processManager.getProcessInfo: failed to read process info" + ); + return { pid, alive: false }; } } diff --git a/src/server-init.ts b/src/server-init.ts index f870555b033..7188606e2ab 100644 --- a/src/server-init.ts +++ b/src/server-init.ts @@ -125,7 +125,7 @@ async function startServer() { // first request after restart. Gated on the feature flag so the // default-off behaviour is unchanged. try { - const { isFeatureFlagEnabled } = await import("./lib/featureFlags"); + const { isFeatureFlagEnabled } = await import("./shared/utils/featureFlags"); const { getSelfHealingManager } = await import("./lib/resilience/anomalyHook"); if (isFeatureFlagEnabled("OMNIROUTE_SELF_HEALING_ENABLED")) { const mgr = getSelfHealingManager(); @@ -135,7 +135,7 @@ async function startServer() { void mgr; // referenced to ensure the singleton is constructed } } catch (err) { - startupLog.warn({ err }, "Self-healing hydration skipped (non-fatal)"); + startupLog.error({ err }, "Self-healing hydration skipped (non-fatal)"); } startupLog.info("Server started with cloud sync initialized"); diff --git a/src/server/ws/liveServer.ts b/src/server/ws/liveServer.ts index 12755455726..cab1dbcef3f 100644 --- a/src/server/ws/liveServer.ts +++ b/src/server/ws/liveServer.ts @@ -17,6 +17,9 @@ import { WebSocketServer, WebSocket } from "ws"; import { jwtVerify } from "jose"; import { createServer, type IncomingMessage, type ServerResponse } from "http"; import { randomUUID } from "crypto"; +import { createLogger } from "@/shared/utils/logger"; + +const log = createLogger("ws:live-server"); // ── Types ───────────────────────────────────────────────────────────────── @@ -476,7 +479,12 @@ export async function startLiveDashboardServer( // does not block the event loop on a cold import — which would starve concurrent // WebSocket handshakes (see loadAuthModule). A failed warm is non-fatal: the // handler retries the import lazily. - await loadAuthModule().catch(() => {}); + await loadAuthModule().catch((err) => + log.error( + { err }, + "liveServer: failed to warm auth module — clients may experience cold-import latency" + ) + ); wss.on("connection", async (ws, request) => { const pendingMessages: string[] = []; diff --git a/tests/unit/quota/keyvQuotaStoreExtras.test.ts b/tests/unit/quota/keyvQuotaStoreExtras.test.ts new file mode 100644 index 00000000000..6b3e2f69c04 --- /dev/null +++ b/tests/unit/quota/keyvQuotaStoreExtras.test.ts @@ -0,0 +1,88 @@ +// @vitest-environment node +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + getKeyvQuotaStore, + __resetKeyvQuotaStoreForTests, +} from "../../../src/lib/quota/keyvQuotaStore"; +import type { ProviderPlan, QuotaPool } from "../../../src/lib/quota/dimensions"; + +/** + * Sanity tests for the "extras" surface of KeyvQuotaStore: + * recordPlanUsage, upsertProviderPlan, listProviderPlans, setPools, getPool. + * + * These methods were originally scoped for a KeyvQuotaStoreExtras class per + * plans/quota-keystore-type-drift-spec.md §8.2, but were folded directly into + * KeyvQuotaStore before the spec landed. We exercise them here against the + * in-memory backing to guarantee the surface stays wired correctly. + */ +describe("KeyvQuotaStoreExtras", () => { + let store: ReturnType; + + beforeEach(() => { + __resetKeyvQuotaStoreForTests(); + store = getKeyvQuotaStore({ uri: "memory://" }); + }); + + afterEach(async () => { + await store.dispose(); + __resetKeyvQuotaStoreForTests(); + }); + + it("recordPlanUsage returns a PlanPoolUsage shape with totalConsumed and lastUpdatedAt populated", async () => { + const rollup = await store.recordPlanUsage( + "conn-1", + "openai", + "pool-1", + [{ unit: "tokens", window: "hourly" }], + 42, + ); + expect(rollup).toBeDefined(); + expect(rollup.totalConsumed).toBe(42); + expect(typeof rollup.lastUpdatedAt).toBe("number"); + expect(rollup.lastUpdatedAt).toBeGreaterThan(0); + }); + + it("upsertProviderPlan writes the plan without throwing", async () => { + const plan: ProviderPlan = { + connectionId: "conn-1", + provider: "openai", + dimensions: [{ unit: "tokens", window: "hourly", limit: 1000 }], + source: "manual", + }; + await expect(store.upsertProviderPlan(plan)).resolves.toBeUndefined(); + }); + + it("listProviderPlans returns [] even after an upsert (independent surface)", async () => { + // upsertProviderPlan and listProviderPlans are independent: the in-memory + // store does not enumerate provider plans, so the list stays empty. + await store.upsertProviderPlan({ + connectionId: "conn-1", + provider: "openai", + dimensions: [{ unit: "tokens", window: "hourly", limit: 1000 }], + source: "manual", + }); + const plans = await store.listProviderPlans(); + expect(plans).toEqual([]); + }); + + it("setPools persists a QuotaPool retrievable via getPool", async () => { + const pool: QuotaPool = { + id: "pool-1", + connectionId: "conn-1", + name: "Primary", + createdAt: new Date().toISOString(), + allocations: [], + }; + await store.setPools([pool]); + const retrieved = await store.getPool("pool-1"); + expect(retrieved).toBeDefined(); + expect(retrieved?.id).toBe("pool-1"); + expect(retrieved?.name).toBe("Primary"); + expect(retrieved?.allocations).toEqual([]); + }); + + it("getPool returns undefined for an unknown poolId", async () => { + const result = await store.getPool("nonexistent-pool"); + expect(result).toBeUndefined(); + }); +});