mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-03 12:17:03 +00:00
* fix(studio-server): keep the desktop app's hand-off and chat history out of the file routes The file and preview routes reach any path inside the project. The desktop app keeps two files there that hold the person's agent session and their chat: .hyperframes/agent-handoff.json and .hyperframes/app-history.jsonl. No route now reads, writes, renames onto, duplicates or uploads over them, by any spelling or through a link. The rest of .hyperframes/ (Studio's motion file, prepared GIF assets) is served as before. * fix(studio-server): keep the desktop app's run log out of the file routes too The app records every chat run, the person's words included, in .hyperframes/agent-runs.jsonl. Only the app's main process reads it. * fix(studio-server): refuse everything under .hyperframes/ but Studio's own files Review (Jerrai, Rames): a list of three names missed the read marks the desktop app and the CLI also keep there, and renaming the folder reached them under a new name. The check is now an allow-list: no route reaches .hyperframes/ or anything in it, by any spelling or through a link, except Studio's studio-motion.json, studio-manual-edits.json and prepared-assets/. Studio's own internal files (backups, preview documents, history id) are read by path inside the server, not through a request path. The project's file tree leaves out the same paths. The upload test now sends a file uploads accept elsewhere, so the folder is the only reason it is refused. * fix(studio-server): serve the app's request pictures, close the renders and rename paths Review (Rames, Jerrai): - Desktop loads each request's picture from .hyperframes/requests/ through the preview route, so that folder joins the allow-list. An allowed folder's own path is allowed too. - The render-file route resolves against renders/, so a renders/ linked into .hyperframes/ reached the records. It now also checks the file against the project. - A rename's reference update walked every JSON/MD file, records included, and could rewrite them. It now skips private paths. - The allow-list reuses STUDIO_MOTION_PATH and STUDIO_MANUAL_EDITS_PATH. * chore(studio-server): keep render.ts's comment share at the base * fix(studio-server): list, view and download only renders the render-file route would serve Review (Jerrai, Rames): the render list found files in renders/ with join and statSync, which follow links, and registered them for /render/:id/view and /download, which read them with no check. A project shipping renders/leak.mp4 -> ../.hyperframes/app-history.jsonl had its chat served. The list now keeps an entry only when resolveWithinProject accepts it and it isn't private, and registers the resolved path. View and download serve only a plain file, so a link swapped in after the list is refused. This predates the PR; it's the same shipped-link case.
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import type { Hono } from "hono";
|
|
import type { StudioApiAdapter } from "../types.js";
|
|
import { isInHiddenOrVendorDir, isPrivateProjectPath, walkDir } from "../helpers/safePath.js";
|
|
import { isCompositionSource } from "../helpers/hfIdPersist.js";
|
|
import { resolveProjectSignature } from "../helpers/projectSignature.js";
|
|
|
|
async function filterCompositionFiles(projectDir: string, files: string[]): Promise<string[]> {
|
|
const htmlFiles = files.filter((f) => f.endsWith(".html") && !isInHiddenOrVendorDir(f));
|
|
const checks = await Promise.all(
|
|
htmlFiles.map(async (f) => {
|
|
try {
|
|
const content = await readFile(join(projectDir, f), "utf-8");
|
|
return isCompositionSource(content);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}),
|
|
);
|
|
return htmlFiles.filter((_, i) => checks[i]);
|
|
}
|
|
|
|
export function registerProjectRoutes(api: Hono, adapter: StudioApiAdapter): void {
|
|
// List all projects
|
|
api.get("/projects", async (c) => {
|
|
const projects = await adapter.listProjects();
|
|
return c.json({ projects });
|
|
});
|
|
|
|
// Resolve session to project (multi-project mode)
|
|
api.get("/resolve-session/:sessionId", async (c) => {
|
|
if (!adapter.resolveSession) {
|
|
return c.json({ error: "not available" }, 404);
|
|
}
|
|
const { sessionId } = c.req.param();
|
|
const result = await adapter.resolveSession(sessionId);
|
|
if (!result) return c.json({ error: "Session not found" }, 404);
|
|
return c.json(result);
|
|
});
|
|
|
|
// Current content signature for a project: a cheap poll target for clients that refresh when files change on disk.
|
|
api.get("/projects/:id/signature", async (c) => {
|
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
return c.json({ signature: resolveProjectSignature(adapter, project.dir) });
|
|
});
|
|
|
|
// Project file tree
|
|
api.get("/projects/:id", async (c) => {
|
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
const files = walkDir(project.dir).filter((file) => !isPrivateProjectPath(file));
|
|
const compositions = await filterCompositionFiles(project.dir, files);
|
|
return c.json({ id: project.id, dir: project.dir, title: project.title, files, compositions });
|
|
});
|
|
}
|