diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 16f08bd283..c030b8a456 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -769,6 +769,29 @@ export const REGISTRY: Record = { ], }, + "ollama-cloud": { + id: "ollama-cloud", + alias: "ollamacloud", + format: "openai", + executor: "default", + baseUrl: "https://api.ollama.com/v1/chat/completions", + modelsUrl: "https://api.ollama.com/v1/models", + authType: "apikey", + authHeader: "bearer", + // Note: rate limits vary by plan (free = "Light usage", Pro = more, Max = 5x Pro). + // Users can generate API keys at https://ollama.com/settings/api-keys + models: [ + { id: "gemma3:27b", name: "Gemma 3 27B" }, + { id: "llama3.3:70b", name: "Llama 3.3 70B" }, + { id: "qwen3:72b", name: "Qwen3 72B" }, + { id: "devstral:24b", name: "Devstral 24B" }, + { id: "deepseek-r2:671b", name: "DeepSeek R2 671B" }, + { id: "phi4:14b", name: "Phi 4 14B" }, + { id: "mistral-small3.2:24b", name: "Mistral Small 3.2 24B" }, + ], + passthroughModels: true, + }, + cohere: { id: "cohere", alias: "cohere", diff --git a/src/app/api/db-backups/export/route.ts b/src/app/api/db-backups/export/route.ts index 77f1d80f1d..e18e2a8970 100644 --- a/src/app/api/db-backups/export/route.ts +++ b/src/app/api/db-backups/export/route.ts @@ -3,14 +3,22 @@ import path from "node:path"; import fs from "node:fs"; import os from "node:os"; import { getDbInstance, SQLITE_FILE } from "@/lib/db/core"; +import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; /** * GET /api/db-backups/export — Download the current database as a .sqlite file. * * Uses SQLite's native backup API to create a consistent snapshot, * then streams it as a downloadable attachment. + * + * 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-2). */ -export async function GET() { +export async function GET(request: Request) { + if (await isAuthRequired()) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } try { if (!SQLITE_FILE || !fs.existsSync(SQLITE_FILE)) { return NextResponse.json({ error: "Database file not found" }, { status: 404 }); diff --git a/src/app/api/db-backups/import/route.ts b/src/app/api/db-backups/import/route.ts index 988df80978..80c47035cb 100644 --- a/src/app/api/db-backups/import/route.ts +++ b/src/app/api/db-backups/import/route.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; import os from "node:os"; import { getDbInstance, resetDbInstance, SQLITE_FILE } from "@/lib/db/core"; import { backupDbFile } from "@/lib/db/backup"; +import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; const MAX_UPLOAD_SIZE = 100 * 1024 * 1024; // 100 MB @@ -16,8 +17,15 @@ const REQUIRED_TABLES = ["provider_connections", "provider_nodes", "combos", "ap * * Accepts multipart/form-data with a single "file" field containing the .sqlite backup. * Validates integrity, schema, and required tables before replacing the active database. + * + * 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-3). */ export async function POST(request: Request) { + if (await isAuthRequired()) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } let tmpPath: string | null = null; try { diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index ead1f38175..c29f6810fd 100644 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { timingSafeEqual } from "node:crypto"; import { getProvider, generateAuthData, @@ -29,6 +30,18 @@ if (!globalThis.__codexCallbackState) { globalThis.__codexCallbackState = null; } +/** + * Constant-time string comparison to prevent timing-oracle attacks (CWE-208). + * Handles null/undefined safely and different-length strings. + */ +function safeEqual(a: string | null | undefined, b: string | null | undefined): boolean { + if (a == null || b == null) return a === b; + const ba = Buffer.from(String(a)); + const bb = Buffer.from(String(b)); + if (ba.length !== bb.length) return false; + return timingSafeEqual(ba, bb); +} + /** * Dynamic OAuth API Route * Handles: authorize, exchange, device-code, poll, start-callback-server, poll-callback @@ -222,11 +235,12 @@ export async function POST( if (tokenData.email) { const existing = await getProviderConnections({ provider }); const match = existing.find((c: any) => { - if (c.email !== tokenData.email || c.authType !== "oauth") return false; + // safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7) + if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false; // For Codex, also check workspaceId to avoid overwriting different workspace connections if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) { const existingWorkspace = c.providerSpecificData?.workspaceId; - return existingWorkspace === tokenData.providerSpecificData.workspaceId; + return safeEqual(existingWorkspace, tokenData.providerSpecificData.workspaceId); } return true; }); @@ -292,11 +306,12 @@ export async function POST( if (result.tokens.email) { const existing = await getProviderConnections({ provider }); const match = existing.find((c: any) => { - if (c.email !== result.tokens.email || c.authType !== "oauth") return false; + // safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-8/9) + if (!safeEqual(c.email, result.tokens.email) || c.authType !== "oauth") return false; // For Codex, also check workspaceId to avoid overwriting different workspace connections if (provider === "codex" && result.tokens.providerSpecificData?.workspaceId) { const existingWorkspace = c.providerSpecificData?.workspaceId; - return existingWorkspace === result.tokens.providerSpecificData.workspaceId; + return safeEqual(existingWorkspace, result.tokens.providerSpecificData.workspaceId); } return true; }); @@ -412,11 +427,12 @@ export async function POST( if (tokenData.email) { const existing = await getProviderConnections({ provider }); const match = existing.find((c: any) => { - if (c.email !== tokenData.email || c.authType !== "oauth") return false; + // safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7) + if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false; // For Codex, also check workspaceId to avoid overwriting different workspace connections if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) { const existingWorkspace = c.providerSpecificData?.workspaceId; - return existingWorkspace === tokenData.providerSpecificData.workspaceId; + return safeEqual(existingWorkspace, tokenData.providerSpecificData.workspaceId); } return true; }); diff --git a/src/app/api/oauth/cursor/auto-import/route.ts b/src/app/api/oauth/cursor/auto-import/route.ts index 7aaf9315c3..07f5c3cfae 100644 --- a/src/app/api/oauth/cursor/auto-import/route.ts +++ b/src/app/api/oauth/cursor/auto-import/route.ts @@ -2,12 +2,21 @@ import { NextResponse } from "next/server"; import { homedir } from "os"; import { join } from "path"; import Database from "better-sqlite3"; +import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; /** * GET /api/oauth/cursor/auto-import - * Auto-detect and extract Cursor tokens from local SQLite database + * Auto-detect and extract Cursor tokens from local SQLite database. + * + * 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-4). */ -export async function GET() { +export async function GET(request: Request) { + if (await isAuthRequired()) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } + try { const platform = process.platform; let dbPath; diff --git a/src/app/api/oauth/kiro/auto-import/route.ts b/src/app/api/oauth/kiro/auto-import/route.ts index 1f1cdb9f42..e13c6f0eb8 100644 --- a/src/app/api/oauth/kiro/auto-import/route.ts +++ b/src/app/api/oauth/kiro/auto-import/route.ts @@ -2,12 +2,21 @@ import { NextResponse } from "next/server"; import { readFile, readdir } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; +import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; /** * GET /api/oauth/kiro/auto-import - * Auto-detect and extract Kiro refresh token from AWS SSO cache + * Auto-detect and extract Kiro refresh token from AWS SSO cache. + * + * 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-5). */ -export async function GET() { +export async function GET(request: Request) { + if (await isAuthRequired()) { + if (!(await isAuthenticated(request))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + } + try { const cachePath = join(homedir(), ".aws/sso/cache");