import { randomUUID } from 'node:crypto'; import { constants, type Stats } from 'node:fs'; import { lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises'; import path from 'node:path'; import type { WorkEntry, WorkEntryProject, WorkLaunchSpec } from './work-entry-contracts'; const ENTRY_FILENAME = 'work-entry.json'; const MAX_ENTRY_BYTES = 16 * 1024; const MAX_URL_LENGTH = 2_048; const MAX_PATH_LENGTH = 1_024; const NO_FOLLOW = constants.O_NOFOLLOW ?? 0; type ProjectRoot = { root: string; rootStat: Stats }; type Locations = ProjectRoot & { directory: string; file: string; directoryStat: Stats }; function invalid(): never { throw new Error('Invalid project work entry'); } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); } function hasOnlyKeys(value: Record, keys: string[]): boolean { return Object.keys(value).every((key) => keys.includes(key)); } function samePath(left: string, right: string): boolean { return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right; } function sameNode(left: Stats, right: Stats): boolean { return left.dev === right.dev && left.ino === right.ino; } function containsControl(value: string, includeSpace = false): boolean { for (let index = 0; index < value.length; index += 1) { const code = value.charCodeAt(index); if (code < (includeSpace ? 33 : 32) || code === 127) return true; } return false; } async function projectRoot(project: WorkEntryProject): Promise { if (typeof project.id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/u.test(project.id) || typeof project.path !== 'string' || !path.isAbsolute(project.path)) invalid(); const root = await realpath(project.path); const rootStat = await lstat(root); if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) invalid(); return { root, rootStat }; } async function assertRoot(target: ProjectRoot): Promise { const stat = await lstat(target.root); if (!stat.isDirectory() || stat.isSymbolicLink() || !sameNode(stat, target.rootStat) || !samePath(await realpath(target.root), target.root)) invalid(); } async function locations(project: WorkEntryProject, createMetadata = false, verifiedRoot?: ProjectRoot): Promise { const { root, rootStat } = verifiedRoot ?? await projectRoot(project); const directory = path.join(root, '.makelore'); const directoryStat = await lstat(directory).catch(async (error: NodeJS.ErrnoException) => { if (error.code !== 'ENOENT' || !createMetadata) throw error; await assertRoot({ root, rootStat }); // Imported folders may not have metadata yet. Create only this direct child; // a concurrent creator must still pass the same directory/link checks below. await mkdir(directory, { mode: 0o700 }).catch((mkdirError: NodeJS.ErrnoException) => { if (mkdirError.code !== 'EEXIST') throw mkdirError; }); return await lstat(directory); }); if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !samePath(await realpath(directory), directory)) invalid(); await assertRoot({ root, rootStat }); return { root, directory, file: path.join(directory, ENTRY_FILENAME), rootStat, directoryStat }; } async function assertLocations(target: Locations): Promise { await assertRoot(target); const directoryStat = await lstat(target.directory); if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !sameNode(directoryStat, target.directoryStat) || !samePath(await realpath(target.directory), target.directory)) invalid(); } function relativePath(value: unknown): string { if (typeof value !== 'string' || !value || value.length > MAX_PATH_LENGTH || containsControl(value) || /[\\:]/u.test(value) || value.trim() !== value || path.posix.isAbsolute(value) || path.win32.isAbsolute(value)) invalid(); const pieces = value.split('/'); if (pieces.length > 32 || pieces.includes('..')) invalid(); return path.posix.normalize(value); } async function relativeTarget(root: string, value: unknown, kind: 'directory' | 'file'): Promise { const relative = relativePath(value); let current = root; for (const piece of relative.split('/').filter((piece) => piece !== '.')) { current = path.join(current, piece); if ((await lstat(current)).isSymbolicLink()) invalid(); } const stat = await lstat(current); if (!(kind === 'directory' ? stat.isDirectory() : stat.isFile()) || !samePath(await realpath(current), current)) invalid(); return relative; } function localUrl(value: unknown): string { if (typeof value !== 'string' || value.length > MAX_URL_LENGTH || containsControl(value, true)) invalid(); const url = new URL(value); if (!['http:', 'https:'].includes(url.protocol) || !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) || url.username || url.password) invalid(); return value; } /** Only structured Vite options are persisted; the launcher supplies the loopback host. */ function viteArgs(value: unknown): string[] { if (!Array.isArray(value) || value.length > 16) invalid(); const result: string[] = []; const seen = new Set(); for (let index = 0; index < value.length; index += 1) { const flag: unknown = value[index]; if (typeof flag !== 'string' || seen.has(flag)) invalid(); seen.add(flag); if (flag === '--strictPort' || flag === '--force') { result.push(flag); continue; } const parameter: unknown = value[++index]; if (typeof parameter !== 'string') invalid(); if (flag === '--port') { if (!/^[1-9][0-9]{0,4}$/u.test(parameter) || Number(parameter) > 65_535) invalid(); } else if (flag === '--mode') { if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(parameter)) invalid(); } else if (flag === '--base') { if (parameter.length > MAX_PATH_LENGTH || !/^(?:\/(?!\/)|\.\/)/u.test(parameter) || containsControl(parameter, true) || /[\\?#:]/u.test(parameter) || parameter.split('/').includes('..')) invalid(); } else invalid(); result.push(flag, parameter); } return result; } async function launchSpec(root: string, value: unknown): Promise { if (!isRecord(value)) invalid(); if (value.kind === 'vite' && hasOnlyKeys(value, ['kind', 'directory', 'args'])) { return { kind: 'vite', directory: await relativeTarget(root, value.directory, 'directory'), args: viteArgs(value.args) }; } if (value.kind === 'static' && hasOnlyKeys(value, ['kind', 'directory', 'entry'])) { const directory = await relativeTarget(root, value.directory, 'directory'); const entry = await relativeTarget(path.join(root, directory), value.entry, 'file'); return { kind: 'static', directory, entry }; } return invalid(); } async function validatedEntry(project: WorkEntryProject, root: string, value: unknown, saving = false): Promise { if (!isRecord(value) || !hasOnlyKeys(value, ['version', 'projectId', 'projectPath', 'url', 'launch', 'documentHash']) || value.version !== 1 || value.projectId !== project.id || typeof value.projectPath !== 'string' || !path.isAbsolute(value.projectPath)) invalid(); const boundPath = saving ? await realpath(value.projectPath) : value.projectPath; if (!samePath(boundPath, root)) invalid(); if (value.documentHash !== undefined && (typeof value.documentHash !== 'string' || !/^[a-f0-9]{64}$/u.test(value.documentHash))) invalid(); return { version: 1, projectId: project.id, projectPath: root, url: localUrl(value.url), ...(value.launch === undefined ? {} : { launch: await launchSpec(root, value.launch) }), ...(value.documentHash === undefined ? {} : { documentHash: value.documentHash }), }; } /** Reads bounded project-owned metadata without following links or starting any work. */ export async function loadWorkEntry(project: WorkEntryProject): Promise { try { const target = await locations(project); const fileStat = await lstat(target.file); if (!fileStat.isFile() || fileStat.isSymbolicLink() || fileStat.size > MAX_ENTRY_BYTES) return null; const handle = await open(target.file, constants.O_RDONLY | NO_FOLLOW); try { const openedStat = await handle.stat(); if (!openedStat.isFile() || openedStat.size > MAX_ENTRY_BYTES || !sameNode(fileStat, openedStat)) return null; const bytes = Buffer.alloc(MAX_ENTRY_BYTES + 1); let size = 0; while (size < bytes.length) { const chunk = await handle.read(bytes, size, bytes.length - size, size); if (!chunk.bytesRead) break; size += chunk.bytesRead; } if (size > MAX_ENTRY_BYTES) return null; await assertLocations(target); const currentStat = await lstat(target.file); if (currentStat.isSymbolicLink() || !sameNode(openedStat, currentStat)) return null; const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size)); return await validatedEntry(project, target.root, JSON.parse(source)); } finally { await handle.close(); } } catch { // Missing, copied, changed or corrupt metadata is never launch authority. return null; } } /** Publishes one complete entry in project metadata, using an exclusive temp file. */ export async function saveWorkEntry(project: WorkEntryProject, entry: WorkEntry): Promise { const root = await projectRoot(project); const value = await validatedEntry(project, root.root, entry, true); const source = `${JSON.stringify(value, null, 2)}\n`; if (Buffer.byteLength(source, 'utf8') > MAX_ENTRY_BYTES) invalid(); const target = await locations(project, true, root); await assertLocations(target); const temporary = path.join(target.directory, `.${ENTRY_FILENAME}.${randomUUID()}.tmp`); try { const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NO_FOLLOW, 0o600); try { await handle.writeFile(source, 'utf8'); await handle.sync(); } finally { await handle.close(); } await assertLocations(target); const existing = await lstat(target.file).catch((error: NodeJS.ErrnoException) => { if (error.code !== 'ENOENT') throw error; return null; }); if (existing && (!existing.isFile() || existing.isSymbolicLink())) invalid(); await rename(temporary, target.file); } finally { await assertLocations(target).then(() => rm(temporary, { force: true })).catch(() => undefined); } }