@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
@@ -1,3 +1,174 @@
1
- import type { SQSHandler } from "aws-lambda";
1
+ import type { SQSHandler, SQSRecord } from "aws-lambda";
2
+ import { Logger } from "@aws-lambda-powertools/logger";
3
+ import type { Track } from "../lib/media/track-verdict.js";
4
+ import type { StoragePort, TranscodePort, TranscribePort } from "../lib/media/media-ports.js";
5
+ import type { MediaModerationProvider } from "../lib/media/moderation-provider.js";
6
+ /**
7
+ * The minimal MediaFile row shape the worker reads. Re-declared (not imported
8
+ * from the Prisma client) so this module compiles in worktrees that have not
9
+ * regenerated the client, mirroring moderation-status.ts's discipline. The
10
+ * shell maps the real Prisma row to this shape at the persistence-port boundary.
11
+ */
12
+ export interface MediaFileRow {
13
+ readonly id: string;
14
+ readonly tenantId: string;
15
+ readonly uploadId: string | null;
16
+ }
17
+ /**
18
+ * A copy of the operative moderation thresholds, snapshotted at job-submission
19
+ * time so historical decisions stay auditable after a threshold change. The
20
+ * shape mirrors Env.media.thresholds; the worker treats it as an opaque JSON
21
+ * blob and never reads individual values (no compiled threshold logic here).
22
+ */
23
+ export type ThresholdSnapshot = Record<string, {
24
+ review: number;
25
+ quarantine: number;
26
+ }>;
27
+ /**
28
+ * The persistence operations the worker needs, narrowed to exactly what it
29
+ * uses. Implemented in production by a thin Prisma adapter; in tests by an
30
+ * in-memory fake. Keeping this narrow keeps the worker testable without the
31
+ * generated client and documents the worker's full DB surface in one place.
32
+ */
33
+ export interface MediaPersistencePort {
34
+ /** Load the MediaFile row for an upload session, or null if none exists. */
35
+ findMediaByUploadId(uploadId: string): Promise<MediaFileRow | null>;
36
+ /** Persist a started per-track moderation job with its threshold snapshot. */
37
+ createModerationJob(input: {
38
+ mediaId: string;
39
+ track: Track;
40
+ jobId: string;
41
+ thresholdSnapshot: ThresholdSnapshot;
42
+ }): Promise<void>;
43
+ /**
44
+ * Persist the REAL content identity of the cleaned bytes onto the MediaFile
45
+ * row, replacing the upload-time `uploadId` placeholder contentHash with the
46
+ * SHA-256 of the transcoded output and recording the future serve key. The
47
+ * completion worker derives the promote target (`cas/{tenant}/{hash}`) from
48
+ * this persisted `contentHash`, so this write MUST happen before moderation
49
+ * fans in — otherwise the object can never promote.
50
+ */
51
+ persistCleanedContent(mediaId: string, content: {
52
+ contentHash: string;
53
+ originalKey: string;
54
+ }): Promise<void>;
55
+ /** Drive a media object's moderationStatus to REVIEW (poison path). */
56
+ markMediaForReview(mediaId: string): Promise<void>;
57
+ }
58
+ /**
59
+ * The slice of Env.media this worker consumes. Operational parameters arrive
60
+ * here as VALUES sourced from Env.media — never as literals in this file.
61
+ */
62
+ export interface MediaProcessingConfig {
63
+ /** Hard duration cap (seconds). From Env.media.maxDurationSeconds. */
64
+ readonly maxDurationSeconds: number;
65
+ /** Current operative thresholds, snapshotted onto each started job. */
66
+ readonly thresholds: ThresholdSnapshot;
67
+ }
68
+ /**
69
+ * All capability seams the orchestration core binds to. The handler builds this
70
+ * from the injected concrete adapters; tests build it from the B0 Mocks + an
71
+ * in-memory persistence fake.
72
+ */
73
+ export interface MediaProcessingDeps {
74
+ readonly storage: StoragePort;
75
+ readonly transcode: TranscodePort;
76
+ readonly transcribe: TranscribePort;
77
+ readonly moderation: MediaModerationProvider;
78
+ readonly persistence: MediaPersistencePort;
79
+ readonly config: MediaProcessingConfig;
80
+ /** The object-storage bucket handle moderation/transcription refs carry. */
81
+ readonly bucket: string;
82
+ /**
83
+ * Deterministic job-name factory for transcription/idempotency. Injected so
84
+ * the shell stays free of Date.now/Math.random in tests; production passes a
85
+ * uuid/time-based generator. `seed` is a stable per-call input (the cas key).
86
+ */
87
+ readonly newJobName: (seed: string) => string;
88
+ readonly logger: Pick<Logger, "info" | "warn" | "error">;
89
+ }
90
+ /**
91
+ * The disposition of one SQS record after orchestration.
92
+ *
93
+ * - `ack` — remove from the queue (success, drop-non-pending, or poison routed
94
+ * to REVIEW). A poison ack carries `poison: true` for observability.
95
+ * - `fail` — leave on the queue for SQS to retry (transient/retryable fault).
96
+ * The handler maps this to a batchItemFailure.
97
+ */
98
+ export type RecordOutcome = {
99
+ readonly disposition: "ack";
100
+ readonly reason: string;
101
+ readonly poison?: boolean;
102
+ } | {
103
+ readonly disposition: "fail";
104
+ readonly reason: string;
105
+ };
106
+ /**
107
+ * Parse a triggering key as a `pending/{tenantId}/{uploadId}` key, validating
108
+ * the FORM by round-tripping the parsed parts back through `pendingKey()`. A
109
+ * key only parses if rebuilding it from its parts yields the identical string —
110
+ * so a path-traversal payload, extra segments, or a malformed id can never pass
111
+ * (cas-keys.ts owns the anchored allowlists).
112
+ *
113
+ * @returns the {tenantId, uploadId} when the key is a canonical pending key,
114
+ * or null for ANY other key (which the caller ack-drops; we never
115
+ * write outputs under pending/, so a non-pending key is not our work).
116
+ */
117
+ export declare function parsePendingKey(key: string): {
118
+ tenantId: string;
119
+ uploadId: string;
120
+ } | null;
121
+ /** Every object key referenced by one SQS record's S3 event notification. */
122
+ export declare function extractObjectKeys(recordBody: string): string[];
123
+ /**
124
+ * Orchestrate processing for ONE already-extracted object key.
125
+ *
126
+ * Steps (every uncertainty fails closed; nothing here can yield APPROVED):
127
+ * 1. Reject any key that is not a canonical `pending/{tenant}/{upload}` key —
128
+ * ack-drop it; outputs are NEVER written under pending/.
129
+ * 2. Load the MediaFile row by uploadId; re-derive tenant FROM THE ROW and
130
+ * assert pendingKey(rowTenant, uploadId) === the triggering key. Mismatch
131
+ * (or missing/uploadId-less row) is a hard reject → REVIEW + ack.
132
+ * 3. Probe duration; over-cap ⇒ poison ⇒ REVIEW + ack (no transcode).
133
+ * 4. Transcode-and-discard ⇒ cleaned bytes at the STAGING key (read back from
134
+ * the cleaned key). The cleaned bytes are NOT written to cas/ here — cas/ is
135
+ * the CDN-served prefix and must hold only APPROVED bytes (promotion happens
136
+ * in the completion worker).
137
+ * 5. Hash the cleaned bytes ⇒ realHash; PERSIST {contentHash: realHash,
138
+ * originalKey: casKey(tenant, realHash)} onto the row, replacing the
139
+ * upload-time uploadId placeholder so the completion worker can derive the
140
+ * promote target.
141
+ * 6. START moderation on the cleaned STAGING object (NOT the raw pending upload,
142
+ * NOT a cas/ key) — moderation must run on EXACTLY the bytes that will be
143
+ * served: provider.startVideoModeration ⇒ persist VISUAL job (+ threshold
144
+ * snapshot); transcribe.startTranscription ⇒ persist AUDIO job (+ snapshot).
145
+ * The worker only STARTS jobs + persists jobIds; it never fetches verdicts.
146
+ */
147
+ export declare function processObjectKey(triggeringKey: string, deps: MediaProcessingDeps): Promise<RecordOutcome>;
148
+ /**
149
+ * Process one SQS record (which may carry several S3 object keys). The record
150
+ * fails (SQS retry) iff ANY of its keys produced a retryable fault; otherwise
151
+ * it is acked. Per-key poison is acked, never failed.
152
+ */
153
+ export declare function processRecord(record: SQSRecord, deps: MediaProcessingDeps): Promise<RecordOutcome>;
154
+ /**
155
+ * Inject the concrete media-processing seams. The consuming app (Skybber) calls
156
+ * this once at Lambda cold start with its ffmpeg/MediaConvert TranscodePort, S3
157
+ * StoragePort, Transcribe TranscribePort, injected MediaModerationProvider, and
158
+ * a Prisma-backed MediaPersistencePort. Core ships NO concrete adapters.
159
+ */
160
+ export declare function setMediaProcessingDeps(deps: MediaProcessingDeps): void;
161
+ /** Test helper: clear injected deps between cases. */
162
+ export declare function __resetMediaProcessingDeps(): void;
163
+ /**
164
+ * The SQS entry point. Preserves `reportBatchItemFailures` semantics: only the
165
+ * messageIds whose records produced a retryable fault are returned as batch
166
+ * item failures; everything else (success / drop / poison→REVIEW) is acked by
167
+ * omission.
168
+ *
169
+ * If no concrete deps were injected, the handler fails CLOSED: it throws, so the
170
+ * whole batch is retried rather than silently dropped. An un-wired worker must
171
+ * never ack-drop real uploads.
172
+ */
2
173
  export declare const handler: SQSHandler;
3
174
  //# sourceMappingURL=media-processing-worker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media-processing-worker.d.ts","sourceRoot":"","sources":["../../src/lambda/media-processing-worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C,eAAO,MAAM,OAAO,EAAE,UA8DrB,CAAC"}
1
+ {"version":3,"file":"media-processing-worker.d.ts","sourceRoot":"","sources":["../../src/lambda/media-processing-worker.ts"],"names":[],"mappings":"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"}
@@ -1,60 +1,354 @@
1
- import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
1
+ // media-processing-worker.ts the P0b media-processing orchestration SHELL.
2
+ //
3
+ // This is the imperative shell over the pure functional-core media units. It is
4
+ // NOT itself a functional-core unit: it performs I/O (object storage, transcode,
5
+ // transcription, moderation, DB writes). BUT all of that I/O arrives through
6
+ // INJECTED capability seams (TranscodePort / StoragePort / TranscribePort /
7
+ // MediaModerationProvider) and a Prisma-shaped persistence port, so the
8
+ // orchestration logic is exercised in unit tests against the B0 in-memory Mocks
9
+ // — no real cloud, no real encoder, no real DB.
10
+ //
11
+ // Per the seam discipline (see lib/media/media-ports.ts and
12
+ // lib/media/moderation-provider.ts): CORE ships the interfaces + mocks; the
13
+ // consuming app (Skybber) injects the concrete cloud adapters at startup via
14
+ // `setMediaProcessingDeps()`. Until they are injected, the handler fails CLOSED
15
+ // (throws → SQS retry), never silently approves or drops work.
16
+ //
17
+ // Fail-closed posture, end to end:
18
+ // - A key that is not a well-formed `pending/{tenant}/{upload}` key is dropped
19
+ // (ack) and NEVER written under — the re-trigger-loop guard.
20
+ // - The tenant is re-derived FROM THE ROW, and the triggering key must equal
21
+ // pendingKey(rowTenant, uploadId); a mismatch is a hard reject (poison →
22
+ // REVIEW + ack), so a forged/odd key cannot make us moderate the wrong cas/.
23
+ // - Over-cap duration is poison → REVIEW + ack (no transcode attempted).
24
+ // - The worker ONLY starts moderation jobs + persists their jobIds; it never
25
+ // fetches verdicts (a separate poller owns fan-in). Moderation runs on the
26
+ // cleaned bytes at the STAGING key, NOT the raw pending upload — and the
27
+ // cleaned bytes are NOT written to cas/ here. cas/ is the CDN-served prefix,
28
+ // so it must only ever hold APPROVED cleaned bytes; the completion worker
29
+ // promotes staging -> cas/ on approval ("cleaned-staging, promote-on-approval").
30
+ // - classifyWorkerError() splits permanent media/payload defects (poison →
31
+ // REVIEW + ack, no DLQ loop) from transient infra faults (retryable → throw
32
+ // → SQS retry → DLQ + alert backstop).
2
33
  import { Logger } from "@aws-lambda-powertools/logger";
34
+ import { createHash } from "node:crypto";
35
+ import { pendingKey, casKey, isCasKeyError, } from "../lib/media/cas-keys.js";
36
+ import { exceedsDurationCap } from "../lib/media/duration-cap.js";
37
+ import { classifyWorkerError } from "../lib/media/classify-worker-error.js";
38
+ // ---------------------------------------------------------------------------
39
+ // Key parsing — pending/{tenantId}/{uploadId}
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * Parse a triggering key as a `pending/{tenantId}/{uploadId}` key, validating
43
+ * the FORM by round-tripping the parsed parts back through `pendingKey()`. A
44
+ * key only parses if rebuilding it from its parts yields the identical string —
45
+ * so a path-traversal payload, extra segments, or a malformed id can never pass
46
+ * (cas-keys.ts owns the anchored allowlists).
47
+ *
48
+ * @returns the {tenantId, uploadId} when the key is a canonical pending key,
49
+ * or null for ANY other key (which the caller ack-drops; we never
50
+ * write outputs under pending/, so a non-pending key is not our work).
51
+ */
52
+ export function parsePendingKey(key) {
53
+ const parts = key.split("/");
54
+ if (parts.length !== 3 || parts[0] !== "pending") {
55
+ return null;
56
+ }
57
+ const tenantId = parts[1];
58
+ const uploadId = parts[2];
59
+ const rebuilt = pendingKey(tenantId, uploadId);
60
+ if (isCasKeyError(rebuilt) || rebuilt !== key) {
61
+ return null;
62
+ }
63
+ return { tenantId, uploadId };
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // S3-event-over-SQS extraction
67
+ // ---------------------------------------------------------------------------
68
+ /** Every object key referenced by one SQS record's S3 event notification. */
69
+ export function extractObjectKeys(recordBody) {
70
+ const parsed = JSON.parse(recordBody);
71
+ const s3Records = parsed.Records ?? [];
72
+ const keys = [];
73
+ for (const r of s3Records) {
74
+ const raw = r?.s3?.object?.key;
75
+ if (typeof raw === "string") {
76
+ // S3 URL-encodes keys and uses '+' for spaces in notifications.
77
+ keys.push(decodeURIComponent(raw.replace(/\+/g, " ")));
78
+ }
79
+ }
80
+ return keys;
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Typed errors the orchestration core throws internally
84
+ // ---------------------------------------------------------------------------
85
+ /** A permanent payload defect: the key did not match the row's tenant/upload. */
86
+ class KeyTenantMismatchError extends Error {
87
+ constructor() {
88
+ // The name is in classify-worker-error's poison fragment set ("validation").
89
+ super("media key/tenant validation mismatch: triggering key does not match the row");
90
+ this.name = "ValidationError";
91
+ }
92
+ }
93
+ /** A permanent payload defect: the probed duration exceeds the configured cap. */
94
+ class DurationCapExceededError extends Error {
95
+ constructor() {
96
+ super("media duration cap exceeded");
97
+ this.name = "DurationCapExceeded";
98
+ }
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Orchestration core — testable against the B0 Mocks
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * Orchestrate processing for ONE already-extracted object key.
105
+ *
106
+ * Steps (every uncertainty fails closed; nothing here can yield APPROVED):
107
+ * 1. Reject any key that is not a canonical `pending/{tenant}/{upload}` key —
108
+ * ack-drop it; outputs are NEVER written under pending/.
109
+ * 2. Load the MediaFile row by uploadId; re-derive tenant FROM THE ROW and
110
+ * assert pendingKey(rowTenant, uploadId) === the triggering key. Mismatch
111
+ * (or missing/uploadId-less row) is a hard reject → REVIEW + ack.
112
+ * 3. Probe duration; over-cap ⇒ poison ⇒ REVIEW + ack (no transcode).
113
+ * 4. Transcode-and-discard ⇒ cleaned bytes at the STAGING key (read back from
114
+ * the cleaned key). The cleaned bytes are NOT written to cas/ here — cas/ is
115
+ * the CDN-served prefix and must hold only APPROVED bytes (promotion happens
116
+ * in the completion worker).
117
+ * 5. Hash the cleaned bytes ⇒ realHash; PERSIST {contentHash: realHash,
118
+ * originalKey: casKey(tenant, realHash)} onto the row, replacing the
119
+ * upload-time uploadId placeholder so the completion worker can derive the
120
+ * promote target.
121
+ * 6. START moderation on the cleaned STAGING object (NOT the raw pending upload,
122
+ * NOT a cas/ key) — moderation must run on EXACTLY the bytes that will be
123
+ * served: provider.startVideoModeration ⇒ persist VISUAL job (+ threshold
124
+ * snapshot); transcribe.startTranscription ⇒ persist AUDIO job (+ snapshot).
125
+ * The worker only STARTS jobs + persists jobIds; it never fetches verdicts.
126
+ */
127
+ export async function processObjectKey(triggeringKey, deps) {
128
+ try {
129
+ // --- 1. Pending-key form gate (re-trigger-loop guard). ---
130
+ const parsed = parsePendingKey(triggeringKey);
131
+ if (parsed === null) {
132
+ deps.logger.info("Dropping non-pending key (not our work)", {
133
+ key: triggeringKey,
134
+ });
135
+ return { disposition: "ack", reason: "non-pending-key" };
136
+ }
137
+ const { uploadId } = parsed;
138
+ // --- 2. Load row; re-derive tenant FROM THE ROW; assert key match. ---
139
+ const row = await deps.persistence.findMediaByUploadId(uploadId);
140
+ if (row === null || row.uploadId === null) {
141
+ // No row, or a row that lost its upload session — cannot certify this
142
+ // object. Permanent w.r.t. these bytes: fail closed to human review.
143
+ throw new KeyTenantMismatchError();
144
+ }
145
+ const rowTenant = row.tenantId;
146
+ const expectedKey = pendingKey(rowTenant, uploadId);
147
+ if (isCasKeyError(expectedKey) || expectedKey !== triggeringKey) {
148
+ // The triggering key's tenant segment disagrees with the owning tenant,
149
+ // OR the row's tenant is itself malformed. Either way: hard reject.
150
+ throw new KeyTenantMismatchError();
151
+ }
152
+ // --- 3. Duration cap (probe BEFORE transcoding — cost + abuse guard). ---
153
+ const probed = await deps.transcode.probeDurationSeconds(triggeringKey);
154
+ if (exceedsDurationCap(probed, deps.config.maxDurationSeconds)) {
155
+ throw new DurationCapExceededError();
156
+ }
157
+ // --- 4. Transcode-and-discard ⇒ cleaned bytes. ---
158
+ // The cleaned output is written to a transient staging key OUTSIDE pending/
159
+ // (so re-uploading the cleaned bytes can never re-trigger this worker).
160
+ const cleanedStagingKey = `processing/${rowTenant}/${uploadId}`;
161
+ const posterStagingKey = `processing/${rowTenant}/${uploadId}.poster`;
162
+ const transcodeResult = await deps.transcode.transcodeVideo({
163
+ inputPath: triggeringKey,
164
+ outputPath: cleanedStagingKey,
165
+ posterPath: posterStagingKey,
166
+ maxDurationSeconds: deps.config.maxDurationSeconds,
167
+ });
168
+ const cleanedStagingKeyOut = transcodeResult.cleanedPath;
169
+ const cleanedBytes = await deps.storage.getObject(cleanedStagingKeyOut);
170
+ // --- 5. Hash the CLEANED bytes ⇒ real content identity; persist it. ---
171
+ // We do NOT write the cleaned bytes to cas/ here: they already live at the
172
+ // STAGING key, and cas/ (the CDN-served prefix) must only ever hold APPROVED
173
+ // bytes. We persist the real hash + future serve key so the completion
174
+ // worker can promote staging -> cas/ on approval.
175
+ const contentHash = createHash("sha256").update(cleanedBytes).digest("hex");
176
+ const cleanedCasKey = casKey(rowTenant, contentHash);
177
+ if (isCasKeyError(cleanedCasKey)) {
178
+ // The hash/tenant failed the CAS allowlist — a permanent defect in our own
179
+ // derivation inputs (e.g. a malformed tenant that slipped the row check).
180
+ // Fail closed: route to review rather than serve un-addressable bytes.
181
+ throw new KeyTenantMismatchError();
182
+ }
183
+ // Replace the upload-time uploadId placeholder contentHash with the REAL
184
+ // hash and record the future serve key (cas/{tenant}/{hash}).
185
+ await deps.persistence.persistCleanedContent(row.id, {
186
+ contentHash,
187
+ originalKey: cleanedCasKey,
188
+ });
189
+ // --- 6. START moderation on the CLEANED STAGING object (the exact bytes ---
190
+ // that will be served), NOT the raw pending upload and NOT a cas/ key.
191
+ const stagingRef = { bucket: deps.bucket, key: cleanedStagingKeyOut };
192
+ const visual = await deps.moderation.startVideoModeration(stagingRef);
193
+ await deps.persistence.createModerationJob({
194
+ mediaId: row.id,
195
+ track: "VISUAL",
196
+ jobId: visual.jobId,
197
+ // Snapshot the CURRENT operative thresholds onto the job at submission.
198
+ thresholdSnapshot: deps.config.thresholds,
199
+ });
200
+ const audio = await deps.transcribe.startTranscription({
201
+ key: cleanedStagingKeyOut,
202
+ jobName: deps.newJobName(cleanedStagingKeyOut),
203
+ });
204
+ await deps.persistence.createModerationJob({
205
+ mediaId: row.id,
206
+ track: "AUDIO",
207
+ jobId: audio.jobId,
208
+ thresholdSnapshot: deps.config.thresholds,
209
+ });
210
+ deps.logger.info("Started per-track moderation jobs", {
211
+ mediaId: row.id,
212
+ stagingKey: cleanedStagingKeyOut,
213
+ casKey: cleanedCasKey,
214
+ visualJobId: visual.jobId,
215
+ audioJobId: audio.jobId,
216
+ });
217
+ return { disposition: "ack", reason: "started-moderation" };
218
+ }
219
+ catch (err) {
220
+ // Single classification point: poison ⇒ REVIEW + ack; retryable ⇒ fail.
221
+ const klass = classifyWorkerError(err);
222
+ if (klass === "poison") {
223
+ // Best-effort route to REVIEW. If we can identify the row, mark it; if we
224
+ // cannot (e.g. the failure was the row lookup itself), there is nothing to
225
+ // mark and the ack simply drops a message that would loop forever.
226
+ const reviewReason = await routePoisonToReview(triggeringKey, deps, err);
227
+ return { disposition: "ack", reason: reviewReason, poison: true };
228
+ }
229
+ deps.logger.error("Retryable media-processing fault — letting SQS retry", {
230
+ key: triggeringKey,
231
+ error: err,
232
+ });
233
+ return { disposition: "fail", reason: "retryable" };
234
+ }
235
+ }
236
+ /**
237
+ * Best-effort: drive the owning MediaFile to REVIEW for a poison failure. Never
238
+ * throws — a failure to mark must not convert a poison ack into an infinite
239
+ * retry. Returns an observability reason string.
240
+ */
241
+ async function routePoisonToReview(triggeringKey, deps, cause) {
242
+ deps.logger.warn("Poison media — routing to REVIEW + ack", {
243
+ key: triggeringKey,
244
+ error: cause,
245
+ });
246
+ const parsed = parsePendingKey(triggeringKey);
247
+ if (parsed === null) {
248
+ return "poison-no-row";
249
+ }
250
+ try {
251
+ const row = await deps.persistence.findMediaByUploadId(parsed.uploadId);
252
+ if (row === null) {
253
+ return "poison-no-row";
254
+ }
255
+ await deps.persistence.markMediaForReview(row.id);
256
+ return "poison-review";
257
+ }
258
+ catch (markErr) {
259
+ deps.logger.error("Failed to mark poison media for REVIEW (acking anyway)", {
260
+ key: triggeringKey,
261
+ error: markErr,
262
+ });
263
+ return "poison-mark-failed";
264
+ }
265
+ }
266
+ /**
267
+ * Process one SQS record (which may carry several S3 object keys). The record
268
+ * fails (SQS retry) iff ANY of its keys produced a retryable fault; otherwise
269
+ * it is acked. Per-key poison is acked, never failed.
270
+ */
271
+ export async function processRecord(record, deps) {
272
+ let keys;
273
+ try {
274
+ keys = extractObjectKeys(record.body);
275
+ }
276
+ catch (err) {
277
+ // A body we cannot even parse is a permanent payload defect (poison): a
278
+ // retry re-parses the same bytes to the same failure. Ack to avoid a loop.
279
+ deps.logger.warn("Unparseable SQS record body — acking as poison", {
280
+ messageId: record.messageId,
281
+ error: err,
282
+ });
283
+ return { disposition: "ack", reason: "unparseable-body", poison: true };
284
+ }
285
+ for (const key of keys) {
286
+ const outcome = await processObjectKey(key, deps);
287
+ if (outcome.disposition === "fail") {
288
+ // First retryable key fails the whole record; SQS redelivers it. Already-
289
+ // started keys are idempotent on the dedupe path (deriveDedupeKey).
290
+ return outcome;
291
+ }
292
+ }
293
+ return { disposition: "ack", reason: "record-complete" };
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Deps injection seam (consuming app wires concrete adapters at startup)
297
+ // ---------------------------------------------------------------------------
298
+ let injectedDeps;
299
+ /**
300
+ * Inject the concrete media-processing seams. The consuming app (Skybber) calls
301
+ * this once at Lambda cold start with its ffmpeg/MediaConvert TranscodePort, S3
302
+ * StoragePort, Transcribe TranscribePort, injected MediaModerationProvider, and
303
+ * a Prisma-backed MediaPersistencePort. Core ships NO concrete adapters.
304
+ */
305
+ export function setMediaProcessingDeps(deps) {
306
+ injectedDeps = deps;
307
+ }
308
+ /** Test helper: clear injected deps between cases. */
309
+ export function __resetMediaProcessingDeps() {
310
+ injectedDeps = undefined;
311
+ }
3
312
  const logger = new Logger({ serviceName: "media-processing-worker" });
4
- const s3 = new S3Client({ region: process.env.AWS_REGION });
313
+ /**
314
+ * The SQS entry point. Preserves `reportBatchItemFailures` semantics: only the
315
+ * messageIds whose records produced a retryable fault are returned as batch
316
+ * item failures; everything else (success / drop / poison→REVIEW) is acked by
317
+ * omission.
318
+ *
319
+ * If no concrete deps were injected, the handler fails CLOSED: it throws, so the
320
+ * whole batch is retried rather than silently dropped. An un-wired worker must
321
+ * never ack-drop real uploads.
322
+ */
5
323
  export const handler = async (event) => {
6
- const failedIds = [];
324
+ if (injectedDeps === undefined) {
325
+ // Fail closed: no backend wired ⇒ retry the batch, never drop. The
326
+ // consuming app must call setMediaProcessingDeps() at startup.
327
+ logger.error("media-processing-worker invoked with no injected deps — refusing to" +
328
+ " process. Call setMediaProcessingDeps() at cold start.");
329
+ throw new Error("media-processing-worker: deps not injected");
330
+ }
331
+ const deps = injectedDeps;
332
+ const batchItemFailures = [];
7
333
  for (const record of event.Records) {
334
+ let outcome;
8
335
  try {
9
- // S3 event notification comes via SQS
10
- const s3Event = JSON.parse(record.body);
11
- const s3Records = s3Event.Records || [];
12
- for (const s3Record of s3Records) {
13
- const bucket = s3Record.s3.bucket.name;
14
- const key = decodeURIComponent(s3Record.s3.object.key.replace(/\+/g, " "));
15
- if (!key.startsWith("originals/"))
16
- continue;
17
- // Get original
18
- const original = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
19
- const chunks = [];
20
- for await (const chunk of original.Body) {
21
- chunks.push(chunk);
22
- }
23
- const buffer = Buffer.concat(chunks);
24
- // Process with Sharp (must be installed as ARM64 binary)
25
- // dynamic import to avoid bundling issues
26
- const sharp = (await import("sharp")).default;
27
- const hash = key.split("/").pop().replace(/\.[^.]+$/, "");
28
- // Thumbnail: 300px WebP
29
- const thumbnail = await sharp(buffer)
30
- .resize(300, 300, { fit: "cover" })
31
- .webp({ quality: 80 })
32
- .toBuffer();
33
- // Optimized: 1200px WebP
34
- const optimized = await sharp(buffer)
35
- .resize(1200, 1200, { fit: "inside", withoutEnlargement: true })
36
- .webp({ quality: 85 })
37
- .toBuffer();
38
- await Promise.all([
39
- s3.send(new PutObjectCommand({
40
- Bucket: bucket, Key: `thumbnails/${hash}.webp`,
41
- Body: thumbnail, ContentType: "image/webp",
42
- })),
43
- s3.send(new PutObjectCommand({
44
- Bucket: bucket, Key: `optimized/${hash}.webp`,
45
- Body: optimized, ContentType: "image/webp",
46
- })),
47
- ]);
48
- logger.info("Media processed", { key, hash });
49
- }
336
+ outcome = await processRecord(record, deps);
50
337
  }
51
338
  catch (err) {
52
- logger.error("Media processing failed", { error: err, messageId: record.messageId });
53
- failedIds.push(record.messageId);
339
+ // Defensive: processRecord is designed not to throw, but if it does, treat
340
+ // it as retryable (fail closed for retry; DLQ + alert is the backstop).
341
+ logger.error("Unexpected throw from processRecord — retrying record", {
342
+ messageId: record.messageId,
343
+ error: err,
344
+ });
345
+ batchItemFailures.push({ itemIdentifier: record.messageId });
346
+ continue;
347
+ }
348
+ if (outcome.disposition === "fail") {
349
+ batchItemFailures.push({ itemIdentifier: record.messageId });
54
350
  }
55
351
  }
56
- if (failedIds.length > 0) {
57
- return { batchItemFailures: failedIds.map((id) => ({ itemIdentifier: id })) };
58
- }
352
+ return { batchItemFailures };
59
353
  };
60
354
  //# sourceMappingURL=media-processing-worker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"media-processing-worker.js","sourceRoot":"","sources":["../../src/lambda/media-processing-worker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"media-processing-worker.js","sourceRoot":"","sources":["../../src/lambda/media-processing-worker.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/lib/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAgB,KAAK,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMlE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAA0B,MAAM,iBAAiB,CAAC;AAC1F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAwElE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,GAAG,CAAC;IAChB,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED,KAAK,MAAM,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AA+BzC;;;;;;GAMG;AACH,iBAAS,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAcnE;AAkBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BzD;AAqDD,6EAA6E;AAC7E,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;AAgH1B;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAEnD,CAAC;AAiCF;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,CA+E3C"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/lib/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAgB,KAAK,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMlE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAA0B,MAAM,iBAAiB,CAAC;AAC1F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAyElE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,GAAG,CAAC;IAChB,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED,KAAK,MAAM,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AA+BzC;;;;;;GAMG;AACH,iBAAS,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAcnE;AAkBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BzD;AAqDD,6EAA6E;AAC7E,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;AAoH1B;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAEnD,CAAC;AAiCF;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,CA+E3C"}
package/dist/lib/app.js CHANGED
@@ -81,6 +81,7 @@ import { privacyRoutes } from "./routes/privacy.js";
81
81
  import { productTaxonomyRoutes } from "./routes/products.js";
82
82
  import { relationshipRoutes } from "./routes/relationships.js";
83
83
  import { sentimentsRoutes } from "./routes/sentiments.js";
84
+ import { settingsRoutes } from "./routes/settings.js";
84
85
  import { setupStatusRoutes } from "./routes/setup-status.js";
85
86
  import { taxonomyRoutes } from "./routes/taxonomy.js";
86
87
  import { taxonomyAnalyticsRoutes } from "./routes/taxonomy-analytics.js";
@@ -319,6 +320,10 @@ const PORTED_ROUTE_SETS = [
319
320
  postsRoutes,
320
321
  productTaxonomyRoutes,
321
322
  sentimentsRoutes,
323
+ // settings.ts: CHANGES_PATTERN is exact (/api/settings/changes) and listed
324
+ // first in the set so it wins over the NAMESPACE_PATTERN ([^/]+) capture in
325
+ // registration order — "changes" is never matched as a :namespace.
326
+ settingsRoutes,
322
327
  setupStatusRoutes,
323
328
  tenantRoutes,
324
329
  tenantAuditRoutes,