const SENSITIVE_FIELD_NAME = 'authorization|cookie|password|secret|client[_-]?secret|access[_-]?token|refresh[_-]?token|(?:x[_-]?)?api[_-]?key|private[_-]?key|token|credentials'; const INLINE_SECRET_FIELD_NAME = 'cookie|password|secret|client[_-]?secret|access[_-]?token|refresh[_-]?token|(?:x[_-]?)?api[_-]?key|token|credentials'; const SENSITIVE_FIELD_PATTERN = new RegExp( `(^|[._-])(${SENSITIVE_FIELD_NAME})$`, 'i', ); const BEARER_CREDENTIAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi; const BASIC_CREDENTIAL_PATTERN = /\bBasic\s+[A-Za-z0-9+/]{4,}={0,2}(?=\s|$|[,;])/gi; const PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/gi; const AUTHORIZATION_CREDENTIAL_PATTERN = /\b(authorization)(\s*[:=]\s*)(Bearer|Basic)\s+\S+/gi; const JWT_PATTERN = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{1,}\.[A-Za-z0-9_-]{8,}\b/g; const JSON_SECRET_PATTERN = new RegExp( `(["'])(${SENSITIVE_FIELD_NAME})\\1(\\s*:\\s*)(["'])(.*?)\\4`, 'gi', ); const INLINE_SECRET_PATTERN = new RegExp( `\\b(${INLINE_SECRET_FIELD_NAME})(\\s*[:=]\\s*)([^\\s,;]+)`, 'gi', ); /** Whether a structured telemetry field name must never retain its value. */ export function isSensitiveTelemetryFieldName(value: string): boolean { return SENSITIVE_FIELD_PATTERN.test(value); } function redactStructuredValue( key: string, value: unknown, seen = new WeakSet(), ): unknown { if (isSensitiveTelemetryFieldName(key)) return '[REDACTED]'; if (typeof value === 'string') return redactPlainTelemetryText(value); if (!value || typeof value !== 'object') return value; if (seen.has(value)) return '[circular]'; seen.add(value); if (Array.isArray(value)) { const redacted = value.map((item) => redactStructuredValue('item', item, seen), ); seen.delete(value); return redacted; } const redacted = Object.fromEntries( Object.entries(value).map(([childKey, childValue]) => [ childKey, redactStructuredValue(childKey, childValue, seen), ]), ); seen.delete(value); return redacted; } function redactPlainTelemetryText(value: string): string { return value .replace(PRIVATE_KEY_PATTERN, '[REDACTED]') .replace(AUTHORIZATION_CREDENTIAL_PATTERN, '$1$2$3 [REDACTED]') .replace(BEARER_CREDENTIAL_PATTERN, 'Bearer [REDACTED]') .replace(BASIC_CREDENTIAL_PATTERN, 'Basic [REDACTED]') .replace(JWT_PATTERN, '[REDACTED_JWT]') .replace( JSON_SECRET_PATTERN, (_match, keyQuote, key, separator, valueQuote) => `${keyQuote}${key}${keyQuote}${separator}${valueQuote}[REDACTED]${valueQuote}`, ) .replace(INLINE_SECRET_PATTERN, '$1$2[REDACTED]'); } /** * Redact credentials from arbitrary diagnostic text. Kept dependency-free so * the play-worker bundle can share exactly the same base redaction rules as * server telemetry without importing the full telemetry emitter. */ export function redactTelemetryText(value: string): string { const trimmed = value.trim(); if ( (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')) ) { try { return JSON.stringify( redactStructuredValue('serializedJson', JSON.parse(trimmed)), ); } catch { // Non-JSON diagnostic strings still use pattern-based redaction below. } } return redactPlainTelemetryText(value); }