574 lines
22 KiB
TypeScript
574 lines
22 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 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");
|
|
}
|