@hyperframes/studio-server 0.8.30 → 0.8.32

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
@@ -364,12 +364,12 @@ import {
364
364
  writeSync,
365
365
  mkdirSync as mkdirSync3,
366
366
  unlinkSync as unlinkSync2,
367
- rmSync as rmSync2,
367
+ rmSync as rmSync3,
368
368
  statSync as statSync2,
369
- renameSync,
369
+ renameSync as renameSync2,
370
370
  readdirSync as readdirSync4
371
371
  } from "fs";
372
- import { resolve as resolve3, dirname as dirname2, join as join6 } from "path";
372
+ import { resolve as resolve3, dirname as dirname3, join as join6 } from "path";
373
373
 
374
374
  // src/helpers/mime.ts
375
375
  var MIME_TYPES = {
@@ -419,8 +419,17 @@ function isAudioFile(name) {
419
419
 
420
420
  // src/helpers/waveform.ts
421
421
  import { spawn } from "child_process";
422
- import { existsSync as existsSync2, writeFileSync, mkdirSync, statSync } from "fs";
423
- import { join as join3 } from "path";
422
+ import {
423
+ existsSync as existsSync2,
424
+ writeFileSync,
425
+ mkdirSync,
426
+ lstatSync as lstatSync2,
427
+ mkdtempSync,
428
+ renameSync,
429
+ rmSync,
430
+ statSync
431
+ } from "fs";
432
+ import { dirname, join as join3 } from "path";
424
433
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
425
434
  var SAMPLE_RATE = 4e3;
426
435
  var PEAK_COUNT = 4e3;
@@ -492,15 +501,39 @@ async function generateWaveformCache(projectDir, assetPath) {
492
501
  const stats = statSync(audioPath);
493
502
  const cacheDir = join3(projectDir, ".waveform-cache");
494
503
  const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
495
- if (existsSync2(cachePath)) return;
504
+ if (isWaveformCacheDirectory(cacheDir) && existsSync2(cachePath)) return;
496
505
  const peaks = await decodeAudioPeaks(audioPath);
497
- mkdirSync(cacheDir, { recursive: true });
498
- writeFileSync(cachePath, JSON.stringify(peaks));
506
+ writeWaveformCache(cachePath, peaks);
507
+ }
508
+ function isWaveformCacheDirectory(cacheDir) {
509
+ return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
510
+ }
511
+ function writeWaveformCache(cachePath, peaks) {
512
+ const cacheDir = dirname(cachePath);
513
+ try {
514
+ mkdirSync(cacheDir, { mode: 448 });
515
+ } catch (error) {
516
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
517
+ }
518
+ if (!isWaveformCacheDirectory(cacheDir)) {
519
+ throw new Error("Waveform cache must be a directory, not a symlink");
520
+ }
521
+ const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
522
+ try {
523
+ const stagingPath = join3(stagingDir, "peaks.json");
524
+ writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
525
+ renameSync(stagingPath, cachePath);
526
+ } finally {
527
+ try {
528
+ rmSync(stagingDir, { recursive: true, force: true });
529
+ } catch {
530
+ }
531
+ }
499
532
  }
500
533
 
501
534
  // src/helpers/mediaValidation.ts
502
535
  import { spawnSync } from "child_process";
503
- import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
536
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
504
537
  import { tmpdir } from "os";
505
538
  import { basename, join as join4 } from "path";
506
539
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
@@ -539,13 +572,13 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
539
572
  }
540
573
  }
541
574
  function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
542
- const tempDir = mkdtempSync(join4(tmpdir(), "hyperframes-upload-"));
575
+ const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
543
576
  const tempPath = join4(tempDir, basename(fileName));
544
577
  try {
545
578
  writeFileSync2(tempPath, buffer);
546
579
  return validateUploadedMedia(tempPath, runner);
547
580
  } finally {
548
- rmSync(tempDir, { recursive: true, force: true });
581
+ rmSync2(tempDir, { recursive: true, force: true });
549
582
  }
550
583
  }
551
584
 
@@ -687,7 +720,7 @@ import { parseHTML as parseHTML2 } from "linkedom";
687
720
  // src/helpers/compositionInsertion.ts
688
721
  import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
689
722
  import { randomUUID as randomUUID2 } from "crypto";
690
- import { dirname, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
723
+ import { dirname as dirname2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
691
724
  import { parseHTML } from "linkedom";
692
725
  var CompositionInsertionError = class extends Error {
693
726
  constructor(message, status) {
@@ -742,7 +775,7 @@ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
742
775
  validateSourcePath(sourcePath);
743
776
  return canonicalProjectPath(
744
777
  projectDir,
745
- resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname(ownerAbs), sourcePath)))
778
+ resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname2(ownerAbs), sourcePath)))
746
779
  );
747
780
  }
748
781
  function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
@@ -815,7 +848,7 @@ function uniqueHostId(root, base) {
815
848
  return `${base}_${suffix}`;
816
849
  }
817
850
  function relativeSourcePath(targetAbs, sourceAbs) {
818
- return relative3(dirname(targetAbs), sourceAbs).split(sep2).join("/");
851
+ return relative3(dirname2(targetAbs), sourceAbs).split(sep2).join("/");
819
852
  }
820
853
  function insertCompositionIntoSource(input) {
821
854
  const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
@@ -1153,7 +1186,7 @@ async function parseMutationBody(c) {
1153
1186
  return { target: body.target, body };
1154
1187
  }
1155
1188
  function ensureDir(filePath) {
1156
- const dir = dirname2(filePath);
1189
+ const dir = dirname3(filePath);
1157
1190
  if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
1158
1191
  }
1159
1192
  function generateCopyPath(projectDir, originalPath) {
@@ -2285,12 +2318,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2285
2318
  if (!isSafePath(projectDir, destPath)) continue;
2286
2319
  let finalPath = destPath;
2287
2320
  let finalName = name;
2321
+ const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
2322
+ const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
2323
+ const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2324
+ const MAX_COPY_INDEX = 1e4;
2325
+ let n = 1;
2288
2326
  if (existsSync4(finalPath)) {
2289
- const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
2290
- const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
2291
- const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
2292
- let n = 2;
2293
- const MAX_COPY_INDEX = 1e4;
2327
+ n = 2;
2294
2328
  while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
2295
2329
  if (n >= MAX_COPY_INDEX) {
2296
2330
  skipped.push(name);
@@ -2306,7 +2340,30 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2306
2340
  invalid.push({ name: finalName, reason: validation.reason });
2307
2341
  continue;
2308
2342
  }
2309
- writeFileSync4(finalPath, buffer);
2343
+ let written = false;
2344
+ while (n < MAX_COPY_INDEX && isSafePath(projectDir, finalPath)) {
2345
+ try {
2346
+ const fd = openSync(finalPath, "wx");
2347
+ try {
2348
+ writeFileSync4(fd, buffer);
2349
+ } finally {
2350
+ closeSync(fd);
2351
+ }
2352
+ written = true;
2353
+ break;
2354
+ } catch (error) {
2355
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
2356
+ throw error;
2357
+ }
2358
+ n++;
2359
+ finalName = `${base} (${n})${ext}`;
2360
+ finalPath = resolve3(targetDir, finalName);
2361
+ }
2362
+ }
2363
+ if (!written) {
2364
+ if (n >= MAX_COPY_INDEX) skipped.push(name);
2365
+ continue;
2366
+ }
2310
2367
  const relativePath = subDir ? join6(subDir, finalName) : finalName;
2311
2368
  uploaded.push(relativePath);
2312
2369
  if (isAudioFile(finalName)) {
@@ -2457,7 +2514,7 @@ function registerFileRoutes(api, adapter) {
2457
2514
  const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
2458
2515
  if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
2459
2516
  if (stat.isDirectory()) {
2460
- rmSync2(res.absPath, { recursive: true });
2517
+ rmSync3(res.absPath, { recursive: true });
2461
2518
  } else {
2462
2519
  unlinkSync2(res.absPath);
2463
2520
  }
@@ -2953,7 +3010,7 @@ function registerFileRoutes(api, adapter) {
2953
3010
  return c.json({ error: "already exists" }, 409);
2954
3011
  }
2955
3012
  ensureDir(newAbs);
2956
- renameSync(res.absPath, newAbs);
3013
+ renameSync2(res.absPath, newAbs);
2957
3014
  const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
2958
3015
  return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
2959
3016
  });
@@ -3073,7 +3130,8 @@ function registerFileRoutes(api, adapter) {
3073
3130
  }
3074
3131
 
3075
3132
  // src/routes/preview.ts
3076
- import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3133
+ import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3134
+ import { Readable } from "stream";
3077
3135
  import { join as join8, resolve as resolve4 } from "path";
3078
3136
  import { createHash as createHash3 } from "crypto";
3079
3137
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
@@ -3714,29 +3772,39 @@ ${runtimeTag}`;
3714
3772
  }
3715
3773
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3716
3774
  }
3717
- const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
3718
- const totalSize = buffer.length;
3775
+ const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
3776
+ const totalSize = textBuffer ? textBuffer.length : statSync3(servedPath).size;
3777
+ const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
3778
+ // Node's web stream type and the DOM one do not overlap for tsc on
3779
+ // every platform's lib set; the double cast is the documented bridge.
3780
+ Readable.toWeb(
3781
+ createReadStream(servedPath, { start, end })
3782
+ )
3783
+ );
3719
3784
  const rangeHeader = c.req.header("Range");
3720
- if (rangeHeader) {
3721
- const match = /bytes=(\d+)-(\d*)/.exec(rangeHeader);
3722
- if (match) {
3723
- const start = parseInt(match[1], 10);
3724
- const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
3725
- const safeEnd = Math.min(end, totalSize - 1);
3726
- const chunkSize = safeEnd - start + 1;
3727
- return new Response(new Uint8Array(buffer.slice(start, safeEnd + 1)), {
3728
- status: 206,
3729
- headers: {
3730
- ...cacheHeaders,
3731
- "Content-Type": servedContentType,
3732
- "Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
3733
- "Accept-Ranges": "bytes",
3734
- "Content-Length": String(chunkSize)
3735
- }
3785
+ const match = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
3786
+ if (match) {
3787
+ const start = parseInt(match[1], 10);
3788
+ const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
3789
+ const safeEnd = Math.min(end, totalSize - 1);
3790
+ if (start > safeEnd) {
3791
+ return new Response(null, {
3792
+ status: 416,
3793
+ headers: { ...cacheHeaders, "Content-Range": `bytes */${totalSize}` }
3736
3794
  });
3737
3795
  }
3796
+ return new Response(bodyFor(start, safeEnd), {
3797
+ status: 206,
3798
+ headers: {
3799
+ ...cacheHeaders,
3800
+ "Content-Type": servedContentType,
3801
+ "Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
3802
+ "Accept-Ranges": "bytes",
3803
+ "Content-Length": String(safeEnd - start + 1)
3804
+ }
3805
+ });
3738
3806
  }
3739
- return new Response(new Uint8Array(buffer), {
3807
+ return new Response(totalSize > 0 ? bodyFor(0, totalSize - 1) : null, {
3740
3808
  headers: {
3741
3809
  ...cacheHeaders,
3742
3810
  "Content-Type": servedContentType,
@@ -4041,8 +4109,8 @@ import {
4041
4109
  mkdirSync as mkdirSync5,
4042
4110
  readFileSync as readFileSync11,
4043
4111
  readdirSync as readdirSync6,
4044
- renameSync as renameSync2,
4045
- rmSync as rmSync3,
4112
+ renameSync as renameSync3,
4113
+ rmSync as rmSync4,
4046
4114
  statSync as statSync5,
4047
4115
  unlinkSync as unlinkSync4,
4048
4116
  writeFileSync as writeFileSync6
@@ -4176,7 +4244,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4176
4244
  const retained = [];
4177
4245
  for (const file of files) {
4178
4246
  if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
4179
- rmSync3(file.path, { force: true });
4247
+ rmSync4(file.path, { force: true });
4180
4248
  } else {
4181
4249
  retained.push(file);
4182
4250
  }
@@ -4196,9 +4264,9 @@ function writeThumbnailAtomically(path, buffer) {
4196
4264
  const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
4197
4265
  try {
4198
4266
  writeFileSync6(temporaryPath, buffer, { flag: "wx" });
4199
- renameSync2(temporaryPath, path);
4267
+ renameSync3(temporaryPath, path);
4200
4268
  } finally {
4201
- rmSync3(temporaryPath, { force: true });
4269
+ rmSync4(temporaryPath, { force: true });
4202
4270
  }
4203
4271
  }
4204
4272
  function registerThumbnailRoutes(api, adapter) {
@@ -4334,7 +4402,7 @@ function registerThumbnailRoutes(api, adapter) {
4334
4402
  }
4335
4403
 
4336
4404
  // src/routes/waveform.ts
4337
- import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, statSync as statSync6 } from "fs";
4405
+ import { existsSync as existsSync9, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
4338
4406
  import { join as join12 } from "path";
4339
4407
  function registerWaveformRoutes(api, adapter) {
4340
4408
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4348,12 +4416,12 @@ function registerWaveformRoutes(api, adapter) {
4348
4416
  if (!stats) return c.json({ error: "file not found" }, 404);
4349
4417
  const cacheDir = join12(project.dir, ".waveform-cache");
4350
4418
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
4351
- if (existsSync9(cachePath)) {
4352
- try {
4419
+ try {
4420
+ if (isWaveformCacheDirectory(cacheDir) && existsSync9(cachePath)) {
4353
4421
  const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
4354
4422
  return c.json({ peaks: peaks2 });
4355
- } catch {
4356
4423
  }
4424
+ } catch {
4357
4425
  }
4358
4426
  let peaks;
4359
4427
  try {
@@ -4362,8 +4430,7 @@ function registerWaveformRoutes(api, adapter) {
4362
4430
  return c.json({ error: "failed to decode audio" }, 500);
4363
4431
  }
4364
4432
  try {
4365
- mkdirSync6(cacheDir, { recursive: true });
4366
- writeFileSync7(cachePath, JSON.stringify(peaks));
4433
+ writeWaveformCache(cachePath, peaks);
4367
4434
  } catch {
4368
4435
  }
4369
4436
  return c.json({ peaks });
@@ -4634,8 +4701,8 @@ function registerSelectionRoutes(api, adapter) {
4634
4701
 
4635
4702
  // src/routes/media.ts
4636
4703
  import { streamSSE as streamSSE2 } from "hono/streaming";
4637
- import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
4638
- import { basename as basename2, dirname as dirname3, extname as extname2, join as join13 } from "path";
4704
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
4705
+ import { basename as basename2, dirname as dirname4, extname as extname2, join as join13 } from "path";
4639
4706
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
4640
4707
  ".mp4",
4641
4708
  ".mov",
@@ -4779,8 +4846,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
4779
4846
  return c.json({ error: "forbidden" }, 403);
4780
4847
  }
4781
4848
  }
4782
- mkdirSync7(dirname3(outputPath), { recursive: true });
4783
- if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
4849
+ mkdirSync6(dirname4(outputPath), { recursive: true });
4850
+ if (backgroundOutputPath) mkdirSync6(dirname4(backgroundOutputPath), { recursive: true });
4784
4851
  const jobId = makeJobId(project.id, mediaJobs);
4785
4852
  const state = adapter.startBackgroundRemoval({
4786
4853
  project,