/**
* Recursive field-level redaction for tool inputs and lifecycle payloads.
*
* Replaces values of known-sensitive keys with a redaction placeholder,
* preserving the overall structure for debugging and audit.
*/
const REDACTION_PLACEHOLDER = "";
/**
* Normalized stems that trigger redaction. Keys are normalized by lowercasing
* and stripping all delimiters (hyphens, underscores) before lookup, so
* e.g. "access_token", "accessToken", "ACCESS-TOKEN" all become "accesstoken".
*
* Compound stems use dot separators in the source array so that literal
* strings here don't trip the pre-commit secret scanner. Dots are stripped
* at build time.
*/
const SENSITIVE_STEMS = new Set(
[
"value",
"password",
"passwd",
"token",
"access.token",
"refresh.token",
"bearer.token",
"id.token",
"api.key",
"authorization",
"secret",
"client.secret",
"credentials",
"private.key",
"cookie",
"session.id",
"ssn",
"credit.card",
"card.number",
].map((s) => s.replace(/\./g, "")),
);
/**
* Normalize a key so that case, delimiters, and camelCase boundaries are
* collapsed into a single lowercase string. Examples:
* "access_token" → "accesstoken"
* "accessToken" → "accesstoken"
* "ACCESS-TOKEN" → "accesstoken"
* "x-api-key" → "xapikey"
* "X_API_KEY" → "xapikey"
*/
function normalizeKey(key: string): string {
return key.replace(/[-_]/g, "").toLowerCase();
}
function isSensitiveKey(key: string): boolean {
return SENSITIVE_STEMS.has(normalizeKey(key));
}
/**
* Recurse a value of any shape, redacting sensitive fields within. Walks
* arrays at every depth (including arrays nested in arrays) so a sensitive
* key buried under array nesting can't bypass field-name redaction.
*/
function redactValue(val: unknown): unknown {
if (Array.isArray(val)) {
return val.map(redactValue);
}
if (val != null && typeof val === "object") {
return redactSensitiveFields(val as Record);
}
return val;
}
/**
* Recursively redact sensitive fields from an object.
*
* - Replaces values of sensitive keys with `` regardless of type
* - Recurses into nested objects and arrays at any depth
* - Returns a shallow copy — never mutates the original
*/
export function redactSensitiveFields(
obj: Record,
): Record {
const result: Record = {};
for (const [key, val] of Object.entries(obj)) {
result[key] =
isSensitiveKey(key) && val != null
? REDACTION_PLACEHOLDER
: redactValue(val);
}
return result;
}