import fs from "node:fs"; import path from "node:path"; import { randomBytes } from "node:crypto"; import { config as loadDotenv, parse as parseEnvFileContents } from "dotenv"; import { resolveConfigPath } from "./store.js"; const JWT_SECRET_ENV_KEY = "PAPERCLIP_AGENT_JWT_SECRET"; function resolveEnvFilePath(configPath?: string) { return path.resolve(path.dirname(resolveConfigPath(configPath)), ".env"); } const loadedEnvFiles = new Set(); function isNonEmpty(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } function parseEnvFile(contents: string) { try { return parseEnvFileContents(contents); } catch { return {}; } } function formatEnvValue(value: string): string { if (/^[A-Za-z0-9_./:@-]+$/.test(value)) { return value; } return JSON.stringify(value); } function renderEnvFile(entries: Record) { const lines = [ "# Paperclip environment variables", "# Generated by Paperclip CLI commands", ...Object.entries(entries).map(([key, value]) => `${key}=${formatEnvValue(value)}`), "", ]; return lines.join("\n"); } export function resolvePaperclipEnvFile(configPath?: string): string { return resolveEnvFilePath(configPath); } export function resolveAgentJwtEnvFile(configPath?: string): string { return resolveEnvFilePath(configPath); } export function loadPaperclipEnvFile(configPath?: string): void { loadAgentJwtEnvFile(resolveEnvFilePath(configPath)); } export function loadAgentJwtEnvFile(filePath = resolveEnvFilePath()): void { if (loadedEnvFiles.has(filePath)) return; if (!fs.existsSync(filePath)) return; loadedEnvFiles.add(filePath); loadDotenv({ path: filePath, override: false, quiet: true }); } export function readAgentJwtSecretFromEnv(configPath?: string): string | null { loadAgentJwtEnvFile(resolveEnvFilePath(configPath)); const raw = process.env[JWT_SECRET_ENV_KEY]; return isNonEmpty(raw) ? raw!.trim() : null; } export function readAgentJwtSecretFromEnvFile(filePath = resolveEnvFilePath()): string | null { if (!fs.existsSync(filePath)) return null; const raw = fs.readFileSync(filePath, "utf-8"); const values = parseEnvFile(raw); const value = values[JWT_SECRET_ENV_KEY]; return isNonEmpty(value) ? value!.trim() : null; } export function ensureAgentJwtSecret(configPath?: string): { secret: string; created: boolean } { const existingEnv = readAgentJwtSecretFromEnv(configPath); if (existingEnv) { return { secret: existingEnv, created: false }; } const envFilePath = resolveEnvFilePath(configPath); const existingFile = readAgentJwtSecretFromEnvFile(envFilePath); const secret = existingFile ?? randomBytes(32).toString("hex"); const created = !existingFile; if (!existingFile) { writeAgentJwtEnv(secret, envFilePath); } return { secret, created }; } export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void { mergePaperclipEnvEntries({ [JWT_SECRET_ENV_KEY]: secret }, filePath); } export function readPaperclipEnvEntries(filePath = resolveEnvFilePath()): Record { if (!fs.existsSync(filePath)) return {}; return parseEnvFile(fs.readFileSync(filePath, "utf-8")); } export function writePaperclipEnvEntries(entries: Record, filePath = resolveEnvFilePath()): void { const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, renderEnvFile(entries), { mode: 0o600, }); } export function mergePaperclipEnvEntries( entries: Record, filePath = resolveEnvFilePath(), ): Record { const current = readPaperclipEnvEntries(filePath); const next = { ...current, ...Object.fromEntries( Object.entries(entries).filter(([, value]) => typeof value === "string" && value.trim().length > 0), ), }; writePaperclipEnvEntries(next, filePath); return next; }