@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
JavaScript
/**
* 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