// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config-utils module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3686-3799 for #903. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import type { RenderedCliResult } from "../output"; import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service"; import { shortSha256Fingerprint } from "../platform-infra-ops-library"; import { codexPoolSentinelSummary, codexPoolSentinelRuntimeImage, readCodexPoolSentinelConfig, renderCodexPoolSentinelManifest, type CodexPoolSentinelConfig, type CodexPoolSentinelProfileSecret, } from "../platform-infra-sub2api-codex-sentinel"; import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import { runSshCommandCapture, type SshCaptureResult } from "../ssh"; import type { CodexSentinelProtectPolicy } from "./types"; import { codexPoolConfigPath } from "./types"; export function normalizeBaseUrl(value: string | null): string | null { if (value === null) return null; const trimmed = value.trim().replace(/\/+$/u, ""); if (trimmed.length === 0) return null; try { const parsed = new URL(trimmed); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null; return parsed.toString().replace(/\/+$/u, ""); } catch { return null; } } export function stringValue(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } export function requiredStringConfigField(obj: Record, key: string, path: string): string { const value = stringValue(obj[key]); const prefix = path.length > 0 ? `${path}.` : ""; if (value === null) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a non-empty string`); return value; } export function readRequiredDescription(value: unknown, key: string, maxChars: number): string { const text = stringValue(value); if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`); if (text.length > maxChars) throw new Error(`${codexPoolConfigPath}.${key} must be at most ${maxChars} characters`); if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`); return text; } export function readRequiredEmail(value: unknown, key: string): string { const text = stringValue(value); if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`); if (!/^[^@\s]+@[^@\s]+[.][^@\s]+$/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must be an email address`); return text; } export function readRequiredBaseUrl(value: unknown, key: string): string { const baseUrl = normalizeBaseUrl(stringValue(value)); if (baseUrl === null) throw new Error(`${codexPoolConfigPath}.${key} must be a valid http(s) URL`); return baseUrl; } export function readRequiredPort(value: unknown, key: string): number { const port = numberValue(value); if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`); } return port; } export function readRequiredPositiveNumber(value: unknown, key: string): number { const number = numberValue(value); if (number === null || number <= 0) throw new Error(`${codexPoolConfigPath}.${key} must be > 0`); return number; } export function requiredRecordConfigField(obj: Record, key: string, path: string): Record { const value = obj[key]; const prefix = path.length > 0 ? `${path}.` : ""; if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a YAML object`); return value; } export function integerConfigField(obj: Record, key: string, path: string): number { const value = obj[key]; const prefix = path.length > 0 ? `${path}.` : ""; if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an integer`); return value; } export function integerArrayConfigField(obj: Record, key: string, path: string): number[] { const value = obj[key]; const prefix = path.length > 0 ? `${path}.` : ""; if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) { throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an array of integers`); } return value; } export function numberValue(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim().length > 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } export function booleanValue(value: unknown): boolean | null { if (typeof value === "boolean") return value; if (typeof value === "string") { if (value.trim() === "true") return true; if (value.trim() === "false") return false; } return null; } export function validateKubernetesName(value: string, key: string, strictDns: boolean): void { const pattern = strictDns ? /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u : /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u; if (!pattern.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`); } export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function fingerprint(value: string): string { return shortSha256Fingerprint(value); } export function codexPoolSentinelProbeConfigFingerprint(input: { accountName: string; profile: string; baseUrl: string; apiKeyFingerprint: string | null; upstreamUserAgent: string | null; openaiResponsesWebSocketsV2Mode: string | null; trustUpstream: boolean; sentinelProtect: CodexSentinelProtectPolicy; }): string { return fingerprint(JSON.stringify({ accountName: input.accountName, profile: input.profile, baseUrl: normalizeBaseUrl(input.baseUrl) ?? input.baseUrl, apiKeyFingerprint: input.apiKeyFingerprint, upstreamUserAgent: input.upstreamUserAgent, openaiResponsesWebSocketsV2Mode: input.openaiResponsesWebSocketsV2Mode, trustUpstream: input.trustUpstream, sentinelProtect: input.sentinelProtect, })); }