Repository navigation
Conversation
… and surface send failures
…h missing or placeholder secrets
…ealthcheck cap-web on /api/status
… CPUs instead of 1
…report draining on /health
…optional host allow-list and compare the secret in constant time
…uplicate framer-motion runtime
…, height, quality or timestamp
Trusted endpoints match host and port, and every redirect hop is checked before fetch or ffmpeg sees it. Drain keeps /health at 200 and waits for probe and audio work, and a missing mail setup reports not_configured instead of failing the invite. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@07rjain is attempting to deploy a commit to the Cap Software Inc Team on Vercel. A member of the Team first needs to authorize it. |
| await assertUrlAllowed(resolved); | ||
| return resolved; |
There was a problem hiding this comment.
materializeHlsPlaylist checks a segment URL but leaves that remote URL in the local playlist. If a public segment redirects to http://127.0.0.1:3000/, ffmpeg follows it without another policy check. Audio extraction, webhook delivery, and storage uploads also still bypass redirect checks.
Route every remote request through the policy, including playlist resources and uploads. Checking a URL before handing it to ffmpeg is not enough.
How this was verified: Checked segment URLs remain remote in the playlist passed to ffmpeg, while audio and webhook paths make remote requests without checking redirect targets.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-video.ts
Line: 324-325
Comment:
**Redirects bypass URL checks**
`materializeHlsPlaylist` checks a segment URL but leaves that remote URL in the local playlist. If a public segment redirects to `http://127.0.0.1:3000/`, ffmpeg follows it without another policy check. Audio extraction, webhook delivery, and storage uploads also still bypass redirect checks.
Route every remote request through the policy, including playlist resources and uploads. Checking a URL before handing it to ffmpeg is not enough.
**How this was verified:** Checked segment URLs remain remote in the playlist passed to ffmpeg, while audio and webhook paths make remote requests without checking redirect targets.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const allowedUrl = await resolveAllowedUrl(videoUrl); | ||
| return await withMediaOperation("probe", () => | ||
| withTimeout(probeMedia(videoUrl), PROBE_TIMEOUT_MS), | ||
| withTimeout(probeMedia(allowedUrl), PROBE_TIMEOUT_MS), |
There was a problem hiding this comment.
Probes wait outside their limits
probeVideo now awaits resolveAllowedUrl before registering the probe or starting its 30-second timeout. The helper sends HEAD and GET requests without an abort signal. If a host stalls on HEAD, the request waits outside the timeout and shutdown drain count.
Concurrent requests can also all pass the six-probe limit before this wait finishes, then start together. Reserve the operation before this network work and include it in the timeout. Pass an abort signal into resolveAllowedUrl.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/lib/media-probe.ts
Line: 129-131
Comment:
**Probes wait outside their limits**
`probeVideo` now awaits `resolveAllowedUrl` before registering the probe or starting its 30-second timeout. The helper sends HEAD and GET requests without an abort signal. If a host stalls on HEAD, the request waits outside the timeout and shutdown drain count.
Concurrent requests can also all pass the six-probe limit before this wait finishes, then start together. Reserve the operation before this network work and include it in the timeout. Pass an abort signal into `resolveAllowedUrl`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const status = getEmailDeliveryStatus(); | ||
| const r = resend(); | ||
| if (!r) { | ||
| return Promise.resolve(); | ||
| if (!status.configured || !r) { |
There was a problem hiding this comment.
sendEmail now requires RESEND_FROM_DOMAIN even when fromOverride supplies a complete sender address. If a self-host has a working Resend key but no default sender domain, account-deletion and content-report notifications now return email_not_configured before reaching their Cap Support <richie@send.cap.so> override. Those messages previously reached Resend.
Check the sender requirement against the actual sender used for the message, so an explicit sender does not require an unused default domain.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/database/emails/config.ts
Line: 77-79
Comment:
**Explicit senders stop working**
`sendEmail` now requires `RESEND_FROM_DOMAIN` even when `fromOverride` supplies a complete sender address. If a self-host has a working Resend key but no default sender domain, account-deletion and content-report notifications now return `email_not_configured` before reaching their `Cap Support <richie@send.cap.so>` override. Those messages previously reached Resend.
Check the sender requirement against the actual sender used for the message, so an explicit sender does not require an unused default domain.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| - Trusted hosts are those in `S3_INTERNAL_ENDPOINT`, `S3_PUBLIC_ENDPOINT`, `CAP_AWS_ENDPOINT`, `MEDIA_SERVER_WEBHOOK_URL` and `WEB_URL` (set them on the media server too), plus anything in `MEDIA_SERVER_ALLOWED_HOSTS`. | ||
| - When `MEDIA_SERVER_ALLOWED_HOSTS` is set, it becomes a strict allow-list: every URL must use one of those hosts or the trusted hosts above. Entries are comma-separated and may use a `*.` prefix, e.g. `minio.railway.internal,*.r2.cloudflarestorage.com`. | ||
|
|
||
| If your storage lives on a private network (MinIO in Docker or on Railway's private network), set `S3_INTERNAL_ENDPOINT` on the media server or add its host to `MEDIA_SERVER_ALLOWED_HOSTS`. Otherwise processing fails with `URL_NOT_ALLOWED`. `MEDIA_SERVER_ALLOW_PRIVATE_URLS=true` turns the private-address check off and also allows `file:` URLs; it's meant for local development and tests only. |
There was a problem hiding this comment.
The new documentation says MEDIA_SERVER_ALLOW_PRIVATE_URLS=true allows file: URLs. But assertUrlAllowed rejects non-HTTP schemes before checking that setting, and the added test explicitly requires this rejection. Operators following this advice will still receive URL_NOT_ALLOWED.
Remove the claim that the setting allows local files.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/content/docs/self-hosting.mdx
Line: 135
Comment:
**Local-file advice cannot work**
The new documentation says `MEDIA_SERVER_ALLOW_PRIVATE_URLS=true` allows `file:` URLs. But `assertUrlAllowed` rejects non-HTTP schemes before checking that setting, and the added test explicitly requires this rejection. Operators following this advice will still receive `URL_NOT_ALLOWED`.
Remove the claim that the setting allows local files.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Playlist and audio inputs are downloaded before ffmpeg runs, and uploads and webhooks check each redirect hop. Probes reserve a slot before that check, and an explicit email sender no longer depends on an unused default domain. Co-authored-by: Cursor <cursoragent@cursor.com>
A remote thumbnail input was still opened by ffmpeg, so a redirect to a private address was fetched. The file is downloaded through the URL check first, and cancellation during that download still aborts the request. Co-authored-by: Cursor <cursoragent@cursor.com>
ffmpeg was still opening the recording URL, so a redirect to a private address was fetched. The bytes are downloaded through the URL check first, and cancellation or the verification deadline still stops that download. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
auth@undefined, and checksMEDIA_SERVER_URLat startup without blocking boot. Coolify’s web healthcheck uses/api/status./healthat HTTP 200 withstatus: "draining", so the healthcheck does not kill the drain. Compose media services wait 30s. Direct conversion concurrency follows CPU count (1 on small machines, up to 4)./embed/[videoId]first-load JavaScript dropped from 3,258,857 bytes (28 chunks) to 1,347,848 bytes (19 chunks). Home and/embedstayed the same.emailDelivery: "not_configured"instead of a 503 after the invite row is saved.DNS is checked, then Bun
fetchand ffmpeg resolve the name again. A host that changes address between those steps can still get through. Redirect targets are checked.Test plan
bun testinapps/media-serverfor url-policy, health, video-capacity, and the video and audio routesmedia-server-startup-checkandagent-api-contractstop_grace_periodis 30s, and cap-web’s healthcheck hits/api/status/embed/[videoId]first-load JS is smallernot_configured/healthstays HTTP 200 while drainingMade with Cursor
Fix the unchecked redirect paths and probe lifecycle gap before merging.
Findings
Fix with agent prompt
Summary
This PR hardens self-host startup, adds media URL checks and shutdown draining, and splits the embed player into smaller chunks.
Acknowledgments:
07rjainexplicitly documents the DNS check/use race as a known limitation; it is not reported.07rjainintentionally supports login codes in server logs when email is unconfigured.07rjainintentionally keeps saved invites and returnsnot_configuredwhen email is unavailable.Reviews (1) · Last reviewed commit: "fix(media-server): close the URL and dra..." · Reviewed by Greptile