Files

62 lines
3.0 KiB
JavaScript

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 };
}