@ttt-productions/media-processing-core 0.8.17 → 0.8.18

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.
Files changed (90) hide show
  1. package/dist/audio/audio-processor.d.ts.map +1 -1
  2. package/dist/audio/audio-processor.js +1 -2
  3. package/dist/audio/audio-processor.js.map +1 -1
  4. package/dist/audio/probe.d.ts.map +1 -1
  5. package/dist/audio/probe.js +0 -2
  6. package/dist/audio/probe.js.map +1 -1
  7. package/dist/duration/media-duration.d.ts +0 -11
  8. package/dist/duration/media-duration.d.ts.map +1 -1
  9. package/dist/duration/media-duration.js +1 -28
  10. package/dist/duration/media-duration.js.map +1 -1
  11. package/dist/image/formats.d.ts +0 -11
  12. package/dist/image/formats.d.ts.map +1 -1
  13. package/dist/image/formats.js +0 -10
  14. package/dist/image/formats.js.map +1 -1
  15. package/dist/image/image-processor.d.ts.map +1 -1
  16. package/dist/image/image-processor.js +1 -20
  17. package/dist/image/image-processor.js.map +1 -1
  18. package/dist/image/resize.d.ts +0 -14
  19. package/dist/image/resize.d.ts.map +1 -1
  20. package/dist/image/resize.js +0 -15
  21. package/dist/image/resize.js.map +1 -1
  22. package/dist/inspection/inspect-media.d.ts +0 -7
  23. package/dist/inspection/inspect-media.d.ts.map +1 -1
  24. package/dist/inspection/inspect-media.js +4 -64
  25. package/dist/inspection/inspect-media.js.map +1 -1
  26. package/dist/io/fs.d.ts.map +1 -1
  27. package/dist/io/fs.js +0 -1
  28. package/dist/io/fs.js.map +1 -1
  29. package/dist/io/local-input.d.ts +0 -20
  30. package/dist/io/local-input.d.ts.map +1 -1
  31. package/dist/io/local-input.js +0 -20
  32. package/dist/io/local-input.js.map +1 -1
  33. package/dist/io/types.d.ts +0 -3
  34. package/dist/io/types.d.ts.map +1 -1
  35. package/dist/io/types.js.map +1 -1
  36. package/dist/moderation/merge.d.ts.map +1 -1
  37. package/dist/moderation/merge.js +0 -1
  38. package/dist/moderation/merge.js.map +1 -1
  39. package/dist/moderation/types.d.ts +0 -2
  40. package/dist/moderation/types.d.ts.map +1 -1
  41. package/dist/moderation/types.js.map +1 -1
  42. package/dist/run-pipeline.d.ts +0 -17
  43. package/dist/run-pipeline.d.ts.map +1 -1
  44. package/dist/run-pipeline.js +2 -27
  45. package/dist/run-pipeline.js.map +1 -1
  46. package/dist/server/download-token.d.ts +0 -6
  47. package/dist/server/download-token.d.ts.map +1 -1
  48. package/dist/server/download-token.js +0 -10
  49. package/dist/server/download-token.js.map +1 -1
  50. package/dist/server/firebase-media-io.d.ts +1 -11
  51. package/dist/server/firebase-media-io.d.ts.map +1 -1
  52. package/dist/server/firebase-media-io.js +0 -5
  53. package/dist/server/firebase-media-io.js.map +1 -1
  54. package/dist/server/gcs-errors.d.ts +0 -8
  55. package/dist/server/gcs-errors.d.ts.map +1 -1
  56. package/dist/server/gcs-errors.js +0 -8
  57. package/dist/server/gcs-errors.js.map +1 -1
  58. package/dist/server/pending-media.d.ts +1 -32
  59. package/dist/server/pending-media.d.ts.map +1 -1
  60. package/dist/server/pending-media.js +1 -24
  61. package/dist/server/pending-media.js.map +1 -1
  62. package/dist/server/r2-retry.d.ts +0 -48
  63. package/dist/server/r2-retry.d.ts.map +1 -1
  64. package/dist/server/r2-retry.js +4 -90
  65. package/dist/server/r2-retry.js.map +1 -1
  66. package/dist/server/staged-object.d.ts +0 -44
  67. package/dist/server/staged-object.d.ts.map +1 -1
  68. package/dist/server/staged-object.js +0 -44
  69. package/dist/server/staged-object.js.map +1 -1
  70. package/dist/server/storage-ops.d.ts +2 -62
  71. package/dist/server/storage-ops.d.ts.map +1 -1
  72. package/dist/server/storage-ops.js +9 -88
  73. package/dist/server/storage-ops.js.map +1 -1
  74. package/dist/types.d.ts +0 -2
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js.map +1 -1
  77. package/dist/utils/safe-path.d.ts.map +1 -1
  78. package/dist/utils/safe-path.js +1 -2
  79. package/dist/utils/safe-path.js.map +1 -1
  80. package/dist/video/ffmpeg.d.ts +0 -22
  81. package/dist/video/ffmpeg.d.ts.map +1 -1
  82. package/dist/video/ffmpeg.js +6 -21
  83. package/dist/video/ffmpeg.js.map +1 -1
  84. package/dist/video/probe.d.ts.map +1 -1
  85. package/dist/video/probe.js +0 -1
  86. package/dist/video/probe.js.map +1 -1
  87. package/dist/video/video-processor.d.ts.map +1 -1
  88. package/dist/video/video-processor.js +2 -15
  89. package/dist/video/video-processor.js.map +1 -1
  90. package/package.json +2 -2
@@ -1,6 +1,5 @@
1
1
  import { type MediaInspectionResult, type MediaFormatId } from '@ttt-productions/media-schemas';
2
2
  import { type MeasureDurationFn } from '../duration/media-duration.js';
3
- /** Bump on any classification-rule change (rides every decision event). */
4
3
  export declare const INSPECTOR_VERSION = "mpc-inspect-2";
5
4
  type SignatureFamily = {
6
5
  family: 'image';
@@ -28,7 +27,6 @@ type SignatureFamily = {
28
27
  };
29
28
  export declare function detectSignatureFamily(b: Uint8Array): SignatureFamily;
30
29
  export interface InspectMediaDeps {
31
- /** Probe the local file; default shells to the hardened `ffprobe`. */
32
30
  probe?: (localPath: string, opts: {
33
31
  timeoutMs: number;
34
32
  signal?: AbortSignal;
@@ -39,15 +37,10 @@ export interface InspectMediaDeps {
39
37
  ok: false;
40
38
  reason: 'timeout' | 'truncated' | 'failed' | 'bad_json';
41
39
  }>;
42
- /** Decode-prove an image; default uses sharp metadata. Returns frame count
43
- * (pages) on success, null on decode failure. */
44
40
  decodeImage?: (localPath: string) => Promise<{
45
41
  frames: number;
46
42
  } | null>;
47
- /** Measure the file's length; default reads its packet timestamps
48
- * (`measureDurationSec`). */
49
43
  measureDuration?: MeasureDurationFn;
50
- /** Injected ffprobe version (default: cached `ffprobe -version` first line). */
51
44
  ffprobeVersion?: string;
52
45
  }
53
46
  export interface InspectMediaArgs {
@@ -1 +1 @@
1
- {"version":3,"file":"inspect-media.d.ts","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"AA8BA,OAAO,EAEL,KAAK,qBAAqB,EAG1B,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AA2DjD,KAAK,eAAe,GAChB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAuB1B,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,UAAU,GAAG,eAAe,CA4CpE;AAID,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,KAAK,CAAC,EAAE,CACN,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAC9C,OAAO,CACR;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAC3B;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAA;KAAE,CACzE,CAAC;IACF;sDACkD;IAClD,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxE;kCAC8B;IAC9B,eAAe,CAAC,EAAE,iBAAiB,CAAC;IACpC,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAqHD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAwJzF"}
1
+ {"version":3,"file":"inspect-media.d.ts","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,qBAAqB,EAG1B,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAqDjD,KAAK,eAAe,GAChB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAqB1B,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,UAAU,GAAG,eAAe,CAyCpE;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,CACN,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAC9C,OAAO,CACR;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAC3B;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAA;KAAE,CACzE,CAAC;IACF,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxE,eAAe,CAAC,EAAE,iBAAiB,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAiHD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuIzF"}
@@ -1,41 +1,12 @@
1
- // =============================================================================
2
- // Canonical media content inspection
3
- // =============================================================================
4
- //
5
- // The ONE server-side classification authority. Consumes a LOCAL file (the
6
- // pipeline's generation-pinned temp download), establishes what the bytes
7
- // actually contain, and returns the bounded `MediaInspectionResult` contract
8
- // from @ttt-productions/media-schemas. Everything downstream — origin/kind
9
- // validation, processing-spec selection, the persisted asset kind, and the
10
- // safety (PhotoDNA) routing — derives from this one result. Client MIME,
11
- // filename, extension, container brand, and payload-byte heuristics are never
12
- // consulted.
13
- //
14
- // Design:
15
- // - family selection by a bounded 64 KiB SIGNATURE read (header only —
16
- // semantic elements are NEVER searched for in arbitrary payload: payload
17
- // bytes can mimic container elements, so a flat scan misclassifies WebM
18
- // audio as video);
19
- // - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened
20
- // `ffprobe` stream table (libavformat's structural parsers), with timeout,
21
- // kill-on-abort, output cap, and strict schema validation;
22
- // - image-family proof by DECODER (sharp) — a signature or `ftyp` brand is a
23
- // candidate, not proof, and image-family ISO-BMFF (AVIF/HEIC) is resolved
24
- // BEFORE the ordinary "any timed video stream wins" rule (a valid AVIF
25
- // probes as a one-frame AV1 `video` stream);
26
- // - FAIL-CLOSED: anything unproven is `indeterminate` with the
27
- // `strict-video-fallback` safety plan — never audio, never publishable.
28
1
  import { open } from 'node:fs/promises';
29
2
  import { z } from 'zod';
30
3
  import { MediaInspectionResultSchema, } from '@ttt-productions/media-schemas';
31
4
  import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
32
5
  import { measureDurationSec } from '../duration/media-duration.js';
33
- /** Bump on any classification-rule change (rides every decision event). */
34
6
  export const INSPECTOR_VERSION = 'mpc-inspect-2';
35
7
  const SIGNATURE_READ_BYTES = 64 * 1024;
36
- const PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback
8
+ const PROBE_SIZE_BYTES = 4 * 1024 * 1024;
37
9
  const ANALYZE_DURATION_US = 10 * 1000 * 1000;
38
- // ---- ffprobe JSON boundary (strict; anything else is malformed) ----
39
10
  const ProbeStreamSchema = z
40
11
  .object({
41
12
  codec_type: z.string().max(32).optional(),
@@ -55,7 +26,6 @@ const ProbeJsonSchema = z
55
26
  streams: z.array(ProbeStreamSchema).max(256).optional(),
56
27
  })
57
28
  .loose();
58
- // ---- codec normalization (bounded — raw strings never escape) ----
59
29
  const CODEC_MAP = {
60
30
  h264: 'h264',
61
31
  hevc: 'hevc',
@@ -84,8 +54,6 @@ function normalizeCodec(name) {
84
54
  return 'amr';
85
55
  return 'other';
86
56
  }
87
- /** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via
88
- * decoder proof BEFORE the timed-video rule — see the AVIF note above. */
89
57
  const ISOBMFF_IMAGE_BRANDS = new Set([
90
58
  'avif',
91
59
  'avis',
@@ -109,7 +77,6 @@ function ascii(b, off, s) {
109
77
  export function detectSignatureFamily(b) {
110
78
  if (b.length < 12)
111
79
  return { family: 'unknown' };
112
- // images (single-format signatures)
113
80
  if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
114
81
  return { family: 'image', formatId: 'jpeg', container: 'jpeg' };
115
82
  if (b[0] === 0x89 && ascii(b, 1, 'PNG'))
@@ -124,7 +91,6 @@ export function detectSignatureFamily(b) {
124
91
  (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {
125
92
  return { family: 'image', formatId: 'tiff', container: 'tiff' };
126
93
  }
127
- // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).
128
94
  {
129
95
  const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))
130
96
  .toString('latin1')
@@ -136,7 +102,6 @@ export function detectSignatureFamily(b) {
136
102
  return { family: 'svg' };
137
103
  }
138
104
  }
139
- // containers
140
105
  if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
141
106
  return { family: 'ebml' };
142
107
  if (ascii(b, 4, 'ftyp')) {
@@ -189,7 +154,7 @@ async function defaultProbe(localPath, opts) {
189
154
  ], { timeoutMs: opts.timeoutMs, signal: opts.signal });
190
155
  }
191
156
  catch {
192
- return { ok: false, reason: 'failed' }; // spawn failure (ffprobe missing) — fail closed
157
+ return { ok: false, reason: 'failed' };
193
158
  }
194
159
  if (r.timedOut)
195
160
  return { ok: false, reason: 'timeout' };
@@ -208,7 +173,7 @@ async function defaultDecodeImage(localPath) {
208
173
  try {
209
174
  const sharp = (await import('sharp')).default;
210
175
  const meta = await sharp(localPath, {
211
- limitInputPixels: 268402689 /* sharp default ~16k x 16k */,
176
+ limitInputPixels: 268402689,
212
177
  }).metadata();
213
178
  if (!meta.width || !meta.height)
214
179
  return null;
@@ -218,7 +183,6 @@ async function defaultDecodeImage(localPath) {
218
183
  return null;
219
184
  }
220
185
  }
221
- // ---- result builders ----
222
186
  function baseResult(partial, ffprobeVersion) {
223
187
  const result = {
224
188
  inspectorVersion: INSPECTOR_VERSION,
@@ -255,7 +219,6 @@ export async function inspectMedia(args) {
255
219
  const probe = args.deps?.probe ?? defaultProbe;
256
220
  const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;
257
221
  const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());
258
- // 1) bounded signature read (header only)
259
222
  let head;
260
223
  try {
261
224
  const fh = await open(args.localPath, 'r');
@@ -272,14 +235,10 @@ export async function inspectMedia(args) {
272
235
  return indeterminate('unknown', 'read_failed', ffprobeVersion);
273
236
  }
274
237
  const sig = detectSignatureFamily(head);
275
- // 2) family routing
276
238
  switch (sig.family) {
277
239
  case 'unknown':
278
240
  return indeterminate('unknown', 'unrecognized_signature', ffprobeVersion);
279
241
  case 'svg':
280
- // Scriptable XML "image" — structurally recognized, deliberately NOT a
281
- // processable format: it can carry script and no sanitize/rasterize step
282
- // exists. Never classified by decoder capability alone.
283
242
  return baseResult({
284
243
  status: 'unsupported',
285
244
  container: 'svg',
@@ -288,8 +247,6 @@ export async function inspectMedia(args) {
288
247
  reasonCode: 'unsupported_format:svg',
289
248
  }, ffprobeVersion);
290
249
  case 'image': {
291
- // Single-format image signature: decoder proof required — a signature is
292
- // a candidate, not a classification.
293
250
  const decoded = await decodeImage(args.localPath);
294
251
  if (!decoded)
295
252
  return baseResult({
@@ -318,19 +275,14 @@ export async function inspectMedia(args) {
318
275
  }
319
276
  case 'isobmff':
320
277
  if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {
321
- // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid
322
- // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +
323
- // successful bounded decode + no ordinary timed A/V presentation.
324
278
  const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });
325
279
  const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;
326
280
  const streams = parsed?.success ? (parsed.data.streams ?? []) : null;
327
- const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true; // unknown ⇒ assume worst
281
+ const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true;
328
282
  const formatId = sig.brand.startsWith('avi') ? 'avif' : 'heic';
329
283
  const decoded = await decodeImage(args.localPath);
330
284
  if (!decoded || hasAudio) {
331
285
  if (formatId === 'heic' && !decoded && !hasAudio) {
332
- // Recognized HEIC-family that the runtime cannot decode: explicit
333
- // unsupported-image (launch policy) — NEVER relabeled video.
334
286
  return baseResult({
335
287
  status: 'unsupported',
336
288
  container: 'heif',
@@ -339,9 +291,6 @@ export async function inspectMedia(args) {
339
291
  reasonCode: 'unsupported_format:heic',
340
292
  }, ffprobeVersion);
341
293
  }
342
- // Undecodable/ambiguous image-brand file, or one carrying audio (a
343
- // real timed presentation wearing an image brand): fail toward the
344
- // stricter visual path.
345
294
  return indeterminate('heif', decoded ? 'image_brand_with_audio' : 'image_decode_failed', ffprobeVersion, formatId);
346
295
  }
347
296
  const frames = decoded.frames;
@@ -367,8 +316,6 @@ export async function inspectMedia(args) {
367
316
  case 'ogg':
368
317
  return classifyAvContainer(args, 'ogg', 'ogg', probe, timeoutMs, ffprobeVersion);
369
318
  case 'avi':
370
- // Real container ffprobe parses; no registry entry (no origin enables it
371
- // today) — the stream truth still classifies it, policy rejects by format.
372
319
  return classifyAvContainer(args, 'avi', undefined, probe, timeoutMs, ffprobeVersion);
373
320
  case 'wav':
374
321
  return classifyAvContainer(args, 'wav', 'wav', probe, timeoutMs, ffprobeVersion);
@@ -406,10 +353,6 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
406
353
  audioCodecs.add(normalizeCodec(s.codec_name));
407
354
  }
408
355
  else if (type === 'video') {
409
- // An attached picture (cover art) is NOT a timed presentation — but the
410
- // disposition flag is untrusted container data, so it is COUNTED and the
411
- // safety plan forces every such picture through the image hash path
412
- // before the audio guard may return clean.
413
356
  if (s.disposition?.attached_pic === 1) {
414
357
  counts.attachedPictures++;
415
358
  }
@@ -430,7 +373,6 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
430
373
  const measure = args.deps?.measureDuration ?? measureDurationSec;
431
374
  const durationSec = await measure(args.localPath, { signal: args.signal });
432
375
  const duration = durationSec !== undefined ? { durationSec } : {};
433
- // classification table (fail-closed)
434
376
  if (counts.timedVideo > 0) {
435
377
  return baseResult({
436
378
  status: 'definitive',
@@ -457,8 +399,6 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
457
399
  reasonCode: 'ok',
458
400
  }, ffprobeVersion);
459
401
  }
460
- // auxiliary-only, unknown stream types alongside audio, or nothing usable:
461
- // never audio — the strict visual fallback owns it.
462
402
  return baseResult({
463
403
  status: 'indeterminate',
464
404
  container,
@@ -1 +1 @@
1
- {"version":3,"file":"inspect-media.js","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"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","sourcesContent":["// =============================================================================\n// Canonical media content inspection\n// =============================================================================\n//\n// The ONE server-side classification authority. Consumes a LOCAL file (the\n// pipeline's generation-pinned temp download), establishes what the bytes\n// actually contain, and returns the bounded `MediaInspectionResult` contract\n// from @ttt-productions/media-schemas. Everything downstream — origin/kind\n// validation, processing-spec selection, the persisted asset kind, and the\n// safety (PhotoDNA) routing — derives from this one result. Client MIME,\n// filename, extension, container brand, and payload-byte heuristics are never\n// consulted.\n//\n// Design:\n// - family selection by a bounded 64 KiB SIGNATURE read (header only —\n// semantic elements are NEVER searched for in arbitrary payload: payload\n// bytes can mimic container elements, so a flat scan misclassifies WebM\n// audio as video);\n// - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened\n// `ffprobe` stream table (libavformat's structural parsers), with timeout,\n// kill-on-abort, output cap, and strict schema validation;\n// - image-family proof by DECODER (sharp) — a signature or `ftyp` brand is a\n// candidate, not proof, and image-family ISO-BMFF (AVIF/HEIC) is resolved\n// BEFORE the ordinary \"any timed video stream wins\" rule (a valid AVIF\n// probes as a one-frame AV1 `video` stream);\n// - FAIL-CLOSED: anything unproven is `indeterminate` with the\n// `strict-video-fallback` safety plan — never audio, never publishable.\n\nimport { open } from 'node:fs/promises';\nimport { z } from 'zod';\nimport {\n MediaInspectionResultSchema,\n type MediaInspectionResult,\n type MediaInspectionStreams,\n type NormalizedCodecId,\n type MediaFormatId,\n} from '@ttt-productions/media-schemas';\nimport { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';\nimport { measureDurationSec, type MeasureDurationFn } from '../duration/media-duration.js';\n\n/** Bump on any classification-rule change (rides every decision event). */\nexport const INSPECTOR_VERSION = 'mpc-inspect-2';\n\nconst SIGNATURE_READ_BYTES = 64 * 1024;\nconst PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback\nconst ANALYZE_DURATION_US = 10 * 1000 * 1000;\n\n// ---- ffprobe JSON boundary (strict; anything else is malformed) ----\n\nconst ProbeStreamSchema = z\n .object({\n codec_type: z.string().max(32).optional(),\n codec_name: z.string().max(64).optional(),\n disposition: z\n .object({ attached_pic: z.number().int().min(0).max(1).optional() })\n .loose()\n .optional(),\n })\n .loose();\n\nconst ProbeJsonSchema = z\n .object({\n format: z\n .object({ format_name: z.string().max(200).optional() })\n .loose()\n .optional(),\n streams: z.array(ProbeStreamSchema).max(256).optional(),\n })\n .loose();\n\n// ---- codec normalization (bounded — raw strings never escape) ----\n\nconst CODEC_MAP: Record<string, NormalizedCodecId> = {\n h264: 'h264',\n hevc: 'hevc',\n vp8: 'vp8',\n vp9: 'vp9',\n av1: 'av1',\n theora: 'theora',\n mpeg4: 'mpeg4',\n mjpeg: 'mjpeg',\n aac: 'aac',\n alac: 'alac',\n opus: 'opus',\n vorbis: 'vorbis',\n mp3: 'mp3',\n flac: 'flac',\n};\n\nfunction normalizeCodec(name: string | undefined): NormalizedCodecId {\n if (!name) return 'other';\n const n = name.toLowerCase();\n if (CODEC_MAP[n]) return CODEC_MAP[n];\n if (n.startsWith('pcm_')) return 'pcm';\n if (n.startsWith('amr')) return 'amr';\n return 'other';\n}\n\n// ---- signature families ----\n\ntype SignatureFamily =\n | { family: 'image'; formatId: MediaFormatId; container: string }\n | { family: 'svg' }\n | { family: 'ebml' }\n | { family: 'isobmff'; brand: string }\n | { family: 'ogg' }\n | { family: 'avi' }\n | { family: 'wav' }\n | { family: 'mp3' }\n | { family: 'flac' }\n | { family: 'unknown' };\n\n/** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via\n * decoder proof BEFORE the timed-video rule — see the AVIF note above. */\nconst ISOBMFF_IMAGE_BRANDS = new Set([\n 'avif',\n 'avis',\n 'heic',\n 'heix',\n 'heim',\n 'heis',\n 'hevc',\n 'hevx',\n 'mif1',\n 'msf1',\n]);\n\nfunction ascii(b: Uint8Array, off: number, s: string): boolean {\n if (off + s.length > b.length) return false;\n for (let i = 0; i < s.length; i++) if (b[off + i] !== s.charCodeAt(i)) return false;\n return true;\n}\n\nexport function detectSignatureFamily(b: Uint8Array): SignatureFamily {\n if (b.length < 12) return { family: 'unknown' };\n // images (single-format signatures)\n if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)\n return { family: 'image', formatId: 'jpeg', container: 'jpeg' };\n if (b[0] === 0x89 && ascii(b, 1, 'PNG'))\n return { family: 'image', formatId: 'png', container: 'png' };\n if (ascii(b, 0, 'GIF8')) return { family: 'image', formatId: 'gif', container: 'gif' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WEBP'))\n return { family: 'image', formatId: 'webp', container: 'webp' };\n if (b[0] === 0x42 && b[1] === 0x4d) return { family: 'image', formatId: 'bmp', container: 'bmp' };\n if (\n (b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) ||\n (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)\n ) {\n return { family: 'image', formatId: 'tiff', container: 'tiff' };\n }\n // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).\n {\n const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))\n .toString('latin1')\n .toLowerCase();\n if (\n prefix.includes('<svg') &&\n (prefix.trimStart().startsWith('<?xml') ||\n prefix.trimStart().startsWith('<svg') ||\n prefix.includes('<!doctype svg'))\n ) {\n return { family: 'svg' };\n }\n }\n // containers\n if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return { family: 'ebml' };\n if (ascii(b, 4, 'ftyp')) {\n const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();\n return { family: 'isobmff', brand };\n }\n if (ascii(b, 0, 'OggS')) return { family: 'ogg' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'AVI ')) return { family: 'avi' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WAVE')) return { family: 'wav' };\n if (ascii(b, 0, 'ID3')) return { family: 'mp3' };\n if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return { family: 'mp3' };\n if (ascii(b, 0, 'fLaC')) return { family: 'flac' };\n return { family: 'unknown' };\n}\n\n// ---- dependency seams (tests inject; production uses the real tools) ----\n\nexport interface InspectMediaDeps {\n /** Probe the local file; default shells to the hardened `ffprobe`. */\n probe?: (\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n ) => Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: 'timeout' | 'truncated' | 'failed' | 'bad_json' }\n >;\n /** Decode-prove an image; default uses sharp metadata. Returns frame count\n * (pages) on success, null on decode failure. */\n decodeImage?: (localPath: string) => Promise<{ frames: number } | null>;\n /** Measure the file's length; default reads its packet timestamps\n * (`measureDurationSec`). */\n measureDuration?: MeasureDurationFn;\n /** Injected ffprobe version (default: cached `ffprobe -version` first line). */\n ffprobeVersion?: string;\n}\n\nlet cachedFfprobeVersion: string | null | undefined;\n\nasync function getFfprobeVersion(): Promise<string | undefined> {\n if (cachedFfprobeVersion !== undefined) return cachedFfprobeVersion ?? undefined;\n try {\n const r = await runCmd('ffprobe', ['-version'], { timeoutMs: 5_000 });\n const first = (r.stdout || r.stderr).split(/\\r?\\n/)[0]?.trim() ?? '';\n cachedFfprobeVersion = first ? first.slice(0, 120) : null;\n } catch {\n cachedFfprobeVersion = null;\n }\n return cachedFfprobeVersion ?? undefined;\n}\n\nasync function defaultProbe(\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n): Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: 'timeout' | 'truncated' | 'failed' | 'bad_json' }\n> {\n let r;\n try {\n r = await runCmd(\n 'ffprobe',\n [\n '-v',\n 'error',\n '-print_format',\n 'json',\n '-show_format',\n '-show_streams',\n '-probesize',\n String(PROBE_SIZE_BYTES),\n '-analyzeduration',\n String(ANALYZE_DURATION_US),\n localPath,\n ],\n { timeoutMs: opts.timeoutMs, signal: opts.signal },\n );\n } catch {\n return { ok: false, reason: 'failed' }; // spawn failure (ffprobe missing) — fail closed\n }\n if (r.timedOut) return { ok: false, reason: 'timeout' };\n if (r.truncated) return { ok: false, reason: 'truncated' };\n if (r.code !== 0) return { ok: false, reason: 'failed' };\n try {\n return { ok: true, json: JSON.parse(r.stdout || '{}') };\n } catch {\n return { ok: false, reason: 'bad_json' };\n }\n}\n\nasync function defaultDecodeImage(localPath: string): Promise<{ frames: number } | null> {\n try {\n const sharp = (await import('sharp')).default;\n const meta = await sharp(localPath, {\n limitInputPixels: 268402689 /* sharp default ~16k x 16k */,\n }).metadata();\n if (!meta.width || !meta.height) return null;\n return { frames: Math.max(1, meta.pages ?? 1) };\n } catch {\n return null;\n }\n}\n\n// ---- result builders ----\n\nfunction baseResult(\n partial: Omit<MediaInspectionResult, 'inspectorVersion' | 'streams' | 'codecs'> &\n Partial<Pick<MediaInspectionResult, 'streams' | 'codecs'>>,\n ffprobeVersion: string | undefined,\n): MediaInspectionResult {\n const result: MediaInspectionResult = {\n inspectorVersion: INSPECTOR_VERSION,\n ...(ffprobeVersion ? { ffprobeVersion } : {}),\n streams: partial.streams ?? {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: 0,\n auxiliary: 0,\n },\n codecs: partial.codecs ?? { audio: [], video: [] },\n status: partial.status,\n container: partial.container,\n safetyPlan: partial.safetyPlan,\n reasonCode: partial.reasonCode,\n ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),\n ...(partial.formatId ? { formatId: partial.formatId } : {}),\n ...(partial.durationSec !== undefined ? { durationSec: partial.durationSec } : {}),\n };\n return MediaInspectionResultSchema.parse(result);\n}\n\nfunction indeterminate(\n container: string,\n reasonCode: string,\n ffprobeVersion: string | undefined,\n formatId?: MediaFormatId,\n): MediaInspectionResult {\n return baseResult(\n {\n status: 'indeterminate',\n container,\n safetyPlan: 'strict-video-fallback',\n reasonCode,\n ...(formatId ? { formatId } : {}),\n },\n ffprobeVersion,\n );\n}\n\n// ---- the inspector ----\n\nexport interface InspectMediaArgs {\n localPath: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n deps?: InspectMediaDeps;\n}\n\nexport async function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult> {\n const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n const probe = args.deps?.probe ?? defaultProbe;\n const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;\n const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());\n\n // 1) bounded signature read (header only)\n let head: Uint8Array;\n try {\n const fh = await open(args.localPath, 'r');\n try {\n const buf = Buffer.alloc(SIGNATURE_READ_BYTES);\n const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);\n head = buf.subarray(0, bytesRead);\n } finally {\n await fh.close();\n }\n } catch {\n return indeterminate('unknown', 'read_failed', ffprobeVersion);\n }\n\n const sig = detectSignatureFamily(head);\n\n // 2) family routing\n switch (sig.family) {\n case 'unknown':\n return indeterminate('unknown', 'unrecognized_signature', ffprobeVersion);\n\n case 'svg':\n // Scriptable XML \"image\" — structurally recognized, deliberately NOT a\n // processable format: it can carry script and no sanitize/rasterize step\n // exists. Never classified by decoder capability alone.\n return baseResult(\n {\n status: 'unsupported',\n container: 'svg',\n formatId: 'svg',\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'unsupported_format:svg',\n },\n ffprobeVersion,\n );\n\n case 'image': {\n // Single-format image signature: decoder proof required — a signature is\n // a candidate, not a classification.\n const decoded = await decodeImage(args.localPath);\n if (!decoded)\n return baseResult(\n {\n status: 'malformed',\n container: sig.container,\n formatId: sig.formatId,\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'image_decode_failed',\n },\n ffprobeVersion,\n );\n const frames = decoded.frames;\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'image',\n container: sig.container,\n formatId: sig.formatId,\n streams: {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: frames,\n auxiliary: 0,\n },\n safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n\n case 'isobmff':\n if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {\n // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid\n // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +\n // successful bounded decode + no ordinary timed A/V presentation.\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;\n const streams = parsed?.success ? (parsed.data.streams ?? []) : null;\n const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true; // unknown ⇒ assume worst\n const formatId: MediaFormatId | undefined = sig.brand.startsWith('avi') ? 'avif' : 'heic';\n const decoded = await decodeImage(args.localPath);\n if (!decoded || hasAudio) {\n if (formatId === 'heic' && !decoded && !hasAudio) {\n // Recognized HEIC-family that the runtime cannot decode: explicit\n // unsupported-image (launch policy) — NEVER relabeled video.\n return baseResult(\n {\n status: 'unsupported',\n container: 'heif',\n formatId,\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'unsupported_format:heic',\n },\n ffprobeVersion,\n );\n }\n // Undecodable/ambiguous image-brand file, or one carrying audio (a\n // real timed presentation wearing an image brand): fail toward the\n // stricter visual path.\n return indeterminate(\n 'heif',\n decoded ? 'image_brand_with_audio' : 'image_decode_failed',\n ffprobeVersion,\n formatId,\n );\n }\n const frames = decoded.frames;\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'image',\n container: 'heif',\n formatId,\n streams: {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: frames,\n auxiliary: 0,\n },\n safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n return classifyAvContainer(args, 'isobmff', 'isobmff', probe, timeoutMs, ffprobeVersion);\n\n case 'ebml':\n return classifyAvContainer(args, 'webm', 'webm', probe, timeoutMs, ffprobeVersion);\n case 'ogg':\n return classifyAvContainer(args, 'ogg', 'ogg', probe, timeoutMs, ffprobeVersion);\n case 'avi':\n // Real container ffprobe parses; no registry entry (no origin enables it\n // today) — the stream truth still classifies it, policy rejects by format.\n return classifyAvContainer(args, 'avi', undefined, probe, timeoutMs, ffprobeVersion);\n case 'wav':\n return classifyAvContainer(args, 'wav', 'wav', probe, timeoutMs, ffprobeVersion);\n case 'mp3':\n return classifyAvContainer(args, 'mp3', 'mp3', probe, timeoutMs, ffprobeVersion);\n case 'flac':\n return classifyAvContainer(args, 'flac', 'flac', probe, timeoutMs, ffprobeVersion);\n }\n}\n\nasync function classifyAvContainer(\n args: InspectMediaArgs,\n container: string,\n formatId: MediaFormatId | undefined,\n probe: NonNullable<InspectMediaDeps['probe']>,\n timeoutMs: number,\n ffprobeVersion: string | undefined,\n): Promise<MediaInspectionResult> {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n if (!probed.ok) {\n return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);\n }\n const parsed = ProbeJsonSchema.safeParse(probed.json);\n if (!parsed.success)\n return indeterminate(container, 'probe_schema_invalid', ffprobeVersion, formatId);\n\n const rawStreams = parsed.data.streams ?? [];\n if (rawStreams.length === 0)\n return indeterminate(container, 'no_streams', ffprobeVersion, formatId);\n\n const counts: MediaInspectionStreams = {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: 0,\n auxiliary: 0,\n };\n const audioCodecs = new Set<NormalizedCodecId>();\n const videoCodecs = new Set<NormalizedCodecId>();\n let sawUnknownType = false;\n\n for (const s of rawStreams) {\n const type = s.codec_type ?? '';\n if (type === 'audio') {\n counts.audio++;\n audioCodecs.add(normalizeCodec(s.codec_name));\n } else if (type === 'video') {\n // An attached picture (cover art) is NOT a timed presentation — but the\n // disposition flag is untrusted container data, so it is COUNTED and the\n // safety plan forces every such picture through the image hash path\n // before the audio guard may return clean.\n if (s.disposition?.attached_pic === 1) {\n counts.attachedPictures++;\n } else {\n counts.timedVideo++;\n videoCodecs.add(normalizeCodec(s.codec_name));\n }\n } else if (type === 'subtitle' || type === 'data' || type === 'attachment') {\n counts.auxiliary++;\n } else {\n counts.auxiliary++;\n sawUnknownType = true;\n }\n }\n\n const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };\n const measure = args.deps?.measureDuration ?? measureDurationSec;\n const durationSec = await measure(args.localPath, { signal: args.signal });\n const duration = durationSec !== undefined ? { durationSec } : {};\n\n // classification table (fail-closed)\n if (counts.timedVideo > 0) {\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'video',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: 'video-frames',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n if (counts.audio > 0 && !sawUnknownType) {\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'audio',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: counts.attachedPictures > 0 ? 'audio-plus-artwork' : 'audio-only',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n // auxiliary-only, unknown stream types alongside audio, or nothing usable:\n // never audio — the strict visual fallback owns it.\n return baseResult(\n {\n status: 'indeterminate',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: 'strict-video-fallback',\n reasonCode: sawUnknownType ? 'unknown_stream_type' : 'auxiliary_only',\n },\n ffprobeVersion,\n );\n}\n"]}
1
+ {"version":3,"file":"inspect-media.js","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"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","sourcesContent":["import { open } from 'node:fs/promises';\nimport { z } from 'zod';\nimport {\n MediaInspectionResultSchema,\n type MediaInspectionResult,\n type MediaInspectionStreams,\n type NormalizedCodecId,\n type MediaFormatId,\n} from '@ttt-productions/media-schemas';\nimport { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';\nimport { measureDurationSec, type MeasureDurationFn } from '../duration/media-duration.js';\n\nexport const INSPECTOR_VERSION = 'mpc-inspect-2';\n\nconst SIGNATURE_READ_BYTES = 64 * 1024;\nconst PROBE_SIZE_BYTES = 4 * 1024 * 1024;\nconst ANALYZE_DURATION_US = 10 * 1000 * 1000;\n\nconst ProbeStreamSchema = z\n .object({\n codec_type: z.string().max(32).optional(),\n codec_name: z.string().max(64).optional(),\n disposition: z\n .object({ attached_pic: z.number().int().min(0).max(1).optional() })\n .loose()\n .optional(),\n })\n .loose();\n\nconst ProbeJsonSchema = z\n .object({\n format: z\n .object({ format_name: z.string().max(200).optional() })\n .loose()\n .optional(),\n streams: z.array(ProbeStreamSchema).max(256).optional(),\n })\n .loose();\n\nconst CODEC_MAP: Record<string, NormalizedCodecId> = {\n h264: 'h264',\n hevc: 'hevc',\n vp8: 'vp8',\n vp9: 'vp9',\n av1: 'av1',\n theora: 'theora',\n mpeg4: 'mpeg4',\n mjpeg: 'mjpeg',\n aac: 'aac',\n alac: 'alac',\n opus: 'opus',\n vorbis: 'vorbis',\n mp3: 'mp3',\n flac: 'flac',\n};\n\nfunction normalizeCodec(name: string | undefined): NormalizedCodecId {\n if (!name) return 'other';\n const n = name.toLowerCase();\n if (CODEC_MAP[n]) return CODEC_MAP[n];\n if (n.startsWith('pcm_')) return 'pcm';\n if (n.startsWith('amr')) return 'amr';\n return 'other';\n}\n\ntype SignatureFamily =\n | { family: 'image'; formatId: MediaFormatId; container: string }\n | { family: 'svg' }\n | { family: 'ebml' }\n | { family: 'isobmff'; brand: string }\n | { family: 'ogg' }\n | { family: 'avi' }\n | { family: 'wav' }\n | { family: 'mp3' }\n | { family: 'flac' }\n | { family: 'unknown' };\n\nconst ISOBMFF_IMAGE_BRANDS = new Set([\n 'avif',\n 'avis',\n 'heic',\n 'heix',\n 'heim',\n 'heis',\n 'hevc',\n 'hevx',\n 'mif1',\n 'msf1',\n]);\n\nfunction ascii(b: Uint8Array, off: number, s: string): boolean {\n if (off + s.length > b.length) return false;\n for (let i = 0; i < s.length; i++) if (b[off + i] !== s.charCodeAt(i)) return false;\n return true;\n}\n\nexport function detectSignatureFamily(b: Uint8Array): SignatureFamily {\n if (b.length < 12) return { family: 'unknown' };\n if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)\n return { family: 'image', formatId: 'jpeg', container: 'jpeg' };\n if (b[0] === 0x89 && ascii(b, 1, 'PNG'))\n return { family: 'image', formatId: 'png', container: 'png' };\n if (ascii(b, 0, 'GIF8')) return { family: 'image', formatId: 'gif', container: 'gif' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WEBP'))\n return { family: 'image', formatId: 'webp', container: 'webp' };\n if (b[0] === 0x42 && b[1] === 0x4d) return { family: 'image', formatId: 'bmp', container: 'bmp' };\n if (\n (b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) ||\n (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)\n ) {\n return { family: 'image', formatId: 'tiff', container: 'tiff' };\n }\n {\n const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))\n .toString('latin1')\n .toLowerCase();\n if (\n prefix.includes('<svg') &&\n (prefix.trimStart().startsWith('<?xml') ||\n prefix.trimStart().startsWith('<svg') ||\n prefix.includes('<!doctype svg'))\n ) {\n return { family: 'svg' };\n }\n }\n if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return { family: 'ebml' };\n if (ascii(b, 4, 'ftyp')) {\n const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();\n return { family: 'isobmff', brand };\n }\n if (ascii(b, 0, 'OggS')) return { family: 'ogg' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'AVI ')) return { family: 'avi' };\n if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WAVE')) return { family: 'wav' };\n if (ascii(b, 0, 'ID3')) return { family: 'mp3' };\n if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return { family: 'mp3' };\n if (ascii(b, 0, 'fLaC')) return { family: 'flac' };\n return { family: 'unknown' };\n}\n\nexport interface InspectMediaDeps {\n probe?: (\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n ) => Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: 'timeout' | 'truncated' | 'failed' | 'bad_json' }\n >;\n decodeImage?: (localPath: string) => Promise<{ frames: number } | null>;\n measureDuration?: MeasureDurationFn;\n ffprobeVersion?: string;\n}\n\nlet cachedFfprobeVersion: string | null | undefined;\n\nasync function getFfprobeVersion(): Promise<string | undefined> {\n if (cachedFfprobeVersion !== undefined) return cachedFfprobeVersion ?? undefined;\n try {\n const r = await runCmd('ffprobe', ['-version'], { timeoutMs: 5_000 });\n const first = (r.stdout || r.stderr).split(/\\r?\\n/)[0]?.trim() ?? '';\n cachedFfprobeVersion = first ? first.slice(0, 120) : null;\n } catch {\n cachedFfprobeVersion = null;\n }\n return cachedFfprobeVersion ?? undefined;\n}\n\nasync function defaultProbe(\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n): Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: 'timeout' | 'truncated' | 'failed' | 'bad_json' }\n> {\n let r;\n try {\n r = await runCmd(\n 'ffprobe',\n [\n '-v',\n 'error',\n '-print_format',\n 'json',\n '-show_format',\n '-show_streams',\n '-probesize',\n String(PROBE_SIZE_BYTES),\n '-analyzeduration',\n String(ANALYZE_DURATION_US),\n localPath,\n ],\n { timeoutMs: opts.timeoutMs, signal: opts.signal },\n );\n } catch {\n return { ok: false, reason: 'failed' };\n }\n if (r.timedOut) return { ok: false, reason: 'timeout' };\n if (r.truncated) return { ok: false, reason: 'truncated' };\n if (r.code !== 0) return { ok: false, reason: 'failed' };\n try {\n return { ok: true, json: JSON.parse(r.stdout || '{}') };\n } catch {\n return { ok: false, reason: 'bad_json' };\n }\n}\n\nasync function defaultDecodeImage(localPath: string): Promise<{ frames: number } | null> {\n try {\n const sharp = (await import('sharp')).default;\n const meta = await sharp(localPath, {\n limitInputPixels: 268402689,\n }).metadata();\n if (!meta.width || !meta.height) return null;\n return { frames: Math.max(1, meta.pages ?? 1) };\n } catch {\n return null;\n }\n}\n\nfunction baseResult(\n partial: Omit<MediaInspectionResult, 'inspectorVersion' | 'streams' | 'codecs'> &\n Partial<Pick<MediaInspectionResult, 'streams' | 'codecs'>>,\n ffprobeVersion: string | undefined,\n): MediaInspectionResult {\n const result: MediaInspectionResult = {\n inspectorVersion: INSPECTOR_VERSION,\n ...(ffprobeVersion ? { ffprobeVersion } : {}),\n streams: partial.streams ?? {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: 0,\n auxiliary: 0,\n },\n codecs: partial.codecs ?? { audio: [], video: [] },\n status: partial.status,\n container: partial.container,\n safetyPlan: partial.safetyPlan,\n reasonCode: partial.reasonCode,\n ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),\n ...(partial.formatId ? { formatId: partial.formatId } : {}),\n ...(partial.durationSec !== undefined ? { durationSec: partial.durationSec } : {}),\n };\n return MediaInspectionResultSchema.parse(result);\n}\n\nfunction indeterminate(\n container: string,\n reasonCode: string,\n ffprobeVersion: string | undefined,\n formatId?: MediaFormatId,\n): MediaInspectionResult {\n return baseResult(\n {\n status: 'indeterminate',\n container,\n safetyPlan: 'strict-video-fallback',\n reasonCode,\n ...(formatId ? { formatId } : {}),\n },\n ffprobeVersion,\n );\n}\n\nexport interface InspectMediaArgs {\n localPath: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n deps?: InspectMediaDeps;\n}\n\nexport async function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult> {\n const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n const probe = args.deps?.probe ?? defaultProbe;\n const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;\n const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());\n\n let head: Uint8Array;\n try {\n const fh = await open(args.localPath, 'r');\n try {\n const buf = Buffer.alloc(SIGNATURE_READ_BYTES);\n const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);\n head = buf.subarray(0, bytesRead);\n } finally {\n await fh.close();\n }\n } catch {\n return indeterminate('unknown', 'read_failed', ffprobeVersion);\n }\n\n const sig = detectSignatureFamily(head);\n\n switch (sig.family) {\n case 'unknown':\n return indeterminate('unknown', 'unrecognized_signature', ffprobeVersion);\n\n case 'svg':\n return baseResult(\n {\n status: 'unsupported',\n container: 'svg',\n formatId: 'svg',\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'unsupported_format:svg',\n },\n ffprobeVersion,\n );\n\n case 'image': {\n const decoded = await decodeImage(args.localPath);\n if (!decoded)\n return baseResult(\n {\n status: 'malformed',\n container: sig.container,\n formatId: sig.formatId,\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'image_decode_failed',\n },\n ffprobeVersion,\n );\n const frames = decoded.frames;\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'image',\n container: sig.container,\n formatId: sig.formatId,\n streams: {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: frames,\n auxiliary: 0,\n },\n safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n\n case 'isobmff':\n if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;\n const streams = parsed?.success ? (parsed.data.streams ?? []) : null;\n const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true;\n const formatId: MediaFormatId | undefined = sig.brand.startsWith('avi') ? 'avif' : 'heic';\n const decoded = await decodeImage(args.localPath);\n if (!decoded || hasAudio) {\n if (formatId === 'heic' && !decoded && !hasAudio) {\n return baseResult(\n {\n status: 'unsupported',\n container: 'heif',\n formatId,\n safetyPlan: 'strict-video-fallback',\n reasonCode: 'unsupported_format:heic',\n },\n ffprobeVersion,\n );\n }\n return indeterminate(\n 'heif',\n decoded ? 'image_brand_with_audio' : 'image_decode_failed',\n ffprobeVersion,\n formatId,\n );\n }\n const frames = decoded.frames;\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'image',\n container: 'heif',\n formatId,\n streams: {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: frames,\n auxiliary: 0,\n },\n safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n return classifyAvContainer(args, 'isobmff', 'isobmff', probe, timeoutMs, ffprobeVersion);\n\n case 'ebml':\n return classifyAvContainer(args, 'webm', 'webm', probe, timeoutMs, ffprobeVersion);\n case 'ogg':\n return classifyAvContainer(args, 'ogg', 'ogg', probe, timeoutMs, ffprobeVersion);\n case 'avi':\n return classifyAvContainer(args, 'avi', undefined, probe, timeoutMs, ffprobeVersion);\n case 'wav':\n return classifyAvContainer(args, 'wav', 'wav', probe, timeoutMs, ffprobeVersion);\n case 'mp3':\n return classifyAvContainer(args, 'mp3', 'mp3', probe, timeoutMs, ffprobeVersion);\n case 'flac':\n return classifyAvContainer(args, 'flac', 'flac', probe, timeoutMs, ffprobeVersion);\n }\n}\n\nasync function classifyAvContainer(\n args: InspectMediaArgs,\n container: string,\n formatId: MediaFormatId | undefined,\n probe: NonNullable<InspectMediaDeps['probe']>,\n timeoutMs: number,\n ffprobeVersion: string | undefined,\n): Promise<MediaInspectionResult> {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n if (!probed.ok) {\n return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);\n }\n const parsed = ProbeJsonSchema.safeParse(probed.json);\n if (!parsed.success)\n return indeterminate(container, 'probe_schema_invalid', ffprobeVersion, formatId);\n\n const rawStreams = parsed.data.streams ?? [];\n if (rawStreams.length === 0)\n return indeterminate(container, 'no_streams', ffprobeVersion, formatId);\n\n const counts: MediaInspectionStreams = {\n audio: 0,\n timedVideo: 0,\n attachedPictures: 0,\n imageFrames: 0,\n auxiliary: 0,\n };\n const audioCodecs = new Set<NormalizedCodecId>();\n const videoCodecs = new Set<NormalizedCodecId>();\n let sawUnknownType = false;\n\n for (const s of rawStreams) {\n const type = s.codec_type ?? '';\n if (type === 'audio') {\n counts.audio++;\n audioCodecs.add(normalizeCodec(s.codec_name));\n } else if (type === 'video') {\n if (s.disposition?.attached_pic === 1) {\n counts.attachedPictures++;\n } else {\n counts.timedVideo++;\n videoCodecs.add(normalizeCodec(s.codec_name));\n }\n } else if (type === 'subtitle' || type === 'data' || type === 'attachment') {\n counts.auxiliary++;\n } else {\n counts.auxiliary++;\n sawUnknownType = true;\n }\n }\n\n const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };\n const measure = args.deps?.measureDuration ?? measureDurationSec;\n const durationSec = await measure(args.localPath, { signal: args.signal });\n const duration = durationSec !== undefined ? { durationSec } : {};\n\n if (counts.timedVideo > 0) {\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'video',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: 'video-frames',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n if (counts.audio > 0 && !sawUnknownType) {\n return baseResult(\n {\n status: 'definitive',\n canonicalKind: 'audio',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: counts.attachedPictures > 0 ? 'audio-plus-artwork' : 'audio-only',\n reasonCode: 'ok',\n },\n ffprobeVersion,\n );\n }\n return baseResult(\n {\n status: 'indeterminate',\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: 'strict-video-fallback',\n reasonCode: sawUnknownType ? 'unknown_stream_type' : 'auxiliary_only',\n },\n ffprobeVersion,\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/io/fs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAK1C,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CA0BV"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/io/fs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAK1C,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAyBV"}
package/dist/io/fs.js CHANGED
@@ -15,7 +15,6 @@ export function createFsIO(args) {
15
15
  const ext = path.extname(localPath);
16
16
  const safeKey = sanitizeKey(key);
17
17
  const out = path.join(args.outputDir, `${safeKey}${ext}`);
18
- // Ensure final path stays within outputDir.
19
18
  const resolvedDir = path.resolve(args.outputDir);
20
19
  const resolvedOut = path.resolve(out);
21
20
  if (!resolvedOut.startsWith(resolvedDir + path.sep) && resolvedOut !== resolvedDir) {
package/dist/io/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/io/fs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,UAAU,UAAU,CAAC,IAI1B;IACC,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE,IAAI,CAAC,SAAS;YACpB,KAAK,CAAC,UAAU,CAAC,SAAS;gBACxB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC5C,CAAC;SACF;QACD,MAAM,EAAE;YACN,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,GAAG;gBAChC,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACjD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;gBACjC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC;gBAE1D,4CAA4C;gBAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACtC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;oBACnF,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;gBACxC,CAAC;gBACD,MAAM,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;gBAC/B,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,GAAG,EAAE,EAAE,CAAC;YAC7C,CAAC;SACF;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { MediaIO } from './types.js';\nimport { copyFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { sanitizeKey } from '../utils/safe-path.js';\n\nexport function createFsIO(args: {\n inputPath: string;\n inputMime?: string;\n outputDir: string;\n}): MediaIO {\n return {\n input: {\n mime: args.inputMime,\n async readToFile(localPath) {\n await copyFile(args.inputPath, localPath);\n },\n },\n output: {\n async writeFromFile(localPath, key) {\n await mkdir(args.outputDir, { recursive: true });\n const ext = path.extname(localPath);\n const safeKey = sanitizeKey(key);\n const out = path.join(args.outputDir, `${safeKey}${ext}`);\n\n // Ensure final path stays within outputDir.\n const resolvedDir = path.resolve(args.outputDir);\n const resolvedOut = path.resolve(out);\n if (!resolvedOut.startsWith(resolvedDir + path.sep) && resolvedOut !== resolvedDir) {\n throw new Error('Unsafe output path');\n }\n await copyFile(localPath, out);\n return { path: out, url: `file://${out}` };\n },\n },\n };\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/io/fs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,UAAU,UAAU,CAAC,IAI1B;IACC,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE,IAAI,CAAC,SAAS;YACpB,KAAK,CAAC,UAAU,CAAC,SAAS;gBACxB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC5C,CAAC;SACF;QACD,MAAM,EAAE;YACN,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,GAAG;gBAChC,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACjD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;gBACjC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC;gBAE1D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACtC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;oBACnF,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;gBACxC,CAAC;gBACD,MAAM,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;gBAC/B,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,GAAG,EAAE,EAAE,CAAC;YAC7C,CAAC;SACF;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { MediaIO } from './types.js';\nimport { copyFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { sanitizeKey } from '../utils/safe-path.js';\n\nexport function createFsIO(args: {\n inputPath: string;\n inputMime?: string;\n outputDir: string;\n}): MediaIO {\n return {\n input: {\n mime: args.inputMime,\n async readToFile(localPath) {\n await copyFile(args.inputPath, localPath);\n },\n },\n output: {\n async writeFromFile(localPath, key) {\n await mkdir(args.outputDir, { recursive: true });\n const ext = path.extname(localPath);\n const safeKey = sanitizeKey(key);\n const out = path.join(args.outputDir, `${safeKey}${ext}`);\n\n const resolvedDir = path.resolve(args.outputDir);\n const resolvedOut = path.resolve(out);\n if (!resolvedOut.startsWith(resolvedDir + path.sep) && resolvedOut !== resolvedDir) {\n throw new Error('Unsafe output path');\n }\n await copyFile(localPath, out);\n return { path: out, url: `file://${out}` };\n },\n },\n };\n}\n"]}
@@ -1,39 +1,19 @@
1
1
  import type { Readable } from 'node:stream';
2
- /** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */
3
2
  export declare class ByteLimitExceededError extends Error {
4
3
  readonly maxBytes: number;
5
4
  constructor(maxBytes: number);
6
5
  }
7
6
  export interface TempFileInput {
8
- /** Absolute path of the fully-written temp file. */
9
7
  localPath: string;
10
- /** Total bytes written (always <= maxBytes). */
11
8
  bytesWritten: number;
12
- /** Removes the temp file and its private directory. Idempotent; never throws. */
13
9
  cleanup: () => Promise<void>;
14
10
  }
15
11
  export interface StreamToTempFileOptions {
16
- /** HARD cap: the stream is destroyed and the temp file removed the moment the
17
- * cap is crossed, so an oversized source never fully lands on disk. */
18
12
  maxBytes: number;
19
- /** Temp-dir prefix (default "media-input-"). */
20
13
  prefix?: string;
21
14
  }
22
- /**
23
- * Streams a Readable into a private temp file with a hard byte cap and
24
- * constant memory. On success the caller MUST invoke `cleanup()` in a
25
- * `finally`. On any failure (cap exceeded, stream error) the temp file is
26
- * removed before the error propagates.
27
- */
28
15
  export declare function streamToTempFile(source: Readable, options: StreamToTempFileOptions): Promise<TempFileInput>;
29
- /** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */
30
16
  export declare function hashFileSha256(localPath: string): Promise<string>;
31
- /** Default header window for signature-family checks (matches the inspector's sniff window). */
32
17
  export declare const FILE_HEADER_BYTES: number;
33
- /**
34
- * Reads at most `maxBytes` (default 64 KiB) from the start of a file — the
35
- * bounded input for {@link detectSignatureFamily}-style checks without loading
36
- * the whole source.
37
- */
38
18
  export declare function readFileHeader(localPath: string, maxBytes?: number): Promise<Uint8Array>;
39
19
  //# sourceMappingURL=local-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-input.d.ts","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,6EAA6E;AAC7E,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM;CAK7B;AAED,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC;4EACwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+BxB;AAED,6FAA6F;AAC7F,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvE;AAED,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,UAAU,CAAC,CASrB"}
1
+ {"version":3,"file":"local-input.d.ts","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM;CAK7B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA+BxB;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvE;AAED,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,UAAU,CAAC,CASrB"}
@@ -1,16 +1,9 @@
1
- // Bounded-memory local-input primitives (canonical-upload-content-classification,
2
- // bounded-memory unit). Safety scanning and hashing must never hold a full media
3
- // source in JS memory: sources stream to a generation-pinned temp file with a
4
- // hard byte cap, hashes stream from disk, and signature checks read only a
5
- // bounded header. These primitives are generic (no storage/vendor deps) — the
6
- // caller supplies the Readable and owns generation pinning.
7
1
  import { createHash } from 'node:crypto';
8
2
  import { createReadStream, createWriteStream } from 'node:fs';
9
3
  import { mkdtemp, open, rm } from 'node:fs/promises';
10
4
  import os from 'node:os';
11
5
  import path from 'node:path';
12
6
  import { pipeline } from 'node:stream/promises';
13
- /** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */
14
7
  export class ByteLimitExceededError extends Error {
15
8
  maxBytes;
16
9
  constructor(maxBytes) {
@@ -19,12 +12,6 @@ export class ByteLimitExceededError extends Error {
19
12
  this.maxBytes = maxBytes;
20
13
  }
21
14
  }
22
- /**
23
- * Streams a Readable into a private temp file with a hard byte cap and
24
- * constant memory. On success the caller MUST invoke `cleanup()` in a
25
- * `finally`. On any failure (cap exceeded, stream error) the temp file is
26
- * removed before the error propagates.
27
- */
28
15
  export async function streamToTempFile(source, options) {
29
16
  const { maxBytes, prefix = 'media-input-' } = options;
30
17
  if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
@@ -54,7 +41,6 @@ export async function streamToTempFile(source, options) {
54
41
  }
55
42
  return { localPath, bytesWritten, cleanup };
56
43
  }
57
- /** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */
58
44
  export async function hashFileSha256(localPath) {
59
45
  const hash = createHash('sha256');
60
46
  await pipeline(createReadStream(localPath), async (chunks) => {
@@ -63,13 +49,7 @@ export async function hashFileSha256(localPath) {
63
49
  });
64
50
  return hash.digest('hex');
65
51
  }
66
- /** Default header window for signature-family checks (matches the inspector's sniff window). */
67
52
  export const FILE_HEADER_BYTES = 64 * 1024;
68
- /**
69
- * Reads at most `maxBytes` (default 64 KiB) from the start of a file — the
70
- * bounded input for {@link detectSignatureFamily}-style checks without loading
71
- * the whole source.
72
- */
73
53
  export async function readFileHeader(localPath, maxBytes = FILE_HEADER_BYTES) {
74
54
  const handle = await open(localPath, 'r');
75
55
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"local-input.js","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounded-memory local-input primitives (canonical-upload-content-classification,\n// bounded-memory unit). Safety scanning and hashing must never hold a full media\n// source in JS memory: sources stream to a generation-pinned temp file with a\n// hard byte cap, hashes stream from disk, and signature checks read only a\n// bounded header. These primitives are generic (no storage/vendor deps) — the\n// caller supplies the Readable and owns generation pinning.\n\nimport { createHash } from 'node:crypto';\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { mkdtemp, open, rm } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport type { Readable } from 'node:stream';\n\n/** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */\nexport class ByteLimitExceededError extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`source exceeded the ${maxBytes}-byte limit`);\n this.name = 'ByteLimitExceededError';\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface TempFileInput {\n /** Absolute path of the fully-written temp file. */\n localPath: string;\n /** Total bytes written (always <= maxBytes). */\n bytesWritten: number;\n /** Removes the temp file and its private directory. Idempotent; never throws. */\n cleanup: () => Promise<void>;\n}\n\nexport interface StreamToTempFileOptions {\n /** HARD cap: the stream is destroyed and the temp file removed the moment the\n * cap is crossed, so an oversized source never fully lands on disk. */\n maxBytes: number;\n /** Temp-dir prefix (default \"media-input-\"). */\n prefix?: string;\n}\n\n/**\n * Streams a Readable into a private temp file with a hard byte cap and\n * constant memory. On success the caller MUST invoke `cleanup()` in a\n * `finally`. On any failure (cap exceeded, stream error) the temp file is\n * removed before the error propagates.\n */\nexport async function streamToTempFile(\n source: Readable,\n options: StreamToTempFileOptions,\n): Promise<TempFileInput> {\n const { maxBytes, prefix = 'media-input-' } = options;\n if (!Number.isFinite(maxBytes) || maxBytes <= 0) {\n throw new Error('streamToTempFile requires a positive finite maxBytes');\n }\n const dir = await mkdtemp(path.join(os.tmpdir(), prefix));\n const localPath = path.join(dir, 'input');\n const cleanup = async () => {\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n let bytesWritten = 0;\n try {\n const sink = createWriteStream(localPath, { flags: 'wx' });\n await pipeline(\n source,\n async function* (chunks: AsyncIterable<Buffer | string>) {\n for await (const chunk of chunks) {\n const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;\n bytesWritten += buf.byteLength;\n if (bytesWritten > maxBytes) throw new ByteLimitExceededError(maxBytes);\n yield buf;\n }\n },\n sink,\n );\n } catch (err) {\n await cleanup();\n throw err;\n }\n return { localPath, bytesWritten, cleanup };\n}\n\n/** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */\nexport async function hashFileSha256(localPath: string): Promise<string> {\n const hash = createHash('sha256');\n await pipeline(createReadStream(localPath), async (chunks: AsyncIterable<Buffer>) => {\n for await (const chunk of chunks) hash.update(chunk);\n });\n return hash.digest('hex');\n}\n\n/** Default header window for signature-family checks (matches the inspector's sniff window). */\nexport const FILE_HEADER_BYTES = 64 * 1024;\n\n/**\n * Reads at most `maxBytes` (default 64 KiB) from the start of a file — the\n * bounded input for {@link detectSignatureFamily}-style checks without loading\n * the whole source.\n */\nexport async function readFileHeader(\n localPath: string,\n maxBytes: number = FILE_HEADER_BYTES,\n): Promise<Uint8Array> {\n const handle = await open(localPath, 'r');\n try {\n const buf = Buffer.alloc(maxBytes);\n const { bytesRead } = await handle.read(buf, 0, maxBytes, 0);\n return buf.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n}\n"]}
1
+ {"version":3,"file":"local-input.js","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,QAAQ,CAAS;IAC1B,YAAY,QAAgB;QAC1B,KAAK,CAAC,uBAAuB,QAAQ,aAAa,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAaD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAgB,EAChB,OAAgC;IAEhC,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClE,CAAC,CAAC;IAEF,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,MAAM,QAAQ,CACZ,MAAM,EACN,KAAK,SAAS,CAAC,EAAE,MAAsC;YACrD,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACjC,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;gBACnE,YAAY,IAAI,GAAG,CAAC,UAAU,CAAC;gBAC/B,IAAI,YAAY,GAAG,QAAQ;oBAAE,MAAM,IAAI,sBAAsB,CAAC,QAAQ,CAAC,CAAC;gBACxE,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,EAAE,CAAC;QAChB,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB;IACpD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAA6B,EAAE,EAAE;QAClF,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAAiB,EACjB,WAAmB,iBAAiB;IAEpC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC7D,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { mkdtemp, open, rm } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport type { Readable } from 'node:stream';\n\nexport class ByteLimitExceededError extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`source exceeded the ${maxBytes}-byte limit`);\n this.name = 'ByteLimitExceededError';\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface TempFileInput {\n localPath: string;\n bytesWritten: number;\n cleanup: () => Promise<void>;\n}\n\nexport interface StreamToTempFileOptions {\n maxBytes: number;\n prefix?: string;\n}\n\nexport async function streamToTempFile(\n source: Readable,\n options: StreamToTempFileOptions,\n): Promise<TempFileInput> {\n const { maxBytes, prefix = 'media-input-' } = options;\n if (!Number.isFinite(maxBytes) || maxBytes <= 0) {\n throw new Error('streamToTempFile requires a positive finite maxBytes');\n }\n const dir = await mkdtemp(path.join(os.tmpdir(), prefix));\n const localPath = path.join(dir, 'input');\n const cleanup = async () => {\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n let bytesWritten = 0;\n try {\n const sink = createWriteStream(localPath, { flags: 'wx' });\n await pipeline(\n source,\n async function* (chunks: AsyncIterable<Buffer | string>) {\n for await (const chunk of chunks) {\n const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;\n bytesWritten += buf.byteLength;\n if (bytesWritten > maxBytes) throw new ByteLimitExceededError(maxBytes);\n yield buf;\n }\n },\n sink,\n );\n } catch (err) {\n await cleanup();\n throw err;\n }\n return { localPath, bytesWritten, cleanup };\n}\n\nexport async function hashFileSha256(localPath: string): Promise<string> {\n const hash = createHash('sha256');\n await pipeline(createReadStream(localPath), async (chunks: AsyncIterable<Buffer>) => {\n for await (const chunk of chunks) hash.update(chunk);\n });\n return hash.digest('hex');\n}\n\nexport const FILE_HEADER_BYTES = 64 * 1024;\n\nexport async function readFileHeader(\n localPath: string,\n maxBytes: number = FILE_HEADER_BYTES,\n): Promise<Uint8Array> {\n const handle = await open(localPath, 'r');\n try {\n const buf = Buffer.alloc(maxBytes);\n const { bytesRead } = await handle.read(buf, 0, maxBytes, 0);\n return buf.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n}\n"]}
@@ -1,11 +1,8 @@
1
1
  export interface MediaInputSource {
2
- /** Copy source media into localPath */
3
2
  readToFile(localPath: string): Promise<void>;
4
- /** Optional mime hint */
5
3
  mime?: string;
6
4
  }
7
5
  export interface MediaOutputTarget {
8
- /** Persist a processed file from localPath */
9
6
  writeFromFile(localPath: string, outputKey: string): Promise<{
10
7
  url?: string;
11
8
  path?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/io/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QACT,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/io/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QACT,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/io/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface MediaInputSource {\n /** Copy source media into localPath */\n readToFile(localPath: string): Promise<void>;\n\n /** Optional mime hint */\n mime?: string;\n}\n\nexport interface MediaOutputTarget {\n /** Persist a processed file from localPath */\n writeFromFile(\n localPath: string,\n outputKey: string,\n ): Promise<{\n url?: string;\n path?: string;\n }>;\n}\n\nexport interface MediaIO {\n input: MediaInputSource;\n output: MediaOutputTarget;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/io/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface MediaInputSource {\n readToFile(localPath: string): Promise<void>;\n\n mime?: string;\n}\n\nexport interface MediaOutputTarget {\n writeFromFile(\n localPath: string,\n outputKey: string,\n ): Promise<{\n url?: string;\n path?: string;\n }>;\n}\n\nexport interface MediaIO {\n input: MediaInputSource;\n output: MediaOutputTarget;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/moderation/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,wBAAgB,eAAe,CAC7B,KAAK,CAAC,EAAE,qBAAqB,GAAG,IAAI,EACpC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GACpC,qBAAqB,GAAG,SAAS,CAuBnC"}
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/moderation/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,wBAAgB,eAAe,CAC7B,KAAK,CAAC,EAAE,qBAAqB,GAAG,IAAI,EACpC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GACpC,qBAAqB,GAAG,SAAS,CAsBnC"}
@@ -7,7 +7,6 @@ export function mergeModeration(input, output) {
7
7
  return b ?? undefined;
8
8
  if (!b)
9
9
  return a ?? undefined;
10
- // worst status wins: rejected > flagged > error > passed
11
10
  const rank = (s) => (s === 'rejected' ? 4 : s === 'flagged' ? 3 : s === 'error' ? 2 : 1);
12
11
  const status = rank(a.status) >= rank(b.status) ? a.status : b.status;
13
12
  const reasons = Array.from(new Set([...(a.reasons ?? []), ...(b.reasons ?? [])]));