126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
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;
|
|
}
|