@de-otio/trellis 0.11.0 → 0.12.1

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 (126) hide show
  1. package/dist/env.d.ts +168 -0
  2. package/dist/env.d.ts.map +1 -1
  3. package/dist/env.js +155 -0
  4. package/dist/env.js.map +1 -1
  5. package/dist/lambda/media-completion-worker.d.ts +175 -0
  6. package/dist/lambda/media-completion-worker.d.ts.map +1 -0
  7. package/dist/lambda/media-completion-worker.js +373 -0
  8. package/dist/lambda/media-completion-worker.js.map +1 -0
  9. package/dist/lambda/media-processing-worker.d.ts +172 -1
  10. package/dist/lambda/media-processing-worker.d.ts.map +1 -1
  11. package/dist/lambda/media-processing-worker.js +343 -49
  12. package/dist/lambda/media-processing-worker.js.map +1 -1
  13. package/dist/lib/exif-stripper.d.ts +37 -22
  14. package/dist/lib/exif-stripper.d.ts.map +1 -1
  15. package/dist/lib/exif-stripper.js +101 -41
  16. package/dist/lib/exif-stripper.js.map +1 -1
  17. package/dist/lib/media/cas-keys.d.ts +63 -0
  18. package/dist/lib/media/cas-keys.d.ts.map +1 -0
  19. package/dist/lib/media/cas-keys.js +102 -0
  20. package/dist/lib/media/cas-keys.js.map +1 -0
  21. package/dist/lib/media/classify-worker-error.d.ts +48 -0
  22. package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
  23. package/dist/lib/media/classify-worker-error.js +319 -0
  24. package/dist/lib/media/classify-worker-error.js.map +1 -0
  25. package/dist/lib/media/dedupe-key.d.ts +29 -0
  26. package/dist/lib/media/dedupe-key.d.ts.map +1 -0
  27. package/dist/lib/media/dedupe-key.js +49 -0
  28. package/dist/lib/media/dedupe-key.js.map +1 -0
  29. package/dist/lib/media/duration-cap.d.ts +30 -0
  30. package/dist/lib/media/duration-cap.d.ts.map +1 -0
  31. package/dist/lib/media/duration-cap.js +37 -0
  32. package/dist/lib/media/duration-cap.js.map +1 -0
  33. package/dist/lib/media/ffmpeg-args.d.ts +83 -0
  34. package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
  35. package/dist/lib/media/ffmpeg-args.js +119 -0
  36. package/dist/lib/media/ffmpeg-args.js.map +1 -0
  37. package/dist/lib/media/media-ports.d.ts +126 -0
  38. package/dist/lib/media/media-ports.d.ts.map +1 -0
  39. package/dist/lib/media/media-ports.js +129 -0
  40. package/dist/lib/media/media-ports.js.map +1 -0
  41. package/dist/lib/media/media-upsert.d.ts +55 -0
  42. package/dist/lib/media/media-upsert.d.ts.map +1 -0
  43. package/dist/lib/media/media-upsert.js +38 -0
  44. package/dist/lib/media/media-upsert.js.map +1 -0
  45. package/dist/lib/media/moderation-provider.d.ts +111 -0
  46. package/dist/lib/media/moderation-provider.d.ts.map +1 -0
  47. package/dist/lib/media/moderation-provider.js +130 -0
  48. package/dist/lib/media/moderation-provider.js.map +1 -0
  49. package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
  50. package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
  51. package/dist/lib/media/moderation-resolved-payload.js +37 -0
  52. package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
  53. package/dist/lib/media/moderation-status.d.ts +98 -0
  54. package/dist/lib/media/moderation-status.d.ts.map +1 -0
  55. package/dist/lib/media/moderation-status.js +122 -0
  56. package/dist/lib/media/moderation-status.js.map +1 -0
  57. package/dist/lib/media/processing-types.d.ts +45 -0
  58. package/dist/lib/media/processing-types.d.ts.map +1 -0
  59. package/dist/lib/media/processing-types.js +9 -0
  60. package/dist/lib/media/processing-types.js.map +1 -0
  61. package/dist/lib/media/promote-decision.d.ts +64 -0
  62. package/dist/lib/media/promote-decision.d.ts.map +1 -0
  63. package/dist/lib/media/promote-decision.js +76 -0
  64. package/dist/lib/media/promote-decision.js.map +1 -0
  65. package/dist/lib/media/quota-check.d.ts +22 -0
  66. package/dist/lib/media/quota-check.d.ts.map +1 -0
  67. package/dist/lib/media/quota-check.js +42 -0
  68. package/dist/lib/media/quota-check.js.map +1 -0
  69. package/dist/lib/media/quota-types.d.ts +15 -0
  70. package/dist/lib/media/quota-types.d.ts.map +1 -0
  71. package/dist/lib/media/quota-types.js +9 -0
  72. package/dist/lib/media/quota-types.js.map +1 -0
  73. package/dist/lib/media/route-upload.d.ts +58 -0
  74. package/dist/lib/media/route-upload.d.ts.map +1 -0
  75. package/dist/lib/media/route-upload.js +80 -0
  76. package/dist/lib/media/route-upload.js.map +1 -0
  77. package/dist/lib/media/serve-gate.d.ts +51 -0
  78. package/dist/lib/media/serve-gate.d.ts.map +1 -0
  79. package/dist/lib/media/serve-gate.js +68 -0
  80. package/dist/lib/media/serve-gate.js.map +1 -0
  81. package/dist/lib/media/tenant-resolution.d.ts +42 -0
  82. package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
  83. package/dist/lib/media/tenant-resolution.js +45 -0
  84. package/dist/lib/media/tenant-resolution.js.map +1 -0
  85. package/dist/lib/media/text-moderation.d.ts +28 -0
  86. package/dist/lib/media/text-moderation.d.ts.map +1 -0
  87. package/dist/lib/media/text-moderation.js +62 -0
  88. package/dist/lib/media/text-moderation.js.map +1 -0
  89. package/dist/lib/media/track-verdict.d.ts +45 -0
  90. package/dist/lib/media/track-verdict.d.ts.map +1 -0
  91. package/dist/lib/media/track-verdict.js +52 -0
  92. package/dist/lib/media/track-verdict.js.map +1 -0
  93. package/dist/lib/media/transcript-moderation.d.ts +47 -0
  94. package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
  95. package/dist/lib/media/transcript-moderation.js +70 -0
  96. package/dist/lib/media/transcript-moderation.js.map +1 -0
  97. package/dist/lib/media-handler.d.ts.map +1 -1
  98. package/dist/lib/media-handler.js +15 -9
  99. package/dist/lib/media-handler.js.map +1 -1
  100. package/dist/lib/post-handler.d.ts.map +1 -1
  101. package/dist/lib/post-handler.js +4 -1
  102. package/dist/lib/post-handler.js.map +1 -1
  103. package/dist/lib/route-helpers.d.ts.map +1 -1
  104. package/dist/lib/route-helpers.js +9 -1
  105. package/dist/lib/route-helpers.js.map +1 -1
  106. package/dist/lib/routes/media.d.ts +21 -0
  107. package/dist/lib/routes/media.d.ts.map +1 -1
  108. package/dist/lib/routes/media.js +584 -483
  109. package/dist/lib/routes/media.js.map +1 -1
  110. package/dist/lib/services/image-normalizer.d.ts +64 -6
  111. package/dist/lib/services/image-normalizer.d.ts.map +1 -1
  112. package/dist/lib/services/image-normalizer.js +88 -6
  113. package/dist/lib/services/image-normalizer.js.map +1 -1
  114. package/dist/lib/services/media-upload-service.d.ts +2 -2
  115. package/dist/lib/services/media-upload-service.d.ts.map +1 -1
  116. package/dist/lib/services/media-upload-service.js +22 -21
  117. package/dist/lib/services/media-upload-service.js.map +1 -1
  118. package/dist/lib/tenant-scope.d.ts.map +1 -1
  119. package/dist/lib/tenant-scope.js +16 -1
  120. package/dist/lib/tenant-scope.js.map +1 -1
  121. package/package.json +2 -1
  122. package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
  123. package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
  124. package/prisma/schema.prisma +95 -17
  125. package/src/lambda/media-completion-worker.ts +567 -0
  126. package/src/lambda/media-processing-worker.ts +508 -59
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Pure functional-core builder for ffmpeg argument arrays.
3
+ *
4
+ * Produces an argv ARRAY passed directly to the ffmpeg process — NEVER a shell
5
+ * string. No interpolation into a shell context means no command injection risk
6
+ * regardless of what inputPath / outputPath / posterPath contain.
7
+ *
8
+ * Hardening applied unconditionally on every invocation:
9
+ * - "-protocol_whitelist","file,pipe" blocks SSRF (no http/https/rtmp/etc.)
10
+ * - "-t", String(maxDurationSeconds) bounds processing time (from spec; never a literal)
11
+ * - "-dn" drop data tracks
12
+ * - "-sn" drop subtitle tracks
13
+ *
14
+ * Video additionally gets:
15
+ * - "-c:v","libx264","-c:a","aac" re-encode to safe codecs
16
+ * - "-movflags","+faststart" progressive streaming
17
+ *
18
+ * Audio-only gets:
19
+ * - "-c:a","aac" re-encode to safe codec
20
+ *
21
+ * Poster frame extraction is a SEPARATE argv (buildPosterArgs). Splitting the
22
+ * two operations keeps the main transcode deterministic and lets the poster be
23
+ * produced in a separate process without retranscoding. The poster job uses
24
+ * "-frames:v","1" to extract exactly one frame and inherits all hardening args.
25
+ *
26
+ * PURITY: no I/O, no AWS SDK, no fs, no Date.now, no Math.random. All
27
+ * operational parameters (maxDurationSeconds) arrive as function arguments
28
+ * sourced from Env.media — never as literals in this file. Ships in the PUBLIC
29
+ * npm tarball: no hard-coded operational numbers here.
30
+ */
31
+ // ---------------------------------------------------------------------------
32
+ // buildFfmpegArgs
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Build the argv for transcoding a media object.
36
+ *
37
+ * The returned array is passed to the child-process launcher directly — never
38
+ * joined into a shell string. The caller is responsible for prepending the
39
+ * ffmpeg binary path or using `execFile`/`spawn` with `shell: false`.
40
+ *
41
+ * The protocol whitelist, duration cap, and track-drop flags are always present
42
+ * in the output regardless of kind. The caller should verify these are present
43
+ * before launching (the test suite does so exhaustively).
44
+ */
45
+ export function buildFfmpegArgs(spec) {
46
+ const args = [
47
+ // SSRF prevention: accept only file: and pipe: protocols. This blocks any
48
+ // attempt to use the ffmpeg process to reach http/https/rtmp/concat/subfile
49
+ // endpoints that an in-VPC worker could reach.
50
+ "-protocol_whitelist",
51
+ "file,pipe",
52
+ // Duration cap: sourced from spec (Env.media), never a compiled literal.
53
+ "-t",
54
+ String(spec.maxDurationSeconds),
55
+ // Input
56
+ "-i",
57
+ spec.inputPath,
58
+ // Drop data tracks (camera metadata, GPS, etc.) and subtitle tracks.
59
+ // Neither is needed for the re-encoded output and both can carry payloads.
60
+ "-dn",
61
+ "-sn",
62
+ ];
63
+ if (spec.kind === "video") {
64
+ args.push("-c:v", "libx264", "-c:a", "aac",
65
+ // Enable progressive download (moov atom first in the container).
66
+ "-movflags", "+faststart");
67
+ }
68
+ else {
69
+ // audio-only
70
+ args.push("-c:a", "aac");
71
+ }
72
+ // Overwrite output without prompt — required for non-interactive subprocess.
73
+ args.push("-y", spec.outputPath);
74
+ return args;
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // buildPosterArgs
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * Build the argv for extracting a poster frame from a video.
81
+ *
82
+ * This is a SEPARATE invocation from {@link buildFfmpegArgs}: splitting the two
83
+ * means the transcode and the poster extraction run independently, the poster
84
+ * can be regenerated without retranscoding, and the transcode argv stays clean.
85
+ *
86
+ * The poster job inherits all hardening (protocol whitelist, duration cap,
87
+ * track drops) and adds:
88
+ * - "-frames:v","1" — extract exactly one video frame
89
+ * - "-an" — no audio output (image output only)
90
+ *
91
+ * Callers should only invoke this when `spec.posterPath` is defined. The
92
+ * function documents this contract via its signature: if posterPath is absent,
93
+ * the output path would be undefined and the caller must guard before spawning.
94
+ * The implementation always uses spec.posterPath so TypeScript callers can call
95
+ * with a spec that has posterPath set and get a well-formed array.
96
+ *
97
+ * @param spec - must have `posterPath` set; calling without it is a caller
98
+ * contract violation (outputPath would be undefined).
99
+ */
100
+ export function buildPosterArgs(spec) {
101
+ return [
102
+ "-protocol_whitelist",
103
+ "file,pipe",
104
+ "-t",
105
+ String(spec.maxDurationSeconds),
106
+ "-i",
107
+ spec.inputPath,
108
+ "-dn",
109
+ "-sn",
110
+ // Extract exactly one video frame.
111
+ "-frames:v",
112
+ "1",
113
+ // No audio stream in the poster image output.
114
+ "-an",
115
+ "-y",
116
+ spec.posterPath,
117
+ ];
118
+ }
119
+ //# sourceMappingURL=ffmpeg-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ffmpeg-args.js","sourceRoot":"","sources":["../../../src/lib/media/ffmpeg-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAuBH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,IAAI,GAAa;QACrB,0EAA0E;QAC1E,4EAA4E;QAC5E,+CAA+C;QAC/C,qBAAqB;QACrB,WAAW;QAEX,yEAAyE;QACzE,IAAI;QACJ,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAE/B,QAAQ;QACR,IAAI;QACJ,IAAI,CAAC,SAAS;QAEd,qEAAqE;QACrE,2EAA2E;QAC3E,KAAK;QACL,KAAK;KACN,CAAC;IAEF,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CACP,MAAM,EACN,SAAS,EACT,MAAM,EACN,KAAK;QACL,kEAAkE;QAClE,WAAW,EACX,YAAY,CACb,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,aAAa;QACb,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,6EAA6E;IAC7E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAEjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAqD;IAErD,OAAO;QACL,qBAAqB;QACrB,WAAW;QAEX,IAAI;QACJ,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAE/B,IAAI;QACJ,IAAI,CAAC,SAAS;QAEd,KAAK;QACL,KAAK;QAEL,mCAAmC;QACnC,WAAW;QACX,GAAG;QAEH,8CAA8C;QAC9C,KAAK;QAEL,IAAI;QACJ,IAAI,CAAC,UAAU;KAChB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,126 @@
1
+ export interface TranscodeVideoInput {
2
+ readonly inputPath: string;
3
+ readonly outputPath: string;
4
+ readonly posterPath: string;
5
+ /** Hard cap on accepted duration; injected from Env.media (never a literal). */
6
+ readonly maxDurationSeconds: number;
7
+ }
8
+ export interface TranscodeVideoResult {
9
+ readonly cleanedPath: string;
10
+ readonly posterPath: string;
11
+ readonly durationSeconds: number;
12
+ }
13
+ export interface TranscodeAudioInput {
14
+ readonly inputPath: string;
15
+ readonly outputPath: string;
16
+ /** Hard cap on accepted duration; injected from Env.media (never a literal). */
17
+ readonly maxDurationSeconds: number;
18
+ }
19
+ export interface TranscodeAudioResult {
20
+ readonly cleanedPath: string;
21
+ readonly durationSeconds: number;
22
+ }
23
+ export interface TranscodePort {
24
+ /** Probe the duration of an input without transcoding it. */
25
+ probeDurationSeconds(inputPath: string): Promise<number>;
26
+ /** Re-encode a video to a clean form and emit a poster frame. */
27
+ transcodeVideo(input: TranscodeVideoInput): Promise<TranscodeVideoResult>;
28
+ /** Re-encode audio to a clean form. */
29
+ transcodeAudio(input: TranscodeAudioInput): Promise<TranscodeAudioResult>;
30
+ }
31
+ export interface StoragePort {
32
+ getObject(key: string): Promise<Buffer>;
33
+ putObject(key: string, body: Buffer, contentType: string): Promise<void>;
34
+ copyObject(fromKey: string, toKey: string): Promise<void>;
35
+ deleteObject(key: string): Promise<void>;
36
+ headObject(key: string): Promise<{
37
+ exists: boolean;
38
+ }>;
39
+ }
40
+ export type TranscriptionStatus = "COMPLETED" | "FAILED" | "IN_PROGRESS";
41
+ export interface TranscribePort {
42
+ startTranscription(input: {
43
+ key: string;
44
+ jobName: string;
45
+ }): Promise<{
46
+ jobId: string;
47
+ }>;
48
+ getTranscription(jobId: string): Promise<{
49
+ status: TranscriptionStatus;
50
+ transcript?: string;
51
+ }>;
52
+ }
53
+ /**
54
+ * In-memory TranscodePort. Returns programmable durations and echoes the
55
+ * requested output/poster paths back, so the shell's path-plumbing can be
56
+ * asserted without invoking a real encoder.
57
+ *
58
+ * Determinism: a single `duration` (default 0) is returned by `probe` and by
59
+ * both transcode calls unless overridden. `transcodeVideo`/`transcodeAudio`
60
+ * never themselves enforce `maxDurationSeconds` — duration policy lives in the
61
+ * functional core (a separate caps unit), and the mock must not silently make
62
+ * that decision for it.
63
+ */
64
+ export declare class MockTranscodePort implements TranscodePort {
65
+ private duration;
66
+ /** Records of each call, for assertions. */
67
+ readonly probeCalls: string[];
68
+ readonly videoCalls: TranscodeVideoInput[];
69
+ readonly audioCalls: TranscodeAudioInput[];
70
+ constructor(opts?: {
71
+ duration?: number;
72
+ });
73
+ /** Program the duration returned by subsequent calls. */
74
+ setDuration(seconds: number): void;
75
+ probeDurationSeconds(inputPath: string): Promise<number>;
76
+ transcodeVideo(input: TranscodeVideoInput): Promise<TranscodeVideoResult>;
77
+ transcodeAudio(input: TranscodeAudioInput): Promise<TranscodeAudioResult>;
78
+ }
79
+ /**
80
+ * In-memory StoragePort backed by a Map. `headObject` reports existence from the
81
+ * map; `getObject` throws on a miss (callers must handle the miss explicitly —
82
+ * a silent empty buffer would mask bugs).
83
+ */
84
+ export declare class MockStoragePort implements StoragePort {
85
+ private readonly objects;
86
+ constructor(seed?: Record<string, Buffer>);
87
+ getObject(key: string): Promise<Buffer>;
88
+ putObject(key: string, body: Buffer, contentType: string): Promise<void>;
89
+ copyObject(fromKey: string, toKey: string): Promise<void>;
90
+ deleteObject(key: string): Promise<void>;
91
+ headObject(key: string): Promise<{
92
+ exists: boolean;
93
+ }>;
94
+ /** Test helper: read the content-type a key was stored with. */
95
+ contentTypeOf(key: string): string | undefined;
96
+ }
97
+ /**
98
+ * In-memory TranscribePort. By default a started job is immediately COMPLETED
99
+ * with an empty transcript; callers program per-job results via `setResult`.
100
+ * Job ids are a deterministic monotonic sequence.
101
+ */
102
+ export declare class MockTranscribePort implements TranscribePort {
103
+ private seq;
104
+ private readonly results;
105
+ /** Records of each start call, for assertions. */
106
+ readonly startCalls: {
107
+ key: string;
108
+ jobName: string;
109
+ }[];
110
+ startTranscription(input: {
111
+ key: string;
112
+ jobName: string;
113
+ }): Promise<{
114
+ jobId: string;
115
+ }>;
116
+ /** Program the result a given job id will report. */
117
+ setResult(jobId: string, result: {
118
+ status: TranscriptionStatus;
119
+ transcript?: string;
120
+ }): void;
121
+ getTranscription(jobId: string): Promise<{
122
+ status: TranscriptionStatus;
123
+ transcript?: string;
124
+ }>;
125
+ }
126
+ //# sourceMappingURL=media-ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-ports.d.ts","sourceRoot":"","sources":["../../../src/lib/media/media-ports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ // CONTRACT: stable — coordinate changes. Shared P0b I/O SEAM definitions.
2
+ //
3
+ // These are the capability seams the media pipeline's imperative SHELL binds to,
4
+ // mirroring the MediaModerationProvider seam discipline (./moderation-provider.ts):
5
+ // core ships the *interfaces* plus test-only Mock implementations; the consuming
6
+ // app (Skybber) injects the concrete cloud adapters (ffmpeg/MediaConvert, S3,
7
+ // Transcribe) at startup. Core imports NO cloud SDK here.
8
+ //
9
+ // IMPORTANT: this file deliberately defines ONLY the seam interfaces and their
10
+ // in-memory Mocks. The mocks are deterministic and side-effect-free w.r.t. the
11
+ // outside world (they touch only their own in-process state) so functional-core
12
+ // units can be exercised against them in property tests. Operational parameters
13
+ // (e.g. maxDurationSeconds) are *arguments*, never literals baked into these
14
+ // interfaces — this file ships in the PUBLIC npm tarball.
15
+ // ===========================================================================
16
+ // Mock implementations (test-only). Deterministic, in-memory, no outside I/O.
17
+ // ===========================================================================
18
+ /**
19
+ * In-memory TranscodePort. Returns programmable durations and echoes the
20
+ * requested output/poster paths back, so the shell's path-plumbing can be
21
+ * asserted without invoking a real encoder.
22
+ *
23
+ * Determinism: a single `duration` (default 0) is returned by `probe` and by
24
+ * both transcode calls unless overridden. `transcodeVideo`/`transcodeAudio`
25
+ * never themselves enforce `maxDurationSeconds` — duration policy lives in the
26
+ * functional core (a separate caps unit), and the mock must not silently make
27
+ * that decision for it.
28
+ */
29
+ export class MockTranscodePort {
30
+ duration;
31
+ /** Records of each call, for assertions. */
32
+ probeCalls = [];
33
+ videoCalls = [];
34
+ audioCalls = [];
35
+ constructor(opts = {}) {
36
+ this.duration = opts.duration ?? 0;
37
+ }
38
+ /** Program the duration returned by subsequent calls. */
39
+ setDuration(seconds) {
40
+ this.duration = seconds;
41
+ }
42
+ async probeDurationSeconds(inputPath) {
43
+ this.probeCalls.push(inputPath);
44
+ return this.duration;
45
+ }
46
+ async transcodeVideo(input) {
47
+ this.videoCalls.push(input);
48
+ return {
49
+ cleanedPath: input.outputPath,
50
+ posterPath: input.posterPath,
51
+ durationSeconds: this.duration,
52
+ };
53
+ }
54
+ async transcodeAudio(input) {
55
+ this.audioCalls.push(input);
56
+ return {
57
+ cleanedPath: input.outputPath,
58
+ durationSeconds: this.duration,
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * In-memory StoragePort backed by a Map. `headObject` reports existence from the
64
+ * map; `getObject` throws on a miss (callers must handle the miss explicitly —
65
+ * a silent empty buffer would mask bugs).
66
+ */
67
+ export class MockStoragePort {
68
+ objects = new Map();
69
+ constructor(seed = {}) {
70
+ for (const [key, body] of Object.entries(seed)) {
71
+ this.objects.set(key, { body, contentType: "application/octet-stream" });
72
+ }
73
+ }
74
+ async getObject(key) {
75
+ const obj = this.objects.get(key);
76
+ if (!obj) {
77
+ throw new Error(`MockStoragePort: no object at key "${key}"`);
78
+ }
79
+ return obj.body;
80
+ }
81
+ async putObject(key, body, contentType) {
82
+ this.objects.set(key, { body, contentType });
83
+ }
84
+ async copyObject(fromKey, toKey) {
85
+ const obj = this.objects.get(fromKey);
86
+ if (!obj) {
87
+ throw new Error(`MockStoragePort: no object at key "${fromKey}" to copy`);
88
+ }
89
+ this.objects.set(toKey, { body: obj.body, contentType: obj.contentType });
90
+ }
91
+ async deleteObject(key) {
92
+ this.objects.delete(key);
93
+ }
94
+ async headObject(key) {
95
+ return { exists: this.objects.has(key) };
96
+ }
97
+ /** Test helper: read the content-type a key was stored with. */
98
+ contentTypeOf(key) {
99
+ return this.objects.get(key)?.contentType;
100
+ }
101
+ }
102
+ /**
103
+ * In-memory TranscribePort. By default a started job is immediately COMPLETED
104
+ * with an empty transcript; callers program per-job results via `setResult`.
105
+ * Job ids are a deterministic monotonic sequence.
106
+ */
107
+ export class MockTranscribePort {
108
+ seq = 0;
109
+ results = new Map();
110
+ /** Records of each start call, for assertions. */
111
+ startCalls = [];
112
+ async startTranscription(input) {
113
+ this.startCalls.push(input);
114
+ this.seq += 1;
115
+ const jobId = `mock-transcribe-${this.seq}`;
116
+ if (!this.results.has(jobId)) {
117
+ this.results.set(jobId, { status: "COMPLETED", transcript: "" });
118
+ }
119
+ return { jobId };
120
+ }
121
+ /** Program the result a given job id will report. */
122
+ setResult(jobId, result) {
123
+ this.results.set(jobId, result);
124
+ }
125
+ async getTranscription(jobId) {
126
+ return this.results.get(jobId) ?? { status: "IN_PROGRESS" };
127
+ }
128
+ }
129
+ //# sourceMappingURL=media-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-ports.js","sourceRoot":"","sources":["../../../src/lib/media/media-ports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pure builder for the MediaFile upsert arguments (T9).
3
+ *
4
+ * Keeping this out of the route handler makes the dedup-safety invariant
5
+ * directly unit-testable: a within-tenant dedup hit (identical bytes
6
+ * re-uploaded) must NOT transfer ownership (`uploadedBy`) or de-publish the
7
+ * canonical row (`moderationStatus`). Subsequent uploaders get a *reference*
8
+ * (via the post→media relation), never a mutation of the shared row.
9
+ *
10
+ * The shell (media.ts) passes the result straight to
11
+ * `db.mediaFile.upsert(buildMediaUpsertArgs(...))`.
12
+ */
13
+ export interface MediaUpsertInput {
14
+ tenantId: string;
15
+ contentHash: string;
16
+ originalKey: string;
17
+ mimeType: string;
18
+ size: number;
19
+ uploadedBy: string;
20
+ width?: number;
21
+ height?: number;
22
+ duration?: number;
23
+ }
24
+ /**
25
+ * The `create`/`update`/`where` payload for `db.mediaFile.upsert`.
26
+ *
27
+ * Typed structurally (not against the generated Prisma client) so this pure
28
+ * module never depends on a regenerated client across worktrees. The shell
29
+ * passes it to Prisma, which validates the shape.
30
+ */
31
+ export interface MediaUpsertArgs {
32
+ where: {
33
+ tenantId_contentHash: {
34
+ tenantId: string;
35
+ contentHash: string;
36
+ };
37
+ };
38
+ create: {
39
+ tenantId: string;
40
+ contentHash: string;
41
+ mimeType: string;
42
+ size: number;
43
+ originalKey: string;
44
+ uploadStatus: "COMPLETE";
45
+ uploadedBy: string;
46
+ width?: number;
47
+ height?: number;
48
+ duration?: number;
49
+ };
50
+ update: {
51
+ uploadStatus: "COMPLETE";
52
+ };
53
+ }
54
+ export declare function buildMediaUpsertArgs(input: MediaUpsertInput): MediaUpsertArgs;
55
+ //# sourceMappingURL=media-upsert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/media/media-upsert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE;QAAE,oBAAoB,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3E,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,UAAU,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,MAAM,EAAE;QAIN,YAAY,EAAE,UAAU,CAAC;KAC1B,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,gBAAgB,GACtB,eAAe,CAwBjB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure builder for the MediaFile upsert arguments (T9).
3
+ *
4
+ * Keeping this out of the route handler makes the dedup-safety invariant
5
+ * directly unit-testable: a within-tenant dedup hit (identical bytes
6
+ * re-uploaded) must NOT transfer ownership (`uploadedBy`) or de-publish the
7
+ * canonical row (`moderationStatus`). Subsequent uploaders get a *reference*
8
+ * (via the post→media relation), never a mutation of the shared row.
9
+ *
10
+ * The shell (media.ts) passes the result straight to
11
+ * `db.mediaFile.upsert(buildMediaUpsertArgs(...))`.
12
+ */
13
+ export function buildMediaUpsertArgs(input) {
14
+ return {
15
+ where: {
16
+ tenantId_contentHash: {
17
+ tenantId: input.tenantId,
18
+ contentHash: input.contentHash,
19
+ },
20
+ },
21
+ create: {
22
+ tenantId: input.tenantId,
23
+ contentHash: input.contentHash,
24
+ mimeType: input.mimeType,
25
+ size: input.size,
26
+ originalKey: input.originalKey,
27
+ uploadStatus: "COMPLETE",
28
+ uploadedBy: input.uploadedBy,
29
+ width: input.width,
30
+ height: input.height,
31
+ duration: input.duration,
32
+ },
33
+ update: {
34
+ uploadStatus: "COMPLETE",
35
+ },
36
+ };
37
+ }
38
+ //# sourceMappingURL=media-upsert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-upsert.js","sourceRoot":"","sources":["../../../src/lib/media/media-upsert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA2CH,MAAM,UAAU,oBAAoB,CAClC,KAAuB;IAEvB,OAAO;QACL,KAAK,EAAE;YACL,oBAAoB,EAAE;gBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,WAAW,EAAE,KAAK,CAAC,WAAW;aAC/B;SACF;QACD,MAAM,EAAE;YACN,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB;QACD,MAAM,EAAE;YACN,YAAY,EAAE,UAAU;SACzB;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,111 @@
1
+ import type { ModerationDecision } from "./moderation-status.js";
2
+ export type { ModerationDecision };
3
+ /** An opaque reference to an already-stored image object (key + bucket handle). */
4
+ export interface ImageRef {
5
+ readonly bucket: string;
6
+ readonly key: string;
7
+ }
8
+ /** An opaque reference to an already-stored object in S3-compatible storage. */
9
+ export interface S3Ref {
10
+ readonly bucket: string;
11
+ readonly key: string;
12
+ }
13
+ /** A single classifier label. `category` is an OPAQUE token, never a real-category string. */
14
+ export interface ModerationLabel {
15
+ readonly category: string;
16
+ readonly confidence: number;
17
+ }
18
+ /**
19
+ * ModerationVerdict — the RESULT object (hub name). The `decision` is the
20
+ * 3-value classifier verdict; `labels` are opaque category tokens with
21
+ * confidences; `provider` identifies which backend produced it.
22
+ */
23
+ export interface ModerationVerdict {
24
+ readonly decision: ModerationDecision;
25
+ readonly labels: ReadonlyArray<ModerationLabel>;
26
+ readonly provider: string;
27
+ }
28
+ /**
29
+ * The one canonical moderation seam. Image moderation is sync-ish (resolves a
30
+ * verdict directly); video moderation is async (start → poll), mirroring the
31
+ * cloud provider's job model. Audio reuses the text-moderation path and adds no
32
+ * method here.
33
+ */
34
+ export interface MediaModerationProvider {
35
+ /** Synchronous-style image moderation: resolves a verdict directly. */
36
+ moderateImage(input: ImageRef): Promise<ModerationVerdict>;
37
+ /** Kicks off async video moderation; returns a handle to poll. */
38
+ startVideoModeration(input: S3Ref): Promise<{
39
+ jobId: string;
40
+ }>;
41
+ /** Polls a previously-started video moderation job for its verdict. */
42
+ getVideoModeration(jobId: string): Promise<ModerationVerdict>;
43
+ }
44
+ export type WarnSink = (message: string, data?: unknown) => void;
45
+ /**
46
+ * A verdict that fails closed: every call resolves to `review` with no labels.
47
+ * Nothing this provider returns can ever auto-approve media. Used as the safe
48
+ * default before a concrete provider is injected (dev only — see the startup
49
+ * guard below).
50
+ */
51
+ export declare class NullModerationProvider implements MediaModerationProvider {
52
+ private readonly warn;
53
+ constructor(warn?: WarnSink);
54
+ private failClosed;
55
+ moderateImage(_input: ImageRef): Promise<ModerationVerdict>;
56
+ startVideoModeration(_input: S3Ref): Promise<{
57
+ jobId: string;
58
+ }>;
59
+ getVideoModeration(_jobId: string): Promise<ModerationVerdict>;
60
+ }
61
+ /**
62
+ * Returns true for the fail-closed Null provider. The startup guard uses this to
63
+ * reject Null outside dev.
64
+ */
65
+ export declare function isNullModerationProvider(provider: MediaModerationProvider): boolean;
66
+ /**
67
+ * A test seam: returns canned verdicts on demand. Default is the fail-closed
68
+ * `review`. Labels use ONLY abstract category tokens (`category_a`,
69
+ * `category_b`); no real-category strings, no real imagery ever.
70
+ */
71
+ export declare class MockModerationProvider implements MediaModerationProvider {
72
+ private imageVerdict;
73
+ private videoVerdict;
74
+ private jobIdSeq;
75
+ constructor(canned?: {
76
+ image?: ModerationVerdict;
77
+ video?: ModerationVerdict;
78
+ });
79
+ /** Program the verdict returned by `moderateImage`. */
80
+ setImageVerdict(verdict: ModerationVerdict): void;
81
+ /** Program the verdict returned by `getVideoModeration`. */
82
+ setVideoVerdict(verdict: ModerationVerdict): void;
83
+ moderateImage(_input: ImageRef): Promise<ModerationVerdict>;
84
+ startVideoModeration(_input: S3Ref): Promise<{
85
+ jobId: string;
86
+ }>;
87
+ getVideoModeration(_jobId: string): Promise<ModerationVerdict>;
88
+ }
89
+ /** Abstract category tokens for Mock verdicts — never real-category strings. */
90
+ export declare const MOCK_CATEGORY_A = "category_a";
91
+ export declare const MOCK_CATEGORY_B = "category_b";
92
+ /**
93
+ * Error raised by the startup guard when the fail-closed Null provider would run
94
+ * outside dev. Carrying a distinct type lets the wiring fail loudly and lets
95
+ * tests assert on it.
96
+ */
97
+ export declare class NullProviderInProductionError extends Error {
98
+ constructor(environment: string);
99
+ }
100
+ /**
101
+ * Startup guard for the seam wiring. Validates that a non-Null provider is
102
+ * injected whenever `environment !== "dev"`, and throws loudly otherwise.
103
+ * Returns the provider unchanged when the check passes, so it can wrap the
104
+ * injection site directly:
105
+ *
106
+ * const provider = assertModerationProviderAllowed(injected, env.ENVIRONMENT);
107
+ *
108
+ * Fail loud, never silently run Null in prod.
109
+ */
110
+ export declare function assertModerationProviderAllowed(provider: MediaModerationProvider, environment: string): MediaModerationProvider;
111
+ //# sourceMappingURL=moderation-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-provider.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-provider.ts"],"names":[],"mappings":"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"}