UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

212 lines 5.85 kB
/** * PII and sensitive data redaction for security */ /** * Default redaction paths for common sensitive data */ export const DEFAULT_REDACT_PATHS = [ 'apiKey', 'token', 'password', 'secret', 'key', 'auth', 'authorization', 'credential', 'credentials', 'privateKey', 'private_key', 'publicKey', 'public_key', 'accessToken', 'access_token', 'oauthToken', 'oauth_token', 'refreshToken', 'refresh_token', ]; /** * Patterns for detecting sensitive data * @internal */ export const SENSITIVE_PATTERNS = [ // API keys (common patterns) /[a-zA-Z0-9]{32,}/gi, // Bearer tokens /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/gi, // Email addresses /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/gi, // IPv4 addresses /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, // UUIDs /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi, ]; /** * Redact a value if it matches sensitive patterns */ export function redactValue(value) { if (typeof value === 'string') { return redactString(value); } if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { return value.map(redactValue); } return redactObject(value); } return value; } /** * Redact sensitive data in strings */ function redactString(str) { let redacted = str; // Apply all sensitive patterns SENSITIVE_PATTERNS.forEach(pattern => { redacted = redacted.replace(pattern, '[REDACTED]'); }); // Additional redactions for specific patterns redacted = redacted // JSON web tokens .replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[JWT-REDACTED]') // Basic auth strings .replace(/Basic\s+[A-Za-z0-9+/=]+/gi, '[BASIC-AUTH-REDACTED]') // Hex strings that might be keys .replace(/\b[0-9a-fA-F]{16,}\b/g, '[HEX-REDACTED]'); return redacted; } /** * Redact sensitive data in objects */ function redactObject(obj) { const redacted = {}; for (const [key, value] of Object.entries(obj)) { if (shouldRedactKey(key)) { redacted[key] = '[REDACTED]'; } else if (typeof value === 'string' && shouldRedactKey(key.toLowerCase())) { redacted[key] = redactValue(value); } else { redacted[key] = redactValue(value); } } return redacted; } /** * Check if a key should be redacted */ function shouldRedactKey(key) { const lowerKey = key.toLowerCase(); // Check exact matches if (DEFAULT_REDACT_PATHS.some(path => lowerKey.includes(path))) { return true; } // Check for email fields if (lowerKey.includes('email')) { return true; } // Check for user ID fields if (lowerKey.includes('userid') || lowerKey.includes('user_id')) { return true; } // Check for credit card fields if (lowerKey.includes('card') || lowerKey.includes('credit')) { return true; } return false; } /** * Redact email addresses with partial masking */ export function redactEmail(email) { const emailRegex = /^[^@]+@[^@]+\.[^@]+$/; if (!emailRegex.test(email)) { return '[INVALID-EMAIL]'; } const parts = email.split('@'); if (parts.length !== 2) { throw new Error('Invalid email'); } const [localPart, domain] = parts; if (localPart.length <= 3) { return `${localPart[0]}***@${domain}`; } return `${localPart.substring(0, 3)}***@${domain}`; } /** * Redact user IDs with partial masking */ export function redactUserId(userId) { const str = String(userId); if (str.length <= 4) { return 'USER_ID_REDACTED'; } return `${str.substring(0, 2)}***${str.substring(str.length - 2)}`; } /** * Create redaction rules based on configuration */ export function createRedactionRules(customPaths = [], emailRedaction = true, userIdRedaction = true) { return { patterns: [...SENSITIVE_PATTERNS], customPaths: [...DEFAULT_REDACT_PATHS, ...customPaths], emailRedaction, userIdRedaction, }; } /** * Apply redaction to a log entry */ export function redactLogEntry(logEntry, rules) { const redacted = {}; for (const [key, value] of Object.entries(logEntry)) { // Skip system fields if (['level', 'time', 'pid', 'hostname', 'msg'].includes(key)) { redacted[key] = value; continue; } // Apply email redaction if enabled if (rules.emailRedaction && key.toLowerCase().includes('email') && typeof value === 'string') { redacted[key] = redactEmail(value); continue; } // Apply user ID redaction if enabled if (rules.userIdRedaction && (key.toLowerCase().includes('userid') || key.toLowerCase().includes('user_id'))) { redacted[key] = redactUserId(String(value)); continue; } // Check custom paths if (rules.customPaths.some(path => key.toLowerCase().includes(path.toLowerCase()))) { redacted[key] = '[REDACTED]'; continue; } // Apply general redaction redacted[key] = redactValue(value); } return redacted; } /** * Validate redaction rules */ export function validateRedactionRules(rules) { if (!Array.isArray(rules.customPaths)) { return false; } if (!Array.isArray(rules.patterns)) { return false; } if (typeof rules.emailRedaction !== 'boolean') { return false; } if (typeof rules.userIdRedaction !== 'boolean') { return false; } return true; } //# sourceMappingURL=redaction.js.map