import { z } from "zod"; const runtimeBoolean = (defaultValue: boolean) => z .enum(["true", "false"]) .default(defaultValue ? "true" : "false") .transform((value) => value === "true"); const browserHttpUrl = z .string() .url() .superRefine((value, context) => { const url = new URL(value); if (!["http:", "https:"].includes(url.protocol)) { context.addIssue({ code: z.ZodIssueCode.custom, message: "仅支持 HTTP(S) 地址" }); } if (url.username || url.password) { context.addIssue({ code: z.ZodIssueCode.custom, message: "浏览器运行时地址不能包含凭据" }); } if (url.hash) { context.addIssue({ code: z.ZodIssueCode.custom, message: "浏览器运行时地址不能包含片段" }); } }); const optionalBrowserHttpUrl = z .string() .trim() .default("") .superRefine((value, context) => { if (!value) return; const parsed = browserHttpUrl.safeParse(value); if (!parsed.success) { context.addIssue({ code: z.ZodIssueCode.custom, message: "必须是安全的 HTTP(S) 地址" }); } }) .transform((value) => value.replace(/\/+$/, "")); const keycloakIssuer = z .string() .trim() .default("") .superRefine((value, context) => { if (!value) return; const parsed = browserHttpUrl.safeParse(value); if (!parsed.success) { context.addIssue({ code: z.ZodIssueCode.custom, message: "Keycloak issuer 必须是安全的 HTTP(S) 地址" }); return; } const url = new URL(value); if (url.search || !/\/realms\/[^/]+\/?$/.test(url.pathname)) { context.addIssue({ code: z.ZodIssueCode.custom, message: "Keycloak issuer 必须指向具体 Realm" }); } }) .transform((value) => value.replace(/\/+$/, "")); const runtimeConfigSchema = z .object({ TJWATER_AUTH_MODE: z.enum(["required", "disabled"]).default("required"), TJWATER_KEYCLOAK_ISSUER: keycloakIssuer, TJWATER_KEYCLOAK_CLIENT_ID: z.string().trim().default("next-tjwater"), TJWATER_MAPBOX_ACCESS_TOKEN: z.string().default(""), TJWATER_TIANDITU_TOKEN: z.string().default(""), TJWATER_MAP_URL: browserHttpUrl.default("https://geoserver.waternetwork.cn/geoserver"), TJWATER_GEOSERVER_WORKSPACE: z.string().trim().min(1).default("tjwater_next"), TJWATER_SERVER_API_BASE_URL: browserHttpUrl.default("http://127.0.0.1:8000"), TJWATER_AGENT_URL: browserHttpUrl.default("http://127.0.0.1:8787"), TJWATER_WORKBENCH_RUNTIME_MODE: z.enum(["auto", "demo", "history", "backend"]).default("auto"), TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: optionalBrowserHttpUrl, TJWATER_PERIPHERAL_SAMPLE_TIME_MODE: z.enum(["demo", "recent"]).default("demo"), TJWATER_PERIPHERAL_SAMPLE_DEMO_START: z .string() .datetime({ offset: true }) .default("2026-09-17T00:00:00Z"), TJWATER_PERIPHERAL_SAMPLE_DEMO_END: z .string() .datetime({ offset: true }) .default("2026-09-17T12:00:00Z"), TJWATER_PERIPHERAL_SAMPLE_RECENT_HOURS: z.coerce.number().int().min(1).max(168).default(8), TJWATER_ENABLE_DEV_PANEL: runtimeBoolean(false), TJWATER_ENABLE_MSW: runtimeBoolean(false) }) .superRefine((config, context) => { const demoStart = Date.parse(config.TJWATER_PERIPHERAL_SAMPLE_DEMO_START); const demoEnd = Date.parse(config.TJWATER_PERIPHERAL_SAMPLE_DEMO_END); if (demoEnd <= demoStart || demoEnd - demoStart > 7 * 24 * 60 * 60 * 1000) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["TJWATER_PERIPHERAL_SAMPLE_DEMO_END"], message: "外围演示窗口必须晚于开始时间且不超过 7 天" }); } if (config.TJWATER_AUTH_MODE !== "required") return; if (!config.TJWATER_KEYCLOAK_ISSUER) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["TJWATER_KEYCLOAK_ISSUER"], message: "启用认证时必须配置 Keycloak issuer" }); } if (!config.TJWATER_KEYCLOAK_CLIENT_ID) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["TJWATER_KEYCLOAK_CLIENT_ID"], message: "启用认证时必须配置 Keycloak client ID" }); } }); declare global { var __TJWATER_CONFIG__: unknown; } export type RuntimeConfig = z.infer; export function parseRuntimeConfig(value: unknown): RuntimeConfig { return runtimeConfigSchema.parse(value ?? {}); } export const env = parseRuntimeConfig(globalThis.__TJWATER_CONFIG__);