import fs from 'fs'; import path from 'path'; import { getStore } from '../db/store.js'; import { requireSelectedCompanyAdmin } from '../db/companyuser.js'; import { getDefaultSkills } from './skill.js'; import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js'; // #region Constants & Configuration // MARK: - Constants & Configuration const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/; const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']); const MIME_TYPES = Object.freeze({ '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.avif': 'image/avif', '.webp': 'image/webp', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.bmp': 'image/bmp', '.pdf': 'application/pdf', '.txt': 'text/plain; charset=utf-8', '.text': 'text/plain; charset=utf-8', '.log': 'text/plain; charset=utf-8', '.csv': 'text/csv; charset=utf-8', '.tsv': 'text/tab-separated-values; charset=utf-8', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.xls': 'application/vnd.ms-excel', '.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12', '.xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', '.md': 'text/markdown; charset=utf-8', '.skill': 'text/markdown; charset=utf-8', '.xml': 'application/xml; charset=utf-8', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.mp4': 'video/mp4', '.webm': 'video/webm', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', }); // #endregion // #region MIME & Path Helpers // MARK: - MIME & Path Helpers export function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); return MIME_TYPES[ext] || 'application/octet-stream'; } function cleanVirtualPath(p, stripDotSlash = false) { let clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, ''); if (stripDotSlash) clean = clean.replace(/^\.\/+/, ''); return clean; } export function extractFullHash(p) { if (typeof p !== 'string' || !p.trim()) return null; const parts = cleanVirtualPath(p).split('/').filter(Boolean); if (parts.length === 0) return null; const first = parts[0].toLowerCase(); const last = parts[parts.length - 1]; const ext = path.extname(last).toLowerCase(); const withoutExt = last.replace(/\.[a-zA-Z0-9]+$/, ''); // Under virtual image folder (e.g. image/aB1cD2eF, images/aB1cD2eF.avif) if (VIRTUAL_IMAGE_FOLDERS.has(first) && parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) { return withoutExt; } // Direct hash (e.g. "aB1cD2eF" or "aB1cD2eF.avif") if (parts.length === 1 && FULL_HASH_REGEX.test(withoutExt) && (!ext || ext === '.avif')) { return withoutExt; } return null; } export function isReadOnlyDatabasePath(filePath) { if (typeof filePath !== 'string' || !filePath.trim()) return false; const clean = cleanVirtualPath(filePath, true); const lower = clean.toLowerCase(); return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean)); } export function isProtectedAdminVfsPath(filePath) { if (typeof filePath !== 'string' || !filePath.trim()) return false; const clean = cleanVirtualPath(filePath, true); const lower = clean.toLowerCase(); return ( lower.endsWith('.skill') || lower.endsWith('.applet.js') || lower.endsWith('.apple.js') || lower === 'apple.js' || lower.endsWith('/apple.js') ); } export function directoryContainsProtectedAdminVfsFile(absDirPath) { try { if (!fs.existsSync(absDirPath)) return false; const stat = fs.statSync(absDirPath); if (!stat.isDirectory()) return isProtectedAdminVfsPath(absDirPath); const entries = fs.readdirSync(absDirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(absDirPath, entry.name); if (entry.isDirectory()) { if (directoryContainsProtectedAdminVfsFile(fullPath)) return true; } else if (isProtectedAdminVfsPath(entry.name)) { return true; } } return false; } catch { return false; } } function assertNotReadOnlyDatabasePath(filePath) { if (isReadOnlyDatabasePath(filePath)) { throw new Error( `Access denied: "${filePath}" matches the read-only database image pattern and cannot be written to. Save artifacts under custom filenames (e.g. "candidate.avif" or "images/candidate.avif").` ); } } // #endregion // #region VFS Root Resolution // MARK: - VFS Root Resolution /** * Resolve the on-disk VFS root for the user's CURRENTLY SELECTED company. * * @param {number} googleAuthId * @returns {Promise} Absolute `data/vfs/` path * @throws {Error} 'No company selected' when the user has no company selected */ export async function resolveVfsRoot(googleAuthId) { return vfsRootFor(await getStore(), googleAuthId); } /** * Shared implementation of {@link resolveVfsRoot}. * * @param {object} store * @param {number} googleAuthId * @returns {Promise} */ async function vfsRootFor(store, googleAuthId) { const row = await store.google_auth.getById(googleAuthId); const companyId = row?.selectedCompanyId; if (companyId == null) { throw new Error('No company selected'); } return path.resolve(process.cwd(), 'data/vfs', String(companyId)); } // #endregion // #region Default Skills & Applets Seeding // MARK: - Default Skills & Applets Seeding /** * Internal helper to write or overwrite default files into the company's VFS. */ async function seedVfsFiles({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot }, items, { overwrite, label }) { const store = passedStore || await getStore(); const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); fs.mkdirSync(vfsRoot, { recursive: true }); const written = []; for (const { name, content } of items) { try { const diskPath = path.resolve(vfsRoot, name); if (overwrite || !fs.existsSync(diskPath)) { fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.writeFileSync(diskPath, content, 'utf8'); written.push(name); } } catch (err) { const action = overwrite ? `restore default ${label}` : `write default ${label}`; const suffix = overwrite ? '' : ' to disk'; console.error(`[vfs] failed to ${action} "${name}"${suffix}:`, err.message); } } return written; } function getVersionedApplets() { return getDefaultApplets().map((a) => ({ name: `${DEFAULT_APPLETS_VERSION}-${a.name}`, content: a.content, })); } /** * Loads default SKILL files into the selected company's on-disk VFS. * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise>} */ export async function loadDefaultSkills(ctx = {}) { return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: false, label: 'skill' }); } /** * Restores all shipped system default SKILL files into the company's VFS. * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise>} */ export async function restoreDefaultSkills(ctx = {}) { if (ctx.googleAuthId != null) { await requireSelectedCompanyAdmin(ctx.googleAuthId); } return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' }); } /** * Loads the shipped example `.applet.js` files into the selected company's VFS. * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise>} */ export async function loadDefaultApplets(ctx = {}) { return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: false, label: 'applet' }); } /** * Restores all shipped system default example applets into the company's VFS. * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise>} */ export async function restoreDefaultApplets(ctx = {}) { if (ctx.googleAuthId != null) { await requireSelectedCompanyAdmin(ctx.googleAuthId); } return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' }); } // #endregion // #region Lifecycle & Initialization // MARK: - Lifecycle & Initialization /** * Initializes the VFS for the selected company with default SKILL files and example applets. * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise} vfsRoot directory path */ export async function initCompanyVfs({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) { const store = passedStore || await getStore(); const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); fs.mkdirSync(vfsRoot, { recursive: true }); await loadDefaultSkills({ googleAuthId, store, vfsRoot }); await loadDefaultApplets({ googleAuthId, store, vfsRoot }); return vfsRoot; } /** * Ensure the local directory for this company's VFS exists and the default skills and example * applets are loaded. * @param {{ googleAuthId: number }} ctx * @returns {Promise} vfsRoot directory path */ export async function prepareVfs({ googleAuthId }) { const store = await getStore(); const vfsRoot = await vfsRootFor(store, googleAuthId); fs.mkdirSync(vfsRoot, { recursive: true }); try { await loadDefaultSkills({ googleAuthId, store, vfsRoot }); } catch (err) { console.warn('[vfs] prepare warning:', err.message); } try { await loadDefaultApplets({ googleAuthId, store, vfsRoot }); } catch (err) { console.warn('[vfs] prepare applet warning:', err.message); } return vfsRoot; } // #endregion // #region Sandboxed VFS Factory // MARK: - Sandboxed VFS Factory /** * Wraps a synchronous function into a Node-style callback function (`process.nextTick`). * Automatically handles optional middle arguments (e.g. `(p, opt, cb)` vs `(p, cb)`). */ function cbWrap(syncFn, arityWithOpt = 3) { return (...args) => { let cb; let callArgs = args; if (typeof args[args.length - 1] === 'function') { cb = args[args.length - 1]; callArgs = args.slice(0, -1); while (callArgs.length < arityWithOpt - 1) { callArgs.push(undefined); } } try { const res = syncFn(...callArgs); if (cb) process.nextTick(cb, null, res); } catch (err) { if (cb) process.nextTick(cb, err); else throw err; } }; } /** * Creates a sandboxed VFS module object mirroring Node's `fs` and `fs/promises`. * @param {string} vfsRoot Absolute root, as returned by `prepareVfs`/`resolveVfsRoot` * @param {{ googleAuthId: number }} [options] */ export function createVfs(vfsRoot, options = {}) { const { googleAuthId, isAdmin = true } = options; const assertWritablePath = (filePath) => { assertNotReadOnlyDatabasePath(filePath); if (!isAdmin && isProtectedAdminVfsPath(filePath)) { throw new Error('Not authorized'); } }; // #region Internal Sandbox Helpers // MARK: - Internal Sandbox Helpers const sanitizeFsError = (err, virtualPath) => { if (!err || typeof err.message !== 'string') return err; const virtual = typeof virtualPath === 'string' ? virtualPath : String(virtualPath); const message = err.message .split(vfsRoot) .join('vfs://') .split(path.sep) .join('/') .replace(/vfs:\/\/\/+/g, 'vfs:///'); return Object.assign(Object.create(Object.getPrototypeOf(err)), err, { message, path: virtual, }); }; const safeResolve = (p) => { if (typeof p !== 'string' || !p.trim()) { throw new Error('Path must be a non-empty string'); } const resolved = path.resolve(vfsRoot, cleanVirtualPath(p)); const relative = path.relative(vfsRoot, resolved); if (relative.startsWith('..') || path.isAbsolute(relative)) { throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`); } return resolved; }; const syncFetchImage = (hash) => { if (!hash) return null; try { const cachePath = path.join(vfsRoot, 'images', `${hash}.avif`); return fs.existsSync(cachePath) ? fs.readFileSync(cachePath) : null; } catch { return null; } }; const formatBuffer = (buf, opt) => { const encoding = typeof opt === 'string' ? opt : opt?.encoding; return encoding ? buf.toString(encoding) : buf; }; const writeCopyBuffer = (buf, src, dest, mode) => { const destDisk = safeResolve(dest); fs.mkdirSync(path.dirname(destDisk), { recursive: true }); if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) { const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`); err.code = 'EEXIST'; err.path = dest; err.syscall = 'copyfile'; throw err; } try { return fs.writeFileSync(destDisk, buf); } catch (err) { throw sanitizeFsError(err, dest); } }; // #endregion // #region VFS Core Operations // MARK: - VFS Core Operations const vfs = { // --- Read Operations --- readFileSync: (p, opt) => { const hash = extractFullHash(p); if (hash) { const buf = syncFetchImage(hash); if (buf) return formatBuffer(buf, opt); } const diskPath = safeResolve(p); try { return fs.readFileSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, readFile: (p, opt, cb) => { const callback = typeof opt === 'function' ? opt : cb; const opts = typeof opt === 'function' ? undefined : opt; vfs.promises.readFile(p, opts) .then((data) => { if (typeof callback === 'function') process.nextTick(callback, null, data); }) .catch((err) => { if (typeof callback === 'function') process.nextTick(callback, err); else throw err; }); }, createReadStream: (p, opt) => { const diskPath = safeResolve(p); if (!fs.existsSync(diskPath)) { const err = new Error(`ENOENT: no such file or directory, open '${String(p)}'`); err.code = 'ENOENT'; err.path = String(p); err.syscall = 'open'; throw sanitizeFsError(err, p); } return fs.createReadStream(diskPath, opt); }, // --- Write & Append Operations --- writeFileSync: (p, data, opt) => { assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); try { return fs.writeFileSync(diskPath, data, opt); } catch (err) { throw sanitizeFsError(err, p); } }, createWriteStream: (p, opt) => { assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); return fs.createWriteStream(diskPath, opt); }, appendFileSync: (p, data, opt) => { assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); try { return fs.appendFileSync(diskPath, data, opt); } catch (err) { throw sanitizeFsError(err, p); } }, // --- Copy, Rename & Truncate Operations --- copyFileSync: (src, dest, mode) => { assertWritablePath(dest); const hash = extractFullHash(src); if (hash) { const buf = syncFetchImage(hash); if (buf) return writeCopyBuffer(buf, src, dest, mode); } const srcDisk = safeResolve(src); const destDisk = safeResolve(dest); fs.mkdirSync(path.dirname(destDisk), { recursive: true }); try { return fs.copyFileSync(srcDisk, destDisk, mode); } catch (err) { throw sanitizeFsError(err, `${src} -> ${dest}`); } }, cpSync: (src, dest, opt) => { assertWritablePath(dest); const srcDisk = safeResolve(src); const destDisk = safeResolve(dest); fs.mkdirSync(path.dirname(destDisk), { recursive: true }); try { return fs.cpSync(srcDisk, destDisk, opt); } catch (err) { throw sanitizeFsError(err, `${src} -> ${dest}`); } }, renameSync: (oldPath, newPath) => { assertWritablePath(oldPath); assertWritablePath(newPath); const oldDisk = safeResolve(oldPath); const newDisk = safeResolve(newPath); if (!isAdmin && (directoryContainsProtectedAdminVfsFile(oldDisk) || directoryContainsProtectedAdminVfsFile(newDisk))) { throw new Error('Not authorized'); } fs.mkdirSync(path.dirname(newDisk), { recursive: true }); try { return fs.renameSync(oldDisk, newDisk); } catch (err) { throw sanitizeFsError(err, `${oldPath} -> ${newPath}`); } }, truncateSync: (p, len) => { assertWritablePath(p); const diskPath = safeResolve(p); try { return fs.truncateSync(diskPath, len); } catch (err) { throw sanitizeFsError(err, p); } }, // --- Metadata & Access Operations --- accessSync: (p, mode) => { const hash = extractFullHash(p); if (hash && syncFetchImage(hash)) { if (typeof mode === 'number' && (mode & fs.constants.W_OK)) { const err = new Error(`EACCES: permission denied, access '${p}'`); err.code = 'EACCES'; err.path = p; err.syscall = 'access'; throw err; } return; } const diskPath = safeResolve(p); try { return fs.accessSync(diskPath, mode); } catch (err) { throw sanitizeFsError(err, p); } }, realpathSync: (p, opt) => { const diskPath = safeResolve(p); try { const resolved = fs.realpathSync(diskPath, opt); const rel = path.relative(vfsRoot, resolved); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`); } return rel ? rel.split(path.sep).join('/') : '.'; } catch (err) { throw sanitizeFsError(err, p); } }, existsSync: (p) => { const hash = extractFullHash(p); if (hash && syncFetchImage(hash)) return true; try { return fs.existsSync(safeResolve(p)); } catch { return false; } }, exists: (p, cb) => { const res = vfs.existsSync(p); if (typeof cb === 'function') process.nextTick(cb, res); }, statSync: (p, opt) => { const hash = extractFullHash(p); if (hash) { const buf = syncFetchImage(hash); if (buf) { const now = new Date(); return { isFile: () => true, isDirectory: () => false, size: buf.length, mtime: now, ctime: now, atime: now, }; } } const diskPath = safeResolve(p); try { return fs.statSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, lstatSync: (p, opt) => vfs.statSync(p, opt), // --- Directory & Removal Operations --- readdirSync: (p = '.', opt) => { const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : ''; if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) { return []; } const diskPath = safeResolve(p); try { return fs.readdirSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, readdir: (p = '.', opt, cb) => { const callback = typeof opt === 'function' ? opt : (typeof p === 'function' ? p : cb); const opts = typeof opt === 'function' ? undefined : opt; const pathArg = typeof p === 'function' ? '.' : p; try { const res = vfs.readdirSync(pathArg, opts); if (typeof callback === 'function') process.nextTick(callback, null, res); } catch (err) { if (typeof callback === 'function') process.nextTick(callback, err); else throw err; } }, mkdirSync: (p, opt) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); try { return fs.mkdirSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, unlinkSync: (p) => { assertWritablePath(p); const diskPath = safeResolve(p); try { return fs.unlinkSync(diskPath); } catch (err) { throw sanitizeFsError(err, p); } }, rmdirSync: (p, opt) => { assertWritablePath(p); const diskPath = safeResolve(p); if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) { throw new Error('Not authorized'); } try { return fs.rmdirSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, rmSync: (p, opt) => { assertWritablePath(p); const diskPath = safeResolve(p); if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) { throw new Error('Not authorized'); } try { return fs.rmSync(diskPath, opt); } catch (err) { throw sanitizeFsError(err, p); } }, // #endregion // #region VFS Promises API // MARK: - VFS Promises API promises: { readFile: async (p, opt) => { const hash = extractFullHash(p); if (hash) { const buf = syncFetchImage(hash); if (buf) return formatBuffer(buf, opt); if (googleAuthId) { try { const store = await getStore(); const img = await store.pic.getImageByHash(googleAuthId, hash); if (img?.buffer) { try { const cachePath = path.join(vfsRoot, 'images', `${hash}.avif`); fs.mkdirSync(path.join(vfsRoot, 'images'), { recursive: true }); fs.writeFileSync(cachePath, img.buffer); } catch { /* ignore cache write error */ } return formatBuffer(img.buffer, opt); } } catch { // fall through } } } return vfs.readFileSync(p, opt); }, writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt), appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt), copyFile: async (src, dest, mode) => { assertWritablePath(dest); const hash = extractFullHash(src); if (hash) { const buf = await vfs.promises.readFile(src); return writeCopyBuffer(buf, src, dest, mode); } return vfs.copyFileSync(src, dest, mode); }, cp: async (src, dest, opt) => vfs.cpSync(src, dest, opt), rename: async (oldPath, newPath) => vfs.renameSync(oldPath, newPath), truncate: async (p, len) => vfs.truncateSync(p, len), access: async (p, mode) => vfs.accessSync(p, mode), realpath: async (p, opt) => vfs.realpathSync(p, opt), stat: async (p, opt) => vfs.statSync(p, opt), lstat: async (p, opt) => vfs.lstatSync(p, opt), readdir: async (p, opt) => vfs.readdirSync(p, opt), mkdir: async (p, opt) => vfs.mkdirSync(p, opt), rmdir: async (p, opt) => vfs.rmdirSync(p, opt), unlink: async (p) => vfs.unlinkSync(p), rm: async (p, opt) => vfs.rmSync(p, opt), constants: fs.constants, }, // #endregion // #region External Sandbox Utilities // MARK: - External Sandbox Utilities /** * Resolve a virtual path to an absolute on-disk path for writing, applying the SAME * containment + read-only guards as `writeFileSync`. */ resolveWritePath: (p) => { assertWritablePath(p); return safeResolve(p); }, /** * Resolve a virtual path to an absolute on-disk path for reading, applying containment checks. */ resolveReadPath: (p) => safeResolve(p), /** * Sanitize on-disk error paths back to virtual paths for display in sandbox transcripts. */ sanitizeError: (err, virtualPath) => sanitizeFsError(err, virtualPath), constants: fs.constants, // #endregion }; // #region Callback Wrapper Bindings // MARK: - Callback Wrapper Bindings vfs.writeFile = cbWrap(vfs.writeFileSync, 4); vfs.appendFile = cbWrap(vfs.appendFileSync, 4); vfs.copyFile = cbWrap(vfs.copyFileSync, 4); vfs.cp = cbWrap(vfs.cpSync, 4); vfs.rename = cbWrap(vfs.renameSync, 3); vfs.truncate = cbWrap(vfs.truncateSync, 3); vfs.access = cbWrap(vfs.accessSync, 3); vfs.realpath = cbWrap(vfs.realpathSync, 3); vfs.stat = cbWrap(vfs.statSync, 3); vfs.lstat = cbWrap(vfs.lstatSync, 3); vfs.mkdir = cbWrap(vfs.mkdirSync, 3); vfs.rmdir = cbWrap(vfs.rmdirSync, 3); vfs.unlink = cbWrap(vfs.unlinkSync, 2); vfs.rm = cbWrap(vfs.rmSync, 3); vfs.realpathSync.native = (p, opt) => vfs.realpathSync(p, opt); vfs.realpath.native = (p, opt, cb) => vfs.realpath(p, opt, cb); vfs.promises.realpath.native = async (p, opt) => vfs.realpathSync(p, opt); // #endregion return vfs; } // #endregion