This commit is contained in:
seb
2026-11-03 06:20:06 +01:00
parent 24563964a6
commit da785da2dd
21 changed files with 273 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import multer from 'multer';
import JSZip from 'jszip';
import env from '../../config/env.js';
import * as db from '../../db/company.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import * as googleAuth from '../../db/googleAuth.js';
import { clearCachedUser } from '../middleware/auth.js';
import { emit } from '../sse.js';
@@ -499,13 +500,25 @@ export function register(app, basePath = '') {
app.post(
`${basePath}/company/import`,
async (req, res, next) => {
try {
const companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
next();
} catch (error) {
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
},
uploadCompanyImportFile,
async (req, res) => {
let companyId;
try {
companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
if (activeImportsByCompany.has(companyId)) {
@@ -968,7 +981,7 @@ export function register(app, basePath = '') {
}
try {
const company = await db.updateCompany(parsedId.companyId, body);
const company = await db.updateCompany(parsedId.companyId, body, req.user.id);
emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } });
res.status(200).json({ company });
} catch (error) {
@@ -989,10 +1002,14 @@ export function register(app, basePath = '') {
}
try {
await db.deleteCompany(parsedId.companyId);
await db.deleteCompany(parsedId.companyId, req.user.id);
clearCompanyPrinters(parsedId.companyId);
res.sendStatus(204);
} catch (error) {
const status = companyErrorStatus(error);
if (status !== 500) {
return res.status(status).json({ error: error.message });
}
console.error('Delete company failed:', error);
res.status(500).json({ error: 'Failed to delete company' });
}

View File

@@ -2,10 +2,13 @@ import fs from 'fs';
import path from 'path';
import multer from 'multer';
import { emit } from '../sse.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import {
prepareVfs,
resolveVfsRoot,
isReadOnlyDatabasePath,
isProtectedAdminVfsPath,
directoryContainsProtectedAdminVfsFile,
getMimeType,
restoreDefaultSkills,
restoreDefaultApplets,
@@ -586,6 +589,8 @@ export function register(app, basePath = '') {
: '';
const uploaded = [];
const parsedEntries = [];
let touchesProtected = false;
for (const f of files) {
const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/');
const relPath = targetDir ? `${targetDir}/${origName}` : origName;
@@ -596,6 +601,18 @@ export function register(app, basePath = '') {
const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) continue;
if (isProtectedAdminVfsPath(parsed.name)) {
touchesProtected = true;
}
parsedEntries.push({ f, parsed, absPath });
}
if (touchesProtected) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
for (const { f, parsed, absPath } of parsedEntries) {
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, f.buffer);
@@ -815,6 +832,15 @@ export function register(app, basePath = '') {
return res.status(404).json({ error: 'Source file or directory not found' });
}
if (
isProtectedAdminVfsPath(parsedOld.name) ||
isProtectedAdminVfsPath(parsedNew.name) ||
directoryContainsProtectedAdminVfsFile(absOld) ||
directoryContainsProtectedAdminVfsFile(absNew)
) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
fs.mkdirSync(path.dirname(absNew), { recursive: true });
fs.renameSync(absOld, absNew);
@@ -961,6 +987,9 @@ export function register(app, basePath = '') {
}
try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) {
@@ -1058,6 +1087,9 @@ export function register(app, basePath = '') {
}
try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) {
@@ -1066,6 +1098,9 @@ export function register(app, basePath = '') {
if (!fs.existsSync(absPath)) {
return res.status(404).json({ error: 'File not found' });
}
if (directoryContainsProtectedAdminVfsFile(absPath)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const stat = fs.statSync(absPath);
if (stat.isDirectory()) {
fs.rmSync(absPath, { recursive: true, force: true });

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.account.create(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.account.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId, printeragentId) {
const store = await getStore();
@@ -16,16 +17,19 @@ export async function findOrCreate(printeragentId, data) {
}
export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.update(googleAuthId, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.carrier.create(googleAuthId, data);
}
export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.carrier.update(googleAuthId, id, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.carrier.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.carrier.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore, getProvider } from './store.js';
import { requireCompanyAdmin } from './companyuser.js';
export async function listCompanies(authId) {
const store = await getStore();
@@ -15,12 +16,18 @@ export async function createCompany(googleAuthId, data) {
return store.company.create({ ...data, googleAuthId });
}
export async function updateCompany(id, data) {
export async function updateCompany(id, data, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, id);
}
const store = await getStore();
return store.company.update(id, data);
}
export async function deleteCompany(companyId) {
export async function deleteCompany(companyId, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, companyId);
}
const store = await getStore();
await store.company.delete(companyId);
}

View File

@@ -1,4 +1,15 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export const ADMIN_ONLY_SETTING_KEYS = new Set([
'ui.theme',
'keys.tavily',
'keys.openrouter',
'keys.serper',
'modelslugimage',
'systemprompt-prefix',
'systemprompt-suffix',
]);
export async function list(googleAuthId, options) {
const store = await getStore();
@@ -11,6 +22,9 @@ export async function getByKey(googleAuthId, key, options) {
}
export async function set(googleAuthId, input) {
if (ADMIN_ONLY_SETTING_KEYS.has(input?.key)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const store = await getStore();
return store.companysetting.set(googleAuthId, input);
}

View File

@@ -1,4 +1,53 @@
import { getStore } from './store.js';
import { getStore, getProvider } from './store.js';
export async function resolveCompanyAccess(googleAuthId, companyId) {
const provider = await getProvider();
const access = await provider.query(
`SELECT company_id, role FROM companyuser WHERE company_id = @companyId AND google_auth_id = @googleAuthId`,
{ companyId, googleAuthId }
);
const accessRow = access.rows[0] ?? null;
if (!accessRow) {
const exists = await provider.query(
`SELECT id FROM company WHERE id = @companyId`,
{ companyId }
);
if (!exists.rows[0]) {
throw new Error('Company not found');
}
throw new Error('Not authorized');
}
return {
companyId: accessRow.company_id,
role: accessRow.role ?? 'user',
};
}
export async function resolveSelectedCompanyAccess(googleAuthId) {
const store = await getStore();
const user = await store.google_auth.getById(googleAuthId);
const companyId = user?.selectedCompanyId ?? null;
if (companyId == null) {
throw new Error('No company selected');
}
return resolveCompanyAccess(googleAuthId, companyId);
}
export async function requireCompanyAdmin(googleAuthId, companyId) {
const ctx = await resolveCompanyAccess(googleAuthId, companyId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function requireSelectedCompanyAdmin(googleAuthId) {
const ctx = await resolveSelectedCompanyAccess(googleAuthId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function list(googleAuthId) {
const store = await getStore();

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -6,21 +7,25 @@ export async function list(googleAuthId) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.customergroup.create(googleAuthId, data);
}
export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.customergroup.update(googleAuthId, id, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.customergroup.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.customergroup.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -6,6 +7,7 @@ export async function list(googleAuthId) {
}
export async function update(googleAuthId, id, input) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.numbers.update(googleAuthId, id, input);
}

View File

@@ -1,4 +1,5 @@
import { getStore, getProvider } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
}
export async function upsert(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.posterminal.upsert(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.posterminal.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printer.create(googleAuthId, data);
}
export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printer.update(googleAuthId, id, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printer.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printer.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -6,16 +7,19 @@ export async function list(googleAuthId) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printeragent.create(googleAuthId, data);
}
export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printeragent.update(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.printeragent.delete(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) {
const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
}
export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.warehouse.create(googleAuthId, data);
}
export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.warehouse.update(googleAuthId, id, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.warehouse.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.warehouse.delete(googleAuthId, id);
}

View File

@@ -344,8 +344,8 @@ export async function runSandboxJob(options) {
throw new Error('Sandbox worker pool is shutting down');
}
const { poolKey, googleAuthId, code, timeoutMs, dashbackToken, appletInput, appletFile } = options;
const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId };
const { poolKey, googleAuthId, isAdmin = true, code, timeoutMs, dashbackToken, appletInput, appletFile } = options;
const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId, isAdmin };
const entry = await acquireWorker(poolKey, googleAuthId);
if (entry) {

View File

@@ -544,7 +544,7 @@ async function runJob(job) {
const appletInput = job.appletInput;
const appletFile = job.appletFile;
const vfs = createVfs(vfsDir, { googleAuthId });
const vfs = createVfs(vfsDir, { googleAuthId, isAdmin: job.isAdmin !== false });
const bridgedExcel = bridgeExcelJS(vfs);
const bridgedPDF = bridgePDFKit(vfs);

View File

@@ -5,6 +5,7 @@ import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { createVfs, prepareVfs } from './vfs.js';
import { resolveSelectedCompanyAccess } from '../db/companyuser.js';
import { runSandboxJob } from '../services/javascriptSandbox.js';
const DEFAULT_TIMEOUT_MS = 20_000;
@@ -184,11 +185,21 @@ function toTypedCsv(result, columns) {
*
* @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx
*/
function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) {
async function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) {
let isAdmin = true;
if (googleAuthId != null) {
try {
const access = await resolveSelectedCompanyAccess(googleAuthId);
isAdmin = access.role === 'admin';
} catch {
isAdmin = false;
}
}
const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' });
return runSandboxJob({
poolKey: vfsRoot,
googleAuthId,
isAdmin,
code,
timeoutMs,
dashbackToken,

View File

@@ -1,9 +1,9 @@
import fs from 'fs';
import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.js';
import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { getStore } from '../db/store.js';
@@ -155,6 +155,10 @@ export function createPatchTool({ googleAuthId }) {
);
}
if (isProtectedAdminVfsPath(clean)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
// A SKILL file must be flat — reject an attempted nested path up front.
const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase());
if (looksSkillByExt && clean.includes('/')) {

View File

@@ -2,7 +2,8 @@ import fs from 'fs';
import path from 'path';
import { defineTool } from 'libllmorbiter';
import { isSkillPath } from './skill.js';
import { resolveVfsRoot } from './vfs.js';
import { resolveVfsRoot, isProtectedAdminVfsPath, directoryContainsProtectedAdminVfsFile } from './vfs.js';
import { resolveSelectedCompanyAccess, requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
/**
@@ -40,6 +41,11 @@ export function createResetVfsTool({ googleAuthId }) {
},
},
invoke: async ({ include_skills = false } = {}) => {
if (include_skills === true) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const access = await resolveSelectedCompanyAccess(googleAuthId);
const isAdmin = access.role === 'admin';
const keepSkills = include_skills !== true;
const keptSkills = [];
const vfsRoot = await resolveVfsRoot(googleAuthId);
@@ -51,8 +57,12 @@ export function createResetVfsTool({ googleAuthId }) {
if (keepSkills && isSkillPath(entry)) {
continue;
}
const fullPath = path.join(vfsRoot, entry);
if (!isAdmin && (isProtectedAdminVfsPath(entry) || directoryContainsProtectedAdminVfsFile(fullPath))) {
continue;
}
try {
fs.rmSync(path.join(vfsRoot, entry), { recursive: true, force: true });
fs.rmSync(fullPath, { recursive: true, force: true });
deletedCount += 1;
} catch (err) {
console.warn(`[reset_vfs] could not remove "${entry}": ${err.message}`);

View File

@@ -1,7 +1,8 @@
import fs from 'fs';
import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js';
import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { isSkillPath } from './skill.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
@@ -94,6 +95,10 @@ export function createRmTool({ googleAuthId }) {
);
}
if (isProtectedAdminVfsPath(clean)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const vfsRoot = await prepareVfs({ googleAuthId });
const { abs, size } = deleteVfsFile(vfsRoot, clean);

View File

@@ -1,6 +1,7 @@
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';
@@ -97,6 +98,39 @@ export function isReadOnlyDatabasePath(filePath) {
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(
@@ -190,6 +224,9 @@ export async function loadDefaultSkills(ctx = {}) {
* @returns {Promise<Array<string>>}
*/
export async function restoreDefaultSkills(ctx = {}) {
if (ctx.googleAuthId != null) {
await requireSelectedCompanyAdmin(ctx.googleAuthId);
}
return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' });
}
@@ -208,6 +245,9 @@ export async function loadDefaultApplets(ctx = {}) {
* @returns {Promise<Array<string>>}
*/
export async function restoreDefaultApplets(ctx = {}) {
if (ctx.googleAuthId != null) {
await requireSelectedCompanyAdmin(ctx.googleAuthId);
}
return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' });
}
@@ -293,7 +333,14 @@ function cbWrap(syncFn, arityWithOpt = 3) {
* @param {{ googleAuthId: number }} [options]
*/
export function createVfs(vfsRoot, options = {}) {
const { googleAuthId } = 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
@@ -405,7 +452,7 @@ export function createVfs(vfsRoot, options = {}) {
// --- Write & Append Operations ---
writeFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
try {
@@ -416,14 +463,14 @@ export function createVfs(vfsRoot, options = {}) {
},
createWriteStream: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
return fs.createWriteStream(diskPath, opt);
},
appendFileSync: (p, data, opt) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
try {
@@ -435,7 +482,7 @@ export function createVfs(vfsRoot, options = {}) {
// --- Copy, Rename & Truncate Operations ---
copyFileSync: (src, dest, mode) => {
assertNotReadOnlyDatabasePath(dest);
assertWritablePath(dest);
const hash = extractFullHash(src);
if (hash) {
const buf = syncFetchImage(hash);
@@ -452,7 +499,7 @@ export function createVfs(vfsRoot, options = {}) {
},
cpSync: (src, dest, opt) => {
assertNotReadOnlyDatabasePath(dest);
assertWritablePath(dest);
const srcDisk = safeResolve(src);
const destDisk = safeResolve(dest);
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
@@ -464,10 +511,13 @@ export function createVfs(vfsRoot, options = {}) {
},
renameSync: (oldPath, newPath) => {
assertNotReadOnlyDatabasePath(oldPath);
assertNotReadOnlyDatabasePath(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);
@@ -477,7 +527,7 @@ export function createVfs(vfsRoot, options = {}) {
},
truncateSync: (p, len) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
try {
return fs.truncateSync(diskPath, len);
@@ -600,7 +650,7 @@ export function createVfs(vfsRoot, options = {}) {
},
unlinkSync: (p) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
try {
return fs.unlinkSync(diskPath);
@@ -610,8 +660,11 @@ export function createVfs(vfsRoot, options = {}) {
},
rmdirSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
throw new Error('Not authorized');
}
try {
return fs.rmdirSync(diskPath, opt);
} catch (err) {
@@ -620,8 +673,11 @@ export function createVfs(vfsRoot, options = {}) {
},
rmSync: (p, opt) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
const diskPath = safeResolve(p);
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
throw new Error('Not authorized');
}
try {
return fs.rmSync(diskPath, opt);
} catch (err) {
@@ -662,7 +718,7 @@ export function createVfs(vfsRoot, options = {}) {
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) => {
assertNotReadOnlyDatabasePath(dest);
assertWritablePath(dest);
const hash = extractFullHash(src);
if (hash) {
const buf = await vfs.promises.readFile(src);
@@ -695,7 +751,7 @@ export function createVfs(vfsRoot, options = {}) {
* containment + read-only guards as `writeFileSync`.
*/
resolveWritePath: (p) => {
assertNotReadOnlyDatabasePath(p);
assertWritablePath(p);
return safeResolve(p);
},