Skip to content

security(video): move temp files out of public directory and require Supabase signed URLs#828

Open
Dasmat13 wants to merge 5 commits into
knoxiboy:mainfrom
Dasmat13:fix/strike-reset-on-block-expiry
Open

security(video): move temp files out of public directory and require Supabase signed URLs#828
Dasmat13 wants to merge 5 commits into
knoxiboy:mainfrom
Dasmat13:fix/strike-reset-on-block-expiry

Conversation

@Dasmat13

@Dasmat13 Dasmat13 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

User description

Fixes #802

Summary

  • Move temp audio and video files out of ./public/ into OS tmp directory
  • Require Supabase Storage and return signed URLs instead of public paths
  • Update cleanup job to remove temp assets from /tmp only
  • Remove public/videos fallback path from video pipeline

Security Impact

Prevents path traversal risk and public exposure of generated media files.


CodeAnt-AI Description

Keep generated videos private and serve them through signed links

What Changed

  • Generated audio and video files now stay out of the public folder and are written to temporary storage instead.
  • Finished videos are uploaded to Supabase Storage and returned as time-limited signed links instead of public URLs.
  • Video generation now fails early when Supabase Storage is not set up, instead of falling back to a local public file path.
  • Temporary audio files are cleaned up from the temporary directory only.
  • Video requests no longer depend on a caller-provided base URL for generated assets.

Impact

✅ No public access to generated videos
✅ Fewer broken video links in production
✅ Safer handling of temporary media files

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Video generation now uses time-limited access links for completed videos, improving secure playback.
    • Generated video jobs now handle temporary files more cleanly in the background.
  • Bug Fixes

    • Improved reliability of video status updates when link generation fails.
    • Admin moderation actions now validate requests more consistently, reducing invalid submissions.

…Supabase signed URLs

- Write audio and rendered videos to OS tmp dir instead of ./public/
- Require Supabase Storage and return signed URLs instead of public paths
- Update cleanup job to remove temp assets from /tmp only
- Remove public/videos fallback path from video pipeline
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

@Dasmat13 is attempting to deploy a commit to the Karan Mani Tripathi 's projects Team on Vercel.

A member of the Team first needs to authorize it.

@codeant-ai

codeant-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@codeant-ai

codeant-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@github-actions github-actions Bot added gssoc'26 GSSoC program issue level:advanced Advanced level task type:security Security fix merge-conflict PR has merge conflicts that need resolution review-needed labels Jul 8, 2026
@github-actions github-actions Bot requested a review from knoxiboy July 8, 2026 19:52
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

@coderabbitai review
@codeantai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Video generation removes the baseUrl contract and public directory writes, moving temp audio/render output to OS temp storage with signed Supabase URLs surfaced via a new getVideoSignedUrl helper. The moderation action route adopts Zod validation with new tests; the doubts route gains explicit type annotations.

Changes

Video pipeline private storage migration

Layer / File(s) Summary
Remove baseUrl from video generate request
src/app/api/video/generate/route.ts
Drops baseUrl derivation/validation and removes baseUrl from the emitted Inngest event payload.
Inngest cleanup and generateVideo updates
src/inngest/functions.ts
cleanupTempAssets scans os.tmpdir() for stale doubtdesk-audio-* directories and video-*.mp4 files instead of public folders; generateVideo drops baseUrl from event data and pipeline call.
Pipeline temp files, upload flow, and cleanup test
src/lib/video/pipeline.ts, src/__tests__/video/pipeline-cleanup.test.ts
VideoPipelineInput drops baseUrl; temp audio and render output move to os.tmpdir(); audioUrl becomes file:// URLs; render is unconditionally uploaded via uploadVideo and locally cleaned up via new cleanupVideoArtifacts, verified by a new test.
Signed URL upload contract
src/lib/video/storage.ts
uploadVideo now throws instead of returning null when storage is unavailable and returns objectName; new getVideoSignedUrl generates a time-limited signed URL.
Signed URL status responses
src/app/api/video/status/route.ts
Completed job snapshots with a videoUrl are re-signed via getVideoSignedUrl, falling back to the stored URL on signing failure.

Moderation action request validation and tests

Layer / File(s) Summary
Zod validation for moderation action route
src/app/api/admin/moderation/action/route.ts
Replaces manual JSON parsing/field checks with a moderationActionSchema and parseAndValidateRequest.
Moderation action test suite
src/__tests__/api/admin-moderation-action.test.ts
Adds mocks for auth, email, audit logging, and DB, and tests unauthorized access, validation failures, missing user lookup, and dismiss/warn/block success paths.

TypeScript-only fixes in doubts route

Layer / File(s) Summary
Explicit typing for tag rows and tag map matches
src/app/api/doubts/route.ts
Adds explicit type annotations for tagRows and existingTagsMap lookups without behavior changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VideoGenerateRoute
  participant Inngest
  participant RunVideoPipeline
  participant TempDir
  participant Storage
  participant VideoStatusRoute

  VideoGenerateRoute->>Inngest: emit video/generate.requested without baseUrl
  Inngest->>RunVideoPipeline: runVideoPipeline({ content, imageUrl })
  RunVideoPipeline->>TempDir: write temp audio and render output in os.tmpdir()
  RunVideoPipeline->>Storage: uploadVideo(outputLocation, objectName)
  Storage-->>RunVideoPipeline: uploaded objectName / videoUrl
  RunVideoPipeline->>TempDir: delete local rendered file and temp audio
  VideoStatusRoute->>Storage: getVideoSignedUrl(objectName)
  Storage-->>VideoStatusRoute: signedUrl
Loading

Possibly related PRs

  • knoxiboy/DoubtDesk#389: Both PRs modify the doubts route's tag query/handling code, one adding pagination/sorting and the other stricter typing.
  • knoxiboy/DoubtDesk#603: Both PRs modify the same admin moderation action route, one adding Zod validation and the other audit log emission.
  • knoxiboy/DoubtDesk#731: Both PRs change the async video job pipeline and generateVideo/status routes, overlapping on the same functions and flow.

Suggested reviewers: knoxiboy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The admin moderation route change and its new test suite are unrelated to the video security fix. Move the admin moderation changes and tests to a separate PR, or remove them from this security-focused change.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main security-focused video storage changes in the PR.
Linked Issues check ✅ Passed The video generation flow now uses OS temp files, Supabase signed URLs, and private storage, satisfying #802.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/m label Jul 8, 2026
@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Jul 8, 2026
@github-actions github-actions Bot removed the size:M This PR changes 30-99 lines, ignoring generated files label Jul 8, 2026
Comment thread src/inngest/functions.ts Outdated
const deletedFiles = await step.run("delete-old-files", async () => {
const tempDir = path.resolve("./public/temp-assets");
const videosDir = path.resolve("./public/videos");
const tempDir = path.join(os.tmpdir(), "doubtdesk-audio-");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The cleanup target path is incorrect for how temp audio directories are created. mkdtempSync(path.join(os.tmpdir(), "doubtdesk-audio-")) creates directories like /tmp/doubtdesk-audio-xxxxxx, but this code checks only /tmp/doubtdesk-audio-, which usually does not exist, so old temp assets are never deleted. [logic error]

Severity Level: Major ⚠️
⚠️ Hourly temp cleanup never touches actual audio temp directories.
⚠️ Orphaned audio files persist after failed video renders.
⚠️ Disk usage grows over time from leaked audio files.
Steps of Reproduction ✅
1. Start the Inngest worker so scheduled functions in `src/inngest/functions.ts` are
active; note the hourly cron for `cleanupTempAssets` at lines 38-40.

2. Trigger a video generation job by emitting the `video/generate.requested` event, which
invokes `generateVideo` at `src/inngest/functions.ts:73-129` and then `runVideoPipeline`
at `src/lib/video/pipeline.ts:95-251`.

3. Within `runVideoPipeline`, observe that temporary audio directory creation uses
`fs.mkdtempSync(path.join(os.tmpdir(), "doubtdesk-audio-"))` at
`src/lib/video/pipeline.ts:180`, producing a directory like `/tmp/doubtdesk-audio-abcdef`
(with a random suffix) that contains audio files.

4. Allow the hourly `cleanupTempAssets` job to run; its implementation at
`src/inngest/functions.ts:41-57` checks only the fixed path `path.join(os.tmpdir(),
"doubtdesk-audio-")`, which does not exist, so `fs.existsSync(tempDir)` is false and no
files under `/tmp/doubtdesk-audio-*` are ever scanned or deleted, leaving any orphaned
audio files from failed pipeline runs on disk.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/inngest/functions.ts
**Line:** 42:42
**Comment:**
	*Logic Error: The cleanup target path is incorrect for how temp audio directories are created. `mkdtempSync(path.join(os.tmpdir(), "doubtdesk-audio-"))` creates directories like `/tmp/doubtdesk-audio-xxxxxx`, but this code checks only `/tmp/doubtdesk-audio-`, which usually does not exist, so old temp assets are never deleted.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread src/lib/video/pipeline.ts
await onProgress({ progress: 65, step: "Generating audio…" });
const tempDir = path.resolve("./public/temp-assets");
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "doubtdesk-audio-"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Each run creates a unique temp directory with mkdtempSync, but only individual audio files are deleted and the directory itself is never removed, causing unbounded empty-directory buildup in /tmp. Remove the temp directory explicitly after file cleanup (and in failure paths). [missing cleanup]

Severity Level: Major ⚠️
⚠️ Every job leaves orphaned audio temp directories in /tmp.
⚠️ Growing directory clutter complicates debugging filesystem state.
⚠️ Slightly increased inode usage on long-lived workers.
Steps of Reproduction ✅
1. Trigger a video generation job via POST /api/video/generate
(src/app/api/video/generate/route.ts:22-84), which creates a video_jobs row and emits the
"video/generate.requested" event consumed by Inngest.

2. The generateVideo function in src/inngest/functions.ts:33-58 invokes runVideoPipeline,
and during step 4 (TTS), runVideoPipeline creates a unique tempDir using
fs.mkdtempSync(path.join(os.tmpdir(), "doubtdesk-audio-")) at
src/lib/video/pipeline.ts:180.

3. For each generated scene, runVideoPipeline writes audio-*.mp3 files inside tempDir
(lines 183-201) and, after rendering, deletes each audio file in the "Clean up temporary
audio files" block at src/lib/video/pipeline.ts:230-241; this cleanup only unlinks files
and never removes the tempDir directory itself.

4. After the pipeline completes, inspecting os.tmpdir() on the worker shows that the
doubtdesk-audio-* directory created at line 180 still exists but is now empty; repeating
successful jobs accumulates more orphaned temp directories under /tmp because
runVideoPipeline never deletes its tempDir.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/video/pipeline.ts
**Line:** 180:180
**Comment:**
	*Missing Cleanup: Each run creates a unique temp directory with `mkdtempSync`, but only individual audio files are deleted and the directory itself is never removed, causing unbounded empty-directory buildup in `/tmp`. Remove the temp directory explicitly after file cleanup (and in failure paths).

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread src/lib/video/pipeline.ts Outdated
Comment on lines +247 to +248
const videoUrl = await uploadVideo(outputLocation, objectName);
await fs.promises.unlink(outputLocation).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The local rendered MP4 is only deleted after uploadVideo succeeds; if upload/signing throws, cleanup is skipped and large files accumulate in /tmp. Move output-file deletion into a finally path so it runs on both success and failure. [resource leak]

Severity Level: Major ⚠️
❌ Failed uploads leak large MP4 files in /tmp.
⚠️ Serverless workers risk disk exhaustion under repeated failures.
⚠️ Manual cleanup required to reclaim worker storage space.
Steps of Reproduction ✅
1. Start a video generation job by calling POST /api/video/generate (request handler at
src/app/api/video/generate/route.ts:22-84), which inserts a row into videoJobsTable and
emits the "video/generate.requested" event with jobId, content, and imageUrl.

2. The Inngest generateVideo function (src/inngest/functions.ts:33-58) handles this event
and calls runVideoPipeline({ content, imageUrl }, onProgress) defined in
src/lib/video/pipeline.ts:95-250, running OCR, script generation, TTS, and Remotion
render.

3. Inside runVideoPipeline, Remotion renders the MP4 to outputLocation under os.tmpdir()
at src/lib/video/pipeline.ts:209, then awaits uploadVideo(outputLocation, objectName) at
line 247; if Supabase is misconfigured or unavailable (e.g., NEXT_PUBLIC_SUPABASE_URL
missing or bucket error), uploadVideo throws inside src/lib/video/storage.ts:23-38 before
returning.

4. Because the exception occurs before fs.promises.unlink(outputLocation).catch(() => {})
at src/lib/video/pipeline.ts:248, the large MP4 file remains in /tmp on the serverless
worker while generateVideo marks the job as failed at src/inngest/functions.ts:75-80;
repeated upload failures leave multiple leaked video files on disk.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/video/pipeline.ts
**Line:** 247:248
**Comment:**
	*Resource Leak: The local rendered MP4 is only deleted after `uploadVideo` succeeds; if upload/signing throws, cleanup is skipped and large files accumulate in `/tmp`. Move output-file deletion into a `finally` path so it runs on both success and failure.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread src/lib/video/storage.ts
const VIDEO_BUCKET = process.env.SUPABASE_VIDEO_BUCKET || "videos";

// How long generated video signed URLs remain valid (1 hour).
const SIGNED_URL_EXPIRY_SECONDS = 60 * 60;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The function now returns a 1-hour signed URL and that URL is persisted in video_jobs, so later status reads will serve an expired link and completed videos become unusable after expiry. Store a stable object key in the job record and generate a fresh signed URL when clients request status/result instead of persisting a short-lived signed URL. [api mismatch]

Severity Level: Critical 🚨
❌ Completed videos become inaccessible after signed URL expiry.
❌ Status endpoint serves expired links, confusing client behavior.
⚠️ Users lose ability to revisit generated explanations later.
Steps of Reproduction ✅
1. Start a video generation by calling POST /api/video/generate
(src/app/api/video/generate/route.ts:22-84), which inserts a video_jobs row and sends a
"video/generate.requested" event with jobId to Inngest.

2. The generateVideo function in src/inngest/functions.ts:33-75 calls runVideoPipeline;
when runVideoPipeline finishes, generateVideo updates videoJobsTable to set videoUrl and
videoType from result.videoUrl and result.videoType at lines 60-71, persisting the
returned videoUrl string in the database.

3. Inside runVideoPipeline (src/lib/video/pipeline.ts:243-250),
uploadVideo(outputLocation, objectName) is called; uploadVideo in
src/lib/video/storage.ts:19-49 uploads the MP4 and then creates a Supabase signed URL via
createSignedUrl(objectName, SIGNED_URL_EXPIRY_SECONDS) at line 42, where
SIGNED_URL_EXPIRY_SECONDS is defined as 60 * 60 (1 hour) at line 8, and returns this
short-lived signedUrl.

4. A client streams GET /api/video/status?jobId=… from
src/app/api/video/status/route.ts:31-151, receiving JobSnapshot events that include
videoUrl and videoType (lines 101-109); the client uses snapshot.videoUrl for playback. If
the user revisits the job after more than one hour, the SSE stream still reports status
"completed" and the same stored videoUrl from videoJobsTable, but Supabase rejects
requests to that expired signed URL, so the completed video can no longer be viewed even
though the job is marked successful.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/video/storage.ts
**Line:** 8:8
**Comment:**
	*Api Mismatch: The function now returns a 1-hour signed URL and that URL is persisted in `video_jobs`, so later status reads will serve an expired link and completed videos become unusable after expiry. Store a stable object key in the job record and generate a fresh signed URL when clients request status/result instead of persisting a short-lived signed URL.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/lib/video/storage.ts (1)

41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Capture the createSignedUrl error for diagnosability.

Only data is destructured, so a failure surfaces as the generic "Failed to create signed URL" without the underlying cause. Destructure error too and include its message.

🪵 Include underlying error
-  const { data } = await supabase.storage
+  const { data, error: signError } = await supabase.storage
     .from(VIDEO_BUCKET)
     .createSignedUrl(objectName, SIGNED_URL_EXPIRY_SECONDS);

-  if (!data?.signedUrl) {
-    throw new Error("Failed to create signed URL for uploaded video");
+  if (signError || !data?.signedUrl) {
+    throw new Error(
+      `Failed to create signed URL for uploaded video${signError ? `: ${signError.message}` : ""}`,
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/video/storage.ts` around lines 41 - 47, The signed URL creation path
in the storage helper only checks data from createSignedUrl, so failures lose
the underlying cause; update the createSignedUrl call in the video storage logic
to also destructure the returned error and include its message in the thrown
error. Keep the fix localized to the signed URL generation flow in the storage
module, and make sure the existing validation around signedUrl still guards the
happy path.
src/app/api/doubts/route.ts (1)

416-418: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the lookup typed as a select row.

existingClassroomTags comes from db.select().from(tagsTable), so the map values are read rows, not insert payloads. The $inferInsert cast weakens the compiler check and makes the later savedTags.push cast unnecessary.

♻️ Suggested fix
-        const match = existingTagsMap.get(name) as typeof tagsTable.$inferInsert | undefined;
-        if (match) {
-          savedTags.push(match as typeof tagsTable.$inferSelect);
+        const match = existingTagsMap.get(name) as typeof tagsTable.$inferSelect | undefined;
+        if (match) {
+          savedTags.push(match);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/doubts/route.ts` around lines 416 - 418, The lookup in the tags
reuse flow is typed too loosely by casting the map value to an insert shape,
which then forces an unnecessary cast when pushing into savedTags. Update the
existingTagsMap / match typing in the doubts route so the values are treated as
select rows from tagsTable (the result of db.select().from(tagsTable)), and
remove the $inferInsert cast from the lookup; this should let savedTags.push
accept the row type directly without the extra $inferSelect cast.
src/app/api/invites/[token]/join/route.ts (1)

102-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the @ts-ignore/any cast here. db.execute<{ membership_id: number }>(...) already types the returned rows, so the escape hatch is unnecessary on this write path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/invites/`[token]/join/route.ts around lines 102 - 103, Remove the
unnecessary TypeScript escape hatch in the join route: the `@ts-ignore` and `as
any` around `db.execute` are no longer needed because `db.execute<{
membership_id: number }>(...)` already provides the correct row typing. Update
the `join` handler in `route.ts` to call `db.execute` directly with the generic
type and keep the `joinResult` usage typed through that inferred result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/inngest/functions.ts`:
- Around line 42-58: The cleanup logic in runVideoPipeline is scanning a fixed
temp directory name that does not match the actual artifacts created by
fs.mkdtempSync and the video render output, so it never removes stale files.
Update the cleanup block to iterate over os.tmpdir() and match both
doubtdesk-audio-* directories and video-*.mp4 files, then delete those matches
recursively for directories and directly for files. Use the existing
tempDir/tempDir pattern code in functions.ts to keep the cleanup aligned with
the actual locations created by the pipeline.

In `@src/lib/video/pipeline.ts`:
- Around line 244-250: The video render cleanup in pipeline.ts is only happening
after uploadVideo succeeds, so failed uploads leave outputLocation behind in
os.tmpdir() and also leave the mkdtemp-created tempDir directory behind. Update
the upload flow around uploadVideo in the pipeline return path to use
try/finally so local cleanup always runs, and make sure the finally block
removes both the rendered output file and its tempDir directory regardless of
whether uploadVideo throws.

In `@src/lib/video/storage.ts`:
- Around line 7-8: The signed video URL is being persisted and later returned
after it may have expired, so update the flow around `src/inngest/functions.ts`,
`src/app/api/video/status/route.ts`, and `src/lib/video/storage.ts` to avoid
storing the signed URL itself in `video_jobs.videoUrl`. Instead, persist the
storage object key (or another stable identifier) and generate a fresh signed
URL when reading the job status, or ensure the job record lifecycle is limited
to `SIGNED_URL_EXPIRY_SECONDS` and always references the current URL.

---

Nitpick comments:
In `@src/app/api/doubts/route.ts`:
- Around line 416-418: The lookup in the tags reuse flow is typed too loosely by
casting the map value to an insert shape, which then forces an unnecessary cast
when pushing into savedTags. Update the existingTagsMap / match typing in the
doubts route so the values are treated as select rows from tagsTable (the result
of db.select().from(tagsTable)), and remove the $inferInsert cast from the
lookup; this should let savedTags.push accept the row type directly without the
extra $inferSelect cast.

In `@src/app/api/invites/`[token]/join/route.ts:
- Around line 102-103: Remove the unnecessary TypeScript escape hatch in the
join route: the `@ts-ignore` and `as any` around `db.execute` are no longer
needed because `db.execute<{ membership_id: number }>(...)` already provides the
correct row typing. Update the `join` handler in `route.ts` to call `db.execute`
directly with the generic type and keep the `joinResult` usage typed through
that inferred result.

In `@src/lib/video/storage.ts`:
- Around line 41-47: The signed URL creation path in the storage helper only
checks data from createSignedUrl, so failures lose the underlying cause; update
the createSignedUrl call in the video storage logic to also destructure the
returned error and include its message in the thrown error. Keep the fix
localized to the signed URL generation flow in the storage module, and make sure
the existing validation around signedUrl still guards the happy path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1d91a95e-1d8a-4d8e-9006-a5f682b96f4d

📥 Commits

Reviewing files that changed from the base of the PR and between de5a7d1 and ceed420.

📒 Files selected for processing (7)
  • src/__tests__/configs/db.test.ts
  • src/app/api/doubts/route.ts
  • src/app/api/invites/[token]/join/route.ts
  • src/app/api/video/generate/route.ts
  • src/inngest/functions.ts
  • src/lib/video/pipeline.ts
  • src/lib/video/storage.ts
💤 Files with no reviewable changes (1)
  • src/app/api/video/generate/route.ts

Comment thread src/inngest/functions.ts Outdated
Comment thread src/lib/video/pipeline.ts Outdated
Comment thread src/lib/video/storage.ts
…l, and signed URL expiry

- Fix cleanupTempAssets to scan /tmp/doubtdesk-audio-* directories
- Remove empty temp directories after cleanup
- Wrap rendered output deletion in finally block
- Store object key in DB; generate fresh signed URL on status read

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/api/video/status/route.ts (1)

112-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log signing failures instead of silently swallowing them.

The catch block discards all errors with no logging. If Supabase Storage is misconfigured or unavailable, completed videos will silently serve broken links with no trace for operators. A console.error is trivial to add and critical for production observability.

♻️ Proposed refactor
         try {
           snapshot.videoUrl = await getVideoSignedUrl(snapshot.videoUrl);
         } catch {
-          // keep stored object key; client will see a broken link rather than no status
+          console.error("Failed to sign video URL:", snapshot.videoUrl, err);
+          // keep stored object key; client will see a broken link rather than no status
         }

Note: the catch parameter needs to be captured:

-        } catch {
+        } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/video/status/route.ts` around lines 112 - 118, The signing
failure in the completed-video branch is being swallowed in the route handler,
so add error logging in the catch block instead of ignoring it. Update the
try/catch around getVideoSignedUrl in the status route to capture the thrown
error and log it with console.error (or the existing logger) while preserving
the current fallback behavior of keeping snapshot.videoUrl unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/video/storage.ts`:
- Around line 52-57: The signed URL creation path in createSignedVideoUrl is
dropping the Supabase failure details by only checking data?.signedUrl. Update
the createSignedUrl call to also read the returned error and, when signing
fails, throw an exception that includes error?.message alongside the existing
context so bucket/object/permission issues are actionable.

---

Nitpick comments:
In `@src/app/api/video/status/route.ts`:
- Around line 112-118: The signing failure in the completed-video branch is
being swallowed in the route handler, so add error logging in the catch block
instead of ignoring it. Update the try/catch around getVideoSignedUrl in the
status route to capture the thrown error and log it with console.error (or the
existing logger) while preserving the current fallback behavior of keeping
snapshot.videoUrl unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a5f69579-4ffd-4b0b-beb9-357fa69238e4

📥 Commits

Reviewing files that changed from the base of the PR and between ceed420 and 1947f0f.

📒 Files selected for processing (4)
  • src/app/api/video/status/route.ts
  • src/inngest/functions.ts
  • src/lib/video/pipeline.ts
  • src/lib/video/storage.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/inngest/functions.ts
  • src/lib/video/pipeline.ts

Comment thread src/lib/video/storage.ts Outdated
@github-actions github-actions Bot added size/l and removed merge-conflict PR has merge conflicts that need resolution size/m labels Jul 8, 2026
@Dasmat13 Dasmat13 force-pushed the fix/strike-reset-on-block-expiry branch from 5b25b64 to 92b9f8e Compare July 8, 2026 21:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/api/admin/moderation/action/route.ts (1)

127-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Line 127 is now unreachable dead code.

With the Zod enum validation at line 15 (z.enum(["dismiss", "warn", "block"])), action is guaranteed to be one of the three values before reaching the control flow. All three are handled by the if blocks above, making this fallback impossible to reach.

♻️ Optional: remove unreachable fallback
         return NextResponse.json({ success: true, message: "User blocked successfully" });
     }
-
-    return NextResponse.json({ error: "Invalid action" }, { status: 400 });
 
     } catch (error: unknown) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/admin/moderation/action/route.ts` at line 127, The fallback in
the moderation action route is unreachable because `action` is already
constrained by the Zod enum in the `route.ts` handler and all valid cases are
covered by the existing `if` branches. Remove the dead `NextResponse.json({
error: "Invalid action" }, { status: 400 })` path from the `action` handling
logic so the control flow only contains the reachable `dismiss`, `warn`, and
`block` cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/api/admin-moderation-action.test.ts`:
- Around line 76-105: The current POST test only covers Zod validation failures,
but it misses the malformed JSON path in parseAndValidateRequest. Add a separate
test case in the admin moderation action suite that sends invalid JSON to POST
and asserts the 400 response with success false and error "Invalid JSON format",
alongside the existing validation cases. Use the existing POST helper and
parseAndValidateRequest flow as the reference points so the new case targets the
distinct JSON parsing branch.

---

Nitpick comments:
In `@src/app/api/admin/moderation/action/route.ts`:
- Line 127: The fallback in the moderation action route is unreachable because
`action` is already constrained by the Zod enum in the `route.ts` handler and
all valid cases are covered by the existing `if` branches. Remove the dead
`NextResponse.json({ error: "Invalid action" }, { status: 400 })` path from the
`action` handling logic so the control flow only contains the reachable
`dismiss`, `warn`, and `block` cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0da95e46-be60-4aa0-af90-bce7baf29c8a

📥 Commits

Reviewing files that changed from the base of the PR and between 1947f0f and 5b25b64.

📒 Files selected for processing (8)
  • src/__tests__/api/admin-moderation-action.test.ts
  • src/__tests__/video/pipeline-cleanup.test.ts
  • src/app/api/admin/moderation/action/route.ts
  • src/app/api/doubts/route.ts
  • src/app/api/video/status/route.ts
  • src/inngest/functions.ts
  • src/lib/video/pipeline.ts
  • src/lib/video/storage.ts
✅ Files skipped from review due to trivial changes (1)
  • src/app/api/doubts/route.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/api/video/status/route.ts
  • src/lib/video/storage.ts
  • src/inngest/functions.ts

Comment on lines +76 to +105
it("returns 400 when body parameters violate the Zod schema", async () => {
requireAdminMock.mockResolvedValue({});
currentUserMock.mockResolvedValue({ primaryEmailAddress: { emailAddress: "admin@example.com" } });

const testCases = [
// Missing all fields
{},
// Invalid email
{ logId: 1, userEmail: "invalid-email", action: "dismiss" },
// Negative logId
{ logId: -1, userEmail: "test@example.com", action: "dismiss" },
// Float logId
{ logId: 1.5, userEmail: "test@example.com", action: "dismiss" },
// Invalid action enum
{ logId: 1, userEmail: "test@example.com", action: "invalid-action" },
];

for (const testCase of testCases) {
const req = new NextRequest("http://localhost/api/admin/moderation/action", {
method: "POST",
body: JSON.stringify(testCase),
});

const res = await POST(req);
expect(res.status).toBe(400);
const json = await res.json();
expect(json.success).toBe(false);
expect(json.error).toBe("Validation failed");
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing test case for invalid JSON body.

parseAndValidateRequest has a distinct code path for malformed JSON that returns { success: false, error: "Invalid JSON format" } with status 400 — separate from the Zod validation failure path (error: "Validation failed"). The current test loop only covers Zod schema violations. Adding an invalid-JSON case would close this coverage gap.

💚 Suggested addition to testCases
         const testCases = [
             // Missing all fields
             {},
             // Invalid email
             { logId: 1, userEmail: "invalid-email", action: "dismiss" },
             // Negative logId
             { logId: -1, userEmail: "test@example.com", action: "dismiss" },
             // Float logId
             { logId: 1.5, userEmail: "test@example.com", action: "dismiss" },
             // Invalid action enum
             { logId: 1, userEmail: "test@example.com", action: "invalid-action" },
         ];
 
         for (const testCase of testCases) {
             const req = new NextRequest("http://localhost/api/admin/moderation/action", {
                 method: "POST",
                 body: JSON.stringify(testCase),
             });
 
             const res = await POST(req);
             expect(res.status).toBe(400);
             const json = await res.json();
             expect(json.success).toBe(false);
             expect(json.error).toBe("Validation failed");
         }
+
+        // Invalid JSON body
+        const invalidJsonReq = new NextRequest("http://localhost/api/admin/moderation/action", {
+            method: "POST",
+            body: "not valid json{",
+        });
+        const invalidJsonRes = await POST(invalidJsonReq);
+        expect(invalidJsonRes.status).toBe(400);
+        const invalidJsonBody = await invalidJsonRes.json();
+        expect(invalidJsonBody.success).toBe(false);
+        expect(invalidJsonBody.error).toBe("Invalid JSON format");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("returns 400 when body parameters violate the Zod schema", async () => {
requireAdminMock.mockResolvedValue({});
currentUserMock.mockResolvedValue({ primaryEmailAddress: { emailAddress: "admin@example.com" } });
const testCases = [
// Missing all fields
{},
// Invalid email
{ logId: 1, userEmail: "invalid-email", action: "dismiss" },
// Negative logId
{ logId: -1, userEmail: "test@example.com", action: "dismiss" },
// Float logId
{ logId: 1.5, userEmail: "test@example.com", action: "dismiss" },
// Invalid action enum
{ logId: 1, userEmail: "test@example.com", action: "invalid-action" },
];
for (const testCase of testCases) {
const req = new NextRequest("http://localhost/api/admin/moderation/action", {
method: "POST",
body: JSON.stringify(testCase),
});
const res = await POST(req);
expect(res.status).toBe(400);
const json = await res.json();
expect(json.success).toBe(false);
expect(json.error).toBe("Validation failed");
}
});
it("returns 400 when body parameters violate the Zod schema", async () => {
requireAdminMock.mockResolvedValue({});
currentUserMock.mockResolvedValue({ primaryEmailAddress: { emailAddress: "admin@example.com" } });
const testCases = [
// Missing all fields
{},
// Invalid email
{ logId: 1, userEmail: "invalid-email", action: "dismiss" },
// Negative logId
{ logId: -1, userEmail: "test@example.com", action: "dismiss" },
// Float logId
{ logId: 1.5, userEmail: "test@example.com", action: "dismiss" },
// Invalid action enum
{ logId: 1, userEmail: "test@example.com", action: "invalid-action" },
];
for (const testCase of testCases) {
const req = new NextRequest("http://localhost/api/admin/moderation/action", {
method: "POST",
body: JSON.stringify(testCase),
});
const res = await POST(req);
expect(res.status).toBe(400);
const json = await res.json();
expect(json.success).toBe(false);
expect(json.error).toBe("Validation failed");
}
// Invalid JSON body
const invalidJsonReq = new NextRequest("http://localhost/api/admin/moderation/action", {
method: "POST",
body: "not valid json{",
});
const invalidJsonRes = await POST(invalidJsonReq);
expect(invalidJsonRes.status).toBe(400);
const invalidJsonBody = await invalidJsonRes.json();
expect(invalidJsonBody.success).toBe(false);
expect(invalidJsonBody.error).toBe("Invalid JSON format");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/api/admin-moderation-action.test.ts` around lines 76 - 105, The
current POST test only covers Zod validation failures, but it misses the
malformed JSON path in parseAndValidateRequest. Add a separate test case in the
admin moderation action suite that sends invalid JSON to POST and asserts the
400 response with success false and error "Invalid JSON format", alongside the
existing validation cases. Use the existing POST helper and
parseAndValidateRequest flow as the reference points so the new case targets the
distinct JSON parsing branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc'26 GSSoC program issue level:advanced Advanced level task review-needed size/l type:security Security fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security]: Video Generation Writes to Public Directory — Path Traversal Risk

1 participant