import { 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 { SshCaptureResult } from "./ssh"; import { capture, fingerprintValues, parseJsonOutput } from "./platform-infra-public-service"; import { yamlBooleanField, yamlIntegerArrayField, yamlIntegerField, yamlKubernetesNameField, yamlObjectField, parseEnvFile, yamlRecord, yamlStringArrayField, yamlStringField, } from "./platform-infra-ops-library"; export { parseEnvFile } from "./platform-infra-ops-library"; const defaultConfigPath = "config/secrets-distribution.yaml"; const fieldManager = "unidesk-secret-distribution"; interface SecretsOptions { configPath: string; scope: string | null; targetId: string | null; confirm: boolean; dryRun: boolean; wait: boolean; full: boolean; raw: boolean; } interface SecretDistributionConfig { configPath: string; version: number; kind: "unidesk-secret-distribution"; metadata: { id: string; owner: string; relatedIssues: number[] }; sources: { root: string; files: SourceFileConfig[] }; targets: DistributionTarget[]; kubernetesSecrets: KubernetesSecretConfig[]; } interface SourceFileConfig { sourceRef: string; type: "env"; requiredKeys: string[]; createIfMissing: { enabled: boolean; values: Record; randomHex: Record; randomBase64Url: Record; }; } interface DistributionTarget { id: string; route: string; namespace: string; scope: string; enabled: boolean; } interface KubernetesSecretConfig { name: string; targetId: string; secretName: string; type: "Opaque"; data: SecretDataMapping[]; } interface SecretDataMapping { sourceRef: string; sourceKey: string; targetKey: string; } interface SourceMaterial { sourceRef: string; sourcePath: string; exists: boolean; requiredKeys: string[]; presentKeys: string[]; missingKeys: string[]; action: "none" | "create" | "update" | "blocked"; generatedKeys: string[]; unmaterializedGeneratedKeys: string[]; values: Record; fingerprint: string | null; } interface SourceInspection { ok: boolean; root: string; entries: Array>; materials: Map; } interface DesiredSecret { name: string; target: DistributionTarget; secretName: string; type: "Opaque"; data: Record; keySources: Array<{ sourceRef: string; sourceKey: string; targetKey: string }>; missingKeys: Array<{ sourceRef: string; sourceKey: string; targetKey: string }>; pendingGeneratedKeys: string[]; fingerprint: string | null; } export interface EnvSourceFileMaterial { sourceRef: string; sourcePath: string; sourcePathRedacted: string; values: Record; valuesPrinted: false; } export function secretsHelp(): Record { return { command: "secrets plan|sync|status", output: "json", usage: [ "bun scripts/cli.ts secrets plan --config config/secrets-distribution.yaml --scope platform-infra", "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm", "bun scripts/cli.ts secrets status --config config/secrets-distribution.yaml --scope platform-infra", ], configTruth: defaultConfigPath, secretPolicy: "Secret values are never printed or reverse-engineered from runtime. YAML sourceRef files are the authority; sync only pushes declared keys to declared Kubernetes Secret keys.", }; } export async function runSecretsCommand(config: UniDeskConfig, args: string[]): Promise> { const [action = "plan"] = args; if (action === "help" || action === "--help" || action === "-h") return secretsHelp(); if (action === "plan") return plan(parseOptions(args.slice(1))); if (action === "sync") return await sync(config, parseOptions(args.slice(1))); if (action === "status") return await status(config, parseOptions(args.slice(1))); return { ok: false, error: "unsupported-secrets-command", args, help: secretsHelp() }; } function parseOptions(args: string[]): SecretsOptions { let configPath = defaultConfigPath; let scope: string | null = null; let targetId: string | null = null; let confirm = false; let dryRun = false; let wait = false; let full = false; let raw = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--config") { configPath = readOptionValue(args, index, "--config"); index += 1; } else if (arg.startsWith("--config=")) { configPath = arg.slice("--config=".length); } else if (arg === "--scope") { scope = simpleId(readOptionValue(args, index, "--scope"), "--scope"); index += 1; } else if (arg.startsWith("--scope=")) { scope = simpleId(arg.slice("--scope=".length), "--scope"); } else if (arg === "--target") { targetId = simpleId(readOptionValue(args, index, "--target"), "--target"); index += 1; } else if (arg.startsWith("--target=")) { targetId = simpleId(arg.slice("--target=".length), "--target"); } else if (arg === "--confirm") { confirm = true; } else if (arg === "--dry-run") { dryRun = true; } else if (arg === "--wait") { wait = true; } else if (arg === "--full") { full = true; } else if (arg === "--raw") { raw = true; full = true; } else { throw new Error(`unsupported secrets option: ${arg}`); } } if (confirm && dryRun) throw new Error("secrets sync accepts only one of --confirm or --dry-run"); return { configPath, scope, targetId, confirm, dryRun, wait, full, raw }; } function plan(options: SecretsOptions): Record { const distribution = readSecretDistributionConfig(options.configPath); assertSelectedTargets(distribution, options); const sources = inspectSources(distribution, false); const desired = desiredSecrets(distribution, options, sources); return { ok: sources.ok && desired.every((secret) => secret.missingKeys.length === 0), action: "secrets-plan", mutation: false, config: configSummary(distribution, options), localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary), policy: { sourceAuthority: "local YAML-declared sourceRef files under sources.root", runtimeReverseEngineering: false, valuesPrinted: false, }, next: { sync: `bun scripts/cli.ts secrets sync --config ${distribution.configPath}${options.scope === null ? "" : ` --scope ${options.scope}`}${options.targetId === null ? "" : ` --target ${options.targetId}`} --confirm`, status: `bun scripts/cli.ts secrets status --config ${distribution.configPath}${options.scope === null ? "" : ` --scope ${options.scope}`}${options.targetId === null ? "" : ` --target ${options.targetId}`}`, }, }; } async function sync(config: UniDeskConfig, options: SecretsOptions): Promise> { const distribution = readSecretDistributionConfig(options.configPath); assertSelectedTargets(distribution, options); if (!options.confirm || options.dryRun) { const planned = plan(options); return { ...planned, action: "secrets-sync", mode: "dry-run", mutation: false }; } if (!options.wait) { const jobArgs = ["bun", "scripts/cli.ts", "secrets", "sync", "--config", distribution.configPath, "--confirm", "--wait"]; if (options.scope !== null) jobArgs.push("--scope", options.scope); if (options.targetId !== null) jobArgs.push("--target", options.targetId); const job = startJob("secrets_sync", jobArgs, "Sync YAML-declared local secret source keys into declared Kubernetes Secrets without printing values"); return { ok: true, action: "secrets-sync", mode: "async-job", mutation: true, config: configSummary(distribution, options), job, statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, }; } const sources = inspectSources(distribution, true); if (!sources.ok) return { ok: false, action: "secrets-sync", mode: "blocked-local-sources", mutation: true, config: configSummary(distribution, options), localSources: sourceSummary(sources) }; const desired = desiredSecrets(distribution, options, sources); const missing = desired.flatMap((secret) => secret.missingKeys); if (missing.length > 0) { return { ok: false, action: "secrets-sync", mode: "blocked-missing-secret-data", mutation: true, config: configSummary(distribution, options), localSources: sourceSummary(sources), missing, valuesPrinted: false }; } const perTarget = await Promise.all(groupDesiredSecretsByTarget(desired).map(async (group) => await applyTargetSecrets(config, group.target, group.secrets, options))); return { ok: perTarget.every((item) => item.ok === true), action: "secrets-sync", mode: "confirmed", mutation: true, config: configSummary(distribution, options), localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary), targets: perTarget, valuesPrinted: false, }; } async function status(config: UniDeskConfig, options: SecretsOptions): Promise> { const distribution = readSecretDistributionConfig(options.configPath); assertSelectedTargets(distribution, options); const sources = inspectSources(distribution, false); const desired = desiredSecrets(distribution, options, sources); const perTarget = await Promise.all(groupDesiredSecretsByTarget(desired).map(async (group) => await statusTargetSecrets(config, group.target, group.secrets, options))); return { ok: perTarget.every((item) => item.ok === true), action: "secrets-status", mutation: false, config: configSummary(distribution, options), localSources: sourceSummary(sources), desiredSecrets: desired.map(desiredSecretSummary), targets: perTarget, valuesPrinted: false, }; } function readSecretDistributionConfig(pathArg: string): SecretDistributionConfig { const configPath = resolveConfigPath(pathArg); const label = displayConfigPath(pathArg); const root = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, label); const version = integerField(root, "version", label); const kind = stringField(root, "kind", label); if (kind !== "unidesk-secret-distribution") throw new Error(`${label}.kind must be unidesk-secret-distribution`); const metadata = objectField(root, "metadata", label); const sources = objectField(root, "sources", label); const config: SecretDistributionConfig = { configPath: label, version, kind, metadata: { id: stringField(metadata, "id", `${label}.metadata`), owner: stringField(metadata, "owner", `${label}.metadata`), relatedIssues: numberArrayField(metadata, "relatedIssues", `${label}.metadata`), }, sources: { root: stringField(sources, "root", `${label}.sources`), files: arrayOfRecords(sources.files, `${label}.sources.files`).map((item, index) => parseSourceFile(item, `${label}.sources.files[${index}]`)), }, targets: arrayOfRecords(root.targets, `${label}.targets`).map((item, index) => parseTarget(item, `${label}.targets[${index}]`)), kubernetesSecrets: arrayOfRecords(root.kubernetesSecrets, `${label}.kubernetesSecrets`).map((item, index) => parseKubernetesSecret(item, `${label}.kubernetesSecrets[${index}]`)), }; validateDistributionConfig(config); return config; } function parseSourceFile(record: Record, path: string): SourceFileConfig { const type = stringField(record, "type", path); if (type !== "env") throw new Error(`${path}.type must be env`); const createRaw = record.createIfMissing === undefined ? {} : objectField(record, "createIfMissing", path); const randomBase64UrlRaw = createRaw.randomBase64Url === undefined ? {} : objectField(createRaw, "randomBase64Url", `${path}.createIfMissing`); return { sourceRef: sourceRefField(record, "sourceRef", path), type, requiredKeys: stringArrayField(record, "requiredKeys", path).map((key, index) => envKeyValue(key, `${path}.requiredKeys[${index}]`)), createIfMissing: { enabled: createRaw.enabled === undefined ? false : booleanField(createRaw, "enabled", `${path}.createIfMissing`), values: createRaw.values === undefined ? {} : stringMapField(createRaw, "values", `${path}.createIfMissing`), randomHex: createRaw.randomHex === undefined ? {} : numberMapField(createRaw, "randomHex", `${path}.createIfMissing`), randomBase64Url: Object.fromEntries(Object.entries(randomBase64UrlRaw).map(([key, value]) => [envKeyValue(key, `${path}.createIfMissing.randomBase64Url`), randomBase64UrlSpec(value, `${path}.createIfMissing.randomBase64Url.${key}`)])), }, }; } function parseTarget(record: Record, path: string): DistributionTarget { return { id: simpleId(stringField(record, "id", path), `${path}.id`), route: stringField(record, "route", path), namespace: kubernetesNameField(record, "namespace", path), scope: simpleId(stringField(record, "scope", path), `${path}.scope`), enabled: booleanField(record, "enabled", path), }; } function parseKubernetesSecret(record: Record, path: string): KubernetesSecretConfig { const type = stringField(record, "type", path); if (type !== "Opaque") throw new Error(`${path}.type must be Opaque`); return { name: simpleId(stringField(record, "name", path), `${path}.name`), targetId: simpleId(stringField(record, "targetId", path), `${path}.targetId`), secretName: kubernetesNameField(record, "secretName", path), type, data: arrayOfRecords(record.data, `${path}.data`).map((item, index) => ({ sourceRef: sourceRefField(item, "sourceRef", `${path}.data[${index}]`), sourceKey: envKeyField(item, "sourceKey", `${path}.data[${index}]`), targetKey: kubernetesSecretKeyField(item, "targetKey", `${path}.data[${index}]`), })), }; } function validateDistributionConfig(config: SecretDistributionConfig): void { if (config.sources.files.length === 0) throw new Error(`${config.configPath}.sources.files must not be empty`); if (config.targets.length === 0) throw new Error(`${config.configPath}.targets must not be empty`); const sources = new Map(config.sources.files.map((item) => [item.sourceRef, item])); const targets = new Set(config.targets.map((item) => item.id)); const targetSecrets = new Set(); for (const secret of config.kubernetesSecrets) { if (!targets.has(secret.targetId)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name}.targetId is not declared in targets`); const secretIdentity = `${secret.targetId}/${secret.secretName}`; if (targetSecrets.has(secretIdentity)) throw new Error(`${config.configPath} declares duplicate target Secret ${secretIdentity}`); targetSecrets.add(secretIdentity); const targetKeys = new Set(); for (const item of secret.data) { const source = sources.get(item.sourceRef); if (source === undefined) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} references undeclared sourceRef ${item.sourceRef}`); if (!source.requiredKeys.includes(item.sourceKey)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} maps ${item.sourceRef}.${item.sourceKey}, but that key is not listed in sources.files.requiredKeys`); if (targetKeys.has(item.targetKey)) throw new Error(`${config.configPath}.kubernetesSecrets.${secret.name} maps duplicate target key ${item.targetKey}`); targetKeys.add(item.targetKey); } } } function inspectSources(config: SecretDistributionConfig, materialize: boolean): SourceInspection { const root = secretRoot(config); const materials = new Map(); const entries = config.sources.files.map((source) => { const sourcePath = join(root, source.sourceRef); const exists = existsSync(sourcePath); const existing = exists ? parseEnvFile(readFileSync(sourcePath, "utf8")) : {}; const next = { ...existing }; const generatedKeys: string[] = []; const unmaterializedGeneratedKeys: string[] = []; if (source.createIfMissing.enabled) { for (const [key, value] of Object.entries(source.createIfMissing.values)) { if (next[key] === undefined || next[key].length === 0) { next[key] = value; generatedKeys.push(key); } } for (const [key, bytes] of Object.entries(source.createIfMissing.randomHex)) { if (next[key] === undefined || next[key].length === 0) { next[key] = materialize ? randomBytes(bytes).toString("hex") : ``; generatedKeys.push(key); if (!materialize) unmaterializedGeneratedKeys.push(key); } } for (const [key, spec] of Object.entries(source.createIfMissing.randomBase64Url)) { if (next[key] === undefined || next[key].length === 0) { next[key] = materialize ? `${spec.prefix}${randomBytes(spec.bytes).toString("base64url")}` : ``; generatedKeys.push(key); if (!materialize) unmaterializedGeneratedKeys.push(key); } } } const missingBefore = source.requiredKeys.filter((key) => existing[key] === undefined || existing[key].length === 0); const missingKeys = source.requiredKeys.filter((key) => next[key] === undefined || next[key].length === 0); const action = missingKeys.length > 0 ? "blocked" : !exists ? "create" : missingBefore.length > 0 ? "update" : "none"; if (materialize && action !== "blocked" && (action === "create" || action === "update")) writeEnvFile(sourcePath, next); const material: SourceMaterial = { sourceRef: source.sourceRef, sourcePath, exists, requiredKeys: source.requiredKeys, presentKeys: source.requiredKeys.filter((key) => next[key] !== undefined && next[key].length > 0), missingKeys, action, generatedKeys, unmaterializedGeneratedKeys, values: next, fingerprint: missingKeys.length === 0 && unmaterializedGeneratedKeys.length === 0 ? fingerprintValues(next, source.requiredKeys) : null, }; materials.set(source.sourceRef, material); return { sourceRef: source.sourceRef, sourcePath: redactRepoPath(sourcePath), exists, requiredKeys: source.requiredKeys, presentKeys: material.presentKeys, missingKeys, action, generatedKeys, unmaterializedGeneratedKeys, fingerprint: material.fingerprint, valuesPrinted: false, }; }); return { ok: entries.every((entry) => (entry.missingKeys as string[]).length === 0), root: redactRepoPath(root), entries, materials, }; } function desiredSecrets(config: SecretDistributionConfig, options: SecretsOptions, sources: SourceInspection): DesiredSecret[] { const selectedTargetIds = new Set(selectedTargets(config, options).map((target) => target.id)); const targets = new Map(config.targets.map((target) => [target.id, target])); return config.kubernetesSecrets .map((secret) => ({ secret, target: targets.get(secret.targetId) })) .filter((item): item is { secret: KubernetesSecretConfig; target: DistributionTarget } => item.target !== undefined && item.target.enabled) .filter(({ target }) => selectedTargetIds.has(target.id)) .map(({ secret, target }) => { const data: Record = {}; const missingKeys: DesiredSecret["missingKeys"] = []; const pendingGeneratedKeys: string[] = []; for (const item of secret.data) { const source = sources.materials.get(item.sourceRef); const value = source?.values[item.sourceKey]; if (value === undefined || value.length === 0) { missingKeys.push({ sourceRef: item.sourceRef, sourceKey: item.sourceKey, targetKey: item.targetKey }); } else { data[item.targetKey] = value; } if (source?.unmaterializedGeneratedKeys.includes(item.sourceKey) === true) pendingGeneratedKeys.push(item.targetKey); } const keys = Object.keys(data); return { name: secret.name, target, secretName: secret.secretName, type: secret.type, data, keySources: secret.data, missingKeys, pendingGeneratedKeys, fingerprint: missingKeys.length === 0 && pendingGeneratedKeys.length === 0 ? fingerprintValues(data, keys) : null, }; }); } function assertSelectedTargets(config: SecretDistributionConfig, options: SecretsOptions): void { const targets = selectedTargets(config, options); if (targets.length > 0) return; const available = config.targets.filter((target) => target.enabled).map((target) => target.id).sort(); throw new Error(`no enabled secrets target matches scope=${options.scope ?? "*"} target=${options.targetId ?? "*"}; available targets: ${available.length > 0 ? available.join(", ") : ""}`); } function selectedTargets(config: SecretDistributionConfig, options: SecretsOptions): DistributionTarget[] { return config.targets .filter((target) => target.enabled) .filter((target) => options.scope === null || target.scope === options.scope) .filter((target) => options.targetId === null || target.id === options.targetId); } async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise> { if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" }; const yaml = renderSecretManifest(target, secrets); const result = await capture(config, target.route, ["sh"], applySecretScript(target, secrets, yaml)); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), target: targetSummary(target), summary: parsed, remote: secretCaptureSummary(result), ...(options.raw ? { rawCaptureOmitted: true, rawPolicy: "Secret distribution never returns raw SSH capture because remote output can contain credential-bearing diagnostics." } : {}), }; } async function statusTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise> { const result = await capture(config, target.route, ["sh"], statusSecretScript(target, secrets)); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), target: targetSummary(target), summary: parsed, remote: secretCaptureSummary(result), ...(options.raw ? { rawCaptureOmitted: true, rawPolicy: "Secret distribution never returns raw SSH capture because remote output can contain credential-bearing diagnostics." } : {}), }; } function secretCaptureSummary(result: SshCaptureResult): Record { return { exitCode: result.exitCode, stdoutBytes: Buffer.byteLength(result.stdout, "utf8"), stderrBytes: Buffer.byteLength(result.stderr, "utf8"), stdoutTailOmitted: true, stderrTailOmitted: true, valuesPrinted: false, }; } function renderSecretManifest(target: DistributionTarget, secrets: DesiredSecret[]): string { return secrets.map((secret) => `apiVersion: v1 kind: Secret metadata: name: ${secret.secretName} namespace: ${target.namespace} labels: app.kubernetes.io/managed-by: unidesk app.kubernetes.io/part-of: ${target.scope} type: ${secret.type} data: ${Object.entries(secret.data).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => ` ${key}: ${Buffer.from(value, "utf8").toString("base64")}`).join("\n")} `).join("---\n"); } function applySecretScript(target: DistributionTarget, secrets: DesiredSecret[], yaml: string): string { const manifestB64 = Buffer.from(yaml, "utf8").toString("base64"); const summaryB64 = Buffer.from(JSON.stringify(secrets.map(remoteSecretSummary)), "utf8").toString("base64"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/secrets.yaml" printf '%s' '${manifestB64}' | base64 -d >"$manifest" kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$tmp/ns.out" 2>"$tmp/ns.err" ns_rc=$? if [ "$ns_rc" -eq 0 ]; then kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err" apply_rc=$? else : >"$tmp/apply.out" printf '%s\\n' 'skipped because namespace sync failed' >"$tmp/apply.err" apply_rc=1 fi python3 - "$ns_rc" "$apply_rc" "$tmp/ns.out" "$tmp/ns.err" "$tmp/apply.out" "$tmp/apply.err" <<'PY' import base64, json, sys ns_rc, apply_rc = int(sys.argv[1]), int(sys.argv[2]) def text(path, limit=5000): try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] except FileNotFoundError: return "" payload = { "ok": ns_rc == 0 and apply_rc == 0, "namespace": "${target.namespace}", "secrets": json.loads(base64.b64decode("${summaryB64}").decode("utf-8")), "valuesPrinted": False, "steps": { "namespace": {"exitCode": ns_rc, "stdout": text(sys.argv[3]), "stderr": text(sys.argv[4])}, "apply": {"exitCode": apply_rc, "stdout": text(sys.argv[5]), "stderr": text(sys.argv[6])}, }, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; } function statusSecretScript(target: DistributionTarget, secrets: DesiredSecret[]): string { const summaryB64 = Buffer.from(JSON.stringify(secrets.map(remoteSecretSummary)), "utf8").toString("base64"); const commands = secrets.map((secret, index) => [ `kubectl -n ${target.namespace} get secret ${secret.secretName} -o json >"$tmp/secret.${index}.json" 2>"$tmp/secret.${index}.err"`, `printf '%s' "$?" >"$tmp/secret.${index}.rc"`, ].join("\n")).join("\n"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT ${commands} python3 - "$tmp" <<'PY' import base64, json, os, sys tmp = sys.argv[1] expected = json.loads(base64.b64decode("${summaryB64}").decode("utf-8")) items = [] ok = True for index, item in enumerate(expected): try: rc = int(open(os.path.join(tmp, f"secret.{index}.rc"), encoding="utf-8").read() or "1") except FileNotFoundError: rc = 1 try: observed = json.load(open(os.path.join(tmp, f"secret.{index}.json"), encoding="utf-8")) except Exception: observed = None data = (observed or {}).get("data") or {} observed_keys = sorted(data.keys()) expected_keys = item.get("keys") or [] missing = [key for key in expected_keys if key not in observed_keys] exists = rc == 0 item_ok = exists and len(missing) == 0 ok = ok and item_ok items.append({ "name": item.get("name"), "secretName": item.get("secretName"), "exists": exists, "keys": observed_keys, "expectedKeys": expected_keys, "missingKeys": missing, "ok": item_ok, "valuesPrinted": False, }) payload = {"ok": ok, "namespace": "${target.namespace}", "secrets": items, "valuesPrinted": False} print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if ok else 1) PY `; } function groupDesiredSecretsByTarget(secrets: DesiredSecret[]): Array<{ target: DistributionTarget; secrets: DesiredSecret[] }> { const groups = new Map(); for (const secret of secrets) { const existing = groups.get(secret.target.id); if (existing === undefined) groups.set(secret.target.id, { target: secret.target, secrets: [secret] }); else existing.secrets.push(secret); } return Array.from(groups.values()); } function configSummary(config: SecretDistributionConfig, options: SecretsOptions): Record { return { path: config.configPath, metadata: config.metadata, root: redactRepoPath(secretRoot(config)), scope: options.scope, targetId: options.targetId, sources: config.sources.files.map((item) => ({ sourceRef: item.sourceRef, requiredKeys: item.requiredKeys, createIfMissing: item.createIfMissing.enabled })), targets: config.targets.filter((target) => (options.scope === null || target.scope === options.scope) && (options.targetId === null || target.id === options.targetId)).map(targetSummary), valuesPrinted: false, }; } function sourceSummary(sources: SourceInspection): Record { return { ok: sources.ok, root: sources.root, entries: sources.entries, valuesPrinted: false }; } function desiredSecretSummary(secret: DesiredSecret): Record { return { name: secret.name, target: targetSummary(secret.target), secretName: secret.secretName, keys: Object.keys(secret.data).sort(), keySources: secret.keySources, missingKeys: secret.missingKeys, pendingGeneratedKeys: secret.pendingGeneratedKeys, fingerprint: secret.fingerprint, valuesPrinted: false, }; } function remoteSecretSummary(secret: DesiredSecret): Record { return { name: secret.name, secretName: secret.secretName, keys: Object.keys(secret.data).sort(), fingerprint: secret.fingerprint, valuesPrinted: false, }; } function targetSummary(target: DistributionTarget): Record { return { id: target.id, route: target.route, namespace: target.namespace, scope: target.scope }; } function readOptionValue(args: string[], index: number, option: string): string { const value = args[index + 1]; if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${option} requires a value`); return value; } function resolveConfigPath(pathArg: string): string { if (pathArg.startsWith("/") || pathArg.includes("\0")) throw new Error("--config must be a repo-relative YAML path"); if (pathArg.includes("..")) throw new Error("--config must not contain .."); return rootPath(pathArg); } function displayConfigPath(pathArg: string): string { if (pathArg.startsWith("/") || pathArg.includes("..")) throw new Error("--config must be a repo-relative YAML path without .."); return pathArg; } function secretRoot(config: SecretDistributionConfig): string { const root = config.sources.root; return isAbsolute(root) ? root : rootPath(root); } export function readTextFile(path: string): string { if (!existsSync(path)) throw new Error(`required secret source ${redactRepoPath(path)} is missing`); return readFileSync(path, "utf8"); } export function readEnvSourceFile(params: { root: string; sourceRef: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial { const sourcePath = join(params.root, params.sourceRef); if (!existsSync(sourcePath)) { throw new Error(params.missingMessage?.(sourcePath) ?? `required secret source ${redactRepoPath(sourcePath)} is missing`); } return { sourceRef: params.sourceRef, sourcePath, sourcePathRedacted: redactRepoPath(sourcePath), values: parseEnvFile(readFileSync(sourcePath, "utf8")), valuesPrinted: false, }; } export function requiredEnvValue(values: Record, key: string, sourceRef: string): string { const value = values[key]; if (value === undefined || value.length === 0) throw new Error(`${sourceRef} is missing required key ${key}`); return value; } export function fingerprintSecretValues(values: Record, keys: string[]): string { return fingerprintValues(values, keys); } function writeEnvFile(path: string, values: Record): void { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); const lines = Object.keys(values).sort().map((key) => `${key}=${quoteEnv(values[key])}`); writeFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); chmodSync(path, 0o600); } function quoteEnv(value: string): string { if (/^[A-Za-z0-9_./:@%?&=+-]+$/u.test(value)) return value; return `'${value.replaceAll("'", "'\"'\"'")}'`; } export function redactRepoPath(path: string): string { const root = rootPath(); return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path; } function boolField(value: Record | null, key: string, fallback: boolean): boolean { return typeof value?.[key] === "boolean" ? value[key] : fallback; } function asRecord(value: unknown, path: string): Record { return yamlRecord(value, path); } function objectField(obj: Record, key: string, path: string): Record { return yamlObjectField(obj, key, path, ""); } function arrayOfRecords(value: unknown, path: string): Record[] { if (!Array.isArray(value)) throw new Error(`${path} must be an array`); return value.map((item, index) => asRecord(item, `${path}[${index}]`)); } function stringField(obj: Record, key: string, path: string): string { return yamlStringField(obj, key, path, ""); } function integerField(obj: Record, key: string, path: string): number { return yamlIntegerField(obj, key, path, ""); } function booleanField(obj: Record, key: string, path: string): boolean { return yamlBooleanField(obj, key, path, ""); } function stringArrayField(obj: Record, key: string, path: string): string[] { return yamlStringArrayField(obj, key, path, ""); } function numberArrayField(obj: Record, key: string, path: string): number[] { return yamlIntegerArrayField(obj, key, path, ""); } function stringMapField(obj: Record, key: string, path: string): Record { const record = objectField(obj, key, path); const result: Record = {}; for (const [itemKey, itemValue] of Object.entries(record)) { result[envKeyValue(itemKey, `${path}.${key}`)] = stringValue(itemValue, `${path}.${key}.${itemKey}`); } return result; } function numberMapField(obj: Record, key: string, path: string): Record { const record = objectField(obj, key, path); const result: Record = {}; for (const [itemKey, itemValue] of Object.entries(record)) { result[envKeyValue(itemKey, `${path}.${key}`)] = randomByteCount(itemValue, `${path}.${key}.${itemKey}`); } return result; } function randomBase64UrlSpec(value: unknown, path: string): { bytes: number; prefix: string } { const record = asRecord(value, path); return { bytes: randomByteCount(record.bytes, `${path}.bytes`), prefix: record.prefix === undefined ? "" : stringValue(record.prefix, `${path}.prefix`), }; } function stringValue(value: unknown, path: string): string { if (typeof value !== "string") throw new Error(`${path} must be a string`); return value; } function randomByteCount(value: unknown, path: string): number { if (typeof value !== "number" || !Number.isInteger(value) || value < 16 || value > 128) throw new Error(`${path} must be an integer in 16..128`); return value; } function sourceRefField(obj: Record, key: string, path: string): string { const value = stringField(obj, key, path); if (value.startsWith("/") || value.includes("..") || value.includes("\0") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${path}.${key} must be a relative source ref without ..`); return value; } function envKeyField(obj: Record, key: string, path: string): string { return envKeyValue(stringField(obj, key, path), `${path}.${key}`); } function envKeyValue(value: string, path: string): string { if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path} must be an uppercase env key`); return value; } function kubernetesNameField(obj: Record, key: string, path: string): string { return yamlKubernetesNameField(obj, key, path, ""); } function kubernetesSecretKeyField(obj: Record, key: string, path: string): string { const value = stringField(obj, key, path); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${key} must be a Kubernetes Secret key`); return value; } function simpleId(value: string, path: string): string { if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} must be a simple id`); return value; }