fix: expose baidu netdisk secret source contract

This commit is contained in:
Codex
2026-05-21 10:09:22 +00:00
parent 1749897d1a
commit f701eab29d
8 changed files with 245 additions and 30 deletions
@@ -152,6 +152,19 @@ for (const item of serviceCases) {
`${item.serviceId} dry-run must narrow Compose recreate command`,
target,
);
if (item.serviceId === "baidu-netdisk") {
const runtimeSecrets = asRecord(plan.runtimeSecrets, "baidu-netdisk dry-run runtimeSecrets");
const secretSource = asRecord(runtimeSecrets.secretSource, "baidu-netdisk dry-run secretSource");
const requirements = asArray(runtimeSecrets.requirements, "baidu-netdisk dry-run requirements").map((requirement, index) => asRecord(requirement, `baidu-netdisk dry-run requirement ${index}`));
assertCondition(runtimeSecrets.check === "runtime-secret-presence", "baidu-netdisk dry-run should expose runtime secret contract", runtimeSecrets);
assertCondition(secretSource.kind === "compose-env-file", "baidu-netdisk dry-run should expose canonical secret source kind", secretSource);
assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, "baidu-netdisk dry-run must not print secret values", runtimeSecrets);
assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", "baidu-netdisk requiredSecretsPresent should be boolean", runtimeSecrets);
assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), "baidu-netdisk missingSecretKeys should be structured", runtimeSecrets);
assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, "baidu-netdisk recommendedAction should be explicit", runtimeSecrets);
assertCondition(requirements.length === 3, "baidu-netdisk should list three required source secret keys", requirements);
assertCondition(requirements.every((requirement) => requirement.valuePrinted === false), "baidu-netdisk should never mark values printed", requirements);
}
assertCondition(validation.some((line) => line.includes("registry /v2 manifest")), `${item.serviceId} must plan registry manifest validation`, validation);
assertCondition(validation.some((line) => line.includes("image labels match service id, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
@@ -1,6 +1,7 @@
import {
baiduNetdiskAuthHealthGateStatus,
baiduNetdiskRuntimeSecretRequirements,
runtimeSecretContractFromEnvText,
runtimeSecretPresenceFromEnvText,
} from "./src/artifact-registry";
@@ -19,6 +20,21 @@ assertCondition(present.every((item) => item.present), "all baidu-netdisk secret
assertCondition(present.map((item) => item.length).join(",") === "31,30,64", "presence reports only lengths", present);
assertCondition(!JSON.stringify(present).includes("0123456789abcdef"), "secret values must not be exposed", present);
const presentContract = runtimeSecretContractFromEnvText(secretEnvText, baiduNetdiskRuntimeSecretRequirements, {
path: "/root/unidesk/.state/docker-compose.env",
exists: true,
workDir: "/root/unidesk",
composeEnvFile: ".state/docker-compose.env",
composeService: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
});
assertCondition(presentContract.secretSource.kind === "compose-env-file", "secret source should name canonical compose env file", presentContract);
assertCondition(presentContract.requiredSecretsPresent === true, "present contract should pass", presentContract);
assertCondition(presentContract.missingSecretKeys.length === 0, "present contract should not list missing keys", presentContract);
assertCondition(presentContract.recommendedAction === "none", "present contract should recommend no action", presentContract);
assertCondition(presentContract.valuesPrinted === false, "present contract must not print values", presentContract);
assertCondition(!JSON.stringify(presentContract).includes("clientsecret"), "contract must not expose fake secret values", presentContract);
const missing = runtimeSecretPresenceFromEnvText(
"UNIDESK_BAIDU_NETDISK_CLIENT_ID=clientid-clientid-clientid-0000\n",
baiduNetdiskRuntimeSecretRequirements,
@@ -30,6 +46,30 @@ assertCondition(
missing,
);
const missingContract = runtimeSecretContractFromEnvText(
"UNIDESK_BAIDU_NETDISK_CLIENT_ID=clientid-clientid-clientid-0000\n",
baiduNetdiskRuntimeSecretRequirements,
{
path: "/root/unidesk/.state/docker-compose.env",
exists: true,
workDir: "/root/unidesk",
composeEnvFile: ".state/docker-compose.env",
composeService: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
},
);
assertCondition(missingContract.requiredSecretsPresent === false, "missing contract should fail", missingContract);
assertCondition(
missingContract.missingSecretKeys.join(",") === "UNIDESK_BAIDU_NETDISK_CLIENT_SECRET,UNIDESK_BAIDU_NETDISK_TOKEN_KEY",
"missing contract should expose missing source keys",
missingContract,
);
assertCondition(
String(missingContract.recommendedAction).includes("canonical Compose env file"),
"missing contract should recommend the canonical source",
missingContract,
);
const healthy = baiduNetdiskAuthHealthGateStatus({
auth: {
configured: true,
@@ -61,10 +101,13 @@ process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"runtime secret presence reports booleans and lengths only",
"runtime secret contract exposes secretSource/requiredSecretsPresent/missingSecretKeys/recommendedAction without values",
"missing Baidu Netdisk env cannot pass the deploy contract",
"auth health gate requires configured/clientId/clientSecret/tokenKey/loggedIn",
],
present,
presentContract,
missing: missing.map((item) => ({ ...item, length: item.length })),
missingContract,
degraded,
}, null, 2)}\n`);
@@ -58,6 +58,20 @@ function assertMainServerComposeConsumer(
assertCondition(target.containerName === expectedContainerName, `${serviceId} container mismatch`, target);
assertCondition(listIncludes(target.forbiddenActions, "docker build"), `${serviceId} plan should forbid docker build`, target);
assertCondition(listIncludes(target.forbiddenActions, "docker compose build"), `${serviceId} plan should forbid compose build`, target);
if (serviceId === "baidu-netdisk") {
const runtimeSecrets = asRecord(artifact.runtimeSecrets, `${serviceId} ${environment} runtimeSecrets`);
const secretSource = asRecord(runtimeSecrets.secretSource, `${serviceId} ${environment} secretSource`);
const requirements = Array.isArray(runtimeSecrets.requirements) ? runtimeSecrets.requirements.map((item, index) => asRecord(item, `${serviceId} ${environment} requirement ${index}`)) : [];
assertCondition(runtimeSecrets.check === "runtime-secret-presence", `${serviceId} should expose secret presence check`, runtimeSecrets);
assertCondition(secretSource.kind === "compose-env-file", `${serviceId} should name compose env secret source`, secretSource);
assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, `${serviceId} must not print secret values`, runtimeSecrets);
assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", `${serviceId} requiredSecretsPresent should be boolean`, runtimeSecrets);
assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), `${serviceId} missingSecretKeys should be an array`, runtimeSecrets);
assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, `${serviceId} recommendedAction should be explicit`, runtimeSecrets);
assertCondition(requirements.length === 3, `${serviceId} should list three required source secrets`, runtimeSecrets);
assertCondition(requirements.every((item) => item.valuePrinted === false), `${serviceId} requirements must not print values`, requirements);
assertCondition(!JSON.stringify(runtimeSecrets).includes("0123456789abcdef"), `${serviceId} must not leak secret-looking values`, runtimeSecrets);
}
}
assertMainServerComposeConsumer("dev", "baidu-netdisk", "baidu-netdisk", "baidu-netdisk-backend");
+141 -21
View File
@@ -166,6 +166,7 @@ interface AuthHealthGate {
interface ComposeArtifactRuntime {
workDir: string;
composeFile: string;
composeEnvFile: string;
envFile: string;
project: string;
command: string[];
@@ -178,6 +179,36 @@ export interface RuntimeSecretPresence {
length: number;
}
export interface RuntimeSecretSource {
kind: "compose-env-file";
path: string;
exists: boolean;
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile";
workDir: string;
composeEnvFile: string;
composeService: string | null;
containerName: string | null;
valuesPrinted: false;
}
export interface RuntimeSecretRequirementStatus {
sourceEnvName: string;
containerEnvName: string;
present: boolean;
valuePrinted: false;
}
export interface RuntimeSecretContract {
check: "runtime-secret-presence";
secretSource: RuntimeSecretSource;
requiredSecretsPresent: boolean;
missingSecretKeys: string[];
recommendedAction: string;
valuesPrinted: false;
requirements: RuntimeSecretRequirementStatus[];
dryRunDisposition: "not-required" | "ready-for-live-apply" | "secret-source-blocked";
}
function todoNoteHealthProbeCommand(): string {
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
}
@@ -1553,13 +1584,12 @@ function composeLockScript(innerScript: string): string {
].join("; ");
}
function parseEnvFile(raw: string): Record<string, string> {
const values: Record<string, string> = {};
function findEnvValueLength(raw: string, key: string): number {
for (const line of raw.split(/\r?\n/u)) {
if (!line.trim() || line.trimStart().startsWith("#")) continue;
const index = line.indexOf("=");
if (index <= 0) continue;
const key = line.slice(0, index);
if (line.slice(0, index) !== key) continue;
let value = line.slice(index + 1);
if (value.startsWith("\"") && value.endsWith("\"")) {
try {
@@ -1568,20 +1598,19 @@ function parseEnvFile(raw: string): Record<string, string> {
value = value.slice(1, -1);
}
}
values[key] = value;
return value.length;
}
return values;
return 0;
}
export function runtimeSecretPresenceFromEnvText(envText: string, requirements: RuntimeSecretRequirement[]): RuntimeSecretPresence[] {
const values = parseEnvFile(envText);
return requirements.map((requirement) => {
const value = values[requirement.sourceEnvName] ?? "";
const length = findEnvValueLength(envText, requirement.sourceEnvName);
return {
sourceEnvName: requirement.sourceEnvName,
containerEnvName: requirement.containerEnvName,
present: value.length > 0,
length: value.length,
present: length > 0,
length,
};
});
}
@@ -1602,10 +1631,97 @@ function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerT
const compose = target.compose;
const workDir = compose.workDir ?? config.providerGateway.upgrade.hostProjectRoot;
const composeFile = compose.composeFile ?? config.providerGateway.upgrade.composeFile;
const envFile = join(workDir, config.providerGateway.upgrade.composeEnvFile);
const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
const envFile = join(workDir, composeEnvFile);
const project = compose.projectHint ?? (config.providerGateway.upgrade.composeProject || config.docker.projectName);
const command = ["docker", "compose", "--env-file", envFile, "-f", join(workDir, composeFile), "-p", project];
return { workDir, composeFile, envFile, project, command };
return { workDir, composeFile, composeEnvFile, envFile, project, command };
}
function runtimeSecretSourceForTarget(config: UniDeskConfig, target: ArtifactConsumerTarget): RuntimeSecretSource {
const runtime = composeArtifactRuntime(config, target);
return {
kind: "compose-env-file",
path: runtime.envFile,
exists: existsSync(runtime.envFile),
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
workDir: runtime.workDir,
composeEnvFile: runtime.composeEnvFile,
composeService: target.compose?.serviceName ?? null,
containerName: target.compose?.containerName ?? null,
valuesPrinted: false,
};
}
function runtimeSecretRecommendedAction(missing: string[]): string {
if (missing.length === 0) return "none";
return "Restore the missing source env keys in the canonical Compose env file without printing values, then rerun deploy apply --env <env> --service <service> --dry-run before any live apply.";
}
function runtimeSecretRecommendedActionForService(contract: RuntimeSecretContract, environment: ArtifactDeployEnvironment, serviceId: string): RuntimeSecretContract {
if (contract.requiredSecretsPresent) return { ...contract, recommendedAction: "none" };
return {
...contract,
recommendedAction: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${serviceId} --dry-run before any live apply.`,
};
}
function runtimeSecretContractFromPresence(
source: RuntimeSecretSource,
requirements: RuntimeSecretRequirement[],
presence: RuntimeSecretPresence[],
): RuntimeSecretContract {
const missingSecretKeys = presence.filter((item) => !item.present).map((item) => item.sourceEnvName);
const requiredSecretsPresent = requirements.length === 0 || (source.exists && missingSecretKeys.length === 0);
return {
check: "runtime-secret-presence",
secretSource: source,
requiredSecretsPresent,
missingSecretKeys,
recommendedAction: runtimeSecretRecommendedAction(missingSecretKeys),
valuesPrinted: false,
requirements: presence.map((item) => ({
sourceEnvName: item.sourceEnvName,
containerEnvName: item.containerEnvName,
present: item.present,
valuePrinted: false,
})),
dryRunDisposition: requirements.length === 0
? "not-required"
: requiredSecretsPresent
? "ready-for-live-apply"
: "secret-source-blocked",
};
}
export function runtimeSecretContractFromEnvText(
envText: string,
requirements: RuntimeSecretRequirement[],
sourceOverrides: Partial<Omit<RuntimeSecretSource, "kind" | "valuesPrinted">> = {},
): RuntimeSecretContract {
const source: RuntimeSecretSource = {
kind: "compose-env-file",
path: sourceOverrides.path ?? "<env-text>",
exists: sourceOverrides.exists ?? true,
configSource: sourceOverrides.configSource ?? "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
workDir: sourceOverrides.workDir ?? "",
composeEnvFile: sourceOverrides.composeEnvFile ?? "",
composeService: sourceOverrides.composeService ?? null,
containerName: sourceOverrides.containerName ?? null,
valuesPrinted: false,
};
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}
export function runtimeSecretContractForComposeTarget(
config: UniDeskConfig,
target: ArtifactConsumerTarget,
requirements: RuntimeSecretRequirement[] | undefined,
): RuntimeSecretContract | undefined {
if (requirements === undefined) return undefined;
const source = runtimeSecretSourceForTarget(config, target);
const envText = source.exists ? readFileSync(source.path, "utf8") : "";
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}
function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } {
@@ -1767,6 +1883,10 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
const config = readConfig();
const runtime = composeArtifactRuntime(config, target);
const secretPreflight = composeArtifactSecretPreflight(runtime.envFile, target.compose.requiredRuntimeSecrets);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined
? undefined
: runtimeSecretRecommendedActionForService(runtimeSecretsRaw, options.environment ?? "prod", spec.serviceId);
if (!secretPreflight.ok) {
return {
ok: false,
@@ -1778,6 +1898,11 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
envFile: runtime.envFile,
requirements: secretPreflight.requirements,
missing: secretPreflight.missing,
secretSource: runtimeSecrets?.secretSource,
requiredSecretsPresent: runtimeSecrets?.requiredSecretsPresent ?? false,
missingSecretKeys: runtimeSecrets?.missingSecretKeys ?? secretPreflight.missing.map((item) => item.sourceEnvName),
recommendedAction: runtimeSecrets?.recommendedAction ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
runtimeSecrets,
valuesLogged: false,
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
};
@@ -1898,6 +2023,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
requiredRuntimeSecrets: secretPreflight.requirements,
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
requiredFields: target.compose.authHealthGate.requiredFields,
passed: true,
@@ -2119,6 +2245,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
const livePolicy = environment === "prod" ? spec.prodLiveApply : "enabled";
const sourceImage = artifactImageRef(options, spec, commit);
const registryEndpoint = `http://127.0.0.1:${options.port}`;
const config = readConfig();
const k3sDeployments = target.k3s === undefined
? []
: [
@@ -2180,6 +2307,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
};
if (spec.kind === "compose" || spec.kind === "d601-compose") {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined ? undefined : runtimeSecretRecommendedActionForService(runtimeSecretsRaw, environment, spec.serviceId);
return {
...common,
target: {
@@ -2227,16 +2356,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
`${spec.serviceId} live apply gates success on /health.auth fields: ${target.compose.authHealthGate.requiredFields.join(", ")}`,
]),
],
runtimeSecrets: target.compose.requiredRuntimeSecrets === undefined ? undefined : {
check: "live-apply-preflight",
valuesPrinted: false,
requirements: target.compose.requiredRuntimeSecrets.map((item) => ({
sourceEnvName: item.sourceEnvName,
containerEnvName: item.containerEnvName,
presence: "not-read-during-dry-run",
})),
dryRunDisposition: "pending-live-check",
},
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? undefined : {
path: "/health",
requiredAuthFields: target.compose.authHealthGate.requiredFields,
+24 -1
View File
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
import { runCommand } from "./command";
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
import { runArtifactRegistryCommand } from "./artifact-registry";
import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "./artifact-registry";
import { startJob } from "./jobs";
import { coreInternalFetch } from "./microservices";
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "./code-queue-source-guard";
@@ -1216,6 +1216,27 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
return targetIsMain(service) ? writeComposeEnvFallbackPath() : "";
}
function redactedSecretContractForService(config: UniDeskConfig | null, service: UniDeskMicroserviceConfig, environment: DeployEnvironment): RuntimeSecretContract | undefined {
if (config === null || service.id !== "baidu-netdisk" || !targetIsMain(service)) return undefined;
const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
const envFile = join(config.providerGateway.upgrade.hostProjectRoot, composeEnvFile);
const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
const contract = runtimeSecretContractFromEnvText(envText, baiduNetdiskRuntimeSecretRequirements, {
path: envFile,
exists: existsSync(envFile),
workDir: config.providerGateway.upgrade.hostProjectRoot,
composeEnvFile,
composeService: service.repository.composeService,
containerName: service.repository.containerName,
});
return {
...contract,
recommendedAction: contract.requiredSecretsPresent
? "none"
: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${service.id} --dry-run before any live apply.`,
};
}
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return targetWorkDir(service);
return "";
@@ -2946,6 +2967,7 @@ function environmentDryRunPlan(
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment);
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
const runtimeSecrets = serviceConfig === null ? undefined : redactedSecretContractForService(config, serviceConfig, environment);
const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null;
const effectiveTarget = unsupported
? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported")
@@ -2992,6 +3014,7 @@ function environmentDryRunPlan(
noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build",
dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
runtimeSecrets,
},
target: effectiveTarget,
validation: unsupported