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

@@ -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);
},