File size: 3,959 Bytes
b152fd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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;
}