@de-otio/trellis 0.11.0 → 0.12.0

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 (123) 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/routes/media.d.ts +21 -0
  104. package/dist/lib/routes/media.d.ts.map +1 -1
  105. package/dist/lib/routes/media.js +584 -483
  106. package/dist/lib/routes/media.js.map +1 -1
  107. package/dist/lib/services/image-normalizer.d.ts +64 -6
  108. package/dist/lib/services/image-normalizer.d.ts.map +1 -1
  109. package/dist/lib/services/image-normalizer.js +88 -6
  110. package/dist/lib/services/image-normalizer.js.map +1 -1
  111. package/dist/lib/services/media-upload-service.d.ts +2 -2
  112. package/dist/lib/services/media-upload-service.d.ts.map +1 -1
  113. package/dist/lib/services/media-upload-service.js +22 -21
  114. package/dist/lib/services/media-upload-service.js.map +1 -1
  115. package/dist/lib/tenant-scope.d.ts.map +1 -1
  116. package/dist/lib/tenant-scope.js +16 -1
  117. package/dist/lib/tenant-scope.js.map +1 -1
  118. package/package.json +2 -1
  119. package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
  120. package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
  121. package/prisma/schema.prisma +95 -17
  122. package/src/lambda/media-completion-worker.ts +567 -0
  123. package/src/lambda/media-processing-worker.ts +508 -59
@@ -0,0 +1,130 @@
1
+ // CONTRACT: stable — coordinate changes.
2
+ //
3
+ // The MediaModerationProvider capability seam — THE interface every media
4
+ // moderation backend binds to, mirroring the RealtimeTransport seam
5
+ // (apps/api/src/lib/realtime/). Core ships the interface plus a fail-closed
6
+ // Null provider and a test-only Mock; the consuming app (Skybber) injects the
7
+ // concrete cloud adapter at startup. Core imports no cloud SDK.
8
+ //
9
+ // Binding rules for every provider implementor:
10
+ // - A verdict carries a 3-value `decision` (approved | review | quarantine).
11
+ // `rejected` is a lifecycle state reached only by human/CSAM action, never a
12
+ // provider decision (see ModerationStatus state machine).
13
+ // - Absence of signal, an internal fault, or any uncertainty MUST fail closed
14
+ // to `review`. A provider must NEVER manufacture `approved` from doubt.
15
+ // - This interface ships in the PUBLIC npm tarball: NO thresholds, secrets, or
16
+ // real-category vocabulary live here. Labels carry opaque category tokens;
17
+ // the operative numeric thresholds are injected via Env (config), not code.
18
+ // - CSAM detection is deliberately NOT on this interface — it is a separate,
19
+ // statutory provider with preserve-and-report duties.
20
+ // - Refs are opaque (key + bucket handle), never raw image/video bytes.
21
+ const NULL_PROVIDER_NAME = "null";
22
+ const NULL_PROVIDER_WARNING = "[NullModerationProvider] No moderation backend injected — failing closed to" +
23
+ ' decision="review". Media will NOT auto-approve. Inject a real provider in' +
24
+ " any non-dev environment.";
25
+ /**
26
+ * A verdict that fails closed: every call resolves to `review` with no labels.
27
+ * Nothing this provider returns can ever auto-approve media. Used as the safe
28
+ * default before a concrete provider is injected (dev only — see the startup
29
+ * guard below).
30
+ */
31
+ export class NullModerationProvider {
32
+ warn;
33
+ constructor(warn = (msg, data) => console.warn(msg, data)) {
34
+ this.warn = warn;
35
+ }
36
+ failClosed() {
37
+ this.warn(NULL_PROVIDER_WARNING);
38
+ return { decision: "review", labels: [], provider: NULL_PROVIDER_NAME };
39
+ }
40
+ async moderateImage(_input) {
41
+ return this.failClosed();
42
+ }
43
+ async startVideoModeration(_input) {
44
+ // Even starting a job warns: there is no backend to do the work.
45
+ this.warn(NULL_PROVIDER_WARNING);
46
+ return { jobId: `${NULL_PROVIDER_NAME}-noop` };
47
+ }
48
+ async getVideoModeration(_jobId) {
49
+ return this.failClosed();
50
+ }
51
+ }
52
+ /**
53
+ * Returns true for the fail-closed Null provider. The startup guard uses this to
54
+ * reject Null outside dev.
55
+ */
56
+ export function isNullModerationProvider(provider) {
57
+ return provider instanceof NullModerationProvider;
58
+ }
59
+ const MOCK_PROVIDER_NAME = "mock";
60
+ const MOCK_DEFAULT_VERDICT = {
61
+ decision: "review",
62
+ labels: [],
63
+ provider: MOCK_PROVIDER_NAME,
64
+ };
65
+ /**
66
+ * A test seam: returns canned verdicts on demand. Default is the fail-closed
67
+ * `review`. Labels use ONLY abstract category tokens (`category_a`,
68
+ * `category_b`); no real-category strings, no real imagery ever.
69
+ */
70
+ export class MockModerationProvider {
71
+ imageVerdict;
72
+ videoVerdict;
73
+ jobIdSeq = 0;
74
+ constructor(canned = {}) {
75
+ this.imageVerdict = canned.image ?? MOCK_DEFAULT_VERDICT;
76
+ this.videoVerdict = canned.video ?? MOCK_DEFAULT_VERDICT;
77
+ }
78
+ /** Program the verdict returned by `moderateImage`. */
79
+ setImageVerdict(verdict) {
80
+ this.imageVerdict = verdict;
81
+ }
82
+ /** Program the verdict returned by `getVideoModeration`. */
83
+ setVideoVerdict(verdict) {
84
+ this.videoVerdict = verdict;
85
+ }
86
+ async moderateImage(_input) {
87
+ return this.imageVerdict;
88
+ }
89
+ async startVideoModeration(_input) {
90
+ this.jobIdSeq += 1;
91
+ return { jobId: `${MOCK_PROVIDER_NAME}-job-${this.jobIdSeq}` };
92
+ }
93
+ async getVideoModeration(_jobId) {
94
+ return this.videoVerdict;
95
+ }
96
+ }
97
+ /** Abstract category tokens for Mock verdicts — never real-category strings. */
98
+ export const MOCK_CATEGORY_A = "category_a";
99
+ export const MOCK_CATEGORY_B = "category_b";
100
+ /**
101
+ * Error raised by the startup guard when the fail-closed Null provider would run
102
+ * outside dev. Carrying a distinct type lets the wiring fail loudly and lets
103
+ * tests assert on it.
104
+ */
105
+ export class NullProviderInProductionError extends Error {
106
+ constructor(environment) {
107
+ super(`NullModerationProvider must not run in environment "${environment}" — a` +
108
+ " real moderation provider must be injected outside dev. Refusing to" +
109
+ " start: an un-moderated, fail-closed backend in production silently" +
110
+ " sends all media to review with no path to approval.");
111
+ this.name = "NullProviderInProductionError";
112
+ }
113
+ }
114
+ /**
115
+ * Startup guard for the seam wiring. Validates that a non-Null provider is
116
+ * injected whenever `environment !== "dev"`, and throws loudly otherwise.
117
+ * Returns the provider unchanged when the check passes, so it can wrap the
118
+ * injection site directly:
119
+ *
120
+ * const provider = assertModerationProviderAllowed(injected, env.ENVIRONMENT);
121
+ *
122
+ * Fail loud, never silently run Null in prod.
123
+ */
124
+ export function assertModerationProviderAllowed(provider, environment) {
125
+ if (environment !== "dev" && isNullModerationProvider(provider)) {
126
+ throw new NullProviderInProductionError(environment);
127
+ }
128
+ return provider;
129
+ }
130
+ //# sourceMappingURL=moderation-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-provider.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Anti-oracle resolved payload for media moderation.
3
+ *
4
+ * When the moderation pipeline settles, downstream consumers (CDN gate,
5
+ * client notifications) need exactly one bit of information: is this
6
+ * media object ready to serve? They must NOT receive the verdict, labels,
7
+ * confidence score, or any other signal that lets a caller infer why a
8
+ * piece of content was held back — that would publish operational
9
+ * thresholds and give bad actors a tuning signal.
10
+ *
11
+ * This module enforces the anti-oracle contract at the type level: the
12
+ * payload type has exactly two fields and the constructor function accepts
13
+ * a full ModerationStatus but discards everything except the binary
14
+ * ready/not-ready distinction.
15
+ *
16
+ * Pure functional core: no I/O, no clock, no randomness. Deterministic
17
+ * in => deterministic out.
18
+ */
19
+ import type { ModerationStatus } from "./moderation-status.js";
20
+ /**
21
+ * The only view of a moderation outcome that leaves the moderation domain.
22
+ *
23
+ * Invariants enforced by the type (structural, not just convention):
24
+ * - Exactly two keys: `mediaId` and `status`. No third key is possible
25
+ * without a type error — there is no `decision`, `labels`, `confidence`,
26
+ * `reason`, or per-track field.
27
+ * - `status` is a binary flag: `"ready"` or `"not-ready"`. The caller
28
+ * cannot distinguish PENDING from REVIEW from QUARANTINED from REJECTED —
29
+ * all four collapse to `"not-ready"`.
30
+ */
31
+ export interface ModerationResolvedPayload {
32
+ readonly mediaId: string;
33
+ readonly status: "ready" | "not-ready";
34
+ }
35
+ /**
36
+ * Construct the anti-oracle resolved payload from a full ModerationStatus.
37
+ *
38
+ * `"ready"` if and only if `status === "APPROVED"`. Every other status —
39
+ * `PENDING`, `REVIEW`, `QUARANTINED`, `REJECTED` — maps to `"not-ready"`.
40
+ * The asymmetry is intentional and fail-closed: uncertainty never yields
41
+ * `"ready"`.
42
+ *
43
+ * @param mediaId - The stable identifier of the media object.
44
+ * @param status - The settled ModerationStatus (sourced from Env.media /
45
+ * the moderation state machine; never hard-coded here).
46
+ */
47
+ export declare function moderationResolvedPayload(mediaId: string, status: ModerationStatus): ModerationResolvedPayload;
48
+ //# sourceMappingURL=moderation-resolved-payload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-resolved-payload.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-resolved-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;CACxC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,gBAAgB,GACvB,yBAAyB,CAK3B"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Anti-oracle resolved payload for media moderation.
3
+ *
4
+ * When the moderation pipeline settles, downstream consumers (CDN gate,
5
+ * client notifications) need exactly one bit of information: is this
6
+ * media object ready to serve? They must NOT receive the verdict, labels,
7
+ * confidence score, or any other signal that lets a caller infer why a
8
+ * piece of content was held back — that would publish operational
9
+ * thresholds and give bad actors a tuning signal.
10
+ *
11
+ * This module enforces the anti-oracle contract at the type level: the
12
+ * payload type has exactly two fields and the constructor function accepts
13
+ * a full ModerationStatus but discards everything except the binary
14
+ * ready/not-ready distinction.
15
+ *
16
+ * Pure functional core: no I/O, no clock, no randomness. Deterministic
17
+ * in => deterministic out.
18
+ */
19
+ /**
20
+ * Construct the anti-oracle resolved payload from a full ModerationStatus.
21
+ *
22
+ * `"ready"` if and only if `status === "APPROVED"`. Every other status —
23
+ * `PENDING`, `REVIEW`, `QUARANTINED`, `REJECTED` — maps to `"not-ready"`.
24
+ * The asymmetry is intentional and fail-closed: uncertainty never yields
25
+ * `"ready"`.
26
+ *
27
+ * @param mediaId - The stable identifier of the media object.
28
+ * @param status - The settled ModerationStatus (sourced from Env.media /
29
+ * the moderation state machine; never hard-coded here).
30
+ */
31
+ export function moderationResolvedPayload(mediaId, status) {
32
+ return {
33
+ mediaId,
34
+ status: status === "APPROVED" ? "ready" : "not-ready",
35
+ };
36
+ }
37
+ //# sourceMappingURL=moderation-resolved-payload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-resolved-payload.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-resolved-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAoBH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAe,EACf,MAAwB;IAExB,OAAO;QACL,OAAO;QACP,MAAM,EAAE,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW;KACtD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Media moderation lifecycle: status union, classifier-decision union, and the
3
+ * pure `nextStatus()` state machine.
4
+ *
5
+ * This module is the **hand-written source of truth** for the moderation
6
+ * lifecycle states. It deliberately has **zero dependency on the
7
+ * Prisma-generated client** so the serve gate and key-adoption code can compile
8
+ * in worktrees that have not regenerated the client. The Prisma
9
+ * `enum ModerationStatus` mirrors {@link ModerationStatus} exactly; the
10
+ * imperative shell maps `Prisma.MediaFile.moderationStatus -> this union` at the
11
+ * I/O boundary.
12
+ *
13
+ * Pure functional core: no I/O, no clock, no Prisma import. Exhaustively
14
+ * property-tested.
15
+ */
16
+ /**
17
+ * The lifecycle state of a media object's moderation, persisted as
18
+ * `MediaFile.moderationStatus`. Hand-written source of truth — the Prisma enum
19
+ * must match this union member-for-member.
20
+ *
21
+ * - `PENDING` — born here; nothing serves until a verdict moves it forward.
22
+ * - `APPROVED` — the only state that serves bytes (the gate is APPROVED-only).
23
+ * - `REVIEW` — classifier was uncertain; awaiting a human moderator.
24
+ * - `QUARANTINED` — classifier flagged it; awaiting a human moderator.
25
+ * - `REJECTED` — terminal; never serves.
26
+ */
27
+ export type ModerationStatus = "PENDING" | "APPROVED" | "REVIEW" | "QUARANTINED" | "REJECTED";
28
+ /**
29
+ * The 3-value classifier verdict (the `decision` field of a moderation
30
+ * provider's result). This is intentionally **not** 4-value: `rejected` is a
31
+ * lifecycle *status* a human (or the CSAM provider) produces, never a classifier
32
+ * decision.
33
+ */
34
+ export type ModerationDecision = "approved" | "review" | "quarantine";
35
+ /**
36
+ * Events that drive a `moderationStatus` transition.
37
+ *
38
+ * - `decision` — the classifier/worker verdict on a `PENDING` object.
39
+ * - `human` — a human moderator resolving a `REVIEW`/`QUARANTINED` object.
40
+ * - `csam` — a confirmed hit from the separate (statutory) CSAM provider;
41
+ * drives `-> REJECTED` from *any* state, with the preserve-and-
42
+ * report duty handled by the shell (a human checkpoint).
43
+ */
44
+ export type ModerationEvent = {
45
+ readonly kind: "decision";
46
+ readonly decision: ModerationDecision;
47
+ } | {
48
+ readonly kind: "human";
49
+ readonly action: "approve" | "reject";
50
+ } | {
51
+ readonly kind: "csam";
52
+ };
53
+ /**
54
+ * A transition that the state machine refuses. Returned (never thrown) so the
55
+ * machine stays a pure total function and callers must handle the illegal case
56
+ * explicitly — an illegal transition must **never** silently no-op into
57
+ * `APPROVED`.
58
+ */
59
+ export type IllegalTransition = {
60
+ readonly ok: false;
61
+ readonly reason: "illegal-transition";
62
+ readonly from: ModerationStatus;
63
+ readonly event: ModerationEvent;
64
+ };
65
+ /** A successful transition to a next status. */
66
+ export type TransitionResult = {
67
+ readonly ok: true;
68
+ readonly status: ModerationStatus;
69
+ } | IllegalTransition;
70
+ /**
71
+ * The pure moderation state machine.
72
+ *
73
+ * Transitions (everything else is an {@link IllegalTransition}):
74
+ *
75
+ * ```
76
+ * PENDING --decision approved--> APPROVED
77
+ * PENDING --decision review--> REVIEW
78
+ * PENDING --decision quarantine--> QUARANTINED
79
+ * REVIEW|QUARANTINED --human approve--> APPROVED
80
+ * REVIEW|QUARANTINED --human reject--> REJECTED
81
+ * (any) --csam--> REJECTED (terminal; from any state)
82
+ * ```
83
+ *
84
+ * Invariants (property-tested):
85
+ * - `APPROVED` and `REJECTED` are absorbing under non-CSAM events.
86
+ * - A CSAM event drives any state to `REJECTED`.
87
+ * - `QUARANTINED`/`REJECTED` never reach `APPROVED` without a human approve.
88
+ * - An illegal transition is reported, never coerced to `APPROVED`.
89
+ *
90
+ * @param current the current persisted status
91
+ * @param event the driving event
92
+ */
93
+ export declare function nextStatus(current: ModerationStatus, event: ModerationEvent): TransitionResult;
94
+ /** All lifecycle states, for exhaustive iteration in tests and the shell. */
95
+ export declare const ALL_MODERATION_STATUSES: readonly ModerationStatus[];
96
+ /** All classifier decisions, for exhaustive iteration in tests and the shell. */
97
+ export declare const ALL_MODERATION_DECISIONS: readonly ModerationDecision[];
98
+ //# sourceMappingURL=moderation-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-status.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,CAAC;AAEf;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACxD,iBAAiB,CAAC;AAwBtB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,eAAe,GACrB,gBAAgB,CAoClB;AA4BD,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAExC,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,SAAS,kBAAkB,EAIxD,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Media moderation lifecycle: status union, classifier-decision union, and the
3
+ * pure `nextStatus()` state machine.
4
+ *
5
+ * This module is the **hand-written source of truth** for the moderation
6
+ * lifecycle states. It deliberately has **zero dependency on the
7
+ * Prisma-generated client** so the serve gate and key-adoption code can compile
8
+ * in worktrees that have not regenerated the client. The Prisma
9
+ * `enum ModerationStatus` mirrors {@link ModerationStatus} exactly; the
10
+ * imperative shell maps `Prisma.MediaFile.moderationStatus -> this union` at the
11
+ * I/O boundary.
12
+ *
13
+ * Pure functional core: no I/O, no clock, no Prisma import. Exhaustively
14
+ * property-tested.
15
+ */
16
+ /**
17
+ * Map a classifier decision to the status it drives a `PENDING` object into.
18
+ *
19
+ * Fail-closed: any value that is not one of the three known decisions resolves
20
+ * to `REVIEW` (never `APPROVED`). This is the only place the union is widened —
21
+ * callers at the I/O boundary may hand us an unexpected provider string, and we
22
+ * must degrade to human review, not to serving.
23
+ */
24
+ function statusForDecision(decision) {
25
+ switch (decision) {
26
+ case "approved":
27
+ return "APPROVED";
28
+ case "review":
29
+ return "REVIEW";
30
+ case "quarantine":
31
+ return "QUARANTINED";
32
+ default:
33
+ // Unknown/unexpected decision => fail closed to human review.
34
+ return "REVIEW";
35
+ }
36
+ }
37
+ /**
38
+ * The pure moderation state machine.
39
+ *
40
+ * Transitions (everything else is an {@link IllegalTransition}):
41
+ *
42
+ * ```
43
+ * PENDING --decision approved--> APPROVED
44
+ * PENDING --decision review--> REVIEW
45
+ * PENDING --decision quarantine--> QUARANTINED
46
+ * REVIEW|QUARANTINED --human approve--> APPROVED
47
+ * REVIEW|QUARANTINED --human reject--> REJECTED
48
+ * (any) --csam--> REJECTED (terminal; from any state)
49
+ * ```
50
+ *
51
+ * Invariants (property-tested):
52
+ * - `APPROVED` and `REJECTED` are absorbing under non-CSAM events.
53
+ * - A CSAM event drives any state to `REJECTED`.
54
+ * - `QUARANTINED`/`REJECTED` never reach `APPROVED` without a human approve.
55
+ * - An illegal transition is reported, never coerced to `APPROVED`.
56
+ *
57
+ * @param current the current persisted status
58
+ * @param event the driving event
59
+ */
60
+ export function nextStatus(current, event) {
61
+ // CSAM is terminal from any state — checked first so it overrides every
62
+ // absorbing/illegal rule below.
63
+ if (event.kind === "csam") {
64
+ return { ok: true, status: "REJECTED" };
65
+ }
66
+ switch (current) {
67
+ case "PENDING": {
68
+ // Only the classifier acts on a freshly-uploaded object.
69
+ if (event.kind === "decision") {
70
+ return { ok: true, status: statusForDecision(event.decision) };
71
+ }
72
+ return illegal(current, event);
73
+ }
74
+ case "REVIEW":
75
+ case "QUARANTINED": {
76
+ // Only a human moderator resolves these.
77
+ if (event.kind === "human") {
78
+ return {
79
+ ok: true,
80
+ status: event.action === "approve" ? "APPROVED" : "REJECTED",
81
+ };
82
+ }
83
+ return illegal(current, event);
84
+ }
85
+ case "APPROVED":
86
+ case "REJECTED":
87
+ // Absorbing under any non-CSAM event.
88
+ return illegal(current, event);
89
+ default:
90
+ return illegal(current, event);
91
+ }
92
+ }
93
+ function illegal(from, event) {
94
+ return { ok: false, reason: "illegal-transition", from, event };
95
+ }
96
+ /**
97
+ * Compile-time exhaustiveness guard for {@link ModerationStatus}.
98
+ *
99
+ * Keyed by the union, so adding a member to `ModerationStatus` without adding it
100
+ * here is a TYPE error (TS2741 "missing key"), and a stale key that is no longer
101
+ * a union member is a TYPE error too. This makes it impossible for
102
+ * {@link ALL_MODERATION_STATUSES} below to silently drift from the union — a
103
+ * drift that would let a new, un-enumerated status bypass the exhaustive
104
+ * anti-oracle property tests (which iterate this array). Pure type-level, no
105
+ * behavior.
106
+ */
107
+ const MODERATION_STATUS_MEMBERS = {
108
+ PENDING: true,
109
+ APPROVED: true,
110
+ REVIEW: true,
111
+ QUARANTINED: true,
112
+ REJECTED: true,
113
+ };
114
+ /** All lifecycle states, for exhaustive iteration in tests and the shell. */
115
+ export const ALL_MODERATION_STATUSES = Object.keys(MODERATION_STATUS_MEMBERS);
116
+ /** All classifier decisions, for exhaustive iteration in tests and the shell. */
117
+ export const ALL_MODERATION_DECISIONS = [
118
+ "approved",
119
+ "review",
120
+ "quarantine",
121
+ ];
122
+ //# sourceMappingURL=moderation-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation-status.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4DH;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,QAA4B;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,aAAa,CAAC;QACvB;YACE,8DAA8D;YAC9D,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,UAAU,CACxB,OAAyB,EACzB,KAAsB;IAEtB,wEAAwE;IACxE,gCAAgC;IAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,yDAAyD;YACzD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjE,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,yCAAyC;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,OAAO;oBACL,EAAE,EAAE,IAAI;oBACR,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;iBAC7D,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU;YACb,sCAAsC;YACtC,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAEjC;YACE,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CACd,IAAsB,EACtB,KAAsB;IAEtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,yBAAyB,GAAG;IAChC,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;CACmC,CAAC;AAEpD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAgC,MAAM,CAAC,IAAI,CAC7E,yBAAyB,CACJ,CAAC;AAExB,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAkC;IACrE,UAAU;IACV,QAAQ;IACR,YAAY;CACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { Track } from "./track-verdict.js";
2
+ /**
3
+ * A unit of work the processing worker dequeues for one uploaded object.
4
+ *
5
+ * - `tenantId` — owning tenant (CUID; validated by ./cas-keys.ts before use).
6
+ * - `uploadId` — the upload session id (CUID).
7
+ * - `key` — the staging/pending storage key the bytes currently live at
8
+ * (built ONLY via ./cas-keys.ts `pendingKey`).
9
+ */
10
+ export interface ProcessingJob {
11
+ readonly tenantId: string;
12
+ readonly uploadId: string;
13
+ readonly key: string;
14
+ }
15
+ /**
16
+ * The outcome of processing one object.
17
+ *
18
+ * - `casKey` — the canonical content-addressed key the cleaned bytes were
19
+ * stored at (built via ./cas-keys.ts `casKey`).
20
+ * - `contentHash` — the 64-char lowercase SHA-256 of the cleaned bytes (the CAS
21
+ * address; validated by ./cas-keys.ts `validateContentHash`).
22
+ * - `visualJobId` — async moderation job handle for the VISUAL track, when a
23
+ * visual track was started; absent for audio-only objects.
24
+ * - `audioJobId` — async transcription/moderation handle for the AUDIO track,
25
+ * when an audio track was started; absent for objects with no
26
+ * audio.
27
+ */
28
+ export interface ProcessingResult {
29
+ readonly casKey: string;
30
+ readonly contentHash: string;
31
+ readonly visualJobId?: string;
32
+ readonly audioJobId?: string;
33
+ }
34
+ /**
35
+ * Input to the per-track dedupe key the worker uses to idempotently fan out /
36
+ * fan in async track jobs without double-processing. Keyed by the content hash
37
+ * (so identical bytes share work), the provider/transcription job id, and which
38
+ * track it is — so the VISUAL and AUDIO tracks of the same bytes never collide.
39
+ */
40
+ export interface DedupeKeyInput {
41
+ readonly contentHash: string;
42
+ readonly jobId: string;
43
+ readonly track: Track;
44
+ }
45
+ //# sourceMappingURL=processing-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processing-types.d.ts","sourceRoot":"","sources":["../../../src/lib/media/processing-types.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB"}
@@ -0,0 +1,9 @@
1
+ // CONTRACT: stable — coordinate changes. Shared P0b processing value types.
2
+ //
3
+ // Pure data contracts that thread a media object through the processing
4
+ // pipeline: the job the worker dequeues, the result it produces, and the input
5
+ // to the per-track dedupe key. NO behavior here — just the shapes the shell and
6
+ // the functional-core units agree on. Ships in the PUBLIC npm tarball: no
7
+ // thresholds, secrets, or real-category vocabulary.
8
+ export {};
9
+ //# sourceMappingURL=processing-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processing-types.js","sourceRoot":"","sources":["../../../src/lib/media/processing-types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,gFAAgF;AAChF,0EAA0E;AAC1E,oDAAoD"}
@@ -0,0 +1,64 @@
1
+ import { type TrackOutcome } from "./track-verdict.js";
2
+ import { type ModerationDecision, type ModerationStatus, type TransitionResult } from "./moderation-status.js";
3
+ /**
4
+ * Everything {@link decidePromotion} needs to decide the worker's action.
5
+ *
6
+ * - `visual` / `audio` — the per-track outcomes (see {@link TrackOutcome}).
7
+ * - `currentStatus` — the object's persisted moderation status *before* this
8
+ * decision is applied. Used to detect replay / illegal
9
+ * transitions (terminal states absorb).
10
+ * - `casObjectPresent` — whether the content-addressed object actually exists in
11
+ * durable storage. Approval alone must not serve bytes
12
+ * that are not there; promotion is gated on presence.
13
+ */
14
+ export interface PromotionInput {
15
+ readonly visual: TrackOutcome;
16
+ readonly audio: TrackOutcome;
17
+ readonly currentStatus: ModerationStatus;
18
+ readonly casObjectPresent: boolean;
19
+ }
20
+ /**
21
+ * The worker's decided action. All side effects are reported as booleans; the
22
+ * shell performs them in the fixed order promote -> persist -> emit.
23
+ *
24
+ * - `combined` — the object-level decision from {@link combineTrackVerdicts}.
25
+ * - `transition` — the result of running `combined` through the state
26
+ * machine from `currentStatus`. `ok:false` means the
27
+ * transition is illegal (e.g. replay on a terminal
28
+ * status) and the action is a no-op.
29
+ * - `shouldPromote` — adopt/publish the CAS object so it can serve. True
30
+ * IFF the transition is legal AND lands on APPROVED AND
31
+ * the CAS object is present.
32
+ * - `shouldPersistStatus` — write the new status. True IFF the transition is legal.
33
+ * - `shouldEmitResolved` — emit the "resolved" event. True IFF the transition is legal.
34
+ */
35
+ export interface PromotionAction {
36
+ readonly combined: ModerationDecision;
37
+ readonly transition: TransitionResult;
38
+ readonly shouldPromote: boolean;
39
+ readonly shouldPersistStatus: boolean;
40
+ readonly shouldEmitResolved: boolean;
41
+ }
42
+ /**
43
+ * Decide the worker's action for a media object whose tracks have both resolved.
44
+ *
45
+ * Logic (total):
46
+ * 1. `combined = combineTrackVerdicts(visual, audio)` — fail-closed track join.
47
+ * 2. `transition = nextStatus(currentStatus, { kind: "decision", decision: combined })`.
48
+ * 3. If `transition.ok === false` (illegal — e.g. replay on a terminal status):
49
+ * idempotent NO-OP — `shouldPromote`/`shouldPersistStatus`/`shouldEmitResolved`
50
+ * are all `false`.
51
+ * 4. If `transition.ok === true`:
52
+ * - `shouldPersistStatus = true`
53
+ * - `shouldEmitResolved = true`
54
+ * - `shouldPromote = transition.status === "APPROVED" && casObjectPresent`
55
+ *
56
+ * Safety invariants (property-tested):
57
+ * - `shouldPromote === true` ⇒ BOTH tracks were decided-and-approved AND the
58
+ * resulting status is APPROVED. Promotion never happens from doubt.
59
+ * - APPROVED is never reached unless both tracks are decided-approved.
60
+ * - A replay/illegal transition on a terminal status yields an all-false no-op.
61
+ * - `casObjectPresent === false` ⇒ `shouldPromote === false`, even at APPROVED.
62
+ */
63
+ export declare function decidePromotion(input: PromotionInput): PromotionAction;
64
+ //# sourceMappingURL=promote-decision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promote-decision.d.ts","sourceRoot":"","sources":["../../../src/lib/media/promote-decision.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAiCtE"}