import { randomUUID } from "node:crypto"; import fs, { type FileHandle } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; export const PUBLIC_CONTENT_ROOTS_ENV = "THOUGHTSTREAM_PUBLIC_CONTENT_ROOTS"; export interface PrivateDestinationOptions { publicContentRoots?: string[] | undefined; beforeFinalize?: (() => void | Promise) | undefined; } export interface PrivateDirectory { readonly path: string; write(fileName: string, content: string | Uint8Array): Promise; close(): Promise; } export function configuredPublicContentRoots(environment: NodeJS.ProcessEnv = process.env): string[] { const configured = (environment[PUBLIC_CONTENT_ROOTS_ENV] ?? "") .split(path.delimiter) .map((entry) => entry.trim()) .filter(Boolean); const home = os.homedir(); return [...new Set([ path.join(home, "code"), path.join(home, "Documents", "Public"), path.join(home, "Documents", "The Coil", "public"), ...configured, ].map((entry) => path.resolve(entry)))]; } export async function assertPrivateDestination( destination: string, options: PrivateDestinationOptions = {}, ): Promise { const absolute = path.resolve(destination); const parent = path.dirname(absolute); const resolvedParent = await resolveProspectivePath(parent); const resolvedDestination = path.join(resolvedParent, path.basename(absolute)); const publicRoots = options.publicContentRoots ?? configuredPublicContentRoots(); for (const root of publicRoots) { const resolvedRoot = await resolveProspectivePath(path.resolve(root)); if (isWithin(resolvedRoot, resolvedDestination)) { throw new Error(`Private output destination is inside a public-content root: ${root}`); } } const gitRoot = await containingGitWorktree(resolvedParent); if (gitRoot) throw new Error(`Private output destination is inside a Git worktree: ${gitRoot}`); return resolvedDestination; } export async function atomicOwnerOnlyWrite(destination: string, content: string | Uint8Array): Promise { const absolute = path.resolve(destination); const directory = await openPrivateDirectory(path.dirname(absolute)); try { await directory.write(path.basename(absolute), content); } finally { await directory.close(); } } export async function openPrivateDirectory( directory: string, options: PrivateDestinationOptions = {}, ): Promise { const absolute = path.resolve(directory); await fs.mkdir(absolute, { recursive: true, mode: 0o700 }); await assertPrivateDestination(path.join(absolute, ".private-write-probe"), options); const handle = await fs.open(absolute, "r"); const identity = await handle.stat(); if (!identity.isDirectory()) { await handle.close(); throw new Error("Private output parent must be a directory"); } await fs.chmod(`/proc/self/fd/${handle.fd}`, 0o700); let closed = false; const anchor = `/proc/self/fd/${handle.fd}`; return { path: absolute, async write(fileName, content) { if (closed) throw new Error("Private output directory is closed"); assertBaseName(fileName); const destination = path.join(anchor, fileName); const existing = await fs.lstat(destination).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return undefined; throw error; }); if (existing && (!existing.isFile() || existing.isSymbolicLink())) { throw new Error("Private output destination must be a regular non-symlink file"); } const temporaryName = `.${fileName}.${process.pid}.${randomUUID()}.tmp`; const temporary = path.join(anchor, temporaryName); let temporaryHandle: FileHandle | undefined; let finalized = false; try { temporaryHandle = await fs.open(temporary, "wx", 0o600); await temporaryHandle.writeFile(content); await temporaryHandle.sync(); await temporaryHandle.chmod(0o600); await temporaryHandle.close(); temporaryHandle = undefined; await options.beforeFinalize?.(); await fs.rename(temporary, destination); finalized = true; await fs.chmod(destination, 0o600); await handle.sync(); await assertSameDirectory(absolute, identity); } catch (error) { if (finalized) await fs.rm(destination, { force: true }).catch(() => undefined); await fs.rm(temporary, { force: true }).catch(() => undefined); await handle.sync().catch(() => undefined); throw error; } finally { if (temporaryHandle) await temporaryHandle.close().catch(() => undefined); await fs.rm(temporary, { force: true }).catch(() => undefined); } }, async close() { if (closed) return; closed = true; await handle.close(); }, }; } export async function ensureOwnerOnlyDirectory(directory: string): Promise { const absolute = path.resolve(directory); await fs.mkdir(absolute, { recursive: true, mode: 0o700 }); await fs.chmod(absolute, 0o700); return absolute; } async function containingGitWorktree(start: string): Promise { let current = start; while (true) { if (await fs.lstat(path.join(current, ".git")).then(() => true).catch(() => false)) return current; const parent = path.dirname(current); if (parent === current) return undefined; current = parent; } } async function resolveProspectivePath(candidate: string): Promise { const unresolved: string[] = []; let current = path.resolve(candidate); while (true) { try { const resolved = await fs.realpath(current); return path.join(resolved, ...unresolved.reverse()); } catch (error) { if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "ENOENT") throw error; const parent = path.dirname(current); if (parent === current) throw error; unresolved.push(path.basename(current)); current = parent; } } } function isWithin(root: string, candidate: string): boolean { const relative = path.relative(root, candidate); return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".."); } function assertBaseName(fileName: string): void { if (!fileName || fileName !== path.basename(fileName) || fileName === "." || fileName === "..") { throw new Error("Private output file name must be one path component"); } } async function assertSameDirectory(directory: string, expected: Awaited>): Promise { const current = await fs.lstat(directory).catch(() => undefined); if (!current || !current.isDirectory() || current.isSymbolicLink() || current.dev !== expected.dev || current.ino !== expected.ino) { throw new Error("Private output parent changed during write"); } }