ai_api / src /app /api /settings /proxy /route.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
9.62 kB
import {
getProxyConfig,
setProxyConfig,
getProxyForLevel,
deleteProxyForLevel,
resolveProxyForConnection,
getProxyAssignments,
getProxyById,
} from "../../../../lib/localDb";
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
import { updateProxyConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import {
createErrorResponse,
createErrorResponseFromUnknown,
type ApiErrorType,
} from "@/lib/api/errorResponse";
import type { z } from "zod";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
const BASE_SUPPORTED_PROXY_TYPES = new Set(["http", "https"]);
type UpdateProxyConfigInput = z.infer<typeof updateProxyConfigSchema>;
type ProxyConfigInput = NonNullable<UpdateProxyConfigInput["proxy"]>;
type ProxyMapInput = Record<string, ProxyConfigInput | null>;
type ApiRouteError = Error & { status?: number; type?: string };
const PROXY_LEVEL_TO_REGISTRY_SCOPE = {
global: "global",
provider: "provider",
combo: "combo",
key: "account",
} as const;
function isSocks5Enabled() {
// Default ON (opt-out): only an explicit falsey value disables SOCKS5.
const raw = (process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase();
return !["false", "0", "no", "off"].includes(raw);
}
function getSupportedProxyTypes() {
if (isSocks5Enabled()) {
return new Set([...BASE_SUPPORTED_PROXY_TYPES, "socks5"]);
}
return BASE_SUPPORTED_PROXY_TYPES;
}
function supportedTypesMessage() {
return isSocks5Enabled() ? "http, https, or socks5" : "http or https";
}
function createInvalidProxyError(message: string): ApiRouteError {
const error = new Error(message) as ApiRouteError;
error.status = 400;
error.type = "invalid_request";
return error;
}
function toApiRouteError(error: unknown): ApiRouteError {
if (error instanceof Error) {
return error as ApiRouteError;
}
return new Error("Unexpected error") as ApiRouteError;
}
function getRegistryScopeForLevel(
level: string
): "global" | "provider" | "combo" | "account" | undefined {
if (!Object.prototype.hasOwnProperty.call(PROXY_LEVEL_TO_REGISTRY_SCOPE, level)) {
return undefined;
}
return PROXY_LEVEL_TO_REGISTRY_SCOPE[
level as keyof typeof PROXY_LEVEL_TO_REGISTRY_SCOPE
];
}
async function getRegistryProxyForLevel(level: string, id: string | null) {
const scope = getRegistryScopeForLevel(level);
if (!scope) return null;
if (scope !== "global" && !id) return null;
const assignments = await getProxyAssignments({ scope });
const assignment =
scope === "global" ? assignments[0] : assignments.find((entry) => entry.scopeId === id);
if (!assignment?.proxyId) return null;
return getProxyById(assignment.proxyId, { includeSecrets: true });
}
function toProxyConfig(proxyData: NonNullable<Awaited<ReturnType<typeof getProxyById>>>) {
return {
type: proxyData.type,
host: proxyData.host,
port: proxyData.port,
username: proxyData.username,
password: proxyData.password,
};
}
function normalizeAndValidateProxy(
proxy: ProxyConfigInput | null | undefined,
pathLabel: string
): ProxyConfigInput | null | undefined {
if (proxy === null || proxy === undefined) return proxy;
if (typeof proxy !== "object" || Array.isArray(proxy)) {
throw createInvalidProxyError(`${pathLabel} must be an object`);
}
const type = String(proxy.type || "http").toLowerCase() as NonNullable<ProxyConfigInput["type"]>;
if (type === "socks5" && !isSocks5Enabled()) {
throw createInvalidProxyError(
"SOCKS5 proxy is disabled (remove ENABLE_SOCKS5_PROXY=false to enable — it is ON by default)"
);
}
if (type.startsWith("socks") && type !== "socks5") {
throw createInvalidProxyError(`${pathLabel}.type must be ${supportedTypesMessage()}`);
}
if (!getSupportedProxyTypes().has(type)) {
throw createInvalidProxyError(`${pathLabel}.type must be ${supportedTypesMessage()}`);
}
return { ...proxy, type } as ProxyConfigInput;
}
function normalizeAndValidateProxyMap(
proxyMap: ProxyMapInput | undefined,
mapName: string
): ProxyMapInput | undefined {
if (proxyMap === undefined) return undefined;
if (proxyMap === null || typeof proxyMap !== "object" || Array.isArray(proxyMap)) {
throw createInvalidProxyError(`${mapName} must be an object`);
}
const normalizedMap: ProxyMapInput = { ...proxyMap };
for (const [id, proxy] of Object.entries(proxyMap) as Array<[string, ProxyConfigInput | null]>) {
const normalizedProxy = normalizeAndValidateProxy(proxy, `${mapName}.${id}`);
normalizedMap[id] = normalizedProxy ?? null;
}
return normalizedMap;
}
function normalizeProxyPayload(body: UpdateProxyConfigInput): UpdateProxyConfigInput {
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw createInvalidProxyError("Request body must be an object");
}
const normalized = { ...body };
if (Object.prototype.hasOwnProperty.call(body, "proxy")) {
normalized.proxy = normalizeAndValidateProxy(body.proxy, "proxy");
}
if (Object.prototype.hasOwnProperty.call(body, "global")) {
normalized.global = normalizeAndValidateProxy(body.global, "global");
}
for (const key of ["providers", "combos", "keys"]) {
if (Object.prototype.hasOwnProperty.call(body, key)) {
normalized[key] = normalizeAndValidateProxyMap(body[key], key);
}
}
return normalized;
}
/**
* GET /api/settings/proxy — get proxy configuration
* Optional query params: ?level=global|provider|combo|key&id=xxx
* Or: ?resolve=connectionId to resolve effective proxy
*/
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const level = searchParams.get("level");
const id = searchParams.get("id");
const resolveId = searchParams.get("resolve");
// Resolve effective proxy for a connection
if (resolveId) {
const result = await resolveProxyForConnection(resolveId);
return Response.json(result);
}
if (level) {
const proxyData = await getRegistryProxyForLevel(level, id);
if (proxyData) {
return Response.json({
level,
id: level === "global" ? null : id,
proxy: toProxyConfig(proxyData),
});
}
const proxy = await getProxyForLevel(level, id);
return Response.json({ level, id, proxy });
}
// Get full config
const config = await getProxyConfig();
const providerAssignments = await getProxyAssignments({ scope: "provider" });
if (providerAssignments.length > 0) {
config.providers = { ...(config.providers || {}) };
const providerProxyResults = await Promise.all(
providerAssignments.map(async (assignment) => {
if (!assignment.scopeId || !assignment.proxyId) {
return null;
}
const proxyData = await getProxyById(assignment.proxyId, { includeSecrets: true });
if (!proxyData) return null;
return { scopeId: assignment.scopeId, proxyData };
})
);
for (const result of providerProxyResults) {
if (!result) continue;
config.providers[result.scopeId] = toProxyConfig(result.proxyData);
}
}
return Response.json(config);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to load proxy config");
}
}
/**
* PUT /api/settings/proxy — update proxy configuration
* Body: { level, id?, proxy } or legacy { global?, providers? }
*/
export async function PUT(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(updateProxyConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const body = validation.data;
const normalizedBody = normalizeProxyPayload(body);
const updated = await setProxyConfig(normalizedBody);
clearDispatcherCache();
return Response.json(updated);
} catch (error) {
const routeError = toApiRouteError(error);
const status = Number(routeError.status) || 500;
const type = (routeError.type ||
(status === 400 ? "invalid_request" : "server_error")) as ApiErrorType;
return createErrorResponse({ status, message: routeError.message, type });
}
}
/**
* DELETE /api/settings/proxy — remove proxy at a level
* Query: ?level=provider&id=xxx
*/
export async function DELETE(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const level = searchParams.get("level");
const id = searchParams.get("id");
if (!level) {
return createErrorResponse({
status: 400,
message: "level is required",
type: "invalid_request",
});
}
const updated = await deleteProxyForLevel(level, id);
clearDispatcherCache();
return Response.json(updated);
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to delete proxy");
}
}