/* * 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 PLAN_PERMISSION = "admin.secrets.plan"; const MAX_PLAN_BODY_BYTES = 32 * 1024; 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 PLAN_FAILURE_PRIORITY = Object.freeze([ "rbac_denied", "leak_blocked", "store_scope_violation", "store_unavailable", "target_collision", "sync_risk", "validation_failed" ]); const KUBERNETES_NAME_RE = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u; const REF_PATH_RE = /^[A-Za-z0-9._/-]+$/u; const REF_KEY_RE = /^[A-Za-z0-9._-]+$/u; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); export async function handleAdminSecretsHttp(request, response, url, options = {}) { try { const planRoute = request.method === "POST" && url.pathname === "/v1/admin/secrets/external-secrets/plan"; const auth = await authenticateAdmin(request, options, { requireAdmin: !planRoute }); 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)); } if (planRoute) { const permission = adminSecretsPlanPermission(auth.actor); if (!permission.allowed) return sendJson(response, 403, planPermissionDeniedPayload(readModel.payload, permission)); const body = await readJsonRequestBody(request, MAX_PLAN_BODY_BYTES); const plan = buildAdminSecretsPlan(readModel.payload, body, auth); return sendJson(response, plan.httpStatus ?? 200, plan); } 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, permissions: { [PLAN_PERMISSION]: true, plan: true, apply: false, valuesPrinted: false, secretMaterialStored: false }, 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 } }; } export function buildAdminSecretsPlan(payload, requestBody = {}, auth = {}) { const generatedAt = new Date().toISOString(); const permission = adminSecretsPlanPermission(auth.actor); if (!permission.allowed) return planPermissionDeniedPayload(payload, permission, generatedAt); const leak = findPlanSecretMaterial(requestBody); if (leak) { return { ...basePlanPayload(payload, generatedAt, permission), ok: false, status: "failed", failureKind: "leak_blocked", checks: [planCheck("leak-block", "Secret material blocked", "failed", "leak_blocked", "Plan request included a field that can carry secret material.")], risks: [planRisk("leak_blocked", "blocked", "Plan request was rejected before planning because it contained a disallowed secret material field.")], error: { code: "SecretLeakBlocked", message: "ExternalSecret plan requests must not include secret values.", layer: "admin-secrets-plan", retryable: false, field: leak.path, valuesPrinted: false }, httpStatus: 422 }; } const input = record(requestBody) ?? {}; const activeTarget = payload.target ?? DEFAULT_TARGET; const targetInput = record(input.target) ?? {}; const target = { node: (text(targetInput.node) || text(activeTarget.node) || DEFAULT_TARGET.node).toUpperCase(), lane: text(targetInput.lane) || text(activeTarget.lane) || DEFAULT_TARGET.lane, namespace: text(targetInput.namespace) || text(targetInput.runtimeNamespace) || text(activeTarget.runtimeNamespace) || DEFAULT_TARGET.runtimeNamespace }; const mode = normalizePlanMode(input.mode); const externalSecretName = text(input.externalSecretName) || text(input.name); const targetSecretName = text(input.targetSecretName) || externalSecretName; const refreshInterval = text(input.refreshInterval) || "1m"; const store = normalizePlanStore(input.store, payload, target); const data = normalizePlanData(input.data); const consumerRollout = normalizePlanConsumerRollout(input.consumerRollout); const existingExternalSecret = payload.externalSecrets.find((item) => item.namespace === target.namespace && item.name === externalSecretName) ?? null; const targetSecretCollision = payload.externalSecrets.find((item) => item.namespace === target.namespace && text(record(item.targetSecret)?.name) === targetSecretName) ?? null; const affectedConsumers = affectedPlanConsumers(existingExternalSecret ?? targetSecretCollision); const checks = []; const risks = []; const failures = new Set(); if (!externalSecretName || !KUBERNETES_NAME_RE.test(externalSecretName) || externalSecretName.length > 63) { checks.push(planCheck("external-secret-name", "ExternalSecret name", "failed", "validation_failed", "Name must be a Kubernetes DNS label with at most 63 characters.")); failures.add("validation_failed"); } else { checks.push(planCheck("external-secret-name", "ExternalSecret name", "passed", "ok", externalSecretName)); } if (!targetSecretName || !KUBERNETES_NAME_RE.test(targetSecretName) || targetSecretName.length > 63) { checks.push(planCheck("target-secret-name", "Target Secret name", "failed", "validation_failed", "Target Secret name must be a Kubernetes DNS label with at most 63 characters.")); failures.add("validation_failed"); } else { checks.push(planCheck("target-secret-name", "Target Secret name", "passed", "ok", targetSecretName)); } if (target.node !== text(activeTarget.node).toUpperCase() || target.lane !== text(activeTarget.lane) || target.namespace !== text(activeTarget.runtimeNamespace)) { checks.push(planCheck("active-target", "Active target", "failed", "validation_failed", "Plan target must match the API active target.")); failures.add("validation_failed"); } else { checks.push(planCheck("active-target", "Active target", "passed", "ok", `${target.node}/${target.lane}/${target.namespace}`)); } const availableStore = findPlanStore(payload, store); if (store.kind === "SecretStore" && store.namespace !== target.namespace) { checks.push(planCheck("store-scope", "Store namespace scope", "failed", "store_scope_violation", "Namespace-scoped SecretStore cannot plan for a different namespace.")); risks.push(planRisk("store_scope_violation", "blocked", "Namespace-scoped Store is limited to the target namespace.")); failures.add("store_scope_violation"); } else { checks.push(planCheck("store-scope", "Store namespace scope", "passed", "ok", store.kind === "ClusterSecretStore" ? `cluster store targets ${target.namespace}` : store.namespace)); } if (!availableStore) { checks.push(planCheck("store-available", "Store availability", "failed", "store_unavailable", `${store.kind}/${store.name} was not found in the active read model.`)); risks.push(planRisk("store_unavailable", "blocked", "Plan cannot confirm the requested Store from the active target.")); failures.add("store_unavailable"); } else { checks.push(planCheck("store-available", "Store availability", "passed", "ok", `${store.kind}/${store.name}`)); } if (!/^\d+[smhd]$/u.test(refreshInterval)) { checks.push(planCheck("refresh-interval", "Refresh interval", "failed", "validation_failed", "Refresh interval must use s/m/h/d suffix.")); failures.add("validation_failed"); } else { checks.push(planCheck("refresh-interval", "Refresh interval", "passed", "ok", refreshInterval)); } for (const failure of validatePlanData(data, checks, risks)) failures.add(failure); if (mode === "create" && (existingExternalSecret || targetSecretCollision)) { const collisionName = existingExternalSecret?.name ?? text(record(targetSecretCollision?.targetSecret)?.name) ?? targetSecretName; checks.push(planCheck("target-collision", "Target collision", "failed", "target_collision", `${target.namespace}/${collisionName} already exists in the read model.`)); risks.push(planRisk("target_collision", "blocked", "Create plan would collide with an existing ExternalSecret or target Secret.")); failures.add("target_collision"); } else { checks.push(planCheck("target-collision", "Target collision", "passed", "ok", mode === "edit" ? "edit mode permits matching object" : "no collision detected")); } checks.push(planCheck("dry-run", "Dry-run boundary", "passed", "ok", "No Kubernetes, Vault, ESO, or GitOps writes are performed.")); checks.push(planCheck("secret-values", "Secret value boundary", "passed", "ok", "Plan request and response do not include secret values.")); risks.push(planRisk("dry_run_only", "info", "This plan is read-only and cannot apply, rotate, or roll back a Secret.")); if (consumerRollout.enabled && affectedConsumers.length === 0) risks.push(planRisk("consumer_rollout_marker", "info", "Consumer rollout marker is recorded as intent only; no consumer was resolved from the read model.")); const failureKind = firstFailureKind(failures); const ok = !failureKind; return { ...basePlanPayload(payload, generatedAt, permission), ok, status: ok ? "planned" : "failed", planId: `esp_${sha256Text(stableJson({ target, mode, externalSecretName, targetSecretName, store, data, refreshInterval, consumerRollout })).slice(0, 16)}`, failureKind: failureKind || null, checks, risks, plan: { operation: mode === "edit" ? "edit-external-secret-dry-run" : "create-external-secret-dry-run", mode, dryRun: true, writes: false, target, externalSecret: { name: externalSecretName, namespace: target.namespace, refreshInterval }, targetSecret: { name: targetSecretName, namespace: target.namespace, presence: targetSecretCollision ? "present" : "not-present", valuesPrinted: false }, store, data: data.map((item) => ({ targetKey: item.targetKey, sourceRef: item.sourceRef, presence: "not-read-dry-run", referenceFingerprint: item.referenceFingerprint, valuesPrinted: false })), affectedConsumers, consumerRollout, valuesPrinted: false, secretMaterialStored: false } }; } function basePlanPayload(payload, generatedAt, permission) { return { contractVersion: CONTRACT_VERSION, generatedAt, target: payload.target, permission, valuesPrinted: false, secretMaterialStored: false }; } function planPermissionDeniedPayload(payload, permission, generatedAt = new Date().toISOString()) { return { ...basePlanPayload(payload, generatedAt, permission), ok: false, status: "failed", failureKind: "rbac_denied", checks: [planCheck("permission", PLAN_PERMISSION, "failed", "rbac_denied", `${PLAN_PERMISSION} is required.`)], risks: [planRisk("rbac_denied", "blocked", "Current actor is not allowed to plan ExternalSecret changes.")], error: { code: "admin_secrets_plan_required", message: `${PLAN_PERMISSION} permission is required.`, layer: "admin-secrets-plan", retryable: false, valuesPrinted: false }, httpStatus: 403 }; } function adminSecretsPlanPermission(actor) { const role = text(actor?.role); const permissions = permissionSetFromActor(actor); if (role === "admin") return { id: PLAN_PERMISSION, allowed: true, source: "admin-role", valuesPrinted: false }; if (permissions.has(PLAN_PERMISSION)) return { id: PLAN_PERMISSION, allowed: true, source: "actor-permission", valuesPrinted: false }; return { id: PLAN_PERMISSION, allowed: false, source: role || "anonymous", valuesPrinted: false }; } function permissionSetFromActor(actor) { const output = new Set(); for (const key of ["permissions", "capabilities"]) { const value = actor?.[key]; if (Array.isArray(value)) for (const item of value) if (text(item)) output.add(text(item)); if (record(value)) for (const [name, status] of Object.entries(value)) if (status === true || status === "available" || status === "allowed") output.add(name); } return output; } async function readJsonRequestBody(request, limitBytes) { let raw = ""; for await (const chunk of request) { raw += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? ""); if (Buffer.byteLength(raw, "utf8") > limitBytes) throw Object.assign(new Error("Plan request body is too large"), { httpStatus: 413, code: "plan_payload_too_large" }); } if (!raw.trim()) return {}; try { return JSON.parse(raw); } catch (error) { throw Object.assign(new Error("Plan request body must be valid JSON"), { httpStatus: 400, code: "plan_payload_parse_error", details: { reason: error.message } }); } } function normalizePlanMode(value) { const mode = text(value).toLowerCase(); return mode === "edit" ? "edit" : "create"; } function normalizePlanStore(value, payload, target) { const input = record(value) ?? {}; const firstStore = record(payload.summary?.stores?.[0]) ?? {}; const kind = text(input.kind) === "SecretStore" ? "SecretStore" : "ClusterSecretStore"; const namespace = kind === "SecretStore" ? text(input.namespace) || target.namespace : text(input.namespace) || text(firstStore.namespace) || text(payload.target?.infraNamespace) || "platform-infra"; return { kind, name: text(input.name) || text(firstStore.name) || "hwlab-secret-plane-vault-cluster", namespace, scope: kind === "SecretStore" ? "namespace" : "cluster", valuesPrinted: false }; } function findPlanStore(payload, store) { const stores = Array.isArray(payload.summary?.stores) ? payload.summary.stores : []; return stores.find((item) => { if (text(item.kind) !== store.kind) return false; if (text(item.name) !== store.name) return false; if (store.kind === "SecretStore" && text(item.namespace) !== store.namespace) return false; return true; }) ?? null; } function normalizePlanData(value) { const items = Array.isArray(value) ? value : [value].filter(Boolean); return items.map((entry, index) => { const item = record(entry) ?? {}; const sourceRef = record(item.sourceRef) ?? record(item.remoteRef) ?? {}; const pathValue = text(sourceRef.path) || text(item.path) || text(item.remotePath); const property = text(sourceRef.property) || text(item.property); const version = text(sourceRef.version) || text(item.version) || null; const targetKey = text(item.targetKey); return { targetKey, sourceRef: { path: pathValue, property, version }, referenceFingerprint: pathValue || property ? `sha256:${sha256Text(`${pathValue}:${property}:${version ?? ""}`).slice(0, 16)}` : null, index }; }); } function normalizePlanConsumerRollout(value) { const input = record(value) ?? {}; const enabled = input.enabled === true || text(input.intent) !== ""; const intent = text(input.intent) || (enabled ? "rollout-marker" : "none"); return { enabled, intent, marker: enabled ? "plan-only" : "none", valuesPrinted: false }; } function validatePlanData(data, checks, risks) { const failures = new Set(); if (data.length === 0) { checks.push(planCheck("source-ref", "Source reference", "failed", "sync_risk", "At least one sourceRef mapping is required.")); risks.push(planRisk("sync_risk", "blocked", "ESO sync cannot be planned without sourceRef path/property and target key.")); failures.add("sync_risk"); return failures; } for (const item of data) { const label = `data[${item.index}]`; if (!item.targetKey || !REF_KEY_RE.test(item.targetKey)) { checks.push(planCheck(`${label}.targetKey`, "Target key", "failed", "validation_failed", "Target key must be non-empty and contain only letters, digits, dot, dash, or underscore.")); failures.add("validation_failed"); } if (!item.sourceRef.path || !REF_PATH_RE.test(item.sourceRef.path)) { checks.push(planCheck(`${label}.sourceRef.path`, "Source path", "failed", "sync_risk", "sourceRef.path is required and must be a safe remote reference path.")); risks.push(planRisk("sync_risk", "blocked", "Remote source path is missing or malformed.")); failures.add("sync_risk"); } if (!item.sourceRef.property || !REF_KEY_RE.test(item.sourceRef.property)) { checks.push(planCheck(`${label}.sourceRef.property`, "Source property", "failed", "sync_risk", "sourceRef.property is required and must be a safe property name.")); risks.push(planRisk("sync_risk", "blocked", "Remote source property is missing or malformed.")); failures.add("sync_risk"); } if (item.sourceRef.version && !REF_KEY_RE.test(item.sourceRef.version)) { checks.push(planCheck(`${label}.sourceRef.version`, "Source version", "failed", "validation_failed", "sourceRef.version contains unsupported characters.")); failures.add("validation_failed"); } } if (failures.size === 0) checks.push(planCheck("source-ref", "Source reference", "passed", "ok", `${data.length} mapping(s)`)); return failures; } function affectedPlanConsumers(secret) { if (!secret || !Array.isArray(secret.consumers)) return []; return secret.consumers.map((item) => ({ serviceId: text(item.serviceId) || "-", kind: text(item.kind) || "-", injectAs: text(item.injectAs) || "-", envName: text(item.envName) || null, rollout: text(item.rollout) || "Unknown", revision: text(item.revision) || null, valuesPrinted: false })); } function firstFailureKind(failures) { for (const kind of PLAN_FAILURE_PRIORITY) if (failures.has(kind)) return kind; return ""; } function planCheck(id, label, status, code, detail) { return { id, label, status, code, detail, valuesPrinted: false }; } function planRisk(code, severity, message) { return { code, severity, message, valuesPrinted: false }; } function findPlanSecretMaterial(value, pathParts = []) { if (Array.isArray(value)) { for (let index = 0; index < value.length; index += 1) { const found = findPlanSecretMaterial(value[index], [...pathParts, String(index)]); if (found) return found; } return null; } if (!value || typeof value !== "object") { if (typeof value === "string" && looksLikeSecretMaterial(value) && !isPlanDescriptorPath(pathParts)) return { path: pathParts.join(".") || "$" }; return null; } for (const [key, item] of Object.entries(value)) { const nextPath = [...pathParts, key]; if (isSecretValueKey(key) && typeof item === "string" && item.trim() && !isPlanDescriptorPath(nextPath)) return { path: nextPath.join(".") }; const found = findPlanSecretMaterial(item, nextPath); if (found) return found; } return null; } function isSecretValueKey(key) { return /^(value|raw|secretValue|decoded|base64|authorization|cookie|token|apiKey|password)$/u.test(key); } function isPlanDescriptorPath(pathParts) { const key = pathParts[pathParts.length - 1]; if (["targetKey", "property", "version", "path", "remotePath", "externalSecretName", "targetSecretName", "name", "namespace", "kind", "intent", "mode", "node", "lane", "refreshInterval"].includes(key)) return true; const joined = pathParts.join("."); return /\.(sourceRef|remoteRef)\.(path|property|version)$/u.test(joined); } function looksLikeSecretMaterial(value) { const item = text(value); if (!item) return false; if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(item)) return true; if (/^(hsk|ghp|github_pat|sk|xox[baprs])-[-A-Za-z0-9_]{16,}$/u.test(item)) return true; if (/^[A-Za-z0-9+/]{48,}={0,2}$/u.test(item) && !item.includes("/")) return true; return false; } function stableJson(value) { return JSON.stringify(sortJson(value)); } function sortJson(value) { if (Array.isArray(value)) return value.map(sortJson); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])])); return value; } 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 }); const events = filterSecretEvents(payload.events, { namespace, name }).slice(0, 200); return { ok: true, status: "ok", contractVersion: CONTRACT_VERSION, valuesPrinted: false, secretMaterialStored: false, generatedAt: payload.generatedAt, target: payload.target, item, events, eventCount: events.length }; } function eventsPayload(payload, url) { const limit = positiveInteger(url.searchParams.get("limit"), 50); const namespace = text(url.searchParams.get("namespace")); const name = text(url.searchParams.get("name")); const items = filterSecretEvents(payload.events, { namespace, name }) .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 }; } function filterSecretEvents(events, { namespace = "", name = "" } = {}) { return events.filter((event) => { if (namespace && event.namespace !== namespace) return false; if (name && event.resourceName !== name) return false; return true; }); } async function authenticateAdmin(request, options, { requireAdmin = true } = {}) { 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 (requireAdmin && 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 statusEvidence = record(item.statusEvidence) ?? {}; 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(statusEvidence.fingerprint) || evidence.fingerprint; const status = normalizeStatus(statusEvidence.status, evidence.status); const name = text(item.externalSecretName) || `external-secret-${index + 1}`; const targetSecretName = text(item.targetSecretName) || name; const lastUpdated = text(statusEvidence.lastUpdated) || text(statusEvidence.validatedAt) || evidence.validatedAt; const createdAt = text(item.createdAt) || text(statusEvidence.createdAt) || null; const cluster = text(item.cluster) || `${target.node}/${target.lane}`; const reason = text(statusEvidence.reason) || `${evidence.source} validated ${cluster} read-only status`; return { id: text(item.id) || name, name, namespace, cluster, target, store: { kind: store.kind, name: store.name, namespace: store.namespace, scope: store.scope, status: store.status }, status, ready: status === "Ready", reason, message: "脱敏 read model only; sensitive material is never read or returned.", refreshInterval: text(item.refreshInterval) || text(secretPlane?.refreshInterval) || "1m", createdAt, lastUpdated, ageSource: createdAt || lastUpdated, lastRefreshTime: lastUpdated, conditions: normalizeConditions(item.conditions, { status, reason, lastUpdated }), targetSecret: { name: targetSecretName, namespace, keys: data.map((entry) => ({ key: entry.targetKey, presence: fingerprint ? "present" : "unknown", fingerprint, valuesPrinted: false })), resourceVersion: text(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 normalizeConditions(value, fallback) { const source = Array.isArray(value) ? value : []; const conditions = source.map((entry) => { const item = record(entry) ?? {}; return { type: text(item.type) || "Ready", status: text(item.status) || fallback.status, reason: text(item.reason) || fallback.reason, lastTransitionTime: text(item.lastTransitionTime) || fallback.lastUpdated, valuesPrinted: false }; }); if (conditions.length > 0) return conditions; return [{ type: "Ready", status: fallback.status, reason: fallback.reason, lastTransitionTime: fallback.lastUpdated, valuesPrinted: 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"); }