import { lstatSync, mkdirSync, chmodSync, unlinkSync, renameSync } from 'node:fs'; import { isAbsolute, join, dirname } from 'node:path'; import { randomBytes } from 'node:crypto'; export const TESTER_ORIGIN = 'http://mosaic-tester.invalid'; export function testerSocketPath(dataRoot) { if (typeof dataRoot !== 'string' || !isAbsolute(dataRoot)) throw Error('tester-socket-path'); const path = join(dataRoot, 'console', 'tester.sock'); if (Buffer.byteLength(path) > 107) throw Error('tester-socket-path'); return path; } export async function bindTesterSocket(server, path) { if (typeof path !== 'string' || !isAbsolute(path) || Buffer.byteLength(path) > 107) throw Error('tester-socket-path'); const parent = dirname(path); try { mkdirSync(parent, { mode: 0o700 }); } catch (e) { if (e.code !== 'EEXIST') throw Error('tester-socket-unsafe'); } const dir = lstatSync(parent); if (!dir.isDirectory() || dir.isSymbolicLink() || dir.uid !== process.getuid() || (dir.mode & 0o077)) throw Error('tester-socket-unsafe'); try { lstatSync(path); throw Error('tester-socket-exists'); } catch (e) { if (e.code !== 'ENOENT') throw e; } // Same length as tester.sock, so the final-path capacity check covers the temporary name. const temporary = join(parent, '.' + randomBytes(4).toString('hex') + '.s'); try { lstatSync(temporary); throw Error('tester-socket-exists'); } catch (e) { if (e.code !== 'ENOENT') throw e; } await new Promise((resolve, reject) => { server.once('error', reject); server.listen(temporary, () => { server.off('error', reject); resolve(); }); }); try { chmodSync(temporary, 0o600); const owned = lstatSync(temporary); if (!owned.isSocket() || owned.uid !== process.getuid() || (owned.mode & 0o777) !== 0o600) throw Error('tester-socket-unsafe'); // Recheck before publishing. Same-uid swaps between lstat and rename/unlink remain // outside the boundary, as Sage's option-1 ruling and the build receipt state. try { lstatSync(path); throw Error('tester-socket-exists'); } catch (e) { if (e.code !== 'ENOENT') throw e; } renameSync(temporary, path); // libuv remembers only the now-absent temporary name. Publish the actual client path. server.address = () => server.listening ? path : null; server.once('close', () => { try { const s = lstatSync(path); if (s.dev === owned.dev && s.ino === owned.ino) unlinkSync(path); } catch (e) { if (e.code !== 'ENOENT') server.emit('error', e); } }); return server; } catch (error) { await new Promise(r => server.close(r)); throw error; } } export async function startTesterSocket({ dataRoot, ...options }) { const { startServer } = await import('./serve.mjs'); return startServer({ ...options, tester: true, testerSocket: testerSocketPath(dataRoot) }); }