// Server data-backend resolution for the vendored Hasna storage kit. // // A server has exactly one technical switch: `sqlite | postgresql`. // A configured DATABASE_URL selects PostgreSQL; otherwise SQLite is // authoritative. The environment contract alone selects the backend (owner // directive 2026-07-29). import { ownString } from "./own.js"; export const SERVER_DATA_BACKENDS = ["sqlite", "postgresql"] as const; export type ServerDataBackend = (typeof SERVER_DATA_BACKENDS)[number]; export type Env = Record; /** Upper-snake env token for an app name, e.g. `todos` -> `TODOS`. */ export function envToken(name: string): string { return name.toUpperCase().replace(/-/g, "_"); } export interface ServerDataBackendEnvKeys { databaseUrlKeys: string[]; } export function serverDataBackendEnvKeys(name: string): ServerDataBackendEnvKeys { const token = envToken(name); return { databaseUrlKeys: [`HASNA_${token}_DATABASE_URL`, `${token}_DATABASE_URL`], }; } // The resolver reads the env as OWN properties. `env` is caller-supplied and // `process.env` is itself prototype-pollutable, so an unguarded `env[key]` let a // polluted `HASNA__DATABASE_URL` flip the backend to postgresql and hand // back a connection string the operator never configured. function firstEnv(env: Env, keys: readonly string[]): { key: string; value: string } | null { for (const key of keys) { const value = ownString(env, key)?.trim(); if (value) return { key, value }; } return null; } export interface ServerDataBackendResolution { backend: ServerDataBackend; source: string; databaseUrlPresent: boolean; databaseUrlSource: string | null; } export function resolveServerDataBackend( name: string, env: Env = process.env, ): ServerDataBackendResolution { const databaseUrl = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys); if (!databaseUrl) { return { backend: "sqlite", source: "default", databaseUrlPresent: false, databaseUrlSource: null, }; } return { backend: "postgresql", source: databaseUrl.key, databaseUrlPresent: true, databaseUrlSource: databaseUrl.key, }; } /** Resolve the database URL without logging it. Returns `null` when unset. */ export function resolveDatabaseUrl(name: string, env: Env = process.env): string | null { const hit = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys); return hit?.value ?? null; }