179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
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 <support@fiscal.local>';
|
|
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<string, string | undefined>;
|
|
|
|
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 isLoopbackHostname(hostname: string) {
|
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
}
|
|
|
|
function replaceOriginHostname(origin: string, hostname: string) {
|
|
const url = new URL(origin);
|
|
url.hostname = hostname;
|
|
url.hash = '';
|
|
url.search = '';
|
|
|
|
const pathName = url.pathname.replace(/\/$/, '');
|
|
return `${url.origin}${pathName === '/' ? '' : pathName}`;
|
|
}
|
|
|
|
function buildTrustedOrigins(publicOrigin: string, configuredOrigins: string | undefined) {
|
|
const trustedOrigins = [publicOrigin];
|
|
const publicOriginUrl = new URL(publicOrigin);
|
|
|
|
if (isLoopbackHostname(publicOriginUrl.hostname)) {
|
|
trustedOrigins.push(replaceOriginHostname(publicOrigin, 'localhost'));
|
|
trustedOrigins.push(replaceOriginHostname(publicOrigin, '127.0.0.1'));
|
|
}
|
|
|
|
trustedOrigins.push(...parseCsvList(configuredOrigins));
|
|
|
|
return toUniqueList(trustedOrigins).join(',');
|
|
}
|
|
|
|
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 = buildTrustedOrigins(publicOrigin, sourceEnv.BETTER_AUTH_TRUSTED_ORIGINS);
|
|
|
|
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
|
|
};
|
|
}
|