export const SECRET_REDACTION_PLACEHOLDER = '[REDACTED_SECRET]'; const BEARER_TOKEN_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/g; const BASIC_CREDENTIAL_RE = /\bBasic\s+[A-Za-z0-9+/]{4,}={0,2}\b/g; // Bound each segment so V8 cannot recursively backtrack across a multi-megabyte // opaque receipt value that contains no dots. Real executor/provider JWT // segments are far smaller; 16 KiB per segment still leaves ample headroom. const JWT_RE = /\b[A-Za-z0-9_-]{8,16384}\.[A-Za-z0-9_-]{1,16384}\.[A-Za-z0-9_-]{8,16384}\b/g; const PRIVATE_KEY_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g; const SENSITIVE_FIELD_NAME = 'authorization|cookie|password|(?:client[_-]?)?secret|(?:access|refresh)[_-]?token|(?:x[_-]?)?api[_-]?key|private[_-]?key|token|credentials'; // Only redact self-identifying credential prefixes here. Generic words such as // `key`, `token`, and `secret` are common in harmless play/run slugs (for // example `absurd-key-rotation-check`) and need an assignment context before // they are sensitive; HIGH_ENTROPY_ASSIGNMENT_RE handles that case below. const COMMON_SECRET_RE = /\b(?:[prs]k_(?:live|test)_|(?:pat|ghp|github_pat)_|xox[baprs]-)[A-Za-z0-9_./+=:-]{12,}\b/gi; const GENERIC_PREFIXED_SECRET_RE = /\b(?:key|token|secret|api[_-]?key)_[A-Za-z0-9_./+=:-]{12,}\b/gi; const URL_SECRET_VALUE_RE = /\b(?:[spr]k|pat|ghp|github_pat|xox)[A-Za-z0-9_./+=:-]{12,}\b/i; const GENERIC_SECRET_ASSIGNMENT_RE = /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*["']?[^"',\s]{12,}["']?/gi; const HIGH_ENTROPY_ASSIGNMENT_RE = /\b(?:api[_-]?key|token|secret|password|authorization|(?:access|refresh)[_-]?token)\b\s*[:=]\s*["']?[^"',\s]{16,}["']?/gi; const SENSITIVE_URL_PARAM_RE = /[?#&](?:\w*(?:key|token)|secret|password|authorization|credential|signature|sig)(?:=|$)/i; const SENSITIVE_FIELD_NAME_RE = new RegExp( `(^|[._-])(?:${SENSITIVE_FIELD_NAME})$`, 'i', ); const SERIALIZED_SECRET_FIELD_RE = new RegExp( `"(${SENSITIVE_FIELD_NAME})"\\s*:\\s*(?:"[^"\\\\]*"|\\{[^{}]*\\})`, 'gi', ); function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } export function redactSecretLikeString(value: string): string { const serializedRedacted = value.replace( SERIALIZED_SECRET_FIELD_RE, `"$1":"${SECRET_REDACTION_PLACEHOLDER}"`, ); const trimmed = serializedRedacted.trim(); if (/^https?:\/\/\S+$/i.test(trimmed)) { if ( /^https?:\/\/[^/?#@]+@/i.test(trimmed) || SENSITIVE_URL_PARAM_RE.test(trimmed) ) { return SECRET_REDACTION_PLACEHOLDER; } if (!URL_SECRET_VALUE_RE.test(serializedRedacted)) { return serializedRedacted; } } return serializedRedacted .replace(PRIVATE_KEY_RE, SECRET_REDACTION_PLACEHOLDER) .replace(BEARER_TOKEN_RE, `Bearer ${SECRET_REDACTION_PLACEHOLDER}`) .replace(BASIC_CREDENTIAL_RE, `Basic ${SECRET_REDACTION_PLACEHOLDER}`) .replace(JWT_RE, SECRET_REDACTION_PLACEHOLDER) .replace(COMMON_SECRET_RE, SECRET_REDACTION_PLACEHOLDER) .replace(GENERIC_PREFIXED_SECRET_RE, SECRET_REDACTION_PLACEHOLDER) .replace(GENERIC_SECRET_ASSIGNMENT_RE, SECRET_REDACTION_PLACEHOLDER) .replace(HIGH_ENTROPY_ASSIGNMENT_RE, (match) => { const separator = match.includes('=') ? '=' : ':'; const [key] = match.split(separator, 1); return `${key.trim()}${separator}${SECRET_REDACTION_PLACEHOLDER}`; }); } export type SecretRedactionContext = { register(value: string): void; containsRegisteredSecret( value: string, options?: { includeEncoded?: boolean; minimumLength?: number }, ): boolean; matchesRegisteredSecret(value: string): boolean; /** Redacts only values registered by the secret resolver, preserving safe diagnostics. */ redactRegisteredSecrets(value: string): string; redactString(value: string): string; redactKnownSecrets(value: T): T; redact(value: T): T; }; export function createSecretRedactionContext( initialValues: readonly string[] = [], ): SecretRedactionContext { const exactSecrets = new Set(); function register(value: string): void { if (value.length > 0) exactSecrets.add(value); } for (const value of initialValues) register(value); function containsRegisteredSecret( value: string, { includeEncoded = false, minimumLength = 1, }: { includeEncoded?: boolean; minimumLength?: number } = {}, ): boolean { for (const secret of exactSecrets) { if (secret.length < minimumLength) continue; if (value.includes(secret)) return true; if (includeEncoded) { try { const encoded = encodeURIComponent(secret); if (encoded !== secret && value.includes(encoded)) return true; } catch { // Malformed surrogate pairs cannot be encoded. The raw comparison is // still safe and handles every other secret value. } } } return false; } function matchesRegisteredSecret(value: string): boolean { return exactSecrets.has(value); } function redactRegisteredSecrets(value: string): string { let output = value; for (const secret of exactSecrets) { output = output.replace( new RegExp(escapeRegExp(secret), 'g'), SECRET_REDACTION_PLACEHOLDER, ); try { const encoded = encodeURIComponent(secret); if (encoded !== secret) { output = output.replace( new RegExp(escapeRegExp(encoded), 'g'), SECRET_REDACTION_PLACEHOLDER, ); } } catch { // encodeURIComponent only fails on malformed surrogate pairs. Exact // redaction above still applies in that rare case. } } return output; } function redactString(value: string): string { const output = redactRegisteredSecrets(value); return redactSecretLikeString(output); } function redact(value: T): T { if (typeof value === 'string') return redactString(value) as T; if (Array.isArray(value)) return value.map((entry) => redact(entry)) as T; if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, entry]) => [ key, SENSITIVE_FIELD_NAME_RE.test(key) ? SECRET_REDACTION_PLACEHOLDER : redact(entry), ]), ) as T; } return value; } function redactKnownSecrets(value: T): T { if (typeof value === 'string') return redactString(value) as T; if (Array.isArray(value)) { return value.map((entry) => redactKnownSecrets(entry)) as T; } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, entry]) => [ key, redactKnownSecrets(entry), ]), ) as T; } return value; } return { register, containsRegisteredSecret, matchesRegisteredSecret, redactRegisteredSecrets, redactString, redactKnownSecrets, redact, }; }