@hyperframes/studio-server 0.8.30 → 0.8.31

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) {
@@ -2457,7 +2490,7 @@ function registerFileRoutes(api, adapter) {
2457
2490
  const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
2458
2491
  if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
2459
2492
  if (stat.isDirectory()) {
2460
- rmSync2(res.absPath, { recursive: true });
2493
+ rmSync3(res.absPath, { recursive: true });
2461
2494
  } else {
2462
2495
  unlinkSync2(res.absPath);
2463
2496
  }
@@ -2953,7 +2986,7 @@ function registerFileRoutes(api, adapter) {
2953
2986
  return c.json({ error: "already exists" }, 409);
2954
2987
  }
2955
2988
  ensureDir(newAbs);
2956
- renameSync(res.absPath, newAbs);
2989
+ renameSync2(res.absPath, newAbs);
2957
2990
  const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
2958
2991
  return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
2959
2992
  });
@@ -3073,7 +3106,8 @@ function registerFileRoutes(api, adapter) {
3073
3106
  }
3074
3107
 
3075
3108
  // src/routes/preview.ts
3076
- import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3109
+ import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3110
+ import { Readable } from "stream";
3077
3111
  import { join as join8, resolve as resolve4 } from "path";
3078
3112
  import { createHash as createHash3 } from "crypto";
3079
3113
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
@@ -3714,29 +3748,39 @@ ${runtimeTag}`;
3714
3748
  }
3715
3749
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3716
3750
  }
3717
- const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
3718
- const totalSize = buffer.length;
3751
+ const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
3752
+ const totalSize = textBuffer ? textBuffer.length : statSync3(servedPath).size;
3753
+ const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
3754
+ // Node's web stream type and the DOM one do not overlap for tsc on
3755
+ // every platform's lib set; the double cast is the documented bridge.
3756
+ Readable.toWeb(
3757
+ createReadStream(servedPath, { start, end })
3758
+ )
3759
+ );
3719
3760
  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
- }
3761
+ const match = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
3762
+ if (match) {
3763
+ const start = parseInt(match[1], 10);
3764
+ const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
3765
+ const safeEnd = Math.min(end, totalSize - 1);
3766
+ if (start > safeEnd) {
3767
+ return new Response(null, {
3768
+ status: 416,
3769
+ headers: { ...cacheHeaders, "Content-Range": `bytes */${totalSize}` }
3736
3770
  });
3737
3771
  }
3772
+ return new Response(bodyFor(start, safeEnd), {
3773
+ status: 206,
3774
+ headers: {
3775
+ ...cacheHeaders,
3776
+ "Content-Type": servedContentType,
3777
+ "Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
3778
+ "Accept-Ranges": "bytes",
3779
+ "Content-Length": String(safeEnd - start + 1)
3780
+ }
3781
+ });
3738
3782
  }
3739
- return new Response(new Uint8Array(buffer), {
3783
+ return new Response(totalSize > 0 ? bodyFor(0, totalSize - 1) : null, {
3740
3784
  headers: {
3741
3785
  ...cacheHeaders,
3742
3786
  "Content-Type": servedContentType,
@@ -4041,8 +4085,8 @@ import {
4041
4085
  mkdirSync as mkdirSync5,
4042
4086
  readFileSync as readFileSync11,
4043
4087
  readdirSync as readdirSync6,
4044
- renameSync as renameSync2,
4045
- rmSync as rmSync3,
4088
+ renameSync as renameSync3,
4089
+ rmSync as rmSync4,
4046
4090
  statSync as statSync5,
4047
4091
  unlinkSync as unlinkSync4,
4048
4092
  writeFileSync as writeFileSync6
@@ -4176,7 +4220,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4176
4220
  const retained = [];
4177
4221
  for (const file of files) {
4178
4222
  if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
4179
- rmSync3(file.path, { force: true });
4223
+ rmSync4(file.path, { force: true });
4180
4224
  } else {
4181
4225
  retained.push(file);
4182
4226
  }
@@ -4196,9 +4240,9 @@ function writeThumbnailAtomically(path, buffer) {
4196
4240
  const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
4197
4241
  try {
4198
4242
  writeFileSync6(temporaryPath, buffer, { flag: "wx" });
4199
- renameSync2(temporaryPath, path);
4243
+ renameSync3(temporaryPath, path);
4200
4244
  } finally {
4201
- rmSync3(temporaryPath, { force: true });
4245
+ rmSync4(temporaryPath, { force: true });
4202
4246
  }
4203
4247
  }
4204
4248
  function registerThumbnailRoutes(api, adapter) {
@@ -4334,7 +4378,7 @@ function registerThumbnailRoutes(api, adapter) {
4334
4378
  }
4335
4379
 
4336
4380
  // src/routes/waveform.ts
4337
- import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, statSync as statSync6 } from "fs";
4381
+ import { existsSync as existsSync9, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
4338
4382
  import { join as join12 } from "path";
4339
4383
  function registerWaveformRoutes(api, adapter) {
4340
4384
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4348,12 +4392,12 @@ function registerWaveformRoutes(api, adapter) {
4348
4392
  if (!stats) return c.json({ error: "file not found" }, 404);
4349
4393
  const cacheDir = join12(project.dir, ".waveform-cache");
4350
4394
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
4351
- if (existsSync9(cachePath)) {
4352
- try {
4395
+ try {
4396
+ if (isWaveformCacheDirectory(cacheDir) && existsSync9(cachePath)) {
4353
4397
  const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
4354
4398
  return c.json({ peaks: peaks2 });
4355
- } catch {
4356
4399
  }
4400
+ } catch {
4357
4401
  }
4358
4402
  let peaks;
4359
4403
  try {
@@ -4362,8 +4406,7 @@ function registerWaveformRoutes(api, adapter) {
4362
4406
  return c.json({ error: "failed to decode audio" }, 500);
4363
4407
  }
4364
4408
  try {
4365
- mkdirSync6(cacheDir, { recursive: true });
4366
- writeFileSync7(cachePath, JSON.stringify(peaks));
4409
+ writeWaveformCache(cachePath, peaks);
4367
4410
  } catch {
4368
4411
  }
4369
4412
  return c.json({ peaks });
@@ -4634,8 +4677,8 @@ function registerSelectionRoutes(api, adapter) {
4634
4677
 
4635
4678
  // src/routes/media.ts
4636
4679
  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";
4680
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
4681
+ import { basename as basename2, dirname as dirname4, extname as extname2, join as join13 } from "path";
4639
4682
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
4640
4683
  ".mp4",
4641
4684
  ".mov",
@@ -4779,8 +4822,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
4779
4822
  return c.json({ error: "forbidden" }, 403);
4780
4823
  }
4781
4824
  }
4782
- mkdirSync7(dirname3(outputPath), { recursive: true });
4783
- if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
4825
+ mkdirSync6(dirname4(outputPath), { recursive: true });
4826
+ if (backgroundOutputPath) mkdirSync6(dirname4(backgroundOutputPath), { recursive: true });
4784
4827
  const jobId = makeJobId(project.id, mediaJobs);
4785
4828
  const state = adapter.startBackgroundRemoval({
4786
4829
  project,