import { existsSync } from "node:fs"; import { join } from "node:path"; export interface DetectedSecret { key: string; value: string; source: ".env" | ".env.local" | ".env.development" | "process.env"; } /** * Known secret key names to look for */ export const KNOWN_SECRET_KEYS = [ // AI/ML APIs "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "COHERE_API_KEY", "HUGGINGFACE_API_KEY", "REPLICATE_API_TOKEN", // Farcaster/Social "NEYNAR_API_KEY", "FARCASTER_PRIVATE_KEY", // Payment "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET", // Database "DATABASE_URL", "POSTGRES_URL", "MONGODB_URI", "REDIS_URL", // Auth "JWT_SECRET", "SESSION_SECRET", "AUTH_SECRET", // Cloud/Infra "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "CLOUDFLARE_API_TOKEN", ]; /** * Patterns that indicate a key is likely a secret */ export const SECRET_KEY_PATTERNS = [ /_API_KEY$/, /_SECRET$/, /_SECRET_KEY$/, /_TOKEN$/, /_PASSWORD$/, /_PRIVATE_KEY$/, ]; /** * Check if a key looks like a secret */ function isSecretKey(key: string): boolean { if (KNOWN_SECRET_KEYS.includes(key)) { return true; } return SECRET_KEY_PATTERNS.some((pattern) => pattern.test(key)); } /** * Parse a .env file content into key-value pairs */ function parseEnvFile(content: string): Record { const result: Record = {}; for (const line of content.split("\n")) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith("#")) { continue; } const eqIndex = trimmed.indexOf("="); if (eqIndex === -1) { continue; } const key = trimmed.slice(0, eqIndex).trim(); let value = trimmed.slice(eqIndex + 1).trim(); // Remove surrounding quotes if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } if (key && value) { result[key] = value; } } return result; } /** * Scan .env files in a directory for secrets */ async function scanEnvFiles(dir: string): Promise { const secrets: DetectedSecret[] = []; const envFiles: Array<{ name: ".env" | ".env.local" | ".env.development"; path: string }> = [ { name: ".env.local", path: join(dir, ".env.local") }, { name: ".env.development", path: join(dir, ".env.development") }, { name: ".env", path: join(dir, ".env") }, ]; for (const { name, path } of envFiles) { if (!existsSync(path)) { continue; } const content = await Bun.file(path).text(); const parsed = parseEnvFile(content); for (const [key, value] of Object.entries(parsed)) { if (isSecretKey(key)) { // Check if already detected (avoid duplicates from multiple files) if (!secrets.some((s) => s.key === key)) { secrets.push({ key, value, source: name }); } } } } return secrets; } /** * Scan process.env for secrets */ function scanProcessEnv(): DetectedSecret[] { const secrets: DetectedSecret[] = []; for (const [key, value] of Object.entries(process.env)) { if (value && isSecretKey(key)) { secrets.push({ key, value, source: "process.env" }); } } return secrets; } /** * Detect all secrets from .env files and process.env */ export async function detectSecrets(projectDir?: string): Promise { const dir = projectDir || process.cwd(); const secrets: DetectedSecret[] = []; const seenKeys = new Set(); // Scan .env files first (higher priority) const envSecrets = await scanEnvFiles(dir); for (const secret of envSecrets) { if (!seenKeys.has(secret.key)) { secrets.push(secret); seenKeys.add(secret.key); } } // Then scan process.env const processSecrets = scanProcessEnv(); for (const secret of processSecrets) { if (!seenKeys.has(secret.key)) { secrets.push(secret); seenKeys.add(secret.key); } } return secrets; } /** * Generate .env file content from secrets */ export function generateEnvFile(secrets: Record): string { const lines: string[] = ["# Generated by jack"]; for (const [key, value] of Object.entries(secrets)) { // Quote values that contain spaces or special characters const needsQuotes = /[\s#"'\\]/.test(value); const escapedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value; lines.push(`${key}=${escapedValue}`); } return `${lines.join("\n")}\n`; } /** * Generate JSON file for wrangler secret bulk */ export function generateSecretsJson(secrets: Record): string { return JSON.stringify(secrets, null, 2); }