fix(sub2api): add JD01 account local proxy

This commit is contained in:
Codex
2026-06-30 10:38:03 +00:00
parent b1fda2a479
commit b073a1c52c
9 changed files with 395 additions and 29 deletions
+53 -2
View File
@@ -15,7 +15,7 @@ import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote }
import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import type { Sub2ApiConfig, Sub2ApiDefaults, Sub2ApiEgressProxyConfig, Sub2ApiHostDockerConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import type { Sub2ApiAccountLocalProxyConfig, Sub2ApiConfig, Sub2ApiDefaults, Sub2ApiEgressProxyConfig, Sub2ApiHostDockerConfig, Sub2ApiMasterShadowsocksConfig, Sub2ApiPublicExposureConfig, Sub2ApiTargetConfig } from "./entry";
import { secretRoot } from "./apply-script";
import { configPath } from "./entry";
import { normalizePublicBaseUrl, validateHostOrIp, validateHostname, validateKubernetesResourceName, validatePort, validateProxyName, validateProxyUrl } from "./manifest";
@@ -174,6 +174,7 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
const hostDocker = parseHostDockerConfig(record.hostDocker, path, runtimeMode);
const publicExposure = parsePublicExposureConfig(record.publicExposure, path);
const egressProxy = parseEgressProxyConfig(record.egressProxy, path);
const accountLocalProxy = parseAccountLocalProxyConfig(record.accountLocalProxy, path);
if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${configPath}.${path}.id must be a simple target id`);
if (!/^[A-Za-z0-9:_./-]+$/u.test(route)) throw new Error(`${configPath}.${path}.route has an unsupported format`);
if (!isKubernetesName(targetNamespace)) throw new Error(`${configPath}.${path}.namespace must be a Kubernetes namespace name`);
@@ -181,8 +182,9 @@ export function parseTargets(root: Record<string, unknown>, defaultTargetId: str
if (redisReplicas < 0) throw new Error(`${configPath}.${path}.redisReplicas must be >= 0`);
if (runtimeMode === "host-docker" && databaseMode !== "external-active") throw new Error(`${configPath}.${path}.databaseMode must be external-active for runtimeMode=host-docker`);
if (runtimeMode === "host-docker" && egressProxy?.enabled === true) throw new Error(`${configPath}.${path}.egressProxy must be disabled or omitted for runtimeMode=host-docker`);
if (runtimeMode === "host-docker" && accountLocalProxy?.enabled === true) throw new Error(`${configPath}.${path}.accountLocalProxy must be disabled or omitted for runtimeMode=host-docker`);
if (runtimeMode === "host-docker" && publicExposure?.enabled === true && publicExposure.mode !== "pk01-local") throw new Error(`${configPath}.${path}.publicExposure.mode must be pk01-local for runtimeMode=host-docker`);
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, hostDocker, publicExposure, egressProxy };
return { id, route, namespace: targetNamespace, runtimeMode, role, enabled, databaseMode, redisMode, appReplicas, redisReplicas, image, dependencyImages, hostDocker, publicExposure, egressProxy, accountLocalProxy };
});
const ids = new Set<string>();
for (const target of targets) {
@@ -372,6 +374,36 @@ export function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgr
return proxy;
}
export function parseAccountLocalProxyConfig(value: unknown, path: string): Sub2ApiAccountLocalProxyConfig | null {
if (value === undefined || value === null) return null;
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.accountLocalProxy must be an object`);
const record = value as Record<string, unknown>;
const sourceConfigRef = stringField(record, "sourceConfigRef", `${path}.accountLocalProxy`);
const source = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.${path}.accountLocalProxy.sourceConfigRef`);
if (source.sourceType !== "master-shadowsocks" || source.masterShadowsocks === null) {
throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference a master-shadowsocks source`);
}
const proxy: Sub2ApiAccountLocalProxyConfig = {
enabled: booleanField(record, "enabled", `${path}.accountLocalProxy`),
containerName: stringField(record, "containerName", `${path}.accountLocalProxy`),
secretName: stringField(record, "secretName", `${path}.accountLocalProxy`),
secretKey: stringField(record, "secretKey", `${path}.accountLocalProxy`),
image: stringField(record, "image", `${path}.accountLocalProxy`),
imagePullPolicy: enumField(record, "imagePullPolicy", `${path}.accountLocalProxy`, ["Always", "IfNotPresent", "Never"] as const),
listenHost: enumField(record, "listenHost", `${path}.accountLocalProxy`, ["127.0.0.1"] as const),
listenPort: integerField(record, "listenPort", `${path}.accountLocalProxy`),
sourceConfigRef,
sourceFingerprint: source.fingerprint,
sourceRef: source.sourceRef,
sourceKey: source.sourceKey,
sourceType: "master-shadowsocks",
masterShadowsocks: source.masterShadowsocks,
healthProbeUrl: source.healthProbeUrl,
};
validateAccountLocalProxyConfig(proxy, path);
return proxy;
}
export function parseMasterShadowsocksConfig(value: unknown, path: string): Sub2ApiMasterShadowsocksConfig {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.${path} must be an object`);
const record = value as Record<string, unknown>;
@@ -582,3 +614,22 @@ export function validateEgressProxyConfig(config: Sub2ApiEgressProxyConfig, path
const url = new URL(config.healthProbeUrl);
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.egressProxy.healthProbeUrl must use http:// or https://`);
}
export function validateAccountLocalProxyConfig(config: Sub2ApiAccountLocalProxyConfig, path: string): void {
validateKubernetesResourceName(config.containerName, `${path}.accountLocalProxy.containerName`);
validateKubernetesResourceName(config.secretName, `${path}.accountLocalProxy.secretName`);
if (config.secretKey !== "config.json") throw new Error(`${configPath}.${path}.accountLocalProxy.secretKey must be config.json`);
if (!isImageReference(config.image)) throw new Error(`${configPath}.${path}.accountLocalProxy.image has an unsupported format`);
if (config.listenHost !== "127.0.0.1") throw new Error(`${configPath}.${path}.accountLocalProxy.listenHost must be 127.0.0.1`);
validatePort(config.listenPort, `${path}.accountLocalProxy.listenPort`);
if (!config.sourceConfigRef.startsWith("config/") || !config.sourceConfigRef.includes("#sources.") || config.sourceConfigRef.includes("..")) {
throw new Error(`${configPath}.${path}.accountLocalProxy.sourceConfigRef must reference config/*.yaml#sources.<id>`);
}
if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceRef has an unsupported format`);
if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.accountLocalProxy.sourceKey must be an env key`);
validateHostOrIp(config.masterShadowsocks.serverHost, `${path}.accountLocalProxy.masterShadowsocks.serverHost`);
validatePort(config.masterShadowsocks.serverPort, `${path}.accountLocalProxy.masterShadowsocks.serverPort`);
if (!/^[A-Za-z0-9-]+$/u.test(config.masterShadowsocks.method)) throw new Error(`${configPath}.${path}.accountLocalProxy.masterShadowsocks.method has an unsupported format`);
const url = new URL(config.healthProbeUrl);
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${configPath}.${path}.accountLocalProxy.healthProbeUrl must use http:// or https://`);
}