| import { readConfigFile } from "./config-file.js"; |
| import { existsSync, realpathSync } from "node:fs"; |
| import { resolve } from "node:path"; |
| import { config as loadDotenv } from "dotenv"; |
| import { resolvePaperclipEnvPath } from "./paths.js"; |
| import { maybeRepairLegacyWorktreeConfigAndEnvFiles } from "./worktree-config.js"; |
| import { |
| AUTH_BASE_URL_MODES, |
| DEPLOYMENT_EXPOSURES, |
| DEPLOYMENT_MODES, |
| SECRET_PROVIDERS, |
| STORAGE_PROVIDERS, |
| type AuthBaseUrlMode, |
| type DeploymentExposure, |
| type DeploymentMode, |
| type SecretProvider, |
| type StorageProvider, |
| } from "@paperclipai/shared"; |
| import { |
| resolveDefaultBackupDir, |
| resolveDefaultEmbeddedPostgresDir, |
| resolveDefaultSecretsKeyFilePath, |
| resolveDefaultStorageDir, |
| resolveHomeAwarePath, |
| } from "./home-paths.js"; |
|
|
| const PAPERCLIP_ENV_FILE_PATH = resolvePaperclipEnvPath(); |
| if (existsSync(PAPERCLIP_ENV_FILE_PATH)) { |
| loadDotenv({ path: PAPERCLIP_ENV_FILE_PATH, override: false, quiet: true }); |
| } |
|
|
| const CWD_ENV_PATH = resolve(process.cwd(), ".env"); |
| const isSameFile = existsSync(CWD_ENV_PATH) && existsSync(PAPERCLIP_ENV_FILE_PATH) |
| ? realpathSync(CWD_ENV_PATH) === realpathSync(PAPERCLIP_ENV_FILE_PATH) |
| : CWD_ENV_PATH === PAPERCLIP_ENV_FILE_PATH; |
| if (!isSameFile && existsSync(CWD_ENV_PATH)) { |
| loadDotenv({ path: CWD_ENV_PATH, override: false, quiet: true }); |
| } |
|
|
| maybeRepairLegacyWorktreeConfigAndEnvFiles(); |
|
|
| type DatabaseMode = "embedded-postgres" | "postgres"; |
|
|
| export interface Config { |
| deploymentMode: DeploymentMode; |
| deploymentExposure: DeploymentExposure; |
| host: string; |
| port: number; |
| allowedHostnames: string[]; |
| authBaseUrlMode: AuthBaseUrlMode; |
| authPublicBaseUrl: string | undefined; |
| authDisableSignUp: boolean; |
| databaseMode: DatabaseMode; |
| databaseUrl: string | undefined; |
| embeddedPostgresDataDir: string; |
| embeddedPostgresPort: number; |
| databaseBackupEnabled: boolean; |
| databaseBackupIntervalMinutes: number; |
| databaseBackupRetentionDays: number; |
| databaseBackupDir: string; |
| serveUi: boolean; |
| uiDevMiddleware: boolean; |
| secretsProvider: SecretProvider; |
| secretsStrictMode: boolean; |
| secretsMasterKeyFilePath: string; |
| storageProvider: StorageProvider; |
| storageLocalDiskBaseDir: string; |
| storageS3Bucket: string; |
| storageS3Region: string; |
| storageS3Endpoint: string | undefined; |
| storageS3Prefix: string; |
| storageS3ForcePathStyle: boolean; |
| heartbeatSchedulerEnabled: boolean; |
| heartbeatSchedulerIntervalMs: number; |
| companyDeletionEnabled: boolean; |
| } |
|
|
| export function loadConfig(): Config { |
| const fileConfig = readConfigFile(); |
| const fileDatabaseMode = |
| (fileConfig?.database.mode === "postgres" ? "postgres" : "embedded-postgres") as DatabaseMode; |
|
|
| const fileDbUrl = |
| fileDatabaseMode === "postgres" |
| ? fileConfig?.database.connectionString |
| : undefined; |
| const fileDatabaseBackup = fileConfig?.database.backup; |
| const fileSecrets = fileConfig?.secrets; |
| const fileStorage = fileConfig?.storage; |
| const strictModeFromEnv = process.env.PAPERCLIP_SECRETS_STRICT_MODE; |
| const secretsStrictMode = |
| strictModeFromEnv !== undefined |
| ? strictModeFromEnv === "true" |
| : (fileSecrets?.strictMode ?? false); |
|
|
| const providerFromEnvRaw = process.env.PAPERCLIP_SECRETS_PROVIDER; |
| const providerFromEnv = |
| providerFromEnvRaw && SECRET_PROVIDERS.includes(providerFromEnvRaw as SecretProvider) |
| ? (providerFromEnvRaw as SecretProvider) |
| : null; |
| const providerFromFile = fileSecrets?.provider; |
| const secretsProvider: SecretProvider = providerFromEnv ?? providerFromFile ?? "local_encrypted"; |
|
|
| const storageProviderFromEnvRaw = process.env.PAPERCLIP_STORAGE_PROVIDER; |
| const storageProviderFromEnv = |
| storageProviderFromEnvRaw && STORAGE_PROVIDERS.includes(storageProviderFromEnvRaw as StorageProvider) |
| ? (storageProviderFromEnvRaw as StorageProvider) |
| : null; |
| const storageProvider: StorageProvider = storageProviderFromEnv ?? fileStorage?.provider ?? "local_disk"; |
| const storageLocalDiskBaseDir = resolveHomeAwarePath( |
| process.env.PAPERCLIP_STORAGE_LOCAL_DIR ?? |
| fileStorage?.localDisk?.baseDir ?? |
| resolveDefaultStorageDir(), |
| ); |
| const storageS3Bucket = process.env.PAPERCLIP_STORAGE_S3_BUCKET ?? fileStorage?.s3?.bucket ?? "paperclip"; |
| const storageS3Region = process.env.PAPERCLIP_STORAGE_S3_REGION ?? fileStorage?.s3?.region ?? "us-east-1"; |
| const storageS3Endpoint = process.env.PAPERCLIP_STORAGE_S3_ENDPOINT ?? fileStorage?.s3?.endpoint ?? undefined; |
| const storageS3Prefix = process.env.PAPERCLIP_STORAGE_S3_PREFIX ?? fileStorage?.s3?.prefix ?? ""; |
| const storageS3ForcePathStyle = |
| process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE !== undefined |
| ? process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE === "true" |
| : (fileStorage?.s3?.forcePathStyle ?? false); |
|
|
| const deploymentModeFromEnvRaw = process.env.PAPERCLIP_DEPLOYMENT_MODE; |
| const deploymentModeFromEnv = |
| deploymentModeFromEnvRaw && DEPLOYMENT_MODES.includes(deploymentModeFromEnvRaw as DeploymentMode) |
| ? (deploymentModeFromEnvRaw as DeploymentMode) |
| : null; |
| const deploymentMode: DeploymentMode = deploymentModeFromEnv ?? fileConfig?.server.deploymentMode ?? "local_trusted"; |
| const deploymentExposureFromEnvRaw = process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE; |
| const deploymentExposureFromEnv = |
| deploymentExposureFromEnvRaw && |
| DEPLOYMENT_EXPOSURES.includes(deploymentExposureFromEnvRaw as DeploymentExposure) |
| ? (deploymentExposureFromEnvRaw as DeploymentExposure) |
| : null; |
| const deploymentExposure: DeploymentExposure = |
| deploymentMode === "local_trusted" |
| ? "private" |
| : (deploymentExposureFromEnv ?? fileConfig?.server.exposure ?? "private"); |
| const authBaseUrlModeFromEnvRaw = process.env.PAPERCLIP_AUTH_BASE_URL_MODE; |
| const authBaseUrlModeFromEnv = |
| authBaseUrlModeFromEnvRaw && |
| AUTH_BASE_URL_MODES.includes(authBaseUrlModeFromEnvRaw as AuthBaseUrlMode) |
| ? (authBaseUrlModeFromEnvRaw as AuthBaseUrlMode) |
| : null; |
| const publicUrlFromEnv = process.env.PAPERCLIP_PUBLIC_URL; |
| const authPublicBaseUrlRaw = |
| process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ?? |
| process.env.BETTER_AUTH_URL ?? |
| process.env.BETTER_AUTH_BASE_URL ?? |
| publicUrlFromEnv ?? |
| fileConfig?.auth?.publicBaseUrl; |
| const authPublicBaseUrl = authPublicBaseUrlRaw?.trim() || undefined; |
| const authBaseUrlMode: AuthBaseUrlMode = |
| authBaseUrlModeFromEnv ?? |
| fileConfig?.auth?.baseUrlMode ?? |
| (authPublicBaseUrl ? "explicit" : "auto"); |
| const disableSignUpFromEnv = process.env.PAPERCLIP_AUTH_DISABLE_SIGN_UP; |
| const authDisableSignUp: boolean = |
| disableSignUpFromEnv !== undefined |
| ? disableSignUpFromEnv === "true" |
| : (fileConfig?.auth?.disableSignUp ?? false); |
| const allowedHostnamesFromEnvRaw = process.env.PAPERCLIP_ALLOWED_HOSTNAMES; |
| const allowedHostnamesFromEnv = allowedHostnamesFromEnvRaw |
| ? allowedHostnamesFromEnvRaw |
| .split(",") |
| .map((value) => value.trim().toLowerCase()) |
| .filter((value) => value.length > 0) |
| : null; |
| const publicUrlHostname = authPublicBaseUrl |
| ? (() => { |
| try { |
| return new URL(authPublicBaseUrl).hostname.trim().toLowerCase(); |
| } catch { |
| return null; |
| } |
| })() |
| : null; |
| const allowedHostnames = Array.from( |
| new Set( |
| [ |
| ...(allowedHostnamesFromEnv ?? fileConfig?.server.allowedHostnames ?? []), |
| ...(publicUrlHostname ? [publicUrlHostname] : []), |
| ] |
| .map((value) => value.trim().toLowerCase()) |
| .filter(Boolean), |
| ), |
| ); |
| const companyDeletionEnvRaw = process.env.PAPERCLIP_ENABLE_COMPANY_DELETION; |
| const companyDeletionEnabled = |
| companyDeletionEnvRaw !== undefined |
| ? companyDeletionEnvRaw === "true" |
| : deploymentMode === "local_trusted"; |
| const databaseBackupEnabled = |
| process.env.PAPERCLIP_DB_BACKUP_ENABLED !== undefined |
| ? process.env.PAPERCLIP_DB_BACKUP_ENABLED === "true" |
| : (fileDatabaseBackup?.enabled ?? true); |
| const databaseBackupIntervalMinutes = Math.max( |
| 1, |
| Number(process.env.PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES) || |
| fileDatabaseBackup?.intervalMinutes || |
| 60, |
| ); |
| const databaseBackupRetentionDays = Math.max( |
| 1, |
| Number(process.env.PAPERCLIP_DB_BACKUP_RETENTION_DAYS) || |
| fileDatabaseBackup?.retentionDays || |
| 30, |
| ); |
| const databaseBackupDir = resolveHomeAwarePath( |
| process.env.PAPERCLIP_DB_BACKUP_DIR ?? |
| fileDatabaseBackup?.dir ?? |
| resolveDefaultBackupDir(), |
| ); |
|
|
| return { |
| deploymentMode, |
| deploymentExposure, |
| host: process.env.HOST ?? fileConfig?.server.host ?? "127.0.0.1", |
| port: Number(process.env.PORT) || fileConfig?.server.port || 3100, |
| allowedHostnames, |
| authBaseUrlMode, |
| authPublicBaseUrl, |
| authDisableSignUp, |
| databaseMode: fileDatabaseMode, |
| databaseUrl: process.env.DATABASE_URL ?? fileDbUrl, |
| embeddedPostgresDataDir: resolveHomeAwarePath( |
| fileConfig?.database.embeddedPostgresDataDir ?? resolveDefaultEmbeddedPostgresDir(), |
| ), |
| embeddedPostgresPort: fileConfig?.database.embeddedPostgresPort ?? 54329, |
| databaseBackupEnabled, |
| databaseBackupIntervalMinutes, |
| databaseBackupRetentionDays, |
| databaseBackupDir, |
| serveUi: |
| process.env.SERVE_UI !== undefined |
| ? process.env.SERVE_UI === "true" |
| : fileConfig?.server.serveUi ?? true, |
| uiDevMiddleware: process.env.PAPERCLIP_UI_DEV_MIDDLEWARE === "true", |
| secretsProvider, |
| secretsStrictMode, |
| secretsMasterKeyFilePath: |
| resolveHomeAwarePath( |
| process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE ?? |
| fileSecrets?.localEncrypted.keyFilePath ?? |
| resolveDefaultSecretsKeyFilePath(), |
| ), |
| storageProvider, |
| storageLocalDiskBaseDir, |
| storageS3Bucket, |
| storageS3Region, |
| storageS3Endpoint, |
| storageS3Prefix, |
| storageS3ForcePathStyle, |
| heartbeatSchedulerEnabled: process.env.HEARTBEAT_SCHEDULER_ENABLED !== "false", |
| heartbeatSchedulerIntervalMs: Math.max(10000, Number(process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS) || 30000), |
| companyDeletionEnabled, |
| }; |
| } |
|
|