import { z } from "zod"; import { ACCOUNT_FALLBACK_STRATEGY_VALUES, ROUTING_STRATEGY_VALUES, } from "@/shared/constants/routingStrategies"; import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints"; import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; import { isForbiddenUpstreamHeaderName, isForbiddenCustomHeaderName, } from "@/shared/constants/upstreamHeaders"; import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; export const proxyConfigSchema = z .object({ type: z .preprocess( (value) => (typeof value === "string" ? value.trim().toLowerCase() : value), z.enum(["http", "https", "socks5"]) ) .optional(), host: z.string().trim().min(1).optional(), port: z.coerce.number().int().min(1).max(65535).optional(), username: z.string().optional(), password: z.string().optional(), }) .strict(); export const updateProxyConfigSchema = z .object({ proxy: proxyConfigSchema.nullable().optional(), global: proxyConfigSchema.nullable().optional(), providers: z.record(z.string().trim().min(1), proxyConfigSchema.nullable()).optional(), combos: z.record(z.string().trim().min(1), proxyConfigSchema.nullable()).optional(), keys: z.record(z.string().trim().min(1), proxyConfigSchema.nullable()).optional(), level: z.enum(["global", "provider", "combo", "key"]).optional(), id: z.string().optional(), }) .strict() .superRefine((value, ctx) => { const hasPayload = value.proxy !== undefined || value.global !== undefined || value.providers !== undefined || value.combos !== undefined || value.keys !== undefined || value.level !== undefined; if (!hasPayload) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "No valid fields to update", path: [], }); } if (value.level !== undefined && value.proxy === undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "proxy is required when level is provided", path: ["proxy"], }); } if (value.level && value.level !== "global" && !value.id?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "id is required for provider/combo/key level updates", path: ["id"], }); } }); export const testProxySchema = z.object({ proxy: z.object({ type: z.string().optional(), host: z.string().trim().min(1, "proxy.host is required"), port: z.union([z.string(), z.number()]), username: z.string().optional(), password: z.string().optional(), }), }); export const inlineProxyAssignmentSchema = z .object({ scope: z.enum(["global", "provider", "account", "combo", "key"]), scopeId: z.string().trim().nullable().optional(), }) .strict() .superRefine((value, ctx) => { if (value.scope !== "global" && !value.scopeId?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scopeId is required for non-global scope", path: ["scopeId"], }); } }); export const proxyRegistryFieldsSchema = z .object({ name: z.string().trim().min(1, "name is required").max(120), type: z .preprocess( (value) => (typeof value === "string" ? value.trim().toLowerCase() : value), z.enum(["http", "https", "socks5", "vercel", "deno", "cloudflare"]) ) .optional() .default("http"), host: z.string().trim().min(1, "host is required").max(255), port: z.coerce.number().int().min(1).max(65535), username: z.string().optional(), password: z.string().optional(), region: z.string().trim().max(64).nullable().optional(), notes: z.string().trim().max(1000).nullable().optional(), status: z.enum(["active", "inactive"]).optional().default("active"), source: z .enum([ "manual", "oneproxy", "dashboard-custom", "vercel-relay", "deno-relay", "cloudflare-relay", ]) .optional(), // Address-family egress policy (#3777): "auto" keeps the prior dual-stack behavior; // "ipv4"/"ipv6" pin the connection to that family (no v4 leak under an IPv6-only proxy). family: z.enum(["auto", "ipv4", "ipv6"]).optional().default("auto"), }) .strict(); export const createProxyRegistrySchema = proxyRegistryFieldsSchema .extend({ assignment: inlineProxyAssignmentSchema.optional(), }) .strict(); export const updateProxyRegistrySchema = proxyRegistryFieldsSchema .partial() .extend({ id: z.string().trim().min(1, "id is required"), assignment: inlineProxyAssignmentSchema.optional(), }) .strict(); export const bulkImportProxiesSchema = z .object({ items: z .array(proxyRegistryFieldsSchema) .min(1, "At least one proxy is required") .max(100, "Maximum 100 proxies per import"), }) .strict(); export const proxyAssignmentSchema = z .object({ scope: z.enum(["global", "provider", "account", "combo", "key"]), scopeId: z.string().trim().nullable().optional(), proxyId: z.string().trim().nullable().optional(), }) .strict() .superRefine((value, ctx) => { if (value.scope !== "global" && !value.scopeId?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scopeId is required for provider/account/combo/key scope", path: ["scopeId"], }); } }); export const bulkProxyAssignmentSchema = z .object({ scope: z.enum(["global", "provider", "account", "combo", "key"]), scopeIds: z.array(z.string().trim().min(1)).optional().default([]), proxyId: z.string().trim().nullable().optional(), }) .strict() .superRefine((value, ctx) => { if ( value.scope !== "global" && (!Array.isArray(value.scopeIds) || value.scopeIds.length === 0) ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scopeIds is required for provider/account/combo/key scope", path: ["scopeIds"], }); } });