62 lines
3.0 KiB
JavaScript
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 };
|
|
}
|