This commit is contained in:
seb
2026-10-18 15:45:03 +02:00
parent 9c50d9efc3
commit 7445a573ab

View File

@@ -1,62 +1,75 @@
import fs from 'fs';
import path from 'path';
import { getStore } from '../db/store.js';
import env from '../config/env.js';
import { getDefaultSkills } from './skill.js';
import { getDefaultApplets, DEFAULT_APPLETS_VERSION, APPLET_EXT } from './applet.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();
switch (ext) {
case '.html':
case '.htm': return 'text/html; charset=utf-8';
case '.css': return 'text/css; charset=utf-8';
case '.js':
case '.mjs': return 'text/javascript; charset=utf-8';
case '.json':
case '.map': return 'application/json; charset=utf-8';
case '.avif': return 'image/avif';
case '.webp': return 'image/webp';
case '.png': return 'image/png';
case '.jpg':
case '.jpeg': return 'image/jpeg';
case '.gif': return 'image/gif';
case '.svg': return 'image/svg+xml';
case '.ico': return 'image/x-icon';
case '.bmp': return 'image/bmp';
case '.pdf': return 'application/pdf';
case '.txt':
case '.text':
case '.log': return 'text/plain; charset=utf-8';
case '.csv': return 'text/csv; charset=utf-8';
case '.tsv': return 'text/tab-separated-values; charset=utf-8';
case '.xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case '.xls': return 'application/vnd.ms-excel';
case '.xlsm': return 'application/vnd.ms-excel.sheet.macroEnabled.12';
case '.xltx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template';
case '.md':
case '.skill': return 'text/markdown; charset=utf-8';
case '.xml': return 'application/xml; charset=utf-8';
case '.mp3': return 'audio/mpeg';
case '.wav': return 'audio/wav';
case '.ogg': return 'audio/ogg';
case '.mp4': return 'video/mp4';
case '.webm': return 'video/webm';
case '.woff': return 'font/woff';
case '.woff2': return 'font/woff2';
case '.ttf': return 'font/ttf';
case '.otf': return 'font/otf';
default: return 'application/octet-stream';
}
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 clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '');
const parts = clean.split('/').filter(Boolean);
const parts = cleanVirtualPath(p).split('/').filter(Boolean);
if (parts.length === 0) return null;
const first = parts[0].toLowerCase();
@@ -65,10 +78,8 @@ export function extractFullHash(p) {
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)) {
if (parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) {
return withoutExt;
}
if (VIRTUAL_IMAGE_FOLDERS.has(first) && parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) {
return withoutExt;
}
// Direct hash (e.g. "aB1cD2eF" or "aB1cD2eF.avif")
@@ -81,33 +92,27 @@ export function extractFullHash(p) {
export function isReadOnlyDatabasePath(filePath) {
if (typeof filePath !== 'string' || !filePath.trim()) return false;
const clean = filePath.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
const clean = cleanVirtualPath(filePath, true);
const lower = clean.toLowerCase();
if (lower === 'image' || lower.startsWith('image/')) return true;
if (extractFullHash(clean)) return true;
return false;
return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean));
}
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").`
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.
*
* This must stay in lockstep with the store, which scopes every `modelvfs` call by
* `google_auth.selected_company` read at call time — see `resolveSelectedCompanyAccess` in
* libdashstore. Callers therefore must NOT pass a `companyId`: a value captured earlier (at
* tool-construction time, or from a request made before a company switch) can disagree with
* what the store will act on, which would hydrate one company's directory from another
* company's rows and delete files from a directory the database never touched.
*
* One authority, read fresh on every call, for both the disk and the database halves.
*
* @param {number} googleAuthId
* @returns {Promise<string>} Absolute `data/vfs/<companyId>` path
* @throws {Error} 'No company selected' when the user has no company selected
@@ -117,9 +122,7 @@ export async function resolveVfsRoot(googleAuthId) {
}
/**
* Shared implementation of {@link resolveVfsRoot}. Takes an already-resolved store so callers
* that need it anyway (e.g. `prepareVfs`, which hydrates from the same store) do not pay for a
* second lookup.
* Shared implementation of {@link resolveVfsRoot}.
*
* @param {object} store
* @param {number} googleAuthId
@@ -134,125 +137,87 @@ async function vfsRootFor(store, googleAuthId) {
return path.resolve(process.cwd(), 'data/vfs', String(companyId));
}
// #endregion
// #region Default Skills & Applets Seeding
// MARK: - Default Skills & Applets Seeding
/**
* Loads default SKILL files into the selected company's on-disk VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of loaded skill file names
* Internal helper to write or overwrite default files into the company's VFS.
*/
export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
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 defaultSkills = getDefaultSkills();
const loaded = [];
for (const skill of defaultSkills) {
const written = [];
for (const { name, content } of items) {
try {
const diskPath = path.resolve(vfsRoot, skill.name);
if (!fs.existsSync(diskPath)) {
const diskPath = path.resolve(vfsRoot, name);
if (overwrite || !fs.existsSync(diskPath)) {
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
loaded.push(skill.name);
fs.writeFileSync(diskPath, content, 'utf8');
written.push(name);
}
} catch (err) {
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
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;
}
return loaded;
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<Array<string>>}
*/
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<Array<string>>} List of restored skill file names
* @returns {Promise<Array<string>>}
*/
export async function restoreDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const defaultSkills = getDefaultSkills();
const restored = [];
for (const skill of defaultSkills) {
try {
const diskPath = path.resolve(vfsRoot, skill.name);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
restored.push(skill.name);
} catch (err) {
console.error(`[vfs] failed to restore default skill "${skill.name}":`, err.message);
}
}
return restored;
export async function restoreDefaultSkills(ctx = {}) {
return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' });
}
/**
* Loads the shipped example `.applet.js` files into the selected company's VFS.
*
* Version-gated: each example is written under a `v1-` prefixed name and only when that exact
* file is absent, so an operator's edits survive and a new example revision ships as a new file.
* Must stay in step with {@link loadDefaultSkills}, which runs alongside it.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of seeded applet file names
* @returns {Promise<Array<string>>}
*/
export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const seeded = [];
for (const applet of getDefaultApplets()) {
const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`;
try {
const diskPath = path.resolve(vfsRoot, name);
if (!fs.existsSync(diskPath)) {
fs.writeFileSync(diskPath, applet.content, 'utf8');
seeded.push(name);
}
} catch (err) {
console.error(`[vfs] failed to write default applet "${name}" to disk:`, err.message);
}
}
return seeded;
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<Array<string>>} List of restored applet file names
* @returns {Promise<Array<string>>}
*/
export async function restoreDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const restored = [];
for (const applet of getDefaultApplets()) {
const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`;
try {
const diskPath = path.resolve(vfsRoot, name);
fs.writeFileSync(diskPath, applet.content, 'utf8');
restored.push(name);
} catch (err) {
console.error(`[vfs] failed to restore default applet "${name}":`, err.message);
}
}
return restored;
export async function restoreDefaultApplets(ctx = {}) {
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<string>} vfsRoot directory path
*/
@@ -269,7 +234,6 @@ export async function initCompanyVfs({ googleAuthId, store: passedStore, 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<string>} vfsRoot directory path
*/
@@ -295,13 +259,42 @@ export async function prepareVfs({ googleAuthId }) {
/**
* Purely disk-based VFS; no database syncing needed. Kept as no-op for backward compatibility.
*
* @param {{ googleAuthId?: number, vfsRoot?: string }} [ctx]
*/
export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) {
export async function syncVfsToDb() {
// Purely disk-based VFS; no database syncing.
}
// #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`
@@ -310,8 +303,9 @@ export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) {
export function createVfs(vfsRoot, options = {}) {
const { googleAuthId } = options;
// Node fs errors embed the REAL absolute on-disk path, which leaks the server's directory
// layout into the model-visible transcript. Rewrite them to use a stable `vfs://` prefix.
// #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);
@@ -322,7 +316,7 @@ export function createVfs(vfsRoot, options = {}) {
.join('/')
.replace(/vfs:\/\/\/+/g, 'vfs:///');
return Object.assign(Object.create(Object.getPrototypeOf(err)), err, {
message,
message,
path: virtual,
});
};
@@ -331,11 +325,10 @@ export function createVfs(vfsRoot, options = {}) {
if (typeof p !== 'string' || !p.trim()) {
throw new Error('Path must be a non-empty string');
}
const cleanVirtual = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '');
const resolved = path.resolve(vfsRoot, cleanVirtual);
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.`);
throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`);
}
return resolved;
};
@@ -343,24 +336,47 @@ export function createVfs(vfsRoot, options = {}) {
const syncFetchImage = (hash) => {
if (!hash) return null;
try {
// Read from storage or cache if available
const cachePath = path.join(vfsRoot, 'images', `${hash}.avif`);
if (fs.existsSync(cachePath)) return fs.readFileSync(cachePath);
return null;
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) {
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? buf.toString(encoding) : buf;
}
if (buf) return formatBuffer(buf, opt);
}
const diskPath = safeResolve(p);
try {
@@ -372,8 +388,8 @@ export function createVfs(vfsRoot, options = {}) {
readFile: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
vfs.promises.readFile(p, options)
const opts = typeof opt === 'function' ? undefined : opt;
vfs.promises.readFile(p, opts)
.then((data) => {
if (typeof callback === 'function') process.nextTick(callback, null, data);
})
@@ -395,6 +411,7 @@ export function createVfs(vfsRoot, options = {}) {
return fs.createReadStream(diskPath, opt);
},
// --- Write & Append Operations ---
writeFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -406,18 +423,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
writeFile: (p, data, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.writeFileSync(p, data, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
createWriteStream: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -436,39 +441,13 @@ export function createVfs(vfsRoot, options = {}) {
}
},
appendFile: (p, data, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.appendFileSync(p, data, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
// --- Copy, Rename & Truncate Operations ---
copyFileSync: (src, dest, mode) => {
assertNotReadOnlyDatabasePath(dest);
const hash = extractFullHash(src);
if (hash) {
const buf = syncFetchImage(hash);
if (buf) {
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);
}
}
if (buf) return writeCopyBuffer(buf, src, dest, mode);
}
const srcDisk = safeResolve(src);
const destDisk = safeResolve(dest);
@@ -480,18 +459,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
copyFile: (src, dest, mode, cb) => {
const callback = typeof mode === 'function' ? mode : cb;
const m = typeof mode === 'function' ? undefined : mode;
try {
vfs.copyFileSync(src, dest, m);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
cpSync: (src, dest, opt) => {
assertNotReadOnlyDatabasePath(dest);
const srcDisk = safeResolve(src);
@@ -504,18 +471,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
cp: (src, dest, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.cpSync(src, dest, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
renameSync: (oldPath, newPath) => {
assertNotReadOnlyDatabasePath(oldPath);
assertNotReadOnlyDatabasePath(newPath);
@@ -529,16 +484,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
rename: (oldPath, newPath, cb) => {
try {
vfs.renameSync(oldPath, newPath);
if (typeof cb === 'function') process.nextTick(cb, null);
} catch (err) {
if (typeof cb === 'function') process.nextTick(cb, err);
else throw err;
}
},
truncateSync: (p, len) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -549,32 +494,18 @@ export function createVfs(vfsRoot, options = {}) {
}
},
truncate: (p, len, cb) => {
const callback = typeof len === 'function' ? len : cb;
const l = typeof len === 'function' ? undefined : len;
try {
vfs.truncateSync(p, l);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
// --- Metadata & Access Operations ---
accessSync: (p, mode) => {
const hash = extractFullHash(p);
if (hash) {
const buf = syncFetchImage(hash);
if (buf) {
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;
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 {
@@ -584,18 +515,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
access: (p, mode, cb) => {
const callback = typeof mode === 'function' ? mode : cb;
const m = typeof mode === 'function' ? undefined : mode;
try {
vfs.accessSync(p, m);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
realpathSync: (p, opt) => {
const diskPath = safeResolve(p);
try {
@@ -610,27 +529,11 @@ export function createVfs(vfsRoot, options = {}) {
}
},
realpath: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
const res = vfs.realpathSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null, res);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
existsSync: (p) => {
const hash = extractFullHash(p);
if (hash) {
const buf = syncFetchImage(hash);
if (buf) return true;
}
if (hash && syncFetchImage(hash)) return true;
try {
const diskPath = safeResolve(p);
return fs.existsSync(diskPath);
return fs.existsSync(safeResolve(p));
} catch {
return false;
}
@@ -665,32 +568,9 @@ export function createVfs(vfsRoot, options = {}) {
}
},
stat: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
const res = vfs.statSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null, res);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
lstatSync: (p, opt) => vfs.statSync(p, opt),
lstat: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
const res = vfs.lstatSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null, res);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
// --- Directory & Removal Operations ---
readdirSync: (p = '.', opt) => {
const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : '';
if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) {
@@ -706,10 +586,10 @@ export function createVfs(vfsRoot, options = {}) {
readdir: (p = '.', opt, cb) => {
const callback = typeof opt === 'function' ? opt : (typeof p === 'function' ? p : cb);
const options = typeof opt === 'function' ? undefined : opt;
const opts = typeof opt === 'function' ? undefined : opt;
const pathArg = typeof p === 'function' ? '.' : p;
try {
const res = vfs.readdirSync(pathArg, options);
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);
@@ -727,18 +607,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
mkdir: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
const res = vfs.mkdirSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null, res);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
unlinkSync: (p) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -749,16 +617,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
unlink: (p, cb) => {
try {
vfs.unlinkSync(p);
if (typeof cb === 'function') process.nextTick(cb, null);
} catch (err) {
if (typeof cb === 'function') process.nextTick(cb, err);
else throw err;
}
},
rmdirSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -769,18 +627,6 @@ export function createVfs(vfsRoot, options = {}) {
}
},
rmdir: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.rmdirSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
rmSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p);
@@ -791,27 +637,17 @@ export function createVfs(vfsRoot, options = {}) {
}
},
rm: (p, opt, cb) => {
const callback = typeof opt === 'function' ? opt : cb;
const options = typeof opt === 'function' ? undefined : opt;
try {
vfs.rmSync(p, options);
if (typeof callback === 'function') process.nextTick(callback, null);
} catch (err) {
if (typeof callback === 'function') process.nextTick(callback, err);
else throw err;
}
},
// #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) {
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? buf.toString(encoding) : buf;
}
if (buf) return formatBuffer(buf, opt);
if (googleAuthId) {
try {
const store = await getStore();
@@ -822,8 +658,7 @@ export function createVfs(vfsRoot, options = {}) {
fs.mkdirSync(path.join(vfsRoot, 'images'), { recursive: true });
fs.writeFileSync(cachePath, img.buffer);
} catch { /* ignore cache write error */ }
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? img.buffer.toString(encoding) : img.buffer;
return formatBuffer(img.buffer, opt);
}
} catch {
// fall through
@@ -839,20 +674,7 @@ export function createVfs(vfsRoot, options = {}) {
const hash = extractFullHash(src);
if (hash) {
const buf = await vfs.promises.readFile(src);
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);
}
return writeCopyBuffer(buf, src, dest, mode);
}
return vfs.copyFileSync(src, dest, mode);
},
@@ -871,16 +693,15 @@ export function createVfs(vfsRoot, options = {}) {
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`. Exposed so non-`fs` writers that
* bypass the VFS (e.g. `sharp(...).toFile()`) can be funnelled back inside the sandbox
* boundary instead of writing wherever the process CWD happens to be.
*
* Deliberately does NOT create parent directories: callers such as `sharp().toFile()`
* must behave like `fs.writeFileSync`, which errors with ENOENT when the parent is
* missing. Auto-creating the whole tree turns a typo'd path into silent success.
*/
* containment + read-only guards as `writeFileSync`.
*/
resolveWritePath: (p) => {
assertNotReadOnlyDatabasePath(p);
return safeResolve(p);
@@ -889,20 +710,43 @@ export function createVfs(vfsRoot, options = {}) {
/**
* Resolve a virtual path to an absolute on-disk path for reading, applying containment checks.
*/
resolveReadPath: (p) => {
return safeResolve(p);
},
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);
vfs.constants = fs.constants;
// #endregion
return vfs;
}
// #endregion