agent-canvas 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { SessionManager } from "@fabrika/canvas-kernel/server";
6
+ import { dispatch } from "@fabrika/daemon-kit";
7
+ import { createFileHandlers } from "./handlers/files";
8
+
9
+ const directories: string[] = [];
10
+
11
+ afterEach(() => {
12
+ for (const directory of directories.splice(0)) {
13
+ rmSync(directory, { recursive: true, force: true });
14
+ }
15
+ });
16
+
17
+ function setup() {
18
+ const directory = mkdtempSync(join(tmpdir(), "agent-canvas-files-"));
19
+ directories.push(directory);
20
+ const projectRoot = join(directory, "project");
21
+ const outsideRoot = join(directory, "project-other");
22
+ mkdirSync(projectRoot);
23
+ mkdirSync(outsideRoot);
24
+ writeFileSync(join(projectRoot, "local.ts"), "export const local = true;");
25
+ writeFileSync(join(outsideRoot, "external.ts"), "export const external = true;");
26
+ writeFileSync(join(outsideRoot, "image.svg"), "<svg xmlns=\"http://www.w3.org/2000/svg\" />");
27
+ const manager = new SessionManager(join(directory, "sessions"));
28
+ manager.upsert("files", new Map([["plan.jsx", "<Section />"]]), projectRoot);
29
+ const routes = createFileHandlers(manager);
30
+
31
+ async function get(endpoint: string, path: string) {
32
+ const url = new URL(`http://localhost:19400/api/${endpoint}`);
33
+ url.searchParams.set("session", "files");
34
+ url.searchParams.set("path", path);
35
+ const response = dispatch(routes, new Request(url), url);
36
+ if (!response) throw new Error(`No route for ${endpoint}`);
37
+ return response;
38
+ }
39
+
40
+ return { get, projectRoot, outsideRoot };
41
+ }
42
+
43
+ test("serves relative paths and absolute paths inside and outside the project root", async () => {
44
+ const { get, projectRoot, outsideRoot } = setup();
45
+ for (const path of ["local.ts", join(projectRoot, "local.ts"), join(outsideRoot, "external.ts")]) {
46
+ const response = await get("file", path);
47
+ expect(response.status).toBe(200);
48
+ expect(await response.json()).toEqual({
49
+ content: path.endsWith("external.ts") ? "export const external = true;" : "export const local = true;",
50
+ language: "typescript",
51
+ });
52
+ }
53
+ });
54
+
55
+ test("serves absolute image and directory paths outside the project root", async () => {
56
+ const { get, outsideRoot } = setup();
57
+ const image = await get("image", join(outsideRoot, "image.svg"));
58
+ expect(image.status).toBe(200);
59
+ expect(image.headers.get("Content-Type")).toBe("image/svg+xml");
60
+ const tree = await get("tree", outsideRoot);
61
+ expect(tree.status).toBe(200);
62
+ expect(await tree.json()).toEqual({ entries: [
63
+ { name: "external.ts", type: "file" },
64
+ { name: "image.svg", type: "file" },
65
+ ] });
66
+ });
67
+
68
+ test("rejects relative traversal into a sibling with the same root prefix", async () => {
69
+ const { get } = setup();
70
+ for (const endpoint of ["file", "image", "tree"]) {
71
+ expect((await get(endpoint, "../project-other/external.ts")).status).toBe(403);
72
+ }
73
+ });
74
+
75
+ test("returns 404 for a missing absolute file", async () => {
76
+ const { get, outsideRoot } = setup();
77
+ expect((await get("file", join(outsideRoot, "missing.ts"))).status).toBe(404);
78
+ });
@@ -1,4 +1,4 @@
1
- import { join, resolve } from "path";
1
+ import { isAbsolute, relative, resolve, sep } from "path";
2
2
  import { readdir } from "fs/promises";
3
3
  import type { SessionManager } from "@fabrika/canvas-kernel/server";
4
4
  import { jsonResponse, type Route } from "@fabrika/daemon-kit";
@@ -18,8 +18,9 @@ function resolveSessionPath(
18
18
  const session = sessionManager.get(sessionId);
19
19
  if (!session) return jsonResponse({ error: "Session not found" }, 404);
20
20
 
21
- const fullPath = resolve(join(session.projectRoot, relPath));
22
- if (!fullPath.startsWith(resolve(session.projectRoot))) {
21
+ const fullPath = resolve(session.projectRoot, relPath);
22
+ const relativePath = relative(resolve(session.projectRoot), fullPath);
23
+ if (!isAbsolute(relPath) && (relativePath === ".." || relativePath.startsWith(`..${sep}`))) {
23
24
  return jsonResponse({ error: "Path traversal rejected" }, 403);
24
25
  }
25
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-canvas",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive visual canvas for Claude Code — rich annotatable documents in the browser",
5
5
  "bin": {
6
6
  "agent-canvas": "bin/agent-canvas.js"
@@ -29,6 +29,10 @@ fresh, so the variable will not survive between commands.
29
29
 
30
30
  ## Instructions
31
31
 
32
+ Always use absolute filesystem paths for local file references in canvas components
33
+ (such as `FilePreview`), and verify that the referenced files exist. Resolve them
34
+ against the user's project directory, not the temporary directory containing the JSX.
35
+
32
36
  Run these commands from the user's project root before using Canvas:
33
37
 
34
38
  ```bash