import { randomUUID } from 'node:crypto'; import { lstat, realpath, rename, rm } from 'node:fs/promises'; import { basename, dirname, relative, resolve, sep } from 'node:path'; const isMissing = (error) => typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'; const assertInsideRepository = (repositoryRoot, path, label) => { const pathRelativeToRoot = relative(repositoryRoot, path); if (pathRelativeToRoot === '' || pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) { throw new Error(`${label} is outside the repository.`); } }; const assertCanonicalDirectory = async (path, label) => { const resolvedPath = resolve(path); let canonicalPath; try { canonicalPath = await realpath(resolvedPath); } catch (error) { if (isMissing(error)) throw new Error(`${label} must be an existing safe directory.`); throw error; } if (canonicalPath !== resolvedPath) throw new Error(`${label} or an ancestor is symlinked or non-canonical.`); const stats = await lstat(resolvedPath); if (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error(`${label} must be a safe directory.`); return stats; }; export const captureDirectoryIdentity = async (path) => { const stats = await assertCanonicalDirectory(path, 'Directory'); return { dev: stats.dev, ino: stats.ino }; }; const sameIdentity = (left, right) => left.dev === right.dev && left.ino === right.ino; const assertOptionalFinalDirectory = async (repositoryRoot, finalRoot) => { assertInsideRepository(repositoryRoot, finalRoot, 'Final output'); if (dirname(finalRoot) !== repositoryRoot || basename(finalRoot) !== 'dist') { throw new Error('Final output must be the repository-contained dist sibling.'); } try { const stats = await lstat(finalRoot); if (stats.isSymbolicLink() || !stats.isDirectory() || await realpath(finalRoot) !== finalRoot) { throw new Error('Final output must be an existing safe directory.'); } return { exists: true, identity: { dev: stats.dev, ino: stats.ino } }; } catch (error) { if (isMissing(error)) return { exists: false, identity: undefined }; throw error; } }; const removeOwnedBackup = async (repositoryRoot, backupRoot) => { assertInsideRepository(repositoryRoot, backupRoot, 'Backup'); if (dirname(backupRoot) !== repositoryRoot || !basename(backupRoot).startsWith('.dist-backup-')) { throw new Error('Refusing to remove an unowned backup path.'); } const stats = await lstat(backupRoot); if (stats.isSymbolicLink() || !stats.isDirectory() || await realpath(backupRoot) !== backupRoot) { throw new Error('Refusing to remove an unsafe backup path.'); } await rm(backupRoot, { recursive: true }); }; export const publishStagedDirectory = async ({ repositoryRoot: repositoryRootInput, finalRoot: finalRootInput, stagingRoot: stagingRootInput, expectedStagingIdentity, renameDirectory = rename, beforeCommit = async () => {}, }) => { const repositoryRoot = resolve(repositoryRootInput); const finalRoot = resolve(finalRootInput); const stagingRoot = resolve(stagingRootInput); await assertCanonicalDirectory(repositoryRoot, 'Repository root'); assertInsideRepository(repositoryRoot, stagingRoot, 'Staging output'); if (dirname(stagingRoot) !== repositoryRoot || !basename(stagingRoot).startsWith('.dist-stage-')) { throw new Error('Staging output must be a repository-contained owned sibling.'); } const stagingIdentity = await captureDirectoryIdentity(stagingRoot); if (!sameIdentity(stagingIdentity, expectedStagingIdentity)) { throw new Error('Staging directory identity changed after verification.'); } const previous = await assertOptionalFinalDirectory(repositoryRoot, finalRoot); await assertCanonicalDirectory(repositoryRoot, 'Repository root'); if (!sameIdentity(await captureDirectoryIdentity(stagingRoot), expectedStagingIdentity)) { throw new Error('Staging directory identity changed before publication.'); } if (previous.exists) { const currentFinal = await captureDirectoryIdentity(finalRoot); if (!sameIdentity(currentFinal, previous.identity)) throw new Error('Final output identity changed before publication.'); } const backupRoot = resolve(repositoryRoot, `.dist-backup-${randomUUID()}`); let backupCreated = false; let stagedPublished = false; try { if (previous.exists) { await renameDirectory(finalRoot, backupRoot); backupCreated = true; } await renameDirectory(stagingRoot, finalRoot); stagedPublished = true; await beforeCommit(); } catch (error) { try { if (stagedPublished) { await renameDirectory(finalRoot, stagingRoot); stagedPublished = false; } if (backupCreated) { await renameDirectory(backupRoot, finalRoot); backupCreated = false; } } catch (rollbackError) { throw new AggregateError([error, rollbackError], 'Publication failed and rollback could not restore the previous output.'); } throw error; } if (backupCreated) await removeOwnedBackup(repositoryRoot, backupRoot); };