Spaces:
Paused
Paused
| 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<string>(); | |
| 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<string, string>) { | |
| 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<string, string> { | |
| if (!fs.existsSync(filePath)) return {}; | |
| return parseEnvFile(fs.readFileSync(filePath, "utf-8")); | |
| } | |
| export function writePaperclipEnvEntries(entries: Record<string, string>, 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<string, string>, | |
| filePath = resolveEnvFilePath(), | |
| ): Record<string, string> { | |
| 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; | |
| } | |