import { config } from 'dotenv'; import { existsSync } from 'node:fs'; import { homedir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { detectFromEnv, loadConfig } from '@mosaicstack/config'; type TierSource = | 'process environment' | 'daemon .env' | 'monorepo-root .env' | 'gateway-local .env' | 'default'; type BootSource = TierSource | 'mosaic.config.json'; export interface GatewayDotenvPaths { daemonEnv: string; monorepoRootEnv: string; gatewayLocalEnv: string; } const here = dirname(fileURLToPath(import.meta.url)); export function resolveGatewayDotenvPaths( anchor: string = here, homeBase: string = homedir(), ): GatewayDotenvPaths { return { daemonEnv: join(homeBase, '.config', 'mosaic', 'gateway', '.env'), monorepoRootEnv: resolve(anchor, '../../..', '.env'), gatewayLocalEnv: resolve(anchor, '..', '.env'), }; } export function resolveGatewayConfigPath(anchor: string = here): string { // GATEWAY_HOME is daemon-created 0700; its env override adds no authority because env can set MOSAIC_STORAGE_TIER. const gatewayHome = resolve( process.env['MOSAIC_GATEWAY_HOME'] ?? join(homedir(), '.config', 'mosaic', 'gateway'), ); const daemonConfig = join(gatewayHome, 'mosaic.config.json'); const gatewayLocalConfig = resolve(anchor, '..', 'mosaic.config.json'); const monorepoRootConfig = resolve(anchor, '../../..', 'mosaic.config.json'); if (existsSync(daemonConfig)) { return daemonConfig; } if (existsSync(gatewayLocalConfig)) { return gatewayLocalConfig; } if (existsSync(monorepoRootConfig)) { return monorepoRootConfig; } return monorepoRootConfig; } export function loadGatewayEnv(anchor: string = here, homeBase: string = homedir()): void { const { daemonEnv, monorepoRootEnv, gatewayLocalEnv } = resolveGatewayDotenvPaths( anchor, homeBase, ); const inheritedTier = process.env['MOSAIC_STORAGE_TIER']; let tierSource: TierSource = inheritedTier === undefined ? 'default' : 'process environment'; const inheritedDatabaseUrl = process.env['DATABASE_URL']; let databaseUrlSource: TierSource = inheritedDatabaseUrl === undefined ? 'default' : 'process environment'; function loadAnchoredDotenv( path: string, sourceLabel: Exclude, ): void { if (!existsSync(path)) { return; } const beforeTier = process.env['MOSAIC_STORAGE_TIER']; const beforeDatabaseUrl = process.env['DATABASE_URL']; config({ path, quiet: true }); if ( beforeTier === undefined && process.env['MOSAIC_STORAGE_TIER'] !== undefined && tierSource === 'default' ) { tierSource = sourceLabel; } if ( beforeDatabaseUrl === undefined && process.env['DATABASE_URL'] !== undefined && databaseUrlSource === 'default' ) { databaseUrlSource = sourceLabel; } } // Load .env from daemon config dir (global install / daemon mode) first. // It takes precedence over file-based local-dev configuration. loadAnchoredDotenv(daemonEnv, 'daemon .env'); // Load .env from the anchored monorepo root, then fill any remaining values // from apps/gateway/.env when present. loadAnchoredDotenv(monorepoRootEnv, 'monorepo-root .env'); loadAnchoredDotenv(gatewayLocalEnv, 'gateway-local .env'); const envOnlyTier = detectFromEnv().tier; const configPath = resolveGatewayConfigPath(anchor); const anchoredConfigExists = existsSync(configPath); const resolvedTier = loadConfig(configPath).tier; const configuredTier = process.env['MOSAIC_STORAGE_TIER']; const databaseUrlDeterminesTier = envOnlyTier === 'standalone' && configuredTier !== 'standalone'; const recognizedTierDeterminesTier = (configuredTier === 'federated' || configuredTier === 'standalone' || configuredTier === 'local') && configuredTier === envOnlyTier; let source: BootSource; if (anchoredConfigExists) { source = 'mosaic.config.json'; } else if (databaseUrlDeterminesTier && databaseUrlSource !== 'default') { source = databaseUrlSource; } else if (recognizedTierDeterminesTier && tierSource !== 'default') { source = tierSource; } else { source = 'default'; } console.info(`[gateway env] storage tier=${resolvedTier} source=${source}`); } loadGatewayEnv();