mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(#255): add Ollama Cloud provider (api.ollama.com/v1) - New registry entry 'ollama-cloud' (alias: ollamacloud) - OpenAI-compatible, Bearer API key auth - 7 pre-populated models: Gemma 3 27B, Llama 3.3 70B, Qwen3 72B, Devstral 24B, DeepSeek R2 671B, Phi 4 14B, Mistral Small 3.2 24B - passthroughModels: true for dynamic model names fix(#258): security hardening — all 9 findings addressed - [CRITICAL] db-backups/export: add isAuthRequired + isAuthenticated guard - [CRITICAL] db-backups/import: add isAuthRequired + isAuthenticated guard - [HIGH] oauth/cursor/auto-import: add isAuthRequired + isAuthenticated guard - [HIGH] oauth/kiro/auto-import: add isAuthRequired + isAuthenticated guard - [LOW x4] oauth/[provider]/[action]: replace === with safeEqual() using crypto.timingSafeEqual() for CWE-208 (timing-oracle attacks) on email and workspaceId comparisons across all 3 handler branches - [FALSE POSITIVE] Finding #1 (package.json 'reset-password' binary name) is not a hardcoded credential — no change needed Tests: 590/594 pass (4 pre-existing failures in combo-circuit-breaker)
This commit is contained in:
@@ -769,6 +769,29 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
],
|
||||
},
|
||||
|
||||
"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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user