const DEFAULT_BIND_HOST = '127.0.0.1'; const DEFAULT_DATABASE_URL = 'file:data/fiscal.sqlite'; const DEFAULT_PORT = '3000'; const DEFAULT_PUBLIC_HOST = 'localhost'; const DEFAULT_SEC_USER_AGENT = 'Fiscal Clone '; const DEFAULT_WORKFLOW_LOCAL_DATA_DIR = '.workflow-data'; const DEFAULT_WORKFLOW_LOCAL_QUEUE_CONCURRENCY = '100'; const PLACEHOLDER_SECRET = 'replace-with-a-long-random-secret'; export const LOCAL_DEV_SECRET = 'fiscal-local-dev-secret-fiscal-local-dev-secret'; type EnvMap = Record; type LocalDevOverrideSummary = { apiBaseChanged: boolean; authOriginChanged: boolean; databaseChanged: boolean; secretFallbackUsed: boolean; workflowChanged: boolean; }; export type LocalDevConfig = { bindHost: string; env: EnvMap; overrides: LocalDevOverrideSummary; port: string; publicOrigin: string; }; function trim(value: string | undefined) { const candidate = value?.trim(); return candidate ? candidate : undefined; } function parseCsvList(value: string | undefined) { return (value ?? '') .split(',') .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function toUniqueList(values: string[]) { return Array.from(new Set(values)); } function coercePort(port: string | undefined) { const parsed = Number.parseInt(port ?? '', 10); if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) { return String(parsed); } return DEFAULT_PORT; } function normalizeOrigin(origin: string) { const url = new URL(origin); if (url.protocol !== 'http:' && url.protocol !== 'https:') { throw new Error(`DEV_PUBLIC_ORIGIN must use http or https. Received: ${origin}`); } url.hash = ''; url.search = ''; const pathName = url.pathname.replace(/\/$/, ''); return `${url.origin}${pathName === '/' ? '' : pathName}`; } function buildPublicOrigin(sourceEnv: EnvMap, port: string) { const explicitOrigin = trim(sourceEnv.DEV_PUBLIC_ORIGIN); if (explicitOrigin) { return normalizeOrigin(explicitOrigin); } const publicHost = trim(sourceEnv.DEV_PUBLIC_HOST) || DEFAULT_PUBLIC_HOST; return normalizeOrigin(`http://${publicHost}:${port}`); } function isSqliteLikeDatabase(databaseUrl: string) { return databaseUrl === ':memory:' || databaseUrl.startsWith('file:') || !databaseUrl.includes('://'); } export function resolveSqlitePath(databaseUrl: string) { let databasePath = databaseUrl.startsWith('file:') ? databaseUrl.slice(5) : databaseUrl; if (databasePath.startsWith('///')) { databasePath = databasePath.slice(2); } return databasePath; } function shouldUseDefaultDatabase(databaseUrl: string | undefined) { if (!databaseUrl) { return true; } if (!isSqliteLikeDatabase(databaseUrl)) { return true; } const path = resolveSqlitePath(databaseUrl); return path.startsWith('/app/'); } export function buildLocalDevConfig(sourceEnv: EnvMap = process.env): LocalDevConfig { const bindHost = trim(sourceEnv.HOSTNAME) || trim(sourceEnv.HOST) || DEFAULT_BIND_HOST; const port = coercePort(trim(sourceEnv.PORT) || trim(sourceEnv.APP_PORT)); const publicOrigin = buildPublicOrigin(sourceEnv, port); const databaseUrl = shouldUseDefaultDatabase(trim(sourceEnv.DATABASE_URL)) ? DEFAULT_DATABASE_URL : trim(sourceEnv.DATABASE_URL) ?? DEFAULT_DATABASE_URL; const secret = trim(sourceEnv.BETTER_AUTH_SECRET); const trustedOrigins = toUniqueList([ publicOrigin, ...parseCsvList(sourceEnv.BETTER_AUTH_TRUSTED_ORIGINS) ]).join(','); const env: EnvMap = { ...sourceEnv, BETTER_AUTH_BASE_URL: publicOrigin, BETTER_AUTH_SECRET: !secret || secret === PLACEHOLDER_SECRET ? LOCAL_DEV_SECRET : secret, BETTER_AUTH_TRUSTED_ORIGINS: trustedOrigins, DATABASE_URL: databaseUrl, HOSTNAME: bindHost, NEXT_PUBLIC_API_URL: '', PORT: port, SEC_USER_AGENT: trim(sourceEnv.SEC_USER_AGENT) || DEFAULT_SEC_USER_AGENT, WORKFLOW_LOCAL_DATA_DIR: trim(sourceEnv.WORKFLOW_LOCAL_DATA_DIR) || DEFAULT_WORKFLOW_LOCAL_DATA_DIR, WORKFLOW_LOCAL_QUEUE_CONCURRENCY: trim(sourceEnv.WORKFLOW_LOCAL_QUEUE_CONCURRENCY) || DEFAULT_WORKFLOW_LOCAL_QUEUE_CONCURRENCY, WORKFLOW_TARGET_WORLD: 'local' }; return { bindHost, env, overrides: { apiBaseChanged: trim(sourceEnv.NEXT_PUBLIC_API_URL) !== undefined, authOriginChanged: trim(sourceEnv.BETTER_AUTH_BASE_URL) !== publicOrigin || trim(sourceEnv.BETTER_AUTH_URL) !== undefined, databaseChanged: databaseUrl !== trim(sourceEnv.DATABASE_URL), secretFallbackUsed: env.BETTER_AUTH_SECRET === LOCAL_DEV_SECRET, workflowChanged: trim(sourceEnv.WORKFLOW_TARGET_WORLD) !== 'local' }, port, publicOrigin }; }