@hyperframes/studio-server 0.8.71 → 0.8.73

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.d.ts CHANGED
@@ -90,6 +90,8 @@ declare const PREVIEW_BUNDLE_OPTIONS: {
90
90
  declare const MIME_TYPES: Record<string, string>;
91
91
  declare function getMimeType(path: string): string;
92
92
 
93
+ declare function requestSubPath(url: string, route: string): string;
94
+
93
95
  interface FileWriteReceipt {
94
96
  path: string;
95
97
  version: string;
@@ -183,4 +185,4 @@ type BackgroundRemovalRender = (options: {
183
185
  }>;
184
186
  declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
185
187
 
186
- export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, settledFileTag, thumbnailDeviceScaleFactor, walkDir };
188
+ export { type AppliedFileMutation, type BackgroundRemovalRender, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, thumbnailDeviceScaleFactor, walkDir };
package/dist/index.js CHANGED
@@ -1018,6 +1018,14 @@ function resolveGsapWriter(env) {
1018
1018
  throw new Error(`Invalid ${GSAP_WRITER_MIGRATION.flag}=${configured}; expected recast or acorn`);
1019
1019
  }
1020
1020
 
1021
+ // src/helpers/requestSubPath.ts
1022
+ function requestSubPath(url, route) {
1023
+ const routeSegments = route.split("/");
1024
+ const segments = new URL(url).pathname.split("/");
1025
+ const start = segments.indexOf(routeSegments[0] ?? "");
1026
+ return decodeURIComponent(segments.slice(start + routeSegments.length).join("/"));
1027
+ }
1028
+
1021
1029
  // src/routes/files.ts
1022
1030
  async function loadGsapParser() {
1023
1031
  return import("@hyperframes/parsers/gsap-parser-recast");
@@ -1040,7 +1048,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
1040
1048
  return true;
1041
1049
  }
1042
1050
  }
1043
- async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1051
+ async function resolveProjectPath(c, adapter, route, opts) {
1044
1052
  const id = c.req.param("id");
1045
1053
  const project = await adapter.resolveProject(id);
1046
1054
  if (!project) {
@@ -1051,7 +1059,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1051
1059
  error: c.json({ error: "not found", why: "project_dir_missing" }, 404)
1052
1060
  };
1053
1061
  }
1054
- const filePath = decodeURIComponent(c.req.path.replace(pathPrefix(project.id), ""));
1062
+ const filePath = requestSubPath(c.req.url, `projects/:id/${route}`);
1055
1063
  if (filePath.includes("\0")) {
1056
1064
  return { error: c.json({ error: "forbidden", why: "nul" }, 403) };
1057
1065
  }
@@ -1068,10 +1076,10 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
1068
1076
  return { project, filePath, absPath };
1069
1077
  }
1070
1078
  function resolveProjectFile(c, adapter, opts) {
1071
- return resolveProjectPath(c, adapter, (id) => `/projects/${id}/files/`, opts);
1079
+ return resolveProjectPath(c, adapter, "files", opts);
1072
1080
  }
1073
1081
  function resolveFileMutationContext(c, adapter, operation) {
1074
- return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
1082
+ return resolveProjectPath(c, adapter, `file-mutations/${operation}`);
1075
1083
  }
1076
1084
  function isOptionalInteger(value) {
1077
1085
  return value === void 0 || Number.isInteger(value);
@@ -3142,7 +3150,7 @@ function registerFileRoutes(api, adapter) {
3142
3150
  }
3143
3151
  );
3144
3152
  api.get("/projects/:id/gsap-animations/*", async (c) => {
3145
- const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-animations/`, {
3153
+ const res = await resolveProjectPath(c, adapter, "gsap-animations", {
3146
3154
  mustExist: true
3147
3155
  });
3148
3156
  if ("error" in res) return res.error;
@@ -3165,7 +3173,7 @@ function registerFileRoutes(api, adapter) {
3165
3173
  return c.json({ atomicOwnershipPairs: true });
3166
3174
  });
3167
3175
  api.post("/projects/:id/gsap-mutations/*", async (c) => {
3168
- const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-mutations/`, {
3176
+ const res = await resolveProjectPath(c, adapter, "gsap-mutations", {
3169
3177
  mustExist: true
3170
3178
  });
3171
3179
  if ("error" in res) return res.error;
@@ -3176,12 +3184,7 @@ function registerFileRoutes(api, adapter) {
3176
3184
  return applyGsapMutations(c, res, [body]);
3177
3185
  });
3178
3186
  api.post("/projects/:id/gsap-mutations-batch/*", async (c) => {
3179
- const res = await resolveProjectPath(
3180
- c,
3181
- adapter,
3182
- (id) => `/projects/${id}/gsap-mutations-batch/`,
3183
- { mustExist: true }
3184
- );
3187
+ const res = await resolveProjectPath(c, adapter, "gsap-mutations-batch", { mustExist: true });
3185
3188
  if ("error" in res) return res.error;
3186
3189
  const body = await c.req.json().catch(() => null);
3187
3190
  if (!body || !Array.isArray(body.mutations) || body.mutations.length === 0) {
@@ -3194,12 +3197,7 @@ function registerFileRoutes(api, adapter) {
3194
3197
  return applyGsapMutations(c, res, body.mutations);
3195
3198
  });
3196
3199
  api.post("/projects/:id/gsap-mutation-rollback/*", async (c) => {
3197
- const res = await resolveProjectPath(
3198
- c,
3199
- adapter,
3200
- (id) => `/projects/${id}/gsap-mutation-rollback/`,
3201
- { mustExist: true }
3202
- );
3200
+ const res = await resolveProjectPath(c, adapter, "gsap-mutation-rollback", { mustExist: true });
3203
3201
  if ("error" in res) return res.error;
3204
3202
  const body = await c.req.json().catch(() => null);
3205
3203
  if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
@@ -3766,9 +3764,7 @@ ${runtimeTag}`;
3766
3764
  const vars = previewVariablesFromRequest(c.req.query("variables"));
3767
3765
  if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
3768
3766
  const previewVariables = vars.values;
3769
- const compPath = decodeURIComponent(
3770
- c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
3771
- );
3767
+ const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
3772
3768
  const compFile = resolveWithinProject(project.dir, compPath);
3773
3769
  if (!compFile || !existsSync5(compFile) || !statSync5(compFile).isFile()) {
3774
3770
  return c.text("not found", 404);
@@ -3802,9 +3798,7 @@ ${runtimeTag}`;
3802
3798
  api.get("/projects/:id/preview/*", async (c) => {
3803
3799
  const project = await adapter.resolveProject(c.req.param("id"));
3804
3800
  if (!project) return c.json({ error: "not found" }, 404);
3805
- const subPath = decodeURIComponent(
3806
- c.req.path.replace(`/projects/${project.id}/preview/`, "").split("?")[0] ?? ""
3807
- );
3801
+ const subPath = requestSubPath(c.req.url, "projects/:id/preview");
3808
3802
  const candidate = resolve4(project.dir, subPath);
3809
3803
  const file = isWithinProjectRoot(project.dir, candidate) ? candidate : null;
3810
3804
  if (!file) {
@@ -4198,7 +4192,10 @@ function registerRenderRoutes(api, adapter) {
4198
4192
 
4199
4193
  // src/routes/thumbnail.ts
4200
4194
  import {
4195
+ closeSync as closeSync3,
4201
4196
  existsSync as existsSync7,
4197
+ fstatSync as fstatSync3,
4198
+ openSync as openSync3,
4202
4199
  mkdirSync as mkdirSync5,
4203
4200
  readFileSync as readFileSync12,
4204
4201
  readdirSync as readdirSync6,
@@ -4285,10 +4282,10 @@ var ThumbnailGenerationCoordinator = class {
4285
4282
  if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
4286
4283
  let entry = this.entries.get(key);
4287
4284
  if (!entry) {
4288
- let resolve5;
4285
+ let resolve6;
4289
4286
  let reject;
4290
4287
  const promise = new Promise((resolvePromise, rejectPromise) => {
4291
- resolve5 = resolvePromise;
4288
+ resolve6 = resolvePromise;
4292
4289
  reject = rejectPromise;
4293
4290
  });
4294
4291
  entry = {
@@ -4298,7 +4295,7 @@ var ThumbnailGenerationCoordinator = class {
4298
4295
  state: "queued",
4299
4296
  work,
4300
4297
  promise,
4301
- resolve: resolve5,
4298
+ resolve: resolve6,
4302
4299
  reject
4303
4300
  };
4304
4301
  this.entries.set(key, entry);
@@ -4312,7 +4309,7 @@ var ThumbnailGenerationCoordinator = class {
4312
4309
  return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
4313
4310
  }
4314
4311
  lease(entry, signal) {
4315
- return new Promise((resolve5, reject) => {
4312
+ return new Promise((resolve6, reject) => {
4316
4313
  let released = false;
4317
4314
  const release = () => {
4318
4315
  if (released) return;
@@ -4336,7 +4333,7 @@ var ThumbnailGenerationCoordinator = class {
4336
4333
  entry.promise.then(
4337
4334
  (value) => {
4338
4335
  release();
4339
- resolve5(value);
4336
+ resolve6(value);
4340
4337
  },
4341
4338
  (reason) => {
4342
4339
  release();
@@ -4418,6 +4415,22 @@ function writeThumbnailAtomically(path, buffer) {
4418
4415
  rmSync4(temporaryPath, { force: true });
4419
4416
  }
4420
4417
  }
4418
+ function readCompositionSource(file) {
4419
+ let fd;
4420
+ try {
4421
+ fd = openSync3(file, "r");
4422
+ } catch (err) {
4423
+ if (err.code === "ENOENT") return "missing";
4424
+ throw err;
4425
+ }
4426
+ try {
4427
+ const stat = fstatSync3(fd);
4428
+ if (!stat.isFile()) return "not-a-file";
4429
+ return { html: readFileSync12(fd, "utf-8"), mtimeMs: stat.mtimeMs };
4430
+ } finally {
4431
+ closeSync3(fd);
4432
+ }
4433
+ }
4421
4434
  function registerThumbnailRoutes(api, adapter) {
4422
4435
  api.get("/projects/:id/thumbnail/*", async (c) => {
4423
4436
  if (!adapter.generateThumbnail) {
@@ -4426,10 +4439,11 @@ function registerThumbnailRoutes(api, adapter) {
4426
4439
  const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
4427
4440
  if (!resolved) return c.json({ error: "not found" }, 404);
4428
4441
  const { project, signature: projectSignature } = resolved;
4429
- let compPath = decodeURIComponent(
4430
- c.req.path.replace(`/projects/${project.id}/thumbnail/`, "").split("?")[0] ?? ""
4431
- );
4442
+ let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
4432
4443
  if (compPath && !compPath.includes(".")) compPath += ".html";
4444
+ const htmlFile = resolveWithinProject(project.dir, compPath);
4445
+ const source = htmlFile ? readCompositionSource(htmlFile) : "not-a-file";
4446
+ if (source === "not-a-file") return c.json({ error: "not found" }, 404);
4433
4447
  const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
4434
4448
  const url = new URL(c.req.url, `http://${c.req.header("host") || "localhost"}`);
4435
4449
  const rawSeekTime = url.searchParams.get("t");
@@ -4449,11 +4463,10 @@ function registerThumbnailRoutes(api, adapter) {
4449
4463
  let compH = vpHeight || 1080;
4450
4464
  let sourceMtime = 0;
4451
4465
  let sourceKey = "";
4452
- const htmlFile = join11(project.dir, compPath);
4453
- if (existsSync7(htmlFile)) {
4454
- const html = readFileSync12(htmlFile, "utf-8");
4466
+ if (source !== "missing") {
4467
+ const { html } = source;
4455
4468
  sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4456
- sourceMtime = Math.round(statSync7(htmlFile).mtimeMs);
4469
+ sourceMtime = Math.round(source.mtimeMs);
4457
4470
  if (!vpWidth) {
4458
4471
  const wMatch = html.match(/data-width=["'](\d+)["']/);
4459
4472
  const hMatch = html.match(/data-height=["'](\d+)["']/);
@@ -4475,7 +4488,8 @@ function registerThumbnailRoutes(api, adapter) {
4475
4488
  motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
4476
4489
  sourceMtime = Math.max(sourceMtime, Math.round(statSync7(motionFile).mtimeMs));
4477
4490
  }
4478
- const previewUrl = compPath === "index.html" ? `http://${c.req.header("host")}/api/projects/${project.id}/preview` : `http://${c.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;
4491
+ const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
4492
+ const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4479
4493
  const cacheDir = join11(project.dir, ".thumbnails");
4480
4494
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4481
4495
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
@@ -4549,15 +4563,17 @@ function registerThumbnailRoutes(api, adapter) {
4549
4563
 
4550
4564
  // src/routes/waveform.ts
4551
4565
  import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
4552
- import { join as join12 } from "path";
4566
+ import { join as join12, resolve as resolve5 } from "path";
4567
+ import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
4553
4568
  function registerWaveformRoutes(api, adapter) {
4554
4569
  api.get("/projects/:id/waveform/*", async (c) => {
4555
4570
  const project = await adapter.resolveProject(c.req.param("id"));
4556
4571
  if (!project) return c.json({ error: "not found" }, 404);
4557
- const assetPath = decodeURIComponent(
4558
- c.req.path.replace(`/projects/${project.id}/waveform/`, "").split("?")[0] ?? ""
4559
- );
4560
- const audioPath = join12(project.dir, assetPath);
4572
+ const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
4573
+ const audioPath = resolve5(project.dir, assetPath);
4574
+ if (!isWithinProjectRoot2(project.dir, audioPath)) {
4575
+ return c.json({ error: "file not found" }, 404);
4576
+ }
4561
4577
  const stats = statSync8(audioPath, { throwIfNoEntry: false });
4562
4578
  if (!stats) return c.json({ error: "file not found" }, 404);
4563
4579
  const cacheDir = join12(project.dir, ".waveform-cache");
@@ -4584,7 +4600,7 @@ function registerWaveformRoutes(api, adapter) {
4584
4600
  }
4585
4601
 
4586
4602
  // src/routes/fonts.ts
4587
- import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync3, openSync as openSync3, readSync } from "fs";
4603
+ import { closeSync as closeSync4, constants as constants2, fstatSync as fstatSync4, openSync as openSync4, readSync } from "fs";
4588
4604
  import {
4589
4605
  collectFontFileEntries,
4590
4606
  fontDirectories,
@@ -4691,12 +4707,12 @@ function registerFontRoutes(api) {
4691
4707
  if (!located) return c.json({ error: "font not found" }, 404);
4692
4708
  let fd;
4693
4709
  try {
4694
- fd = openSync3(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
4710
+ fd = openSync4(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
4695
4711
  } catch {
4696
4712
  return c.json({ error: "font file not accessible" }, 404);
4697
4713
  }
4698
4714
  try {
4699
- const stat = fstatSync3(fd);
4715
+ const stat = fstatSync4(fd);
4700
4716
  if (stat.size > SYSTEM_FONT_SIZE_LIMIT) {
4701
4717
  return c.json({ error: "font file too large" }, 413);
4702
4718
  }
@@ -4713,7 +4729,7 @@ function registerFontRoutes(api) {
4713
4729
  } catch {
4714
4730
  return c.json({ error: "failed to read font file" }, 500);
4715
4731
  } finally {
4716
- closeSync3(fd);
4732
+ closeSync4(fd);
4717
4733
  }
4718
4734
  });
4719
4735
  }
@@ -5237,6 +5253,7 @@ export {
5237
5253
  parseSourceDocument,
5238
5254
  patchElementInHtml,
5239
5255
  removeElementFromHtml,
5256
+ requestSubPath,
5240
5257
  settledFileTag,
5241
5258
  splitElementInHtml,
5242
5259
  thumbnailDeviceScaleFactor,