@hyperframes/studio-server 0.8.141 → 0.8.143

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
@@ -17,7 +17,7 @@ import {
17
17
  splitElementInHtml,
18
18
  unwrapElementsFromHtml,
19
19
  wrapElementsInHtml
20
- } from "./chunk-CLWFZQUU.js";
20
+ } from "./chunk-JJ4N3PXT.js";
21
21
  import {
22
22
  isCompositionSource,
23
23
  stampProjectHfIds
@@ -55,16 +55,19 @@ import {
55
55
  import {
56
56
  DEFAULT_TARGET_LUFS,
57
57
  absoluteLoudnessPlan,
58
+ decodeWellFormedEscapes,
58
59
  measureAudio,
60
+ requestSubPath,
61
+ requestedProjectPath,
59
62
  requiredFfmpeg,
60
63
  resolveLocalAudioPath
61
- } from "./chunk-DSVQ2U7J.js";
64
+ } from "./chunk-32IH7WBY.js";
62
65
  import {
63
66
  injectMediaCodecMap,
64
67
  isAutoProxyEnabled,
65
68
  proxyEtagSalt,
66
69
  resolvePreviewMediaCodecProbeCache
67
- } from "./chunk-5VCIZYUE.js";
70
+ } from "./chunk-AYKLBPP2.js";
68
71
  import {
69
72
  CACHE_DIR_NAME,
70
73
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -75,7 +78,7 @@ import {
75
78
  proxyActivityMark,
76
79
  resolveProxy,
77
80
  waitForProxy
78
- } from "./chunk-QP7LKFU2.js";
81
+ } from "./chunk-6EFY2WPN.js";
79
82
  import {
80
83
  PROXY_VARIANT_CONFIG,
81
84
  decideMediaProxyEligibility,
@@ -84,7 +87,7 @@ import {
84
87
  probeMediaMetadata,
85
88
  recordProxyRequest,
86
89
  resolveProxyVariantRequest
87
- } from "./chunk-74JVVCWL.js";
90
+ } from "./chunk-RN33UALN.js";
88
91
  import {
89
92
  folderGone,
90
93
  isInHiddenOrVendorDir,
@@ -158,6 +161,12 @@ function registerProjectRoutes(api, adapter) {
158
161
  }
159
162
 
160
163
  // src/routes/files.ts
164
+ import { encodeUrlPath, decodedUrlPath, decodeCssEscapes } from "@hyperframes/parsers/asset-paths";
165
+ import {
166
+ scanHtmlOpeningTags,
167
+ decodeAuthoredAttribute,
168
+ HTML_ATTRIBUTE_ENTITIES
169
+ } from "@hyperframes/parsers";
161
170
  import { bodyLimit } from "hono/body-limit";
162
171
  import {
163
172
  closeSync,
@@ -174,7 +183,7 @@ import {
174
183
  readdirSync as readdirSync2,
175
184
  realpathSync
176
185
  } from "fs";
177
- import { resolve as resolve2, dirname as dirname3, join as join6 } from "path";
186
+ import { resolve as resolve2, dirname as dirname3, join as join6, sep as sep2 } from "path";
178
187
 
179
188
  // src/helpers/mime.ts
180
189
  var MIME_TYPES = {
@@ -1191,14 +1200,6 @@ function resolveGsapWriter(env) {
1191
1200
  throw new Error(`Invalid ${GSAP_WRITER_MIGRATION.flag}=${configured}; expected recast or acorn`);
1192
1201
  }
1193
1202
 
1194
- // src/helpers/requestSubPath.ts
1195
- function requestSubPath(url, route) {
1196
- const routeSegments = route.split("/");
1197
- const segments = new URL(url).pathname.split("/");
1198
- const start = segments.indexOf(routeSegments[0] ?? "");
1199
- return decodeURIComponent(segments.slice(start + routeSegments.length).join("/"));
1200
- }
1201
-
1202
1203
  // src/routes/files.ts
1203
1204
  import {
1204
1205
  hasCompositionOutsideTemplates,
@@ -1505,15 +1506,114 @@ var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1505
1506
  var SEPARATOR = String.raw`\\{0,2}[\\/]`;
1506
1507
  var REFERENCE_START = String.raw`(?<![\w./\\+-])`;
1507
1508
  var FILE_END = String.raw`(?![\w-]|\.\w)`;
1509
+ var ENTITY_SPELLINGS = HTML_ATTRIBUTE_ENTITIES;
1510
+ function referenceSpellings(char) {
1511
+ const spellings = [escapeRegExp(char)];
1512
+ const encoded = encodeUrlPath(char);
1513
+ if (encoded !== char)
1514
+ spellings.push(encoded.replace(/[A-F]/g, (hex) => `[${hex}${hex.toLowerCase()}]`));
1515
+ if (ENTITY_SPELLINGS[char]) spellings.push(...ENTITY_SPELLINGS[char]);
1516
+ if (["'", '"', "`", "$", " ", "(", ")"].includes(char)) {
1517
+ spellings.push(escapeRegExp(`&#92;${char}`), escapeRegExp(`&#92;${escapeHtmlReference(char)}`));
1518
+ }
1519
+ const scriptSpelling = escapeQuotedReference(char, '"');
1520
+ if (scriptSpelling !== char) spellings.push(escapeRegExp(scriptSpelling));
1521
+ if (["'", "`", "$", " ", "(", ")"].includes(char)) spellings.push(escapeRegExp(`\\${char}`));
1522
+ if (/[\t\n\f\r =<>`]/.test(char)) spellings.push(`&#${char.codePointAt(0)};`);
1523
+ if (char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) {
1524
+ spellings.push(escapeRegExp(`\\${char.charCodeAt(0).toString(16)} `));
1525
+ }
1526
+ return spellings.length === 1 ? spellings[0] : `(?:${spellings.join("|")})`;
1527
+ }
1528
+ function escapeHtmlReference(path) {
1529
+ return path.replace(/[&"']/g, (char) => ENTITY_SPELLINGS[char][0]);
1530
+ }
1531
+ function escapeQuotedReference(path, quote) {
1532
+ const escaped = JSON.stringify(path).slice(1, -1);
1533
+ return quote === '"' ? escaped : escaped.replace(new RegExp(escapeRegExp(quote), "g"), `\\${quote}`);
1534
+ }
1535
+ function escapeRawUrlPath(path) {
1536
+ const encodeSpaces = path.startsWith(" ") || path.endsWith(" ");
1537
+ return path.replace(/[\s\S]/g, (char) => {
1538
+ if (char.charCodeAt(0) < 32 || ":?%#\\".includes(char) || encodeSpaces && char === " ") {
1539
+ return encodeUrlPath(char);
1540
+ }
1541
+ return char;
1542
+ });
1543
+ }
1544
+ function spellLike(reference, path, oldPath, context) {
1545
+ const { syntax, quote, url, encodeAll } = context;
1546
+ if (url && (encodeAll || reference !== oldPath && /%[0-9A-Fa-f]{2}/.test(reference)))
1547
+ return encodeUrlPath(path);
1548
+ switch (syntax) {
1549
+ case "html": {
1550
+ const escaped = escapeHtmlReference(url ? escapeRawUrlPath(path) : path);
1551
+ return ["'", '"'].includes(quote) ? escaped : escaped.replace(/[\t\n\f\r =<>`]/g, (char) => `&#${char.codePointAt(0)};`);
1552
+ }
1553
+ case "script": {
1554
+ if (!["'", '"', "`"].includes(quote)) return path;
1555
+ const escaped = escapeQuotedReference(path, quote);
1556
+ return quote === "`" ? escaped.replace(/\$\{/g, "\\${") : escaped;
1557
+ }
1558
+ case "css":
1559
+ return url ? encodeUrlPath(path) : path;
1560
+ case "plain":
1561
+ return path;
1562
+ }
1563
+ }
1564
+ function referenceSyntax(file) {
1565
+ if (/\.html$/i.test(file)) return "html";
1566
+ if (/\.css$/i.test(file)) return "css";
1567
+ if (/\.(?:[cm]?js|jsx|tsx?|json)$/i.test(file)) return "script";
1568
+ return "plain";
1569
+ }
1570
+ function decodeQuotedReference(text2, syntax) {
1571
+ if (syntax === "css") return decodeCssEscapes(text2);
1572
+ if (syntax === "script") {
1573
+ const controls = { b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
1574
+ return text2.replace(
1575
+ /\\(?:u([0-9a-fA-F]{4})|([bfnrt"'`$\\/]))/g,
1576
+ (_escaped, hex, char) => {
1577
+ if (hex !== void 0) return String.fromCharCode(Number.parseInt(hex, 16));
1578
+ return controls[char] ?? char;
1579
+ }
1580
+ );
1581
+ }
1582
+ return text2;
1583
+ }
1508
1584
  function referencePattern(oldPath, isDirectory) {
1509
- const name = oldPath.split("/").map(escapeRegExp).join(SEPARATOR);
1585
+ const name = oldPath.split("/").map((segment) => Array.from(segment, referenceSpellings).join("")).join(SEPARATOR);
1510
1586
  const end = isDirectory ? `(?=${SEPARATOR})` : FILE_END;
1511
1587
  return new RegExp(
1512
1588
  String.raw`${REFERENCE_START}(?<lead>(?:\.{1,2}${SEPARATOR}|${SEPARATOR}){0,4})${name}${end}`,
1513
1589
  "g"
1514
1590
  );
1515
1591
  }
1592
+ function rewriteEntityAttributes(text2, rewrite) {
1593
+ const parts = [];
1594
+ let cursor = 0;
1595
+ for (const tag of scanHtmlOpeningTags(text2)) {
1596
+ for (const attr of tag.attributes) {
1597
+ if (attr.kind !== "value") continue;
1598
+ const decoded = decodeAuthoredAttribute(attr.value);
1599
+ if (decoded === attr.value) continue;
1600
+ let value = escapeHtmlReference(decoded);
1601
+ if (!attr.quote)
1602
+ value = value.replace(/[\t\n\f\r =<>`]/g, (char) => `&#${char.codePointAt(0)};`);
1603
+ const source = `<x ${attr.name}=${attr.quote}${value}${attr.quote}>`;
1604
+ const updated = rewrite(source);
1605
+ if (updated === source) continue;
1606
+ const replacement = scanHtmlOpeningTags(updated)[0].attributes[0];
1607
+ if (replacement.kind !== "value") throw new Error("Reference rewrite removed its attribute");
1608
+ parts.push(text2.slice(cursor, attr.valueStart), replacement.value);
1609
+ cursor = attr.valueEnd;
1610
+ }
1611
+ }
1612
+ parts.push(text2.slice(cursor));
1613
+ return parts.join("");
1614
+ }
1516
1615
  function referenceRewriter(oldPath, newPath, isDirectory, existing = []) {
1616
+ const oldIdentity = sep2 === "\\" ? oldPath.replace(/\\/g, "/") : oldPath;
1517
1617
  const pattern = referencePattern(oldPath, isDirectory);
1518
1618
  const around = /* @__PURE__ */ new Map();
1519
1619
  const afterLengths = /* @__PURE__ */ new Set();
@@ -1527,24 +1627,123 @@ function referenceRewriter(oldPath, newPath, isDirectory, existing = []) {
1527
1627
  before
1528
1628
  );
1529
1629
  afterLengths.add(after.length);
1530
- window = Math.max(window, path.length * 3);
1531
- }
1532
- }
1533
- const normalized = (text2) => text2.replace(/\\{0,2}[\\/]/g, "/");
1534
- return (text2) => text2.replace(pattern, (...args) => {
1535
- const [match, offset] = [args[0], args.at(-3)];
1536
- const { lead } = args.at(-1);
1537
- const at = offset + lead.length;
1538
- const head = normalized(text2.slice(Math.max(0, at - window), at));
1539
- const tail = normalized(text2.slice(offset + match.length, offset + match.length + window));
1540
- const inLonger = [...afterLengths].some((after) => {
1541
- const befores = tail.length >= after && around.get(tail.slice(0, after));
1542
- return befores && [...befores].some(
1543
- ([length, set]) => head.length >= length && set.has(head.slice(head.length - length))
1630
+ window = Math.max(window, path.length * 9);
1631
+ }
1632
+ }
1633
+ const normalized = (text2, context, full = false) => {
1634
+ let value = text2;
1635
+ if (context.syntax === "html" || context.htmlAttribute) {
1636
+ value = decodeAuthoredAttribute(value);
1637
+ }
1638
+ value = decodeQuotedReference(value, context.syntax);
1639
+ if (context.url) {
1640
+ value = value.replace(/\\/g, "/");
1641
+ value = full ? decodedUrlPath(value) : decodeWellFormedEscapes(value);
1642
+ }
1643
+ return sep2 === "\\" ? value.replace(/\\/g, "/") : value;
1644
+ };
1645
+ return function rewriteText(text2, syntax = "html", normalizeEntities = true) {
1646
+ if (syntax === "html" && normalizeEntities) {
1647
+ text2 = rewriteEntityAttributes(text2, (source) => rewriteText(source, "html", false));
1648
+ }
1649
+ pattern.lastIndex = 0;
1650
+ if (!pattern.test(text2)) return text2;
1651
+ pattern.lastIndex = 0;
1652
+ const tags = syntax === "html" ? scanHtmlOpeningTags(text2) : [];
1653
+ const inlineRegions = tags.filter(
1654
+ (tag) => tag.closed && ["script", "style"].includes(tag.name) && tag.rawTextEnd !== null
1655
+ ).map((tag) => ({
1656
+ start: tag.end,
1657
+ end: tag.rawTextEnd,
1658
+ syntax: tag.name === "style" ? "css" : "script"
1659
+ }));
1660
+ const attributes = tags.flatMap(
1661
+ (tag) => tag.attributes.flatMap((attr) => attr.kind === "value" ? [attr] : [])
1662
+ );
1663
+ const cssContexts = syntax === "css" ? [{ start: 0, end: text2.length, htmlAttribute: false }] : [
1664
+ ...inlineRegions.filter((region) => region.syntax === "css").map((region) => ({ ...region, htmlAttribute: false })),
1665
+ ...attributes.filter((attr) => attr.name === "style").map((attr) => ({ ...attr, htmlAttribute: true }))
1666
+ ];
1667
+ const cssFields = (pattern2) => cssContexts.flatMap(
1668
+ (context) => [...text2.slice(context.start, context.end).matchAll(pattern2)].map((match) => {
1669
+ const prefix = /^(?:url\(\s*|@import\s+)/i.exec(match[0])[0];
1670
+ const rest = match[0].slice(prefix.length);
1671
+ const delimiter = context.htmlAttribute ? /^(?:["']|&quot;|&#39;|&apos;)/.exec(rest)?.[0] : /^["']/.exec(rest)?.[0];
1672
+ const start = context.start + match.index + prefix.length + (delimiter?.length ?? 0);
1673
+ return {
1674
+ start,
1675
+ end: context.start + match.index + match[0].length,
1676
+ syntax: "css",
1677
+ htmlAttribute: context.htmlAttribute,
1678
+ encodeAll: true
1679
+ };
1680
+ })
1681
+ );
1682
+ const urlRegions = [
1683
+ ...attributes.filter((attr) => ["src", "href", "poster", "xlink:href"].includes(attr.name)).map((attr) => ({
1684
+ start: attr.valueStart + (/^[\t\n\f\r ]*/.exec(attr.value)?.[0].length ?? 0),
1685
+ end: attr.valueEnd,
1686
+ syntax: "html",
1687
+ htmlAttribute: true,
1688
+ encodeAll: false
1689
+ })),
1690
+ ...attributes.filter((attr) => attr.name === "srcset").flatMap(
1691
+ (attr) => [...attr.value.matchAll(/(^|,)[\t\n\f\r ]*([^,\t\n\f\r ]+)/g)].map((match) => ({
1692
+ start: attr.valueStart + match.index + match[0].length - match[2].length,
1693
+ end: attr.valueStart + match.index + match[0].length,
1694
+ syntax: "html",
1695
+ htmlAttribute: true,
1696
+ encodeAll: true
1697
+ }))
1698
+ ),
1699
+ ...cssFields(
1700
+ /\burl\((?:\s*"(?:\\.|[^"\\])*"\s*|\s*'(?:\\.|[^'\\])*'\s*|(?:\\.|[^)\\])*)\)/gi
1701
+ ),
1702
+ ...cssFields(/@import\s+(?:"[^"]*"|'[^']*')/gi)
1703
+ ].sort((a, b) => a.start - b.start);
1704
+ let urlIndex = 0;
1705
+ let regionIndex = 0;
1706
+ return text2.replace(pattern, (...args) => {
1707
+ const [match, offset] = [args[0], args.at(-3)];
1708
+ const { lead } = args.at(-1);
1709
+ while (inlineRegions[regionIndex] && inlineRegions[regionIndex].end <= offset) regionIndex++;
1710
+ const region = inlineRegions[regionIndex];
1711
+ const matchSyntax = region && offset >= region.start ? region.syntax : syntax;
1712
+ while (urlRegions[urlIndex] && urlRegions[urlIndex].end <= offset) urlIndex++;
1713
+ const urlRegion = urlRegions[urlIndex];
1714
+ const url = matchSyntax !== "script" && !!urlRegion && offset >= urlRegion.start;
1715
+ const context = {
1716
+ syntax: url ? urlRegion.syntax : matchSyntax,
1717
+ url,
1718
+ htmlAttribute: url && urlRegion.htmlAttribute,
1719
+ encodeAll: url && urlRegion.encodeAll,
1720
+ quote: text2[offset - 1] ?? ""
1721
+ };
1722
+ if (url && offset !== urlRegion.start) return match;
1723
+ const reference = match.slice(lead.length);
1724
+ if (normalized(reference, context, true) !== oldIdentity) return match;
1725
+ if (isDirectory && !normalized(
1726
+ text2.slice(offset + match.length, offset + match.length + 4),
1727
+ context
1728
+ ).startsWith("/"))
1729
+ return match;
1730
+ const at = offset + lead.length;
1731
+ const head = normalized(text2.slice(Math.max(0, at - window), at), context);
1732
+ const tail = normalized(
1733
+ text2.slice(offset + match.length, offset + match.length + window),
1734
+ context,
1735
+ context.url
1544
1736
  );
1737
+ const inLonger = [...afterLengths].some((after) => {
1738
+ const befores = tail.length >= after && around.get(tail.slice(0, after));
1739
+ return befores && [...befores].some(
1740
+ ([length, set]) => head.length >= length && set.has(head.slice(head.length - length))
1741
+ );
1742
+ });
1743
+ if (inLonger) return match;
1744
+ return `${lead}${spellLike(reference, newPath, oldPath, context)}`;
1545
1745
  });
1546
- return inLonger ? match : `${lead}${newPath}`;
1547
- });
1746
+ };
1548
1747
  }
1549
1748
  function occurrences(text2, part) {
1550
1749
  const at = [];
@@ -1577,7 +1776,7 @@ function updateReferences(projectDir, oldPath, newPath, isDirectory) {
1577
1776
  if (!isSafePath(projectDir, file) || isPrivateProjectFile(projectDir, file)) continue;
1578
1777
  const content = readableText(file);
1579
1778
  if (content === null) continue;
1580
- const updated = rewrite(content);
1779
+ const updated = rewrite(content, referenceSyntax(file));
1581
1780
  if (updated !== content) {
1582
1781
  replaceFileAtomically2(file, updated, statSync3(file).mode);
1583
1782
  updatedCount++;
@@ -3454,10 +3653,11 @@ function registerFileRoutes(api, adapter) {
3454
3653
  const res = await resolveProjectFile(c, adapter, { mustExist: true });
3455
3654
  if ("error" in res) return res.error;
3456
3655
  const body = await c.req.json();
3457
- if (!body.newPath || body.newPath.includes("\0")) {
3656
+ const newPath = body !== null && typeof body === "object" && "newPath" in body ? body.newPath : void 0;
3657
+ if (typeof newPath !== "string" || !newPath || newPath.includes("\0") || /[\uD800-\uDFFF]/u.test(newPath)) {
3458
3658
  return c.json({ error: "newPath required" }, 400);
3459
3659
  }
3460
- const newAbs = resolveWithinProject(res.project.dir, body.newPath);
3660
+ const newAbs = resolveWithinProject(res.project.dir, newPath);
3461
3661
  if (!newAbs) {
3462
3662
  return c.json({ error: "forbidden" }, 403);
3463
3663
  }
@@ -3467,8 +3667,8 @@ function registerFileRoutes(api, adapter) {
3467
3667
  const isDirectory = statSync3(res.absPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
3468
3668
  ensureDir(res.project.dir, newAbs);
3469
3669
  renameSync2(res.absPath, newAbs);
3470
- const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath, isDirectory);
3471
- return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
3670
+ const updatedFiles = updateReferences(res.project.dir, res.filePath, newPath, isDirectory);
3671
+ return c.json({ ok: true, path: newPath, updatedReferences: updatedFiles });
3472
3672
  });
3473
3673
  api.post("/projects/:id/duplicate-file", async (c) => {
3474
3674
  const project = await adapter.resolveProject(c.req.param("id"));
@@ -4339,10 +4539,25 @@ function registerLintRoutes(api, adapter) {
4339
4539
  // src/routes/render.ts
4340
4540
  import { streamSSE } from "hono/streaming";
4341
4541
  import { existsSync as existsSync6, lstatSync as lstatSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
4342
- import { join as join10 } from "path";
4542
+ import { dirname as dirname5, join as join10 } from "path";
4343
4543
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4344
4544
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
4345
4545
  var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
4546
+ var RENDER_MIME = {
4547
+ ".mp4": "video/mp4",
4548
+ ".webm": "video/webm",
4549
+ ".mov": "video/quicktime"
4550
+ };
4551
+ var RENDER_EXTENSIONS = Object.keys(RENDER_MIME);
4552
+ var RENDER_ARTIFACT_EXTENSIONS = [...RENDER_EXTENSIONS, ".meta.json"];
4553
+ function resolveRenderArtifacts(directories, jobId) {
4554
+ const artifacts = [...new Set(directories)].filter(existsSync6).flatMap(
4555
+ (directory) => RENDER_ARTIFACT_EXTENSIONS.map(
4556
+ (extension) => resolveWithinProject(directory, `${jobId}${extension}`)
4557
+ )
4558
+ );
4559
+ return artifacts.every((path) => path !== null) ? artifacts : null;
4560
+ }
4346
4561
  function contentDispositionHeader(disposition, filename) {
4347
4562
  const fallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
4348
4563
  const encoded = encodeURIComponent(filename).replace(
@@ -4460,12 +4675,6 @@ function registerRenderRoutes(api, adapter) {
4460
4675
  }
4461
4676
  return c.json({ status: job.status });
4462
4677
  });
4463
- const RENDER_MIME = {
4464
- ".mp4": "video/mp4",
4465
- ".webm": "video/webm",
4466
- ".mov": "video/quicktime"
4467
- };
4468
- const RENDER_EXTENSIONS = Object.keys(RENDER_MIME);
4469
4678
  const isPlainFile = (path) => {
4470
4679
  try {
4471
4680
  return lstatSync3(path).isFile();
@@ -4511,17 +4720,22 @@ function registerRenderRoutes(api, adapter) {
4511
4720
  }
4512
4721
  });
4513
4722
  });
4514
- api.delete("/render/:jobId", (c) => {
4723
+ async function renderDirectories(jobId) {
4724
+ const outputPath = renderJobs.get(jobId)?.outputPath;
4725
+ if (outputPath) return [dirname5(outputPath)];
4726
+ const projects = await adapter.listProjects();
4727
+ return projects.map((project) => adapter.rendersDir(project));
4728
+ }
4729
+ api.delete("/render/:jobId", async (c) => {
4515
4730
  const { jobId } = c.req.param();
4516
- for (const [, state] of renderJobs) {
4517
- if (state.id === jobId && state.outputPath) {
4518
- const dir = state.outputPath.replace(/\/[^/]+$/, "");
4519
- for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
4520
- const fp = join10(dir, `${jobId}${ext}`);
4521
- if (existsSync6(fp)) unlinkSync3(fp);
4522
- }
4523
- break;
4524
- }
4731
+ if (jobId.includes("/") || jobId.includes("\\") || jobId.includes("\0")) {
4732
+ return c.json({ error: "forbidden" }, 403);
4733
+ }
4734
+ const directories = await renderDirectories(jobId);
4735
+ const artifacts = resolveRenderArtifacts(directories, jobId);
4736
+ if (!artifacts) return c.json({ error: "forbidden" }, 403);
4737
+ for (const path of artifacts) {
4738
+ if (existsSync6(path)) unlinkSync3(path);
4525
4739
  }
4526
4740
  renderJobs.delete(jobId);
4527
4741
  return c.json({ deleted: true });
@@ -4746,12 +4960,7 @@ async function generateThumbnail(path, signal) {
4746
4960
  async function resolveSource(adapter, id, url) {
4747
4961
  const project = await adapter.resolveProject(id);
4748
4962
  if (!project) throw new HTTPException(404);
4749
- let relative5;
4750
- try {
4751
- relative5 = requestSubPath(url, "projects/:id/image-thumbnail");
4752
- } catch {
4753
- throw new HTTPException(400);
4754
- }
4963
+ const relative5 = requestSubPath(url, "projects/:id/image-thumbnail");
4755
4964
  if (!/\.jpe?g$/i.test(relative5)) throw new HTTPException(415);
4756
4965
  const path = pinWithinProject(project.dir, relative5);
4757
4966
  if (!path) throw new HTTPException(404);
@@ -5065,7 +5274,7 @@ function registerWaveformRoutes(api, adapter) {
5065
5274
  // src/routes/freezeFrame.ts
5066
5275
  import { execFile } from "child_process";
5067
5276
  import { readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync8 } from "fs";
5068
- import { basename as basename2, dirname as dirname5, join as join13, relative as relative4, sep as sep2 } from "path";
5277
+ import { basename as basename2, dirname as dirname6, join as join13, relative as relative4, sep as sep3 } from "path";
5069
5278
  import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries";
5070
5279
  import {
5071
5280
  atomicTempPath,
@@ -5312,14 +5521,14 @@ async function extractThenPublishWhole(extract, mediaPath, mediaTime, imagePath)
5312
5521
  }
5313
5522
  }
5314
5523
  async function extractStill(projectDir, absPath, source, playhead, tools) {
5315
- const fileDir = dirname5(absPath);
5524
+ const fileDir = dirname6(absPath);
5316
5525
  const mediaPath = pinWithinProject(projectDir, relative4(projectDir, join13(fileDir, source.src)));
5317
5526
  if (!mediaPath) return { error: `forbidden media path: ${source.src}`, status: 403 };
5318
5527
  const freezeDir = join13(projectDir, ...FREEZE_DIR);
5319
5528
  const fileName = freezeStillFileName(source.id, playhead, tools.stillToken());
5320
5529
  mkdirWithinProject(projectDir, freezeDir);
5321
5530
  const imagePath = pinWithinProject(projectDir, join13(...FREEZE_DIR, fileName));
5322
- if (!imagePath || dirname5(imagePath) !== freezeDir) {
5531
+ if (!imagePath || dirname6(imagePath) !== freezeDir) {
5323
5532
  return { error: `forbidden freeze path: ${fileName}`, status: 403 };
5324
5533
  }
5325
5534
  const failed = await extractThenPublishWhole(
@@ -5329,7 +5538,7 @@ async function extractStill(projectDir, absPath, source, playhead, tools) {
5329
5538
  imagePath
5330
5539
  );
5331
5540
  if (failed) return failed;
5332
- const depth = relative4(projectDir, fileDir).split(sep2).filter(Boolean).length;
5541
+ const depth = relative4(projectDir, fileDir).split(sep3).filter(Boolean).length;
5333
5542
  const stillPath = `${FREEZE_DIR.join("/")}/${fileName}`;
5334
5543
  return { imageSrc: `${"../".repeat(depth)}${stillPath}`, stillPath, imagePath };
5335
5544
  }
@@ -5883,7 +6092,7 @@ function registerSelectionRoutes(api, adapter) {
5883
6092
  // src/routes/media.ts
5884
6093
  import { streamSSE as streamSSE2 } from "hono/streaming";
5885
6094
  import { existsSync as existsSync10 } from "fs";
5886
- import { basename as basename3, dirname as dirname6, extname, join as join15, posix } from "path";
6095
+ import { basename as basename3, dirname as dirname7, extname, join as join15, posix } from "path";
5887
6096
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5888
6097
  ".mp4",
5889
6098
  ".mov",
@@ -5906,9 +6115,6 @@ function isVideoPath(path) {
5906
6115
  function isImagePath(path) {
5907
6116
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
5908
6117
  }
5909
- function normalizeProjectAssetPath(path) {
5910
- return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
5911
- }
5912
6118
  function containsNullByte(path) {
5913
6119
  return path.includes("\0");
5914
6120
  }
@@ -5973,7 +6179,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
5973
6179
  api.get("/projects/:id/media/metadata", async (c) => {
5974
6180
  const project = await adapter.resolveProject(c.req.param("id"));
5975
6181
  if (!project) return c.json({ error: "not found" }, 404);
5976
- const assetPath = normalizeProjectAssetPath(c.req.query("path") ?? "");
6182
+ const assetPath = requestedProjectPath(project.dir, c.req.query("path") ?? "");
5977
6183
  if (!assetPath) return c.json({ error: "path required" }, 400);
5978
6184
  if (containsNullByte(assetPath)) return c.json({ error: "forbidden" }, 403);
5979
6185
  if (/^(?:https?:|data:|blob:)/i.test(assetPath)) {
@@ -5995,7 +6201,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
5995
6201
  const project = await adapter.resolveProject(c.req.param("id"));
5996
6202
  if (!project) return c.json({ error: "not found" }, 404);
5997
6203
  const body = await c.req.json().catch(() => ({}));
5998
- const inputAssetPath = body.inputPath ? normalizeProjectAssetPath(body.inputPath) : "";
6204
+ const inputAssetPath = body.inputPath ? requestedProjectPath(project.dir, body.inputPath) : "";
5999
6205
  if (!inputAssetPath) return c.json({ error: "inputPath required" }, 400);
6000
6206
  if (containsNullByte(inputAssetPath)) return c.json({ error: "forbidden" }, 403);
6001
6207
  if (/^(?:https?:|data:|blob:)/i.test(inputAssetPath)) {
@@ -6009,7 +6215,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
6009
6215
  if (!inputIsVideo && !inputIsImage) {
6010
6216
  return c.json({ error: "background removal supports video or image assets only" }, 400);
6011
6217
  }
6012
- const requestedOutput = body.outputPath ? posix.normalize(normalizeProjectAssetPath(body.outputPath)) : "";
6218
+ const requestedOutput = body.outputPath ? posix.normalize(requestedProjectPath(project.dir, body.outputPath)) : "";
6013
6219
  if (requestedOutput && containsNullByte(requestedOutput)) {
6014
6220
  return c.json({ error: "forbidden" }, 403);
6015
6221
  }
@@ -6039,8 +6245,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
6039
6245
  return c.json({ error: "forbidden" }, 403);
6040
6246
  }
6041
6247
  }
6042
- mkdirWithinProject(project.dir, dirname6(outputPath));
6043
- if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
6248
+ mkdirWithinProject(project.dir, dirname7(outputPath));
6249
+ if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname7(backgroundOutputPath));
6044
6250
  const jobId = makeJobId(project.id, mediaJobs);
6045
6251
  const state = adapter.startBackgroundRemoval({
6046
6252
  project,
@@ -6483,6 +6689,7 @@ export {
6483
6689
  createStudioManualEditsRenderBodyScript,
6484
6690
  createStudioMotionRenderBodyScript,
6485
6691
  createStudioPositionSeekReapplyScript,
6692
+ decodeWellFormedEscapes,
6486
6693
  dedupeClonedCompositionId,
6487
6694
  duplicateElementInHtml,
6488
6695
  fileContentVersion,