@de-otio/trellis 0.10.11 → 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 (199) hide show
  1. package/dist/env.d.ts +232 -0
  2. package/dist/env.d.ts.map +1 -1
  3. package/dist/env.js +221 -0
  4. package/dist/env.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/lambda/media-completion-worker.d.ts +175 -0
  10. package/dist/lambda/media-completion-worker.d.ts.map +1 -0
  11. package/dist/lambda/media-completion-worker.js +373 -0
  12. package/dist/lambda/media-completion-worker.js.map +1 -0
  13. package/dist/lambda/media-processing-worker.d.ts +172 -1
  14. package/dist/lambda/media-processing-worker.d.ts.map +1 -1
  15. package/dist/lambda/media-processing-worker.js +343 -49
  16. package/dist/lambda/media-processing-worker.js.map +1 -1
  17. package/dist/lib/app.d.ts.map +1 -1
  18. package/dist/lib/app.js +5 -0
  19. package/dist/lib/app.js.map +1 -1
  20. package/dist/lib/encrypted-settings/config.d.ts +13 -0
  21. package/dist/lib/encrypted-settings/config.d.ts.map +1 -0
  22. package/dist/lib/encrypted-settings/config.js +19 -0
  23. package/dist/lib/encrypted-settings/config.js.map +1 -0
  24. package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts +57 -0
  25. package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts.map +1 -0
  26. package/dist/lib/encrypted-settings/encrypted-settings-handler.js +178 -0
  27. package/dist/lib/encrypted-settings/encrypted-settings-handler.js.map +1 -0
  28. package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts +110 -0
  29. package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts.map +1 -0
  30. package/dist/lib/encrypted-settings/encrypted-settings-store.js +103 -0
  31. package/dist/lib/encrypted-settings/encrypted-settings-store.js.map +1 -0
  32. package/dist/lib/encrypted-settings/types.d.ts +26 -0
  33. package/dist/lib/encrypted-settings/types.d.ts.map +1 -0
  34. package/dist/lib/encrypted-settings/types.js +27 -0
  35. package/dist/lib/encrypted-settings/types.js.map +1 -0
  36. package/dist/lib/exif-stripper.d.ts +37 -22
  37. package/dist/lib/exif-stripper.d.ts.map +1 -1
  38. package/dist/lib/exif-stripper.js +101 -41
  39. package/dist/lib/exif-stripper.js.map +1 -1
  40. package/dist/lib/media/cas-keys.d.ts +63 -0
  41. package/dist/lib/media/cas-keys.d.ts.map +1 -0
  42. package/dist/lib/media/cas-keys.js +102 -0
  43. package/dist/lib/media/cas-keys.js.map +1 -0
  44. package/dist/lib/media/classify-worker-error.d.ts +48 -0
  45. package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
  46. package/dist/lib/media/classify-worker-error.js +319 -0
  47. package/dist/lib/media/classify-worker-error.js.map +1 -0
  48. package/dist/lib/media/dedupe-key.d.ts +29 -0
  49. package/dist/lib/media/dedupe-key.d.ts.map +1 -0
  50. package/dist/lib/media/dedupe-key.js +49 -0
  51. package/dist/lib/media/dedupe-key.js.map +1 -0
  52. package/dist/lib/media/duration-cap.d.ts +30 -0
  53. package/dist/lib/media/duration-cap.d.ts.map +1 -0
  54. package/dist/lib/media/duration-cap.js +37 -0
  55. package/dist/lib/media/duration-cap.js.map +1 -0
  56. package/dist/lib/media/ffmpeg-args.d.ts +83 -0
  57. package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
  58. package/dist/lib/media/ffmpeg-args.js +119 -0
  59. package/dist/lib/media/ffmpeg-args.js.map +1 -0
  60. package/dist/lib/media/media-ports.d.ts +126 -0
  61. package/dist/lib/media/media-ports.d.ts.map +1 -0
  62. package/dist/lib/media/media-ports.js +129 -0
  63. package/dist/lib/media/media-ports.js.map +1 -0
  64. package/dist/lib/media/media-upsert.d.ts +55 -0
  65. package/dist/lib/media/media-upsert.d.ts.map +1 -0
  66. package/dist/lib/media/media-upsert.js +38 -0
  67. package/dist/lib/media/media-upsert.js.map +1 -0
  68. package/dist/lib/media/moderation-provider.d.ts +111 -0
  69. package/dist/lib/media/moderation-provider.d.ts.map +1 -0
  70. package/dist/lib/media/moderation-provider.js +130 -0
  71. package/dist/lib/media/moderation-provider.js.map +1 -0
  72. package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
  73. package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
  74. package/dist/lib/media/moderation-resolved-payload.js +37 -0
  75. package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
  76. package/dist/lib/media/moderation-status.d.ts +98 -0
  77. package/dist/lib/media/moderation-status.d.ts.map +1 -0
  78. package/dist/lib/media/moderation-status.js +122 -0
  79. package/dist/lib/media/moderation-status.js.map +1 -0
  80. package/dist/lib/media/processing-types.d.ts +45 -0
  81. package/dist/lib/media/processing-types.d.ts.map +1 -0
  82. package/dist/lib/media/processing-types.js +9 -0
  83. package/dist/lib/media/processing-types.js.map +1 -0
  84. package/dist/lib/media/promote-decision.d.ts +64 -0
  85. package/dist/lib/media/promote-decision.d.ts.map +1 -0
  86. package/dist/lib/media/promote-decision.js +76 -0
  87. package/dist/lib/media/promote-decision.js.map +1 -0
  88. package/dist/lib/media/quota-check.d.ts +22 -0
  89. package/dist/lib/media/quota-check.d.ts.map +1 -0
  90. package/dist/lib/media/quota-check.js +42 -0
  91. package/dist/lib/media/quota-check.js.map +1 -0
  92. package/dist/lib/media/quota-types.d.ts +15 -0
  93. package/dist/lib/media/quota-types.d.ts.map +1 -0
  94. package/dist/lib/media/quota-types.js +9 -0
  95. package/dist/lib/media/quota-types.js.map +1 -0
  96. package/dist/lib/media/route-upload.d.ts +58 -0
  97. package/dist/lib/media/route-upload.d.ts.map +1 -0
  98. package/dist/lib/media/route-upload.js +80 -0
  99. package/dist/lib/media/route-upload.js.map +1 -0
  100. package/dist/lib/media/serve-gate.d.ts +51 -0
  101. package/dist/lib/media/serve-gate.d.ts.map +1 -0
  102. package/dist/lib/media/serve-gate.js +68 -0
  103. package/dist/lib/media/serve-gate.js.map +1 -0
  104. package/dist/lib/media/tenant-resolution.d.ts +42 -0
  105. package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
  106. package/dist/lib/media/tenant-resolution.js +45 -0
  107. package/dist/lib/media/tenant-resolution.js.map +1 -0
  108. package/dist/lib/media/text-moderation.d.ts +28 -0
  109. package/dist/lib/media/text-moderation.d.ts.map +1 -0
  110. package/dist/lib/media/text-moderation.js +62 -0
  111. package/dist/lib/media/text-moderation.js.map +1 -0
  112. package/dist/lib/media/track-verdict.d.ts +45 -0
  113. package/dist/lib/media/track-verdict.d.ts.map +1 -0
  114. package/dist/lib/media/track-verdict.js +52 -0
  115. package/dist/lib/media/track-verdict.js.map +1 -0
  116. package/dist/lib/media/transcript-moderation.d.ts +47 -0
  117. package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
  118. package/dist/lib/media/transcript-moderation.js +70 -0
  119. package/dist/lib/media/transcript-moderation.js.map +1 -0
  120. package/dist/lib/media-handler.d.ts.map +1 -1
  121. package/dist/lib/media-handler.js +15 -9
  122. package/dist/lib/media-handler.js.map +1 -1
  123. package/dist/lib/notification-handler.d.ts +11 -4
  124. package/dist/lib/notification-handler.d.ts.map +1 -1
  125. package/dist/lib/notification-handler.js +161 -29
  126. package/dist/lib/notification-handler.js.map +1 -1
  127. package/dist/lib/post-handler.d.ts.map +1 -1
  128. package/dist/lib/post-handler.js +4 -1
  129. package/dist/lib/post-handler.js.map +1 -1
  130. package/dist/lib/realtime/block-store.d.ts +61 -0
  131. package/dist/lib/realtime/block-store.d.ts.map +1 -0
  132. package/dist/lib/realtime/block-store.js +0 -0
  133. package/dist/lib/realtime/block-store.js.map +1 -0
  134. package/dist/lib/realtime/channel.d.ts +34 -0
  135. package/dist/lib/realtime/channel.d.ts.map +1 -0
  136. package/dist/lib/realtime/channel.js +100 -0
  137. package/dist/lib/realtime/channel.js.map +1 -0
  138. package/dist/lib/realtime/delivery-policy.d.ts +51 -0
  139. package/dist/lib/realtime/delivery-policy.d.ts.map +1 -0
  140. package/dist/lib/realtime/delivery-policy.js +98 -0
  141. package/dist/lib/realtime/delivery-policy.js.map +1 -0
  142. package/dist/lib/realtime/index.d.ts +21 -0
  143. package/dist/lib/realtime/index.d.ts.map +1 -0
  144. package/dist/lib/realtime/index.js +39 -0
  145. package/dist/lib/realtime/index.js.map +1 -0
  146. package/dist/lib/realtime/no-op-transport.d.ts +10 -0
  147. package/dist/lib/realtime/no-op-transport.d.ts.map +1 -0
  148. package/dist/lib/realtime/no-op-transport.js +44 -0
  149. package/dist/lib/realtime/no-op-transport.js.map +1 -0
  150. package/dist/lib/realtime/poll-transport.d.ts +11 -0
  151. package/dist/lib/realtime/poll-transport.d.ts.map +1 -0
  152. package/dist/lib/realtime/poll-transport.js +68 -0
  153. package/dist/lib/realtime/poll-transport.js.map +1 -0
  154. package/dist/lib/realtime/push-notifier.d.ts +39 -0
  155. package/dist/lib/realtime/push-notifier.d.ts.map +1 -0
  156. package/dist/lib/realtime/push-notifier.js +76 -0
  157. package/dist/lib/realtime/push-notifier.js.map +1 -0
  158. package/dist/lib/realtime/realtime-transport.d.ts +2 -0
  159. package/dist/lib/realtime/realtime-transport.d.ts.map +1 -0
  160. package/dist/lib/realtime/realtime-transport.js +23 -0
  161. package/dist/lib/realtime/realtime-transport.js.map +1 -0
  162. package/dist/lib/realtime/setting-store.d.ts +30 -0
  163. package/dist/lib/realtime/setting-store.d.ts.map +1 -0
  164. package/dist/lib/realtime/setting-store.js +0 -0
  165. package/dist/lib/realtime/setting-store.js.map +1 -0
  166. package/dist/lib/realtime/types.d.ts +200 -0
  167. package/dist/lib/realtime/types.d.ts.map +1 -0
  168. package/dist/lib/realtime/types.js +61 -0
  169. package/dist/lib/realtime/types.js.map +1 -0
  170. package/dist/lib/routes/index.d.ts.map +1 -1
  171. package/dist/lib/routes/index.js +3 -0
  172. package/dist/lib/routes/index.js.map +1 -1
  173. package/dist/lib/routes/media.d.ts +21 -0
  174. package/dist/lib/routes/media.d.ts.map +1 -1
  175. package/dist/lib/routes/media.js +584 -483
  176. package/dist/lib/routes/media.js.map +1 -1
  177. package/dist/lib/routes/settings.d.ts +17 -0
  178. package/dist/lib/routes/settings.d.ts.map +1 -0
  179. package/dist/lib/routes/settings.js +187 -0
  180. package/dist/lib/routes/settings.js.map +1 -0
  181. package/dist/lib/services/image-normalizer.d.ts +64 -6
  182. package/dist/lib/services/image-normalizer.d.ts.map +1 -1
  183. package/dist/lib/services/image-normalizer.js +88 -6
  184. package/dist/lib/services/image-normalizer.js.map +1 -1
  185. package/dist/lib/services/media-upload-service.d.ts +2 -2
  186. package/dist/lib/services/media-upload-service.d.ts.map +1 -1
  187. package/dist/lib/services/media-upload-service.js +22 -21
  188. package/dist/lib/services/media-upload-service.js.map +1 -1
  189. package/dist/lib/tenant-scope.d.ts.map +1 -1
  190. package/dist/lib/tenant-scope.js +18 -1
  191. package/dist/lib/tenant-scope.js.map +1 -1
  192. package/package.json +23 -22
  193. package/prisma/migrations/20260620051144_add_encrypted_user_settings/migration.sql +24 -0
  194. package/prisma/migrations/20260620120000_add_blocked_users/migration.sql +29 -0
  195. package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
  196. package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
  197. package/prisma/schema.prisma +133 -15
  198. package/src/lambda/media-completion-worker.ts +567 -0
  199. package/src/lambda/media-processing-worker.ts +508 -59
@@ -0,0 +1,175 @@
1
+ import type { SQSHandler } from "aws-lambda";
2
+ import type { Track } from "../lib/media/track-verdict.js";
3
+ import type { TextModerationProvider } from "../lib/media/text-moderation.js";
4
+ import type { StoragePort, TranscribePort } from "../lib/media/media-ports.js";
5
+ import type { MediaModerationProvider, ModerationVerdict } from "../lib/media/moderation-provider.js";
6
+ import type { ModerationDecision, ModerationStatus } from "../lib/media/moderation-status.js";
7
+ /** The persisted moderation-job row, looked up by its provider jobId. */
8
+ export interface ModerationJobRow {
9
+ readonly mediaId: string;
10
+ readonly track: Track;
11
+ /** Threshold snapshot captured at submission time (opaque JSON). */
12
+ readonly thresholdSnapshot: unknown;
13
+ }
14
+ /**
15
+ * The persistence seam. Every method is idempotent-friendly and total; the
16
+ * shell never reaches around it to a concrete client.
17
+ */
18
+ export interface CompletionStore {
19
+ /**
20
+ * Attempt to claim a message for processing. Returns `true` if THIS call
21
+ * inserted the row (first delivery), `false` if the row already existed
22
+ * (duplicate — caller must ack-drop). Implemented with an INSERT ...
23
+ * ON CONFLICT DO NOTHING so it is atomic across concurrent deliveries.
24
+ */
25
+ claimMessage(dedupeKey: string): Promise<boolean>;
26
+ /** Look up the job row by its provider jobId. `null` if unknown. */
27
+ findJobByJobId(jobId: string): Promise<ModerationJobRow | null>;
28
+ /** Persist this track's resolved decision onto its job row. */
29
+ persistTrackDecision(jobId: string, decision: ModerationDecision): Promise<void>;
30
+ /**
31
+ * Read the sibling track's resolved decision for a media object. Returns the
32
+ * decision if the other track's job exists AND has resolved; otherwise the
33
+ * `state` distinguishes a job that exists-but-unresolved from no-such-job.
34
+ */
35
+ readOtherTrack(mediaId: string, thisTrack: Track): Promise<OtherTrackState>;
36
+ /** Read the media object's current persisted moderation status + CAS coords. */
37
+ findMedia(mediaId: string): Promise<MediaCoords | null>;
38
+ /** Persist a new moderation status for the media object. */
39
+ persistMediaStatus(mediaId: string, status: ModerationStatus): Promise<void>;
40
+ }
41
+ /**
42
+ * The sibling track's state, used to build its {@link TrackOutcome}.
43
+ * - `decided` — the other track has a resolved decision.
44
+ * - `pending` — the other track's job exists but has not resolved yet.
45
+ * - `absent` — there is no job for the other track on this media object.
46
+ */
47
+ export type OtherTrackState = {
48
+ readonly state: "decided";
49
+ readonly decision: ModerationDecision;
50
+ } | {
51
+ readonly state: "pending";
52
+ } | {
53
+ readonly state: "absent";
54
+ };
55
+ /**
56
+ * The media coordinates the shell needs to gate promotion and build keys. The
57
+ * store returns the RAW identity columns ({@link tenantId}, {@link uploadId},
58
+ * {@link contentHash}); the shell derives every storage key from them via the
59
+ * canonical cas-keys builders (so key construction is centralized here and the
60
+ * store never hand-rolls a key string). {@link contentHash} is the post-
61
+ * transcode SHA-256 the processing worker persisted (NOT the upload-time
62
+ * uploadId placeholder).
63
+ */
64
+ export interface MediaCoords {
65
+ readonly moderationStatus: ModerationStatus;
66
+ /** Tenant that owns this object (cas-keys input). */
67
+ readonly tenantId: string;
68
+ /** Upload session id — addresses the raw pending + cleaned staging keys. */
69
+ readonly uploadId: string;
70
+ /** 64-char lowercase SHA-256 of the CLEANED bytes (addresses the cas/ key). */
71
+ readonly contentHash: string;
72
+ }
73
+ /**
74
+ * Pure re-interpreter for a VISUAL verdict against the job's threshold snapshot.
75
+ *
76
+ * Injected (not implemented here) so this PUBLIC module carries NO operational
77
+ * thresholds. It receives the SNAPSHOT taken at submission time — never live
78
+ * Env — so a config edit between deliveries cannot flip a replayed verdict. Must
79
+ * be total and fail-closed: any uncertainty maps to `"review"`, never `"approved"`.
80
+ */
81
+ export type VisualVerdictReinterpreter = (verdict: ModerationVerdict, thresholdSnapshot: unknown) => ModerationDecision;
82
+ /** Everything the per-record processor binds to. */
83
+ export interface CompletionDeps {
84
+ readonly store: CompletionStore;
85
+ readonly moderation: MediaModerationProvider;
86
+ readonly transcribe: TranscribePort;
87
+ readonly textModeration: TextModerationProvider;
88
+ readonly storage: StoragePort;
89
+ /** Re-interpret a visual verdict using the job's threshold snapshot. */
90
+ readonly reinterpretVisual: VisualVerdictReinterpreter;
91
+ /** Emit the anti-oracle resolved event. Best-effort; must not throw to ack. */
92
+ readonly emitResolved: (payload: {
93
+ readonly mediaId: string;
94
+ readonly status: "ready" | "not-ready";
95
+ }) => Promise<void>;
96
+ /** Structured logger seam (defaults to a no-op in tests). */
97
+ readonly log?: {
98
+ info?: (msg: string, data?: unknown) => void;
99
+ warn?: (msg: string, data?: unknown) => void;
100
+ error?: (msg: string, data?: unknown) => void;
101
+ };
102
+ }
103
+ /**
104
+ * The outcome of processing one SQS record. `retry` is the ONLY value that
105
+ * causes the message to be returned to the queue (and eventually DLQ'd);
106
+ * everything else is an ack-drop (idempotent no-op or fail-closed terminal).
107
+ */
108
+ export type RecordOutcome = {
109
+ readonly kind: "duplicate";
110
+ } | {
111
+ readonly kind: "unroutable";
112
+ } | {
113
+ readonly kind: "illegal-transition";
114
+ } | {
115
+ readonly kind: "applied";
116
+ readonly status: ModerationStatus;
117
+ } | {
118
+ readonly kind: "retry";
119
+ readonly reason: string;
120
+ };
121
+ /**
122
+ * Extract the provider job id from an untrusted completion message body.
123
+ *
124
+ * Two shapes are accepted; everything else (and any embedded verdict/status) is
125
+ * ignored:
126
+ * - Rekognition via SNS: { "Message": "{...\"JobId\":\"...\"}" } OR a body
127
+ * that itself directly carries { "JobId": "..." }.
128
+ * - Transcribe via EventBridge: { "detail": { "TranscriptionJobName": "..." } }
129
+ * OR a body that directly carries { "TranscriptionJobName": "..." }.
130
+ *
131
+ * Returns the job id and which track it belongs to, or `null` when no job id can
132
+ * be recovered (fail-closed: the caller ack-drops an unroutable message rather
133
+ * than DLQ-looping a permanently-malformed pointer).
134
+ *
135
+ * Pure & total: never throws.
136
+ */
137
+ export declare function extractJobPointer(body: string): {
138
+ readonly jobId: string;
139
+ readonly track: Track;
140
+ } | null;
141
+ /**
142
+ * Re-fetch THIS track's authoritative decision from the provider, ignoring the
143
+ * message body entirely. Fail-closed: a non-terminal / failed / unknown result
144
+ * yields `"errored"`-equivalent `null` so the caller records it as an errored
145
+ * track outcome (never `"approved"`).
146
+ *
147
+ * VISUAL: `getVideoModeration(jobId)` returns a settled verdict; the verdict is
148
+ * re-interpreted against the JOB's threshold snapshot (not live Env).
149
+ *
150
+ * AUDIO: `getTranscription(jobId)` is polled; only a COMPLETED transcription is
151
+ * fed to `transcriptToModerationDecision` (which is itself fail-closed). Any
152
+ * other status (IN_PROGRESS / FAILED) yields `null` (errored — fail closed).
153
+ */
154
+ export declare function refetchTrackDecision(pointer: {
155
+ readonly jobId: string;
156
+ readonly track: Track;
157
+ }, job: ModerationJobRow, deps: CompletionDeps): Promise<ModerationDecision | null>;
158
+ /**
159
+ * Process one completion message. Returns a {@link RecordOutcome}; only
160
+ * `kind: "retry"` should be surfaced to SQS as a batch-item failure. Throws are
161
+ * caught by the handler and converted to a retry.
162
+ */
163
+ export declare function processCompletion(body: string, deps: CompletionDeps): Promise<RecordOutcome>;
164
+ /**
165
+ * Build the SQS handler from injected deps. The consuming app provides the
166
+ * concrete adapters; tests provide mocks and call {@link processCompletion}
167
+ * directly.
168
+ *
169
+ * A record that yields `kind: "retry"` (or throws) is reported as a batch-item
170
+ * failure so SQS retries / DLQs it. Every other outcome is an ack (the message
171
+ * is consumed): duplicates, unroutable pointers, and illegal transitions are all
172
+ * fail-closed ack-drops — they must never DLQ-loop.
173
+ */
174
+ export declare function makeHandler(deps: CompletionDeps): SQSHandler;
175
+ //# sourceMappingURL=media-completion-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-completion-worker.d.ts","sourceRoot":"","sources":["../../src/lambda/media-completion-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,373 @@
1
+ // media-completion-worker.ts — imperative SHELL for the standardized media
2
+ // job-completion SQS queue (B2).
3
+ //
4
+ // One queue drains the completion notifications of BOTH async moderation
5
+ // tracks of a media object:
6
+ //
7
+ // - VISUAL: the image/video moderation provider finishes a job and publishes
8
+ // a completion to SNS, which is fanned into this SQS queue. The SNS envelope
9
+ // carries `{ Message: "<json>" }` whose inner JSON has a `JobId`.
10
+ // - AUDIO: speech-to-text (Transcribe) finishes and emits an EventBridge event
11
+ // fanned into this SQS queue, whose `detail` carries `TranscriptionJobName`.
12
+ //
13
+ // THREAT MODEL: the SQS message body is an UNTRUSTED POINTER. A replay, a forged
14
+ // body, or a spoofed verdict must not move a media object toward "approved". So
15
+ // the worker treats the body as nothing more than a job-id pointer and re-fetches
16
+ // the authoritative state from the provider. The body's own verdict/status (if
17
+ // any) is ALWAYS ignored.
18
+ //
19
+ // SAFETY ORDER (fixed, never reordered):
20
+ // 0. parse body -> jobId ONLY
21
+ // 1. DEDUPE FIRST: insert ProcessedModerationMessage(dedupeKey); if it already
22
+ // exists, ack-drop (idempotent no-op) BEFORE any side effect.
23
+ // 2. RE-FETCH authoritative track state from the provider (visual) or the
24
+ // transcription seam (audio). Derive THIS track's decision SOLELY from the
25
+ // re-fetched result. Non-terminal / failed / unknown => errored (fail closed).
26
+ // 3. Look up the MediaModerationJob by jobId => mediaId + track. Persist this
27
+ // track's decision. Read the OTHER track's decision. Build both TrackOutcomes.
28
+ // 4. decidePromotion({ visual, audio, currentStatus, casObjectPresent }).
29
+ // casObjectPresent = the cleaned bytes are available to serve: TRUE iff the
30
+ // cas/ key exists (a prior promote) OR the cleaned STAGING key exists (the
31
+ // processing worker left them there, pre-promote).
32
+ // 5. APPLY in fixed order:
33
+ // a. if shouldPromote: copyObject(STAGING -> cas) — promote the CLEANED
34
+ // STAGING bytes (the exact bytes that were moderated), NEVER the raw
35
+ // pending upload. Then best-effort deleteObject(pending) (raw-original
36
+ // cleanup) AND deleteObject(staging) (staging cleanup) — tolerate
37
+ // already-deleted on both. cas/ thus only ever holds APPROVED cleaned
38
+ // bytes ("cleaned-staging, promote-on-approval").
39
+ // b. if shouldPersistStatus: persist transition.status.
40
+ // c. if shouldEmitResolved: emit moderation.resolved with
41
+ // moderationResolvedPayload(mediaId, status) — ready|not-ready ONLY.
42
+ // An illegal transition (transition.ok === false) is ack-dropped, never DLQ.
43
+ //
44
+ // THRESHOLD SNAPSHOT: when (re)interpreting the re-fetched verdict we use the
45
+ // threshold snapshot stored ON THE JOB ROW (job.thresholdSnapshot), never live
46
+ // Env — so a config edit landing between the original submission and a (re)delivery
47
+ // cannot flip a replayed verdict. The reinterpreter is an injected pure function
48
+ // (no operational numbers live in this PUBLIC tarball).
49
+ //
50
+ // This is the imperative shell: it sequences I/O and delegates EVERY decision to
51
+ // the pure functional-core units (decidePromotion, combineTrackVerdicts,
52
+ // transcriptToModerationDecision, deriveDedupeKey, moderationResolvedPayload).
53
+ import { decidePromotion } from "../lib/media/promote-decision.js";
54
+ import { casKey, pendingKey, isCasKeyError } from "../lib/media/cas-keys.js";
55
+ import { deriveDedupeKey } from "../lib/media/dedupe-key.js";
56
+ import { moderationResolvedPayload } from "../lib/media/moderation-resolved-payload.js";
57
+ import { transcriptToModerationDecision } from "../lib/media/transcript-moderation.js";
58
+ // ---------------------------------------------------------------------------
59
+ // Body parsing — extract ONLY the job id from an untrusted pointer.
60
+ // ---------------------------------------------------------------------------
61
+ /**
62
+ * Extract the provider job id from an untrusted completion message body.
63
+ *
64
+ * Two shapes are accepted; everything else (and any embedded verdict/status) is
65
+ * ignored:
66
+ * - Rekognition via SNS: { "Message": "{...\"JobId\":\"...\"}" } OR a body
67
+ * that itself directly carries { "JobId": "..." }.
68
+ * - Transcribe via EventBridge: { "detail": { "TranscriptionJobName": "..." } }
69
+ * OR a body that directly carries { "TranscriptionJobName": "..." }.
70
+ *
71
+ * Returns the job id and which track it belongs to, or `null` when no job id can
72
+ * be recovered (fail-closed: the caller ack-drops an unroutable message rather
73
+ * than DLQ-looping a permanently-malformed pointer).
74
+ *
75
+ * Pure & total: never throws.
76
+ */
77
+ export function extractJobPointer(body) {
78
+ let root;
79
+ try {
80
+ root = JSON.parse(body);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ if (root === null || typeof root !== "object")
86
+ return null;
87
+ const obj = root;
88
+ // Transcribe (AUDIO): EventBridge `detail.TranscriptionJobName`, or direct.
89
+ const detail = typeof obj.detail === "object" && obj.detail !== null
90
+ ? obj.detail
91
+ : undefined;
92
+ const transcriptionName = pickString(detail?.TranscriptionJobName) ??
93
+ pickString(obj.TranscriptionJobName);
94
+ if (transcriptionName !== null) {
95
+ return { jobId: transcriptionName, track: "AUDIO" };
96
+ }
97
+ // Rekognition (VISUAL): SNS `Message` is a JSON string carrying `JobId`, or
98
+ // the body carries `JobId` directly.
99
+ const directJobId = pickString(obj.JobId);
100
+ if (directJobId !== null) {
101
+ return { jobId: directJobId, track: "VISUAL" };
102
+ }
103
+ const snsMessage = pickString(obj.Message);
104
+ if (snsMessage !== null) {
105
+ let inner;
106
+ try {
107
+ inner = JSON.parse(snsMessage);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ if (inner !== null && typeof inner === "object") {
113
+ const innerJobId = pickString(inner.JobId);
114
+ if (innerJobId !== null) {
115
+ return { jobId: innerJobId, track: "VISUAL" };
116
+ }
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ /** Return a non-empty string value, or null for anything else. */
122
+ function pickString(v) {
123
+ return typeof v === "string" && v.length > 0 ? v : null;
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Re-fetch authoritative track decision (fail-closed).
127
+ // ---------------------------------------------------------------------------
128
+ /**
129
+ * Re-fetch THIS track's authoritative decision from the provider, ignoring the
130
+ * message body entirely. Fail-closed: a non-terminal / failed / unknown result
131
+ * yields `"errored"`-equivalent `null` so the caller records it as an errored
132
+ * track outcome (never `"approved"`).
133
+ *
134
+ * VISUAL: `getVideoModeration(jobId)` returns a settled verdict; the verdict is
135
+ * re-interpreted against the JOB's threshold snapshot (not live Env).
136
+ *
137
+ * AUDIO: `getTranscription(jobId)` is polled; only a COMPLETED transcription is
138
+ * fed to `transcriptToModerationDecision` (which is itself fail-closed). Any
139
+ * other status (IN_PROGRESS / FAILED) yields `null` (errored — fail closed).
140
+ */
141
+ export async function refetchTrackDecision(pointer, job, deps) {
142
+ if (pointer.track === "VISUAL") {
143
+ const verdict = await deps.moderation.getVideoModeration(pointer.jobId);
144
+ if (verdict == null || typeof verdict !== "object")
145
+ return null;
146
+ const decision = deps.reinterpretVisual(verdict, job.thresholdSnapshot);
147
+ return normalizeDecision(decision);
148
+ }
149
+ // AUDIO
150
+ const res = await deps.transcribe.getTranscription(pointer.jobId);
151
+ if (res == null || res.status !== "COMPLETED") {
152
+ // Non-terminal or failed transcription — fail closed (errored).
153
+ return null;
154
+ }
155
+ const transcript = res.transcript ?? "";
156
+ const decision = await transcriptToModerationDecision(transcript, deps.textModeration);
157
+ return normalizeDecision(decision);
158
+ }
159
+ /** Accept only the three known decisions; anything else fails closed to null. */
160
+ function normalizeDecision(d) {
161
+ return d === "approved" || d === "review" || d === "quarantine" ? d : null;
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Build a TrackOutcome from a (possibly null) decision.
165
+ // ---------------------------------------------------------------------------
166
+ /** A resolved decision => `decided`; a null (failed/unknown) => `errored`. */
167
+ function outcomeFromDecision(d) {
168
+ return d === null ? { state: "errored" } : { state: "decided", decision: d };
169
+ }
170
+ /** Map the sibling track's persisted state to a TrackOutcome (fail-closed). */
171
+ function outcomeFromOther(other) {
172
+ switch (other.state) {
173
+ case "decided":
174
+ return { state: "decided", decision: other.decision };
175
+ case "absent":
176
+ return { state: "absent" };
177
+ case "pending":
178
+ default:
179
+ // A sibling job that exists but has not resolved is NOT approval — the
180
+ // combinator treats `absent` and `errored` alike (both degrade away from
181
+ // approved), and we have no positive evidence yet, so fail closed.
182
+ return { state: "errored" };
183
+ }
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Per-record processor (the heart; mock-friendly).
187
+ // ---------------------------------------------------------------------------
188
+ /**
189
+ * Process one completion message. Returns a {@link RecordOutcome}; only
190
+ * `kind: "retry"` should be surfaced to SQS as a batch-item failure. Throws are
191
+ * caught by the handler and converted to a retry.
192
+ */
193
+ export async function processCompletion(body, deps) {
194
+ // 0. Extract ONLY the job id from the untrusted pointer.
195
+ const pointer = extractJobPointer(body);
196
+ if (pointer === null) {
197
+ deps.log?.warn?.("completion: no job pointer in body — dropping");
198
+ return { kind: "unroutable" };
199
+ }
200
+ // 1. Look up the job row first — needed for the contentHash that scopes the
201
+ // dedupe key and for the threshold snapshot. (No side effect yet.)
202
+ const job = await deps.store.findJobByJobId(pointer.jobId);
203
+ if (job === null) {
204
+ deps.log?.warn?.("completion: unknown jobId — dropping", {
205
+ jobId: pointer.jobId,
206
+ });
207
+ return { kind: "unroutable" };
208
+ }
209
+ // The media object's content hash addresses the dedupe key so identical bytes
210
+ // share fan-in across tenants; we read it from the media coords below. But the
211
+ // dedupe MUST happen before ANY side effect, and persistTrackDecision is a
212
+ // side effect — so we resolve the media first (read-only) to obtain the hash.
213
+ const media = await deps.store.findMedia(job.mediaId);
214
+ if (media === null) {
215
+ deps.log?.warn?.("completion: media row missing — dropping", {
216
+ mediaId: job.mediaId,
217
+ });
218
+ return { kind: "unroutable" };
219
+ }
220
+ // 2. DEDUPE FIRST — before any side effect. The dedupe key binds the content
221
+ // hash, the jobId, and the track so the two tracks of the same bytes never
222
+ // collide and a redelivery of the SAME completion is a no-op.
223
+ const dedupeKey = deriveDedupeKey({
224
+ contentHash: media.contentHash,
225
+ jobId: pointer.jobId,
226
+ track: pointer.track,
227
+ });
228
+ const claimed = await deps.store.claimMessage(dedupeKey);
229
+ if (!claimed) {
230
+ deps.log?.info?.("completion: duplicate delivery — ack-drop", {
231
+ jobId: pointer.jobId,
232
+ });
233
+ return { kind: "duplicate" };
234
+ }
235
+ // 3. RE-FETCH authoritative state for THIS track (body verdict ignored).
236
+ const thisDecision = await refetchTrackDecision(pointer, job, deps);
237
+ // Persist this track's decision (the side effect we just earned the right to
238
+ // perform). An errored re-fetch persists nothing on the row (decision stays
239
+ // null) but still contributes an `errored` outcome to the combine.
240
+ if (thisDecision !== null) {
241
+ await deps.store.persistTrackDecision(pointer.jobId, thisDecision);
242
+ }
243
+ // Read the OTHER track's decision and build both outcomes.
244
+ const other = await deps.store.readOtherTrack(job.mediaId, pointer.track);
245
+ const thisOutcome = outcomeFromDecision(thisDecision);
246
+ const otherOutcome = outcomeFromOther(other);
247
+ const visual = pointer.track === "VISUAL" ? thisOutcome : otherOutcome;
248
+ const audio = pointer.track === "AUDIO" ? thisOutcome : otherOutcome;
249
+ // Derive every storage key from the row's identity columns via the canonical
250
+ // cas-keys builders — key construction is centralized here, never trusted from
251
+ // the store. A malformed identity (should be impossible past the upload gate)
252
+ // fails closed to a retry rather than touching an un-addressable key.
253
+ const casK = casKey(media.tenantId, media.contentHash);
254
+ const pendingK = pendingKey(media.tenantId, media.uploadId);
255
+ if (isCasKeyError(casK) || isCasKeyError(pendingK)) {
256
+ deps.log?.error?.("completion: un-addressable media identity — retry", {
257
+ mediaId: job.mediaId,
258
+ });
259
+ return { kind: "retry", reason: "cas-key-error" };
260
+ }
261
+ // The cleaned bytes live at the STAGING key until promoted to cas/.
262
+ const stagingK = `processing/${media.tenantId}/${media.uploadId}`;
263
+ // 4. Decide (pure). Promotion is gated on the cleaned bytes being available to
264
+ // serve: present in cas/ (after a prior promote) OR at the staging key
265
+ // (before promote). Either satisfies casObjectPresent.
266
+ const casPresent = (await deps.storage.headObject(casK)).exists ||
267
+ (await deps.storage.headObject(stagingK)).exists;
268
+ const action = decidePromotion({
269
+ visual,
270
+ audio,
271
+ currentStatus: media.moderationStatus,
272
+ casObjectPresent: casPresent,
273
+ });
274
+ // An illegal transition (e.g. replay on a terminal APPROVED/REJECTED) is an
275
+ // idempotent ack-drop — NEVER a DLQ.
276
+ if (action.transition.ok === false) {
277
+ deps.log?.info?.("completion: illegal/absorbing transition — ack-drop", {
278
+ mediaId: job.mediaId,
279
+ from: media.moderationStatus,
280
+ });
281
+ return { kind: "illegal-transition" };
282
+ }
283
+ const nextStatusValue = action.transition.status;
284
+ // 5. APPLY in fixed order: promote -> persist -> emit.
285
+ // 5a. PROMOTE: copy the CLEANED STAGING bytes (the exact bytes that were
286
+ // moderated) to cas/ so they can serve — NEVER the raw pending upload.
287
+ // copyObject is idempotent (content-derived target key). Then best-effort
288
+ // remove BOTH the raw original (pending/) and the staging copy. cas/ thus
289
+ // only ever holds APPROVED cleaned bytes.
290
+ if (action.shouldPromote) {
291
+ await deps.storage.copyObject(stagingK, casK);
292
+ // Best-effort raw-original cleanup. Tolerate already-deleted (a prior
293
+ // delivery or lifecycle expiry) — the cas/ copy is what matters.
294
+ try {
295
+ await deps.storage.deleteObject(pendingK);
296
+ }
297
+ catch (err) {
298
+ deps.log?.warn?.("completion: pending delete tolerated", {
299
+ mediaId: job.mediaId,
300
+ error: String(err),
301
+ });
302
+ }
303
+ // Best-effort staging cleanup. Same tolerance.
304
+ try {
305
+ await deps.storage.deleteObject(stagingK);
306
+ }
307
+ catch (err) {
308
+ deps.log?.warn?.("completion: staging delete tolerated", {
309
+ mediaId: job.mediaId,
310
+ error: String(err),
311
+ });
312
+ }
313
+ }
314
+ // 5b. PERSIST the new status.
315
+ if (action.shouldPersistStatus) {
316
+ await deps.store.persistMediaStatus(job.mediaId, nextStatusValue);
317
+ }
318
+ // 5c. EMIT the anti-oracle resolved event (ready|not-ready ONLY).
319
+ if (action.shouldEmitResolved) {
320
+ const payload = moderationResolvedPayload(job.mediaId, nextStatusValue);
321
+ await deps.emitResolved(payload);
322
+ }
323
+ deps.log?.info?.("completion: applied", {
324
+ mediaId: job.mediaId,
325
+ status: nextStatusValue,
326
+ });
327
+ return { kind: "applied", status: nextStatusValue };
328
+ }
329
+ // ---------------------------------------------------------------------------
330
+ // SQS handler — the thin adapter. Wiring of concrete adapters (Prisma,
331
+ // Rekognition/Transcribe SDK clients, S3, the event emitter, the threshold-
332
+ // snapshot reinterpreter) is done by the consuming app at startup and bound into
333
+ // `buildDeps`; THIS file ships SDK-free except for the lambda type import.
334
+ // ---------------------------------------------------------------------------
335
+ /**
336
+ * Build the SQS handler from injected deps. The consuming app provides the
337
+ * concrete adapters; tests provide mocks and call {@link processCompletion}
338
+ * directly.
339
+ *
340
+ * A record that yields `kind: "retry"` (or throws) is reported as a batch-item
341
+ * failure so SQS retries / DLQs it. Every other outcome is an ack (the message
342
+ * is consumed): duplicates, unroutable pointers, and illegal transitions are all
343
+ * fail-closed ack-drops — they must never DLQ-loop.
344
+ */
345
+ export function makeHandler(deps) {
346
+ return async (event) => {
347
+ const failedIds = [];
348
+ for (const record of event.Records) {
349
+ try {
350
+ const outcome = await processCompletion(record.body, deps);
351
+ if (outcome.kind === "retry") {
352
+ deps.log?.error?.("completion: retry", {
353
+ messageId: record.messageId,
354
+ reason: outcome.reason,
355
+ });
356
+ failedIds.push(record.messageId);
357
+ }
358
+ }
359
+ catch (err) {
360
+ // Unexpected (transient I/O) failure — return to the queue for retry.
361
+ deps.log?.error?.("completion: unexpected failure — retry", {
362
+ messageId: record.messageId,
363
+ error: String(err),
364
+ });
365
+ failedIds.push(record.messageId);
366
+ }
367
+ }
368
+ if (failedIds.length > 0) {
369
+ return { batchItemFailures: failedIds.map((id) => ({ itemIdentifier: id })) };
370
+ }
371
+ };
372
+ }
373
+ //# sourceMappingURL=media-completion-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-completion-worker.js","sourceRoot":"","sources":["../../src/lambda/media-completion-worker.ts"],"names":[],"mappings":"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"}