@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,73 @@
1
+ -- P0b: moderation job tracking — schema additions.
2
+ --
3
+ -- Summary of changes:
4
+ -- ~ media_files.original_key: DROP NOT NULL (nullable for video pending transcode)
5
+ -- ~ media_files.content_hash: DROP NOT NULL (null until the worker hashes the
6
+ -- transcoded bytes; @@unique([tenant, content_hash]) tolerates many NULLs)
7
+ -- + media_files.upload_id (TEXT, UNIQUE, nullable) — client idempotency key
8
+ -- + CREATE TYPE moderation_track AS ENUM ('VISUAL', 'AUDIO')
9
+ -- + CREATE TABLE media_moderation_jobs — per-track job records with FK + indexes
10
+ -- + CREATE TABLE processed_moderation_messages — SQS exactly-once dedup table
11
+ --
12
+ -- Not deployed standalone: consumed by Trellis as an npm dependency; migration
13
+ -- is applied when Trellis bumps @de-otio/trellis and runs prisma migrate deploy.
14
+
15
+ -- AlterTable: make original_key nullable (video rows have no key until post-transcode)
16
+ ALTER TABLE "media_files"
17
+ ALTER COLUMN "original_key" DROP NOT NULL;
18
+
19
+ -- AlterTable: make content_hash nullable (video has no hash until the worker
20
+ -- hashes the transcoded bytes; the within-tenant unique tolerates many NULLs)
21
+ ALTER TABLE "media_files"
22
+ ALTER COLUMN "content_hash" DROP NOT NULL;
23
+
24
+ -- AlterTable: add upload_id for client-side idempotency
25
+ ALTER TABLE "media_files"
26
+ ADD COLUMN "upload_id" TEXT;
27
+
28
+ -- CreateIndex: unique constraint on upload_id (one row per upload session)
29
+ CREATE UNIQUE INDEX "media_files_upload_id_key" ON "media_files"("upload_id");
30
+
31
+ -- CreateEnum: moderation track discriminator
32
+ CREATE TYPE "ModerationTrack" AS ENUM ('VISUAL', 'AUDIO');
33
+
34
+ -- CreateTable: per-track moderation job records
35
+ CREATE TABLE "media_moderation_jobs" (
36
+ "id" TEXT NOT NULL,
37
+ "media_id" TEXT NOT NULL,
38
+ "track" "ModerationTrack" NOT NULL,
39
+ "job_id" TEXT NOT NULL,
40
+ "decision" TEXT,
41
+ "threshold_snapshot" JSONB NOT NULL,
42
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
+ "updated_at" TIMESTAMP(3) NOT NULL,
44
+
45
+ CONSTRAINT "media_moderation_jobs_pkey" PRIMARY KEY ("id")
46
+ );
47
+
48
+ -- CreateIndex: unique constraint on job_id (one row per provider job)
49
+ CREATE UNIQUE INDEX "media_moderation_jobs_job_id_key" ON "media_moderation_jobs"("job_id");
50
+
51
+ -- CreateIndex: lookup jobs by media (used by the pipeline to find in-flight jobs)
52
+ CREATE INDEX "media_moderation_jobs_media_id_idx" ON "media_moderation_jobs"("media_id");
53
+
54
+ -- CreateIndex: lookup by job_id (used by the result-callback path)
55
+ CREATE INDEX "media_moderation_jobs_job_id_idx" ON "media_moderation_jobs"("job_id");
56
+
57
+ -- AddForeignKey: media_moderation_jobs -> media_files (cascade on delete)
58
+ ALTER TABLE "media_moderation_jobs"
59
+ ADD CONSTRAINT "media_moderation_jobs_media_id_fkey"
60
+ FOREIGN KEY ("media_id") REFERENCES "media_files"("id") ON DELETE CASCADE ON UPDATE CASCADE;
61
+
62
+ -- CreateTable: SQS exactly-once dedup for moderation result messages
63
+ CREATE TABLE "processed_moderation_messages" (
64
+ "id" TEXT NOT NULL,
65
+ "message_dedupe_key" TEXT NOT NULL,
66
+ "processed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
67
+
68
+ CONSTRAINT "processed_moderation_messages_pkey" PRIMARY KEY ("id")
69
+ );
70
+
71
+ -- CreateIndex: unique constraint on dedupe key (collision = duplicate message)
72
+ CREATE UNIQUE INDEX "processed_moderation_messages_message_dedupe_key_key"
73
+ ON "processed_moderation_messages"("message_dedupe_key");
@@ -66,6 +66,18 @@ enum EntityStatus {
66
66
  TRANSFERRED
67
67
  }
68
68
 
69
+ // Media moderation lifecycle state. Mirrors the hand-written ModerationStatus
70
+ // union in apps/api/src/lib/media/moderation-status.ts — spelling must match.
71
+ // Every new MediaFile is born PENDING (fail-closed); only the moderation
72
+ // pipeline can advance to APPROVED.
73
+ enum ModerationStatus {
74
+ PENDING
75
+ APPROVED
76
+ REVIEW
77
+ QUARANTINED
78
+ REJECTED
79
+ }
80
+
69
81
  model PostGeoIndex {
70
82
  postUri String @id @map("post_uri")
71
83
  // Multi-tenancy — denormalized from the owning post; location data is
@@ -180,7 +192,7 @@ model User {
180
192
  // migration. (WebFinger/AP still resolve on `username` until the federation
181
193
  // repoint — that change is code-only, no data migration, since `handle` is the
182
194
  // stable key.)
183
- handle String @unique
195
+ handle String @unique
184
196
  createdAt DateTime @default(now()) @map("created_at")
185
197
 
186
198
  // Cognito Auth integration
@@ -360,6 +372,11 @@ model User {
360
372
  notifications Notification[]
361
373
  notificationPreference NotificationPreference?
362
374
 
375
+ // Realtime / server-blind settings sync
376
+ encryptedSettings EncryptedUserSetting[]
377
+ blocksInitiated BlockedUser[] @relation("BlockedUserBlocker")
378
+ blockedBy BlockedUser[] @relation("BlockedUserBlocked")
379
+
363
380
  // Connection codes (out-of-band invite mechanism)
364
381
  createdConnectionCodes ConnectionCode[] @relation("ConnectionCodeCreator")
365
382
  redeemedConnectionCodes ConnectionCodeRedemption[]
@@ -600,16 +617,32 @@ model Post {
600
617
  @@map("posts")
601
618
  }
602
619
 
603
- // Media files (content-addressed storage)
620
+ // Media files (tenant-scoped content-addressed storage).
621
+ // tenantId is NOT NULL — every object belongs to exactly one tenant.
622
+ // Dedup is within-tenant: @@unique([tenantId, contentHash]).
623
+ // Every new object is born PENDING (fail-closed); the moderation pipeline
624
+ // advances objects to APPROVED before they can be served.
604
625
  model MediaFile {
605
626
  id String @id @default(cuid())
606
- contentHash String @unique @map("content_hash") // SHA-256 hash
627
+ tenantId String @map("tenant_id") // Tenant that owns this object (NOT NULL)
628
+ contentHash String? @map("content_hash") // SHA-256 hash (lowercase hex); null until post-transcode for video (set by the P0b worker)
607
629
  cid String? @unique // AT Protocol CID (optional, for compatibility)
608
630
  mimeType String @map("mime_type")
609
631
  size Int
610
- originalKey String @map("original_key") // S3 key: media/{hash}.{ext}
611
- thumbnailKey String? @map("thumbnail_key") // S3 key: media/{hash}_thumb.webp
612
- optimizedKey String? @map("optimized_key") // S3 key: media/{hash}_opt.webp
632
+ // uploadId: client-side idempotency key for the upload session. Nullable
633
+ // because rows created before P0b (or via the reconciliation path) have no
634
+ // upload session. UNIQUE so the same upload cannot race into two rows.
635
+ uploadId String? @unique @map("upload_id")
636
+ // originalKey: S3 CAS key for the raw ingested object. Nullable for video —
637
+ // the CAS key is computed post-transcode by the pipeline worker; the row
638
+ // exists before transcoding completes.
639
+ originalKey String? @map("original_key") // S3 key: cas/{tenantId}/{hash} (null until transcode for video)
640
+ thumbnailKey String? @map("thumbnail_key") // S3 key: cas/{tenantId}/{hash}/thumb
641
+ optimizedKey String? @map("optimized_key") // S3 key: cas/{tenantId}/{hash}/opt
642
+
643
+ // Moderation state — the lifecycle gate (see ModerationStatus enum above).
644
+ // Default PENDING means every object is fail-closed until approved.
645
+ moderationStatus ModerationStatus @default(PENDING) @map("moderation_status")
613
646
 
614
647
  // Media metadata (extracted from file headers)
615
648
  width Int? // Image/video width in pixels
@@ -622,13 +655,13 @@ model MediaFile {
622
655
  videoMetadata Json? @map("video_metadata")
623
656
 
624
657
  // Denormalized/unified metadata fields
625
- dateTaken DateTime? @map("date_taken")
626
- gpsLatitude Float? @map("gps_latitude")
627
- gpsLongitude Float? @map("gps_longitude")
628
- keywords String[] @default([]) @map("keywords")
658
+ // GPS coordinates are NOT stored — dropped at ingestion (data-minimization).
659
+ dateTaken DateTime? @map("date_taken")
660
+ keywords String[] @default([]) @map("keywords")
629
661
 
630
- // Privacy flags
631
- metadataVisible Boolean @default(true) @map("metadata_visible")
662
+ // Privacy flags. metadataVisible defaults false: metadata is private unless
663
+ // the owner explicitly shares it (data-minimization default).
664
+ metadataVisible Boolean @default(false) @map("metadata_visible")
632
665
  locationVisible Boolean @default(false) @map("location_visible")
633
666
 
634
667
  // Media visibility and deletion tracking
@@ -654,16 +687,18 @@ model MediaFile {
654
687
  updatedAt DateTime @updatedAt @map("updated_at")
655
688
 
656
689
  // Relations
657
- posts PostMedia[] // Many-to-many with posts
690
+ posts PostMedia[] // Many-to-many with posts
691
+ moderationJobs MediaModerationJob[] // P0b: per-track moderation job records
658
692
 
659
- @@index([contentHash])
693
+ @@unique([tenantId, contentHash]) // Within-tenant dedup (replaces bare contentHash @unique)
660
694
  @@index([cid])
695
+ @@index([moderationStatus]) // Gate queries: find PENDING for the moderation queue
696
+ @@index([tenantId, contentHash]) // Serve path: tenant-scoped CAS lookup
661
697
  @@index([hidden, deletedAt]) // For filtering visible media
662
698
  @@index([createdAt]) // For chronological sorting
663
699
  @@index([dateTaken])
664
700
  @@index([metadataVisible])
665
701
  @@index([locationVisible])
666
- @@index([gpsLatitude, gpsLongitude])
667
702
  @@index([uploadStatus])
668
703
  @@index([uploadedBy])
669
704
  @@index([uploadBatchId])
@@ -688,6 +723,54 @@ model UploadSession {
688
723
  @@map("upload_sessions")
689
724
  }
690
725
 
726
+ // ============================================================================
727
+ // P0b — Moderation job tracking
728
+ // ============================================================================
729
+
730
+ // Discriminator for the two parallel moderation tracks.
731
+ // VISUAL — frame/thumbnail-level visual content analysis.
732
+ // AUDIO — speech-to-text + audio content analysis.
733
+ enum ModerationTrack {
734
+ VISUAL
735
+ AUDIO
736
+ }
737
+
738
+ // One row per (media, track) moderation job submitted to an external provider.
739
+ // decision is String (not an enum) so the schema stays decoupled from
740
+ // ModerationDecision evolution; app code validates the value on read.
741
+ // thresholdSnapshot captures the operative thresholds at job-submission time
742
+ // so historical decisions remain auditable after a threshold change.
743
+ model MediaModerationJob {
744
+ id String @id @default(cuid())
745
+ mediaId String @map("media_id")
746
+ media MediaFile @relation(fields: [mediaId], references: [id], onDelete: Cascade)
747
+ track ModerationTrack
748
+ // jobId: the external provider's job identifier (unique per provider call).
749
+ jobId String @unique @map("job_id")
750
+ // decision: resolved ModerationDecision value, or null while the job is in flight.
751
+ decision String?
752
+ // thresholdSnapshot: a copy of env.media.thresholds at submission time (JSON).
753
+ thresholdSnapshot Json @map("threshold_snapshot")
754
+ createdAt DateTime @default(now()) @map("created_at")
755
+ updatedAt DateTime @updatedAt @map("updated_at")
756
+
757
+ @@index([mediaId])
758
+ @@index([jobId])
759
+ @@map("media_moderation_jobs")
760
+ }
761
+
762
+ // Deduplication table for SQS moderation-result messages (exactly-once delivery).
763
+ // A row is inserted (with ON CONFLICT DO NOTHING / unique-constraint) before
764
+ // processing; duplicate deliveries of the same message find the row present and
765
+ // are skipped. Rows may be pruned after a retention window by a cron worker.
766
+ model ProcessedModerationMessage {
767
+ id String @id @default(cuid())
768
+ messageDedupeKey String @unique @map("message_dedupe_key")
769
+ processedAt DateTime @default(now()) @map("processed_at")
770
+
771
+ @@map("processed_moderation_messages")
772
+ }
773
+
691
774
  // Post media
692
775
  model PostMedia {
693
776
  id String @id @default(cuid())
@@ -1748,6 +1831,7 @@ model Tenant {
1748
1831
  connectionCodes ConnectionCode[]
1749
1832
  connectionCodeRedemptions ConnectionCodeRedemption[]
1750
1833
  notifications Notification[]
1834
+ blockedUsers BlockedUser[]
1751
1835
 
1752
1836
  // Denormalized tenant_id on by-relation children (P1 — for direct RLS).
1753
1837
  postMedia PostMedia[]
@@ -2013,3 +2097,37 @@ enum EntityRelationshipStatus {
2013
2097
  CONFIRMED
2014
2098
  REJECTED
2015
2099
  }
2100
+
2101
+ // Realtime / server-blind settings sync — opaque AEAD blob the server cannot read.
2102
+ model EncryptedUserSetting {
2103
+ id String @id @default(cuid())
2104
+ userId String @map("user_id")
2105
+ namespace String // allowlisted, e.g. "feed_filters"
2106
+ ciphertext String @db.Text // opaque AEAD blob; the server cannot read it
2107
+ version Int @default(1) // monotonic per (user, namespace)
2108
+ updatedAt DateTime @updatedAt @map("updated_at")
2109
+ createdAt DateTime @default(now()) @map("created_at")
2110
+
2111
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2112
+
2113
+ @@unique([userId, namespace])
2114
+ @@index([userId])
2115
+ @@map("encrypted_user_settings")
2116
+ }
2117
+
2118
+ // Realtime delivery safety floor — blocked-sender relation (who blocks whom).
2119
+ model BlockedUser {
2120
+ id String @id @default(cuid())
2121
+ tenantId String @map("tenant_id")
2122
+ blockerId String @map("blocker_id")
2123
+ blockedId String @map("blocked_id")
2124
+ createdAt DateTime @default(now()) @map("created_at")
2125
+
2126
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2127
+ blocker User @relation("BlockedUserBlocker", fields: [blockerId], references: [id], onDelete: Cascade)
2128
+ blocked User @relation("BlockedUserBlocked", fields: [blockedId], references: [id], onDelete: Cascade)
2129
+
2130
+ @@unique([tenantId, blockerId, blockedId])
2131
+ @@index([tenantId, blockerId])
2132
+ @@map("blocked_users")
2133
+ }