Backport generic tool and preview improvements

This commit is contained in:
udo committed 2026-08-26 12:48:31 +00:00
1 parent 80b9cc1547
commit 83a1049fe2
6 files changed
+236 -66

No files matched your search

+47 -1
View File
@@ -64,7 +64,7 @@ const allowedOrigins = new Set(
);
const SECURITY_HEADERS = {
csp: "default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'none';",
csp: "default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data: blob:; frame-src 'self' blob:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'none';",
hsts: "max-age=31536000; includeSubDomains; preload",
referrerPolicy: "strict-origin-when-cross-origin",
frameOptions: "DENY",
@@ -389,6 +389,12 @@ function requireEditAuth(req: express.Request, res: express.Response, next: expr
res.status(401).json({ ok: false, error: "unauthorized" });
}
function requireHttpAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
if (!isAllowedWsOrigin(req.header("Origin"), req.header("Host"))) return res.status(403).type("text/plain").send("Forbidden origin");
if (isValidToken(tokenFromRequest(req))) return next();
res.status(401).type("text/plain").send("Unauthorized");
}
function fileStamp(filePath: string): string {
try {
const stat = fs.statSync(filePath);
@@ -1467,6 +1473,46 @@ app.delete("/api/skills/user/:id", requireEditAuth, async (req, res) => {
// Serve static client files in production after API routes.
const clientDist = path.resolve(__dirname, "../dist");
const TEXT_PREVIEW_EXTENSIONS = new Set([
".txt", ".md", ".markdown", ".json", ".jsonl", ".csv", ".tsv", ".log", ".xml", ".html", ".htm", ".css", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".php", ".sh", ".bash", ".zsh", ".yaml", ".yml", ".toml", ".ini", ".conf", ".sql",
]);
async function resolveHomeFilePath(requested: string): Promise<string | undefined> {
const trimmed = requested.trim();
if (!trimmed) return undefined;
const home = await fs.promises.realpath(os.homedir()).catch(() => undefined);
const requestedPath = path.resolve(path.isAbsolute(trimmed) ? trimmed : path.join(os.homedir(), trimmed));
const resolved = await fs.promises.realpath(requestedPath).catch(() => undefined);
if (!home || !resolved || (resolved !== home && !resolved.startsWith(`${home}${path.sep}`))) return undefined;
return resolved;
}
function canPreviewTextFile(filePath: string, size: number): boolean {
return size <= 2 * 1024 * 1024 && TEXT_PREVIEW_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}
app.get("/api/files", requireHttpAuth, async (req, res) => {
const requested = typeof req.query.path === "string" ? req.query.path : "";
const filePath = await resolveHomeFilePath(requested);
if (!filePath) return res.status(403).type("text/plain").send("File is outside the workspace");
const stats = await fs.promises.stat(filePath).catch(() => undefined);
if (!stats?.isFile()) return res.status(404).type("text/plain").send("File not found");
if (req.query.download === "1") return res.download(filePath, path.basename(filePath));
if (req.query.inline === "1") {
const inlineTypes: Record<string, string> = { ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
const type = inlineTypes[path.extname(filePath).toLowerCase()];
if (!type) return res.status(415).type("text/plain").send("File type is not previewable");
res.type(type);
return res.sendFile(filePath);
}
if (req.query.raw === "1") {
if (!canPreviewTextFile(filePath, stats.size)) return res.status(415).type("text/plain").send("File type is not previewable as text");
res.type("text/plain");
return res.sendFile(filePath);
}
return res.status(400).type("text/plain").send("Use the file preview page");
});
app.use(express.static(clientDist));
app.get("/{*path}", (_req, res) => {
res.sendFile(path.join(clientDist, "client", "index.html"));