import { createRequire } from "node:module"; import type { ClawdbotConfig } from "../config/config.js"; const requireConfig = createRequire(import.meta.url); export type RedactSensitiveMode = "off" | "tools"; const DEFAULT_REDACT_MODE: RedactSensitiveMode = "tools"; const DEFAULT_REDACT_MIN_LENGTH = 18; const DEFAULT_REDACT_KEEP_START = 6; const DEFAULT_REDACT_KEEP_END = 4; const DEFAULT_REDACT_PATTERNS: string[] = [ // ENV-style assignments. String.raw`\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1`, // JSON fields. String.raw`"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"`, // CLI flags. String.raw`--(?:api[-_]?key|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1`, // Authorization headers. String.raw`Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)`, String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`, // PEM blocks. String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`, // Common token prefixes. String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`, String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`, String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`, String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`, String.raw`\b(xapp-[A-Za-z0-9-]{10,})\b`, String.raw`\b(gsk_[A-Za-z0-9_-]{10,})\b`, String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`, String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`, String.raw`\b(npm_[A-Za-z0-9]{10,})\b`, String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`, ]; type RedactOptions = { mode?: RedactSensitiveMode; patterns?: string[]; }; function normalizeMode(value?: string): RedactSensitiveMode { return value === "off" ? "off" : DEFAULT_REDACT_MODE; } function parsePattern(raw: string): RegExp | null { if (!raw.trim()) return null; const match = raw.match(/^\/(.+)\/([gimsuy]*)$/); try { if (match) { const flags = match[2].includes("g") ? match[2] : `${match[2]}g`; return new RegExp(match[1], flags); } return new RegExp(raw, "gi"); } catch { return null; } } function resolvePatterns(value?: string[]): RegExp[] { const source = value?.length ? value : DEFAULT_REDACT_PATTERNS; return source.map(parsePattern).filter((re): re is RegExp => Boolean(re)); } function maskToken(token: string): string { if (token.length < DEFAULT_REDACT_MIN_LENGTH) return "***"; const start = token.slice(0, DEFAULT_REDACT_KEEP_START); const end = token.slice(-DEFAULT_REDACT_KEEP_END); return `${start}…${end}`; } function redactPemBlock(block: string): string { const lines = block.split(/\r?\n/).filter(Boolean); if (lines.length < 2) return "***"; return `${lines[0]}\n…redacted…\n${lines[lines.length - 1]}`; } function redactMatch(match: string, groups: string[]): string { if (match.includes("PRIVATE KEY-----")) return redactPemBlock(match); const token = groups.filter((value) => typeof value === "string" && value.length > 0).at(-1) ?? match; const masked = maskToken(token); if (token === match) return masked; return match.replace(token, masked); } function redactText(text: string, patterns: RegExp[]): string { let next = text; for (const pattern of patterns) { next = next.replace(pattern, (...args: string[]) => redactMatch(args[0], args.slice(1, args.length - 2)), ); } return next; } function resolveConfigRedaction(): RedactOptions { let cfg: ClawdbotConfig["logging"] | undefined; try { const loaded = requireConfig("../config/config.js") as { loadConfig?: () => ClawdbotConfig; }; cfg = loaded.loadConfig?.().logging; } catch { cfg = undefined; } return { mode: normalizeMode(cfg?.redactSensitive), patterns: cfg?.redactPatterns, }; } export function redactSensitiveText(text: string, options?: RedactOptions): string { if (!text) return text; const resolved = options ?? resolveConfigRedaction(); if (normalizeMode(resolved.mode) === "off") return text; const patterns = resolvePatterns(resolved.patterns); if (!patterns.length) return text; return redactText(text, patterns); } export function redactToolDetail(detail: string): string { const resolved = resolveConfigRedaction(); if (normalizeMode(resolved.mode) !== "tools") return detail; return redactSensitiveText(detail, resolved); } export function getDefaultRedactPatterns(): string[] { return [...DEFAULT_REDACT_PATTERNS]; }