// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. secrets-and-egress module for scripts/src/platform-infra.ts. // Moved mechanically from scripts/src/platform-infra.ts:1974-2225 for #903. import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import { startJob } from "../jobs"; import type { RenderedCliResult } from "../output"; import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy"; import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service"; import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library"; import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import type { EgressProxySecretMaterial, EgressProxySubscriptionDiagnostics, ExternalActiveSecretMaterial, PublicExposureSecretMaterial, Sub2ApiConfig, Sub2ApiEgressProxyConfig, Sub2ApiTargetConfig } from "./entry"; import { secretRoot, writeEnvFile } from "./apply-script"; import { configPath, requiredSecretKeys } from "./entry"; import { analyzeEgressProxySubscription, renderSingBoxConfig, renderSingBoxMasterShadowsocksConfig } from "./pk01-public-exposure"; export function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean { return yaml.split(/^---\s*$/mu).some((document) => { return /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document) && /^\s*name:\s*allow-all\s*$/mu.test(document) && new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document) && /^\s*podSelector:\s*\{\}\s*$/mu.test(document) && /^\s*-\s*Ingress\s*$/mu.test(document) && /^\s*-\s*Egress\s*$/mu.test(document) && /^\s*ingress:\s*\n\s*-\s*\{\}\s*$/mu.test(document) && /^\s*egress:\s*\n\s*-\s*\{\}\s*$/mu.test(document); }); } export function hasDeploymentReplicas(yaml: string, name: string, replicas: number): boolean { return yaml.split(/^---\s*$/mu).some((document) => { return /^\s*kind:\s*Deployment\s*$/mu.test(document) && new RegExp(`^\\s*name:\\s*${escapeRegExp(name)}\\s*$`, "mu").test(document) && new RegExp(`^\\s*replicas:\\s*${replicas}\\s*$`, "mu").test(document); }); } export function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } export function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalActiveSecretMaterial { const database = sub2api.runtime.database; const root = secretRoot(sub2api); const dbSource = readEnvSourceFile({ root, sourceRef: database.sourceRef, missingMessage: (sourcePath) => `external-active requires ${redactRepoPath(sourcePath)}; run platform-db postgres apply/status first to materialize the PK01 DB credential source`, }); const dbValues = dbSource.values; const dbUser = requiredEnvValue(dbValues, database.sourceKeys.user, database.sourceRef); const dbPassword = requiredEnvValue(dbValues, database.sourceKeys.password, database.sourceRef); const dbName = requiredEnvValue(dbValues, database.sourceKeys.dbName, database.sourceRef); if (dbUser !== database.user) throw new Error(`${database.sourceRef}.${database.sourceKeys.user} does not match ${configPath}.runtime.database.user`); if (dbName !== database.dbName) throw new Error(`${database.sourceRef}.${database.sourceKeys.dbName} does not match ${configPath}.runtime.database.dbName`); const appSourcePath = join(root, sub2api.runtime.secrets.appSourceRef); const existedBefore = existsSync(appSourcePath); const existing = existedBefore ? parseEnvFile(readFileSync(appSourcePath, "utf8")) : {}; const next = { ...existing }; next.POSTGRES_PASSWORD = dbPassword; if (next.ADMIN_PASSWORD === undefined || next.ADMIN_PASSWORD.length === 0) next.ADMIN_PASSWORD = randomBytes(16).toString("hex"); if (next.JWT_SECRET === undefined || next.JWT_SECRET.length === 0) next.JWT_SECRET = randomBytes(32).toString("hex"); if (next.TOTP_ENCRYPTION_KEY === undefined || next.TOTP_ENCRYPTION_KEY.length === 0) next.TOTP_ENCRYPTION_KEY = randomBytes(32).toString("hex"); const values: Record = { POSTGRES_PASSWORD: next.POSTGRES_PASSWORD, ADMIN_PASSWORD: next.ADMIN_PASSWORD, JWT_SECRET: next.JWT_SECRET, TOTP_ENCRYPTION_KEY: next.TOTP_ENCRYPTION_KEY, }; const missing = requiredSecretKeys.filter((key) => values[key].length === 0); if (missing.length > 0) throw new Error(`external-active app secret source is missing ${missing.join(", ")}`); const action = !existedBefore ? "create" : requiredSecretKeys.some((key) => existing[key] !== values[key]) ? "update" : "none"; if (action !== "none") writeEnvFile(appSourcePath, { ...existing, ...values }); return { sourceRef: database.sourceRef, sourcePath: dbSource.sourcePathRedacted, appSourceRef: sub2api.runtime.secrets.appSourceRef, appSourcePath: redactRepoPath(appSourcePath), action, fingerprint: fingerprintSecretValues(values, [...requiredSecretKeys]), values, }; } export function preparePublicExposureSecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): PublicExposureSecretMaterial | null { const exposure = target.publicExposure; if (exposure === null || !exposure.enabled) return null; return prepareFrpcSecret({ secretRoot: secretRoot(sub2api), exposure, sourcePathRedactor: redactRepoPath, parseEnvFile, requiredEnvValue, readTextFile: (sourcePath) => { if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${exposure.frpc.tokenSourceKey}; copy the PK01 frps token into the local secret source first`); return readTextFile(sourcePath); }, }); } export function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): EgressProxySecretMaterial | null { const proxy = target.egressProxy; if (proxy === null || !proxy.enabled) return null; const source = readEnvSourceFile({ root: secretRoot(sub2api), sourceRef: proxy.sourceRef, missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`, }); const values = source.values; if (proxy.sourceType === "master-shadowsocks") { const password = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef); const configJson = renderSingBoxMasterShadowsocksConfig(proxy, password); const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`; const noProxy = proxy.noProxy.join(","); return { sourceRef: proxy.sourceRef, sourcePath: source.sourcePathRedacted, secretName: proxy.secretName, secretKey: proxy.secretKey, fingerprint: fingerprintSecretValues({ password, configJson }, ["password", "configJson"]), sourceBytes: Buffer.byteLength(password, "utf8"), selectedOutbound: "shadowsocks", sourceDiagnostics: masterShadowsocksDiagnostics(proxy), configJson, proxyUrl, noProxy, valuesPrinted: false, }; } if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`); const subscriptionUrl = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef); const subscription = fetchSubscription(subscriptionUrl); const decoded = decodeSubscription(subscription.body); const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean); const analyzed = analyzeEgressProxySubscription(nodes, proxy.preferredOutbound); if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${proxy.preferredOutbound} is absent from subscription; no fallback outbound was selected`); const selected = analyzed.selected; const configJson = renderSingBoxConfig(selected, proxy); const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`; const noProxy = proxy.noProxy.join(","); return { sourceRef: proxy.sourceRef, sourcePath: source.sourcePathRedacted, secretName: proxy.secretName, secretKey: proxy.secretKey, fingerprint: fingerprintSecretValues({ subscription: subscription.body, configJson }, ["subscription", "configJson"]), sourceBytes: Buffer.byteLength(subscription.body, "utf8"), selectedOutbound: selected.kind, sourceDiagnostics: analyzed.diagnostics, configJson, proxyUrl, noProxy, valuesPrinted: false, }; } export function fetchSubscription(subscriptionUrl: string): { body: string } { const shellUrl = shQuote(subscriptionUrl); const command = `curl -fsSL --connect-timeout 10 --max-time 30 ${shellUrl}`; const proc = Bun.spawnSync(["bash", "-lc", command], { stdout: "pipe", stderr: "pipe" }); if (proc.exitCode !== 0) { const stderr = new TextDecoder().decode(proc.stderr).slice(-500); throw new Error(`failed to fetch master VPN subscription: exit=${proc.exitCode} stderr=${stderr}`); } const body = new TextDecoder().decode(proc.stdout).trim(); if (body.length === 0) throw new Error("master VPN subscription is empty"); return { body }; } export function decodeSubscription(raw: string): string { if (raw.includes("://")) return raw; const normalized = raw.replace(/\s+/gu, ""); const padding = "=".repeat((4 - (normalized.length % 4)) % 4); try { return Buffer.from(`${normalized}${padding}`, "base64").toString("utf8"); } catch (error) { throw new Error(`master VPN subscription is neither URI list nor base64 URI list: ${String(error)}`); } } export function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics { try { if (proxy.sourceType === "master-shadowsocks") return masterShadowsocksDiagnostics(proxy); const source = readEnvSourceFile({ root: secretRoot(sub2api), sourceRef: proxy.sourceRef, missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`, }); if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`); const subscriptionUrl = requiredEnvValue(source.values, proxy.sourceKey, proxy.sourceRef); const subscription = fetchSubscription(subscriptionUrl); const decoded = decodeSubscription(subscription.body); const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean); return analyzeEgressProxySubscription(nodes, proxy.preferredOutbound).diagnostics; } catch (error) { return { ok: false, sourceType: proxy.sourceType, preferredOutbound: proxy.preferredOutbound, candidateCount: 0, supportedCount: 0, unsupportedCount: 0, byKind: {}, selectedOutbound: null, selectedFingerprint: null, candidates: [], error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500), valuesPrinted: false, }; } } export function masterShadowsocksDiagnostics(proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics { const shadowsocks = proxy.masterShadowsocks; if (shadowsocks === null) { return { ok: false, sourceType: proxy.sourceType, preferredOutbound: proxy.preferredOutbound, candidateCount: 0, supportedCount: 0, unsupportedCount: 0, byKind: {}, selectedOutbound: null, selectedFingerprint: null, candidates: [], error: "masterShadowsocks is missing", valuesPrinted: false, }; } const fingerprint = fingerprintSecretValues({ outbound: `${shadowsocks.serverHost}:${shadowsocks.serverPort}:${shadowsocks.method}` }, ["outbound"]); return { ok: true, sourceType: "master-shadowsocks", preferredOutbound: null, candidateCount: 1, supportedCount: 1, unsupportedCount: 0, byKind: { shadowsocks: 1 }, selectedOutbound: "shadowsocks", selectedFingerprint: fingerprint, candidates: [ { sourceLine: 1, kind: "shadowsocks", fingerprint, paramKeys: ["serverHost", "serverPort", "method"], selected: true, }, ], valuesPrinted: false, }; }