import { existsSync, lstatSync, realpathSync } from 'node:fs'; import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; export const SOURCE_ROOT = realpathSync(fileURLToPath(new URL('..', import.meta.url))); /** Resolve even a not-yet-created leaf through its nearest existing parent. */ export function canonicalPath(value) { if (typeof value !== 'string' || !isAbsolute(value)) throw new Error('An absolute path is required'); let existing = resolve(value); const missing = []; while (!existsSync(existing)) { // Do not treat a dangling symlink as a new directory or database. try { if (lstatSync(existing).isSymbolicLink()) throw new Error('Invalid path'); } catch (error) { if (error.code !== 'ENOENT') throw error; } const parent = dirname(existing); if (parent === existing) throw new Error('Invalid path'); missing.unshift(existing.slice(parent.length + (parent.endsWith(sep) ? 0 : 1))); existing = parent; } return join(realpathSync(existing), ...missing); } export function isInside(root, path) { const rel = relative(root, path); return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel)); } export function privateDatabasePath(value, distRoot = join(SOURCE_ROOT, 'dist')) { // better-sqlite3 trims filenames. Reject aliases before filesystem/native work // so exclusive backup reservation, path checks and SQLite address the same file. if (typeof value !== 'string' || value !== value.trim()) throw new Error('Invalid database path'); const path = canonicalPath(value); if (path !== path.trim()) throw new Error('Invalid database path'); if ([SOURCE_ROOT, canonicalPath(distRoot), canonicalPath(join(SOURCE_ROOT, 'public'))].some(root => isInside(root, path))) { throw new Error('Database must be outside source and public files'); } return path; } export function parsePublicOrigin(value) { if (typeof value !== 'string' || value.trim() !== value) throw new Error('Invalid PUBLIC_ORIGIN'); let url; try { url = new URL(value); } catch { throw new Error('Invalid PUBLIC_ORIGIN'); } if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash || !url.hostname) { throw new Error('Invalid PUBLIC_ORIGIN'); } return url.origin; } export function readConfig(env = process.env) { const distRoot = join(SOURCE_ROOT, 'dist'); const dbPath = privateDatabasePath(env.DB_PATH, distRoot); const portText = env.PORT ?? '3600'; if (!/^[0-9]+$/.test(portText) || +portText < 1 || +portText > 65535) throw new Error('Invalid PORT'); const port = +portText; const host = env.HOST ?? '127.0.0.1'; if (!host || typeof host !== 'string' || /\s/.test(host)) throw new Error('Invalid HOST'); if (env.NODE_ENV === 'production' && !env.PUBLIC_ORIGIN) throw new Error('PUBLIC_ORIGIN is required in production'); const publicOrigin = parsePublicOrigin(env.PUBLIC_ORIGIN ?? `http://127.0.0.1:${port}`); return { dbPath, distRoot, host, port, publicOrigin }; }