mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 12:28:10 +02:00
Completes the post-managed-OAuth connector lifecycle, Paperclip Cloud provisioning defaults, governed test flows, and consolidated Apps UI.\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing>
146 lines
5.0 KiB
TypeScript
146 lines
5.0 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 { updateEnvFileContents, writeEnvFileAtomicallyIfChanged } from "@paperclipai/shared/env-file";
|
|
import { resolveConfigPath } from "./store.js";
|
|
|
|
const JWT_SECRET_ENV_KEY = "PAPERCLIP_AGENT_JWT_SECRET";
|
|
const TOOL_ACTION_SIGNING_SECRET_ENV_KEY = "PAPERCLIP_TOOL_ACTION_SIGNING_SECRET";
|
|
const PAPERCLIP_OWNED_ENV_KEY_PATTERN = /^PAPERCLIP_[A-Z0-9_]+$/;
|
|
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 emptyEnvFileContents() {
|
|
return [
|
|
"# Paperclip environment variables",
|
|
"# Generated by Paperclip CLI commands",
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
function paperclipOwnedEntries(entries: Record<string, string>): Record<string, string> {
|
|
return Object.fromEntries(
|
|
Object.entries(entries).filter(
|
|
([key, value]) => PAPERCLIP_OWNED_ENV_KEY_PATTERN.test(key) && value.trim().length > 0,
|
|
),
|
|
);
|
|
}
|
|
|
|
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 ensureToolActionSigningSecret(configPath?: string): { secret: string; created: boolean } {
|
|
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
|
|
const existingEnv = process.env[TOOL_ACTION_SIGNING_SECRET_ENV_KEY];
|
|
if (isNonEmpty(existingEnv)) {
|
|
return { secret: existingEnv.trim(), created: false };
|
|
}
|
|
|
|
const envFilePath = resolveEnvFilePath(configPath);
|
|
const existingFile = readPaperclipEnvEntries(envFilePath)[TOOL_ACTION_SIGNING_SECRET_ENV_KEY];
|
|
const secret = isNonEmpty(existingFile) ? existingFile.trim() : randomBytes(32).toString("hex");
|
|
const created = !isNonEmpty(existingFile);
|
|
|
|
if (created) {
|
|
mergePaperclipEnvEntries({ [TOOL_ACTION_SIGNING_SECRET_ENV_KEY]: 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 previousContents = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
const nextContents = updateEnvFileContents(previousContents ?? emptyEnvFileContents(), paperclipOwnedEntries(entries), {
|
|
valueEncoding: "minimal",
|
|
});
|
|
writeEnvFileAtomicallyIfChanged(filePath, previousContents, nextContents);
|
|
}
|
|
|
|
export function mergePaperclipEnvEntries(
|
|
entries: Record<string, string>,
|
|
filePath = resolveEnvFilePath(),
|
|
): Record<string, string> {
|
|
const current = readPaperclipEnvEntries(filePath);
|
|
const managedEntries = paperclipOwnedEntries(entries);
|
|
const next = {
|
|
...current,
|
|
...managedEntries,
|
|
};
|
|
writePaperclipEnvEntries(managedEntries, filePath);
|
|
return next;
|
|
}
|