/** * 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; }