1025 lines
42 KiB
TypeScript
1025 lines
42 KiB
TypeScript
/*
|
|
* 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");
|
|
}
|