@hyperframes/studio-server 0.8.70 → 0.8.72

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.
package/dist/index.js CHANGED
@@ -302,8 +302,10 @@ import { bodyLimit } from "hono/body-limit";
302
302
  import {
303
303
  closeSync,
304
304
  existsSync as existsSync3,
305
+ lstatSync as lstatSync3,
305
306
  openSync,
306
307
  readFileSync as readFileSync5,
308
+ readlinkSync,
307
309
  writeFileSync as writeFileSync4,
308
310
  writeSync,
309
311
  mkdirSync as mkdirSync3,
@@ -1020,6 +1022,24 @@ function resolveGsapWriter(env) {
1020
1022
  async function loadGsapParser() {
1021
1023
  return import("@hyperframes/parsers/gsap-parser-recast");
1022
1024
  }
1025
+ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
1026
+ if (!isSafePath(projectDir, dirname3(lexicalPath))) return false;
1027
+ let stats;
1028
+ try {
1029
+ stats = lstatSync3(lexicalPath);
1030
+ } catch {
1031
+ return false;
1032
+ }
1033
+ if (!stats.isSymbolicLink()) return false;
1034
+ const target = resolve3(dirname3(lexicalPath), readlinkSync(lexicalPath));
1035
+ if (!isSafePath(projectDir, target)) return false;
1036
+ try {
1037
+ statSync3(lexicalPath);
1038
+ return false;
1039
+ } catch {
1040
+ return true;
1041
+ }
1042
+ }
1023
1043
  async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1024
1044
  const id = c.req.param("id");
1025
1045
  const project = await adapter.resolveProject(id);
@@ -1037,6 +1057,9 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1037
1057
  }
1038
1058
  const absPath = resolveWithinProject(project.dir, filePath);
1039
1059
  if (!absPath) {
1060
+ if (isDanglingSymlinkInProject(project.dir, resolve3(project.dir, filePath))) {
1061
+ return { error: c.json({ error: "not found", why: "dangling_symlink" }, 404) };
1062
+ }
1040
1063
  return { error: c.json({ error: "forbidden", why: "outside_project" }, 403) };
1041
1064
  }
1042
1065
  if (opts?.mustExist && !existsSync3(absPath)) {
@@ -2410,21 +2433,31 @@ function registerFileRoutes(api, adapter) {
2410
2433
  api.get("/projects/:id/files/*", async (c) => {
2411
2434
  const res = await resolveProjectFile(c, adapter);
2412
2435
  if ("error" in res) return res.error;
2413
- if (!existsSync3(res.absPath)) {
2436
+ let fd;
2437
+ try {
2438
+ fd = openSync(res.absPath, "r");
2439
+ } catch {
2414
2440
  if (c.req.query("optional") === "1") {
2415
2441
  return c.json({ filename: res.filePath, content: "", missing: true });
2416
2442
  }
2417
2443
  return c.json({ error: "not found" }, 404);
2418
2444
  }
2419
- const content = readFileSync5(res.absPath);
2420
- const version = fileContentVersion(content);
2421
- c.header("ETag", version);
2422
- return c.json({
2423
- filename: res.filePath,
2424
- content: content.toString("utf-8"),
2425
- version,
2426
- missing: false
2427
- });
2445
+ try {
2446
+ if (!fstatSync(fd).isFile()) {
2447
+ return c.json({ error: "not found", why: "not_a_file" }, 404);
2448
+ }
2449
+ const content = readFileSync5(fd);
2450
+ const version = fileContentVersion(content);
2451
+ c.header("ETag", version);
2452
+ return c.json({
2453
+ filename: res.filePath,
2454
+ content: content.toString("utf-8"),
2455
+ version,
2456
+ missing: false
2457
+ });
2458
+ } finally {
2459
+ closeSync(fd);
2460
+ }
2428
2461
  });
2429
2462
  api.put("/projects/:id/files/*", async (c) => {
2430
2463
  const res = await resolveProjectFile(c, adapter);