/* * SPEC: HWLAB-ESO-UI-20260630 ESO UI draft-2026-06-30-p1-readonly-api. * Responsibility: read-only, redacted admin secrets API model for Cloud Web. */ import { existsSync, readFileSync } from "node:fs"; import { createHash } from "node:crypto"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { sendJson } from "./server-http-utils.ts"; const CONTRACT_VERSION = "hwlab-admin-secrets-v1"; const DEFAULT_CONFIG_RELATIVE_PATH = "config/hwlab-v03/secrets.yaml"; const DEFAULT_TARGET = Object.freeze({ node: "JD01", lane: "v03", runtimeNamespace: "hwlab-v03", infraNamespace: "platform-infra", publicUrl: "https://hwlab.pikapython.com", source: "yaml-active-target" }); const STATUS_PRIORITY = Object.freeze([ "SecretLeakBlocked", "StoreUnavailable", "SyncError", "TargetMissing", "RolloutPending", "ValidationFailed", "Ready", "Unknown" ]); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); export async function handleAdminSecretsHttp(request, response, url, options = {}) { try { const auth = await authenticateAdmin(request, options); if (!auth.ok) return sendJson(response, numericStatus(auth.httpStatus) ?? numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth); const readModel = buildAdminSecretsReadModel({ env: options.env ?? process.env }); if (readModel.leakBlocked) return sendJson(response, 502, readModel.payload); if (request.method === "GET" && url.pathname === "/v1/admin/secrets/summary") { return sendJson(response, 200, readModel.payload); } if (request.method === "GET" && url.pathname === "/v1/admin/secrets/external-secrets") { return sendJson(response, 200, externalSecretsListPayload(readModel.payload, url)); } const detailMatch = url.pathname.match(/^\/v1\/admin\/secrets\/external-secrets\/([^/]+)\/([^/]+)$/u); if (request.method === "GET" && detailMatch) { const detail = externalSecretDetailPayload(readModel.payload, decodeURIComponent(detailMatch[1]), decodeURIComponent(detailMatch[2])); return sendJson(response, detail.httpStatus ?? 200, detail); } if (request.method === "GET" && url.pathname === "/v1/admin/secrets/events") { return sendJson(response, 200, eventsPayload(readModel.payload, url)); } return sendJson(response, 404, secretsError("not_found", "Admin secrets route is not implemented", 404)); } catch (error) { return sendJson(response, error.httpStatus ?? 500, secretsError(error.code ?? "admin_secrets_failed", error.message ?? "Admin secrets read model failed", error.httpStatus ?? 500, error.details)); } } export function buildAdminSecretsReadModel({ env = process.env } = {}) { const { config, sourcePath, sourceRelativePath, sourceError } = readSecretPlaneConfig(env); const secretPlane = record(config?.secretPlane); const readModelConfig = record(secretPlane?.readModel); const target = normalizeTarget(readModelConfig?.target, env, sourceRelativePath); const evidence = normalizeEvidence(readModelConfig?.statusEvidence); const store = normalizeStore(secretPlane?.store); const externalSecrets = normalizeExternalSecrets(secretPlane, target, store, evidence, sourceRelativePath); const events = buildEvents({ target, store, externalSecrets, evidence, sourceError }); const summaryStatus = sourceError ? "Unknown" : aggregateStatus([ ...componentStatuses(evidence), ...externalSecrets.map((item) => item.status) ]); const leakBlocked = hasLeakRisk({ target, store, externalSecrets, events }); const generatedAt = new Date().toISOString(); const common = { ok: !leakBlocked, status: leakBlocked ? "blocked" : "ok", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, generatedAt, target, source: { kind: "yaml", path: sourceRelativePath, sourceRef: `${sourceRelativePath}#secretPlane`, present: Boolean(sourcePath && !sourceError), error: sourceError ? { code: "secret_plane_config_unavailable", message: sourceError } : null } }; if (leakBlocked) { return { leakBlocked: true, payload: { ...common, error: { code: "SecretLeakBlocked", message: "Admin secrets read model contained disallowed secret material indicators.", layer: "admin-secrets-readmodel", retryable: false, valuesPrinted: false } } }; } return { leakBlocked: false, payload: { ...common, summary: { status: summaryStatus, priority: STATUS_PRIORITY.indexOf(summaryStatus), counts: { externalSecrets: externalSecrets.length, ready: externalSecrets.filter((item) => item.status === "Ready").length, pending: externalSecrets.filter((item) => item.status === "RolloutPending" || item.status === "Unknown").length, errors: externalSecrets.filter((item) => ["SecretLeakBlocked", "StoreUnavailable", "SyncError", "TargetMissing", "ValidationFailed"].includes(item.status)).length, events: events.length }, eso: { controller: evidence.components.esoController, webhook: evidence.components.esoWebhook, certController: evidence.components.esoCertController }, stores: [store], consumerRollout: consumerRolloutSummary(externalSecrets), latestEvidence: { source: evidence.source, issue: evidence.issue, command: evidence.command, validatedAt: evidence.validatedAt, fingerprint: evidence.fingerprint, valuesPrinted: false } }, externalSecrets, events } }; } function externalSecretsListPayload(payload, url) { const node = text(url.searchParams.get("node")).toUpperCase(); const lane = text(url.searchParams.get("lane")).toLowerCase(); const namespace = text(url.searchParams.get("namespace")); const items = payload.externalSecrets.filter((item) => { if (node && item.target.node !== node) return false; if (lane && item.target.lane.toLowerCase() !== lane) return false; if (namespace && item.namespace !== namespace) return false; return true; }); return { ok: true, status: "ok", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, generatedAt: payload.generatedAt, target: payload.target, items, count: items.length }; } function externalSecretDetailPayload(payload, namespace, name) { const item = payload.externalSecrets.find((secret) => secret.namespace === namespace && secret.name === name); if (!item) return secretsError("external_secret_not_found", "ExternalSecret read model entry was not found", 404, { namespace, name }); return { ok: true, status: "ok", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, generatedAt: payload.generatedAt, target: payload.target, item }; } function eventsPayload(payload, url) { const limit = positiveInteger(url.searchParams.get("limit"), 50); const namespace = text(url.searchParams.get("namespace")); const items = payload.events .filter((event) => !namespace || event.namespace === namespace) .slice(0, Math.min(limit, 200)); return { ok: true, status: "ok", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, generatedAt: payload.generatedAt, target: payload.target, events: items, count: items.length }; } async function authenticateAdmin(request, options) { const controller = options.accessController; if (!controller || typeof controller.authenticate !== "function") return secretsError("access_controller_unavailable", "HWLAB access controller is not available", 503); const auth = await controller.authenticate(request, { required: true }); if (!auth.ok) return auth; if (auth.actor?.role !== "admin") return secretsError("admin_required", "Only admin users can read admin secrets status", 403); return auth; } function readSecretPlaneConfig(env) { const configuredPath = text(env.HWLAB_ADMIN_SECRETS_CONFIG_PATH); const candidates = [ configuredPath, path.join(repoRoot, DEFAULT_CONFIG_RELATIVE_PATH), path.join(process.cwd(), DEFAULT_CONFIG_RELATIVE_PATH), path.join("/workspace/hwlab", DEFAULT_CONFIG_RELATIVE_PATH) ].filter(Boolean); const sourcePath = candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? ""; const sourceRelativePath = sourcePath.includes(DEFAULT_CONFIG_RELATIVE_PATH) ? DEFAULT_CONFIG_RELATIVE_PATH : sourcePath; if (!sourcePath || !existsSync(sourcePath)) return { config: null, sourcePath, sourceRelativePath, sourceError: `missing ${sourceRelativePath}` }; try { return { config: parseAdminSecretsYaml(readFileSync(sourcePath, "utf8")), sourcePath, sourceRelativePath, sourceError: null }; } catch (error) { return { config: null, sourcePath, sourceRelativePath, sourceError: `invalid yaml: ${error.message ?? String(error)}` }; } } function parseAdminSecretsYaml(raw) { const root = {}; const stack = [{ indent: -1, value: root, pending: false, parent: null, key: "" }]; for (const [lineIndex, rawLine] of String(raw ?? "").split(/\r?\n/u).entries()) { if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue; const indent = rawLine.match(/^ */u)?.[0].length ?? 0; const content = rawLine.trim(); while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop(); let current = stack[stack.length - 1]; if (content.startsWith("- ")) { current = ensureYamlArrayContainer(current); const itemText = content.slice(2).trim(); if (!itemText) { const item = {}; current.value.push(item); stack.push({ indent, value: item, pending: false, parent: current.value, key: String(current.value.length - 1) }); continue; } const keyValue = splitYamlKeyValue(itemText); if (keyValue) { const item = {}; current.value.push(item); assignYamlKeyValue(item, keyValue, lineIndex); stack.push({ indent, value: item, pending: false, parent: current.value, key: String(current.value.length - 1) }); } else { current.value.push(parseYamlScalar(itemText)); } continue; } const parent = current.value; if (!record(parent)) throw new Error(`unsupported yaml mapping at line ${lineIndex + 1}`); const keyValue = splitYamlKeyValue(content); if (!keyValue) throw new Error(`unsupported yaml line ${lineIndex + 1}`); const assigned = assignYamlKeyValue(parent, keyValue, lineIndex); if (assigned.pending) stack.push({ indent, value: assigned.value, pending: true, parent, key: keyValue.key }); } return root; } function ensureYamlArrayContainer(entry) { if (Array.isArray(entry.value)) return entry; if (entry.pending && entry.parent && entry.key) { const items = []; entry.parent[entry.key] = items; entry.value = items; entry.pending = false; return entry; } throw new Error("unsupported yaml sequence placement"); } function splitYamlKeyValue(value) { const match = String(value ?? "").match(/^([^:]+):(.*)$/u); if (!match) return null; return { key: match[1].trim(), value: match[2].trim() }; } function assignYamlKeyValue(target, keyValue, lineIndex) { if (!keyValue.key) throw new Error(`empty yaml key at line ${lineIndex + 1}`); if (!keyValue.value) { const child = {}; target[keyValue.key] = child; return { value: child, pending: true }; } target[keyValue.key] = parseYamlScalar(keyValue.value); return { value: target[keyValue.key], pending: false }; } function parseYamlScalar(value) { const textValue = String(value ?? "").trim(); if (!textValue) return ""; if (textValue === "true") return true; if (textValue === "false") return false; if (textValue === "null" || textValue === "~") return null; if (/^-?\d+(?:\.\d+)?$/u.test(textValue)) return Number(textValue); if ((textValue.startsWith("\"") && textValue.endsWith("\"")) || (textValue.startsWith("'") && textValue.endsWith("'"))) return textValue.slice(1, -1); return textValue; } function normalizeTarget(value, env, sourceRelativePath) { const input = record(value); const node = text(env.HWLAB_ADMIN_SECRETS_ACTIVE_NODE) || text(input?.node) || DEFAULT_TARGET.node; const lane = text(env.HWLAB_ADMIN_SECRETS_ACTIVE_LANE) || text(input?.lane) || DEFAULT_TARGET.lane; return { node: node.toUpperCase(), lane, runtimeNamespace: text(input?.runtimeNamespace) || DEFAULT_TARGET.runtimeNamespace, infraNamespace: text(input?.infraNamespace) || DEFAULT_TARGET.infraNamespace, publicUrl: text(input?.publicUrl) || DEFAULT_TARGET.publicUrl, source: text(input?.source) || DEFAULT_TARGET.source, sourceRef: text(input?.sourceRef) || `${sourceRelativePath}#secretPlane.readModel.target`, upstreamSourceRef: text(input?.upstreamSourceRef) || "config/platform-infra/secret-plane.yaml#activeTarget" }; } function normalizeEvidence(value) { const input = record(value); const components = record(input?.components); return { source: text(input?.source) || "pikasTech/unidesk#1282", issue: text(input?.issue) || "pikasTech/HWLAB#2277", command: text(input?.command) || "platform-infra secret-plane status --target JD01", validatedAt: text(input?.validatedAt) || "2026-06-30T03:21:49Z", fingerprint: text(input?.fingerprint), status: normalizeStatus(input?.status, "Ready"), components: { esoController: normalizeStatus(components?.esoController, "Ready"), esoWebhook: normalizeStatus(components?.esoWebhook, "Ready"), esoCertController: normalizeStatus(components?.esoCertController, "Ready"), secretStore: normalizeStatus(components?.secretStore, "Ready"), clusterSecretStore: normalizeStatus(components?.clusterSecretStore, "Ready"), sync: normalizeStatus(components?.sync, "Ready"), consumer: normalizeStatus(components?.consumer, "Ready") } }; } function normalizeStore(value) { const input = record(value); return { kind: text(input?.kind) || "ClusterSecretStore", name: text(input?.name) || "hwlab-secret-plane-vault-cluster", namespace: text(input?.namespace) || "platform-infra", scope: text(input?.scope) || (text(input?.kind) === "SecretStore" ? "namespace" : "cluster"), status: "Ready", reason: "JD01 secret-plane status evidence reports Store Ready", valuesPrinted: false }; } function normalizeExternalSecrets(secretPlane, target, store, evidence, sourceRelativePath) { const secrets = Array.isArray(secretPlane?.secrets) ? secretPlane.secrets : []; return secrets.map((secret, index) => normalizeExternalSecret(secret, index, secretPlane, target, store, evidence, sourceRelativePath)); } function normalizeExternalSecret(secret, index, secretPlane, target, store, evidence, sourceRelativePath) { const item = record(secret) ?? {}; const namespace = text(item.targetNamespace) || target.runtimeNamespace; const data = Array.isArray(item.data) ? item.data.map((entry, dataIndex) => normalizeSecretData(entry, dataIndex)) : []; const consumers = Array.isArray(item.consumers) ? item.consumers.map((consumer, consumerIndex) => normalizeConsumer(consumer, consumerIndex, evidence)) : []; const fingerprint = text(record(item.statusEvidence)?.fingerprint) || evidence.fingerprint; const status = normalizeStatus(record(item.statusEvidence)?.status, evidence.status); const name = text(item.externalSecretName) || `external-secret-${index + 1}`; const targetSecretName = text(item.targetSecretName) || name; return { id: text(item.id) || name, name, namespace, target, store: { kind: store.kind, name: store.name, namespace: store.namespace, scope: store.scope, status: store.status }, status, ready: status === "Ready", reason: text(record(item.statusEvidence)?.reason) || `${evidence.source} validated JD01 read-only status`, message: "脱敏 read model only; secret values are never read or returned.", refreshInterval: text(item.refreshInterval) || text(secretPlane?.refreshInterval) || "1m", createdAt: null, lastRefreshTime: evidence.validatedAt, targetSecret: { name: targetSecretName, namespace, keys: data.map((entry) => ({ key: entry.targetKey, presence: fingerprint ? "present" : "unknown", fingerprint, valuesPrinted: false })), resourceVersion: text(record(item.statusEvidence)?.resourceVersion) || null, fingerprint, valuesPrinted: false }, data, consumers, provenance: { configSource: `${sourceRelativePath}#secretPlane.secrets[${index}]`, statusSource: evidence.source, issue: evidence.issue, command: evidence.command, validatedAt: evidence.validatedAt }, valuesPrinted: false, secretMaterialStored: false }; } function normalizeSecretData(entry, index) { const item = record(entry) ?? {}; const remoteRef = text(item.remoteRef); return { targetKey: text(item.targetKey) || `key-${index + 1}`, remoteRef: { path: redactRemoteRef(remoteRef), property: text(item.property) || null, version: text(item.version) || null, fingerprint: remoteRef ? sha256Text(remoteRef).slice(0, 16) : null, redacted: true } }; } function normalizeConsumer(value, index, evidence) { const input = record(value) ?? {}; const serviceId = text(input.serviceId) || `consumer-${index + 1}`; return { serviceId, kind: text(input.kind) || "Deployment", injectAs: text(input.injectAs) || "env", envName: text(input.envName) || null, status: evidence.components.consumer, rollout: evidence.components.consumer === "Ready" ? "Ready" : "RolloutPending", revision: text(input.revision) || null, valuesPrinted: false }; } function buildEvents({ target, store, externalSecrets, evidence, sourceError }) { const events = []; if (sourceError) { events.push(event("Unknown", "ConfigUnavailable", target.runtimeNamespace, "secret-plane", sourceError, target, evidence)); } for (const secret of externalSecrets) { events.push(event(secret.status, `${secret.status}ExternalSecret`, secret.namespace, secret.name, secret.reason, target, evidence, { resourceKind: "ExternalSecret", resourceName: secret.name })); events.push(event(store.status, `${store.status}Store`, store.namespace, store.name, "SecretStore / ClusterSecretStore readiness is exposed as redacted status.", target, evidence, { resourceKind: store.kind, resourceName: store.name })); } return events.sort((left, right) => String(right.observedAt).localeCompare(String(left.observedAt))); } function event(status, reason, namespace, name, message, target, evidence, extra = {}) { return { status, reason, namespace, resourceKind: extra.resourceKind || "SecretPlane", resourceName: extra.resourceName || name, message: summarizeMessage(message), observedAt: evidence.validatedAt || new Date().toISOString(), target, traceId: null, jobId: null, source: evidence.source, valuesPrinted: false }; } function componentStatuses(evidence) { return Object.values(evidence.components); } function consumerRolloutSummary(externalSecrets) { const consumers = externalSecrets.flatMap((item) => item.consumers); return { total: consumers.length, ready: consumers.filter((item) => item.rollout === "Ready").length, pending: consumers.filter((item) => item.rollout !== "Ready").length, revision: consumers.find((item) => item.revision)?.revision ?? null }; } function aggregateStatus(statuses) { for (const status of STATUS_PRIORITY) if (statuses.includes(status)) return status; return "Unknown"; } function normalizeStatus(value, fallback = "Unknown") { const textValue = text(value); if (STATUS_PRIORITY.includes(textValue)) return textValue; if (/^(ready|ok|healthy|synced)$/iu.test(textValue)) return "Ready"; if (/^(notready|unavailable)$/iu.test(textValue)) return "StoreUnavailable"; if (/^(pending|progressing)$/iu.test(textValue)) return "RolloutPending"; if (/^(failed|error)$/iu.test(textValue)) return "SyncError"; return fallback; } function hasLeakRisk(value) { const stack = [value]; while (stack.length > 0) { const current = stack.pop(); if (Array.isArray(current)) { stack.push(...current); continue; } if (!current || typeof current !== "object") continue; for (const [key, item] of Object.entries(current)) { if (/^(value|raw|secretValue|decoded|base64|authorization|cookie|token|apiKey|password)$/u.test(key) && typeof item === "string" && item.trim()) return true; if (item && typeof item === "object") stack.push(item); } } return false; } function redactRemoteRef(value) { const textValue = text(value); if (!textValue) return null; const parts = textValue.split("/").filter(Boolean); if (parts.length <= 1) return `${textValue.slice(0, 4)}...`; return `${parts[0]}/.../${parts[parts.length - 1]}`; } function summarizeMessage(value) { const message = text(value); return message.length > 220 ? `${message.slice(0, 217)}...` : message; } function secretsError(code, message, status = 500, details = {}) { return { ok: false, status: "failed", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, error: { code, message, layer: "admin-secrets", retryable: false, valuesPrinted: false, ...details }, httpStatus: status }; } function numericStatus(value) { const number = Number(value); return Number.isInteger(number) && number >= 100 && number <= 599 ? number : null; } function positiveInteger(value, fallback) { const number = Number.parseInt(String(value ?? ""), 10); return Number.isInteger(number) && number > 0 ? number : fallback; } function record(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function text(value) { if (value === null || value === undefined) return ""; return String(value).trim(); } function sha256Text(value) { return createHash("sha256").update(text(value)).digest("hex"); }