diff --git a/.npmignore b/.npmignore index 3a1109429e..69b962ff51 100644 --- a/.npmignore +++ b/.npmignore @@ -28,6 +28,8 @@ scripts/ .vscode/ .agents/ .env* +app/.env +app/.env* eslint.config.mjs prettier.config.mjs postcss.config.mjs diff --git a/src/app/api/db-backups/exportAll/route.ts b/src/app/api/db-backups/exportAll/route.ts index 64fd705a82..3a786ae6a9 100644 --- a/src/app/api/db-backups/exportAll/route.ts +++ b/src/app/api/db-backups/exportAll/route.ts @@ -1,14 +1,20 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { getDbInstance, SQLITE_FILE } from "@/lib/db/core"; import fs from "fs"; import path from "path"; import os from "os"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; /** * GET /api/db-backups/exportAll * Exports the entire database + settings as a ZIP archive + * Security: Requires admin authentication. */ -export async function GET() { +export async function GET(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { if (!SQLITE_FILE) { return NextResponse.json( diff --git a/src/app/api/db-backups/route.ts b/src/app/api/db-backups/route.ts index c816d4b8db..167a081742 100644 --- a/src/app/api/db-backups/route.ts +++ b/src/app/api/db-backups/route.ts @@ -1,12 +1,18 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { listDbBackups, restoreDbBackup, backupDbFile } from "@/lib/localDb"; import { dbBackupRestoreSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; /** * PUT /api/db-backups — Trigger a manual backup snapshot. + * Security: Requires admin authentication. */ -export async function PUT() { +export async function PUT(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const result = backupDbFile("manual"); if (!result) { @@ -21,8 +27,13 @@ export async function PUT() { /** * GET /api/db-backups — List available database backups. + * Security: Requires admin authentication. */ -export async function GET() { +export async function GET(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const backups = await listDbBackups(); return NextResponse.json({ backups }); @@ -35,8 +46,13 @@ export async function GET() { /** * POST /api/db-backups — Restore a specific backup. * Body: { backupId: "db_2026-02-11T14-00-00-000Z_pre-write.json" } + * Security: Requires admin authentication. */ -export async function POST(request) { +export async function POST(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + let rawBody; try { rawBody = await request.json(); diff --git a/src/app/api/translator/save/route.ts b/src/app/api/translator/save/route.ts index c5ab45af66..337eb84ade 100644 --- a/src/app/api/translator/save/route.ts +++ b/src/app/api/translator/save/route.ts @@ -1,10 +1,15 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import fs from "fs"; import path from "path"; import { translatorSaveSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; + +export async function POST(request: NextRequest) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } -export async function POST(request) { let rawBody; try { rawBody = await request.json(); diff --git a/src/shared/utils/apiKey.ts b/src/shared/utils/apiKey.ts index b0969cd857..1b60814bb4 100644 --- a/src/shared/utils/apiKey.ts +++ b/src/shared/utils/apiKey.ts @@ -6,9 +6,12 @@ if (!process.env.API_KEY_SECRET) { } function getApiKeySecret(): string { - const secret = process.env.API_KEY_SECRET || "omniroute-default-insecure-api-key-secret"; + const secret = process.env.API_KEY_SECRET; if (!secret || secret.trim() === "") { - throw new Error("API_KEY_SECRET is required for API key CRC operations"); + throw new Error( + "API_KEY_SECRET is required for API key CRC operations. " + + "The startup validator (instrumentation-node.ts) should have set this automatically." + ); } return secret; } diff --git a/tests/unit/chatcore-sanitization.test.mjs b/tests/unit/chatcore-sanitization.test.mjs index 87214231b3..d25c924d45 100644 --- a/tests/unit/chatcore-sanitization.test.mjs +++ b/tests/unit/chatcore-sanitization.test.mjs @@ -365,9 +365,9 @@ test("chatCore resolves stream mode from body.stream and Accept header", async ( }); assert.equal(explicitTrue.call.headers.Accept, "text/event-stream"); - assert.equal(explicitFalse.call.headers.Accept, undefined); + assert.equal(explicitFalse.call.headers.Accept, "application/json"); assert.equal(acceptDriven.call.headers.Accept, "text/event-stream"); - assert.equal(jsonDefault.call.headers.Accept, undefined); + assert.equal(jsonDefault.call.headers.Accept, "application/json"); }); test("chatCore injects memories when enabled and memories are found", async () => { diff --git a/tests/unit/executor-codex.test.mjs b/tests/unit/executor-codex.test.mjs index 20fee2d4ad..a4002382f2 100644 --- a/tests/unit/executor-codex.test.mjs +++ b/tests/unit/executor-codex.test.mjs @@ -67,7 +67,7 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for assert.equal(standardHeaders.Authorization, "Bearer codex-token"); assert.equal(standardHeaders.Accept, "text/event-stream"); assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1"); - assert.equal(compactHeaders.Accept, undefined); + assert.equal(compactHeaders.Accept, "application/json"); }); test("CodexExecutor.transformRequest injects default instructions, clamps reasoning and strips unsupported fields", () => { diff --git a/tests/unit/plan3-p0.test.mjs b/tests/unit/plan3-p0.test.mjs index b4b6a0347a..8abba76620 100644 --- a/tests/unit/plan3-p0.test.mjs +++ b/tests/unit/plan3-p0.test.mjs @@ -249,7 +249,7 @@ test("CodexExecutor does not request SSE accept header for compact requests", () }, false ); - assert.equal(headers.Accept, undefined); + assert.equal(headers.Accept, "application/json"); }); test("CodexExecutor preserves native responses payloads for Codex passthrough", () => { diff --git a/tests/unit/qoder-executor.test.mjs b/tests/unit/qoder-executor.test.mjs index 9cbbeb73ba..e13f879852 100644 --- a/tests/unit/qoder-executor.test.mjs +++ b/tests/unit/qoder-executor.test.mjs @@ -29,6 +29,7 @@ test("QoderExecutor: buildHeaders inherits configured user agent, auth and strea "Content-Type": "application/json", "User-Agent": "Qoder-Cli", Authorization: "Bearer token", + Accept: "application/json", }); }); @@ -115,7 +116,10 @@ test("validateQoderCliPat succeeds when the validation endpoint returns OK", asy if (urlStr.includes("/ping")) { return new Response("pong", { status: 200 }); } - assert.match(urlStr, /api1\.qoder\.sh\/algo\/api\/v2\/service\/pro\/sse\/agent_chat_generation/); + assert.match( + urlStr, + /api1\.qoder\.sh\/algo\/api\/v2\/service\/pro\/sse\/agent_chat_generation/ + ); assert.equal(options.method, "POST"); assert.match(String(options.headers.Authorization), /^Bearer COSY\./); return new Response("{}", { status: 200 });