Skip to content

Add inline library previews and PDF exports#5

Open
Acex187x wants to merge 2 commits into
mainfrom
t3code/fa841013
Open

Add inline library previews and PDF exports#5
Acex187x wants to merge 2 commits into
mainfrom
t3code/fa841013

Conversation

@Acex187x

@Acex187x Acex187x commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add inline file previews on library file detail pages for images, PDF, video, audio, Office docs, Markdown, and plain text.
  • Introduce a new PDF download route that serves stored PDFs when available and falls back to server-side conversion for Office and Markdown files.
  • Add generated thumbnail support, including theme-aware light/dark variants and a new thumbnailer service for Markdown and PDF rendering.
  • Update file cards and file-type icons to surface richer preview state, file metadata, and improved visual treatment.
  • Extend the backend schema and Convex file APIs to support thumbnail/PDF fields and preview-related data.

Testing

  • Not run (not requested).
  • Reviewed the diff for preview route wiring, thumbnail generation flow, and new detail-page preview components.
  • Confirmed the PR adds the required backend/frontend support for PDF export and inline previews across supported file types.

Summary by CodeRabbit

Release Notes

New Features

  • File previews now support inline rendering for images, videos, audio, PDFs, Office documents, Markdown, and text
  • PDF downloads include multiple variants (office, dark, styled/MDX, and plain), with quick hover previews when available

Improvements

  • File cards were redesigned with simplified metadata, improved title wrapping, and smarter thumbnail display (including light/dark variants)

Documentation

  • Added documentation for the thumbnail rendering service used to generate previews and PDF outputs

- Render Markdown, Office, PDF, and text previews in the file detail view
- Add thumbnail generation, PDF conversion endpoints, and MDX preview plumbing
- Update file cards to show generated thumbnails and file-type icons
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete file thumbnail and preview system to the library app. A new standalone thumbnailer microservice converts PDFs, Office documents, and MDX/Markdown to JPEG thumbnails and PDFs via Gotenberg and sharp. The Convex backend schema gains thumbnail lifecycle fields; a generate action orchestrates production and storage. The Next.js frontend adds inline file previews, MDX rendering, a GET /api/pdf download route, and updated file card thumbnails.

Changes

Library Thumbnail & Preview Pipeline

Layer / File(s) Summary
Schema, type contracts, and preview classification
packages/backend/convex/schema.ts, packages/backend/convex/library/thumbnail_kind.ts, apps/library/lib/preview.ts
libraryFiles schema gains five optional thumbnail/PDF storage ID fields and a thumbnailStatus lifecycle union with a by_thumbnail_status index. ThumbnailKind and thumbnailKind() provide backend MIME classification. PreviewKind, classifyPreview, pdfConvertKind, and officeViewerUrl provide frontend preview classification.
Convex backend thumbnail generation pipeline
packages/backend/convex/library/files.ts, packages/backend/convex/library/thumbnails.ts, packages/backend/package.json, packages/backend/convex.json
files.ts adds thumbnailUrlsFor, schedules generation on file create, enriches list/get responses with thumbnail URLs, expands blob cleanup on delete, and exposes internal query/mutation API. thumbnails.ts implements the generate action: local sharp resize for images and per-kind render flows via external service for PDF/Office/Markdown. sharp added to backend deps; Convex node.externalPackages changed from wildcard to explicit allowlist.
Thumbnailer microservice
services/thumbnailer/server.mjs, services/thumbnailer/Dockerfile, services/thumbnailer/docker-compose.yml, services/thumbnailer/package.json, services/thumbnailer/.env.example, services/thumbnailer/.dockerignore, services/thumbnailer/README.md
New Express service with GET /health, POST /render (JPEG), and POST /pdf endpoints. Implements SSRF protection, pdftoppm-based PDF page extraction, sharp JPEG encoding, Gotenberg-backed Office and Markdown conversion, and dark-mode PDF recoloring. Packaged with Dockerfile, Docker Compose (with Gotenberg sidecar), .env.example, .dockerignore, and README.
MDX compilation and rendering pipeline
apps/library/app/actions/compile-mdx.ts, apps/library/lib/mdx-components.tsx, apps/library/components/mdx-view.tsx, apps/library/app/internal/md-thumb/page.tsx, apps/library/app/layout.tsx, apps/library/next.config.ts, apps/library/package.json
compileMdxAction server action compiles MDX source. mdxComponents merges shared component map with typography. MdxView client component compiles and evaluates MDX via run(). /internal/md-thumb page renders MDX for screenshot with token+URL validation and light/dark themes. KaTeX CSS added to root layout. @wikipefia/mdx-renderer added to transpilePackages.
Frontend file preview components and file card
apps/library/components/file-type-icon.tsx, apps/library/components/file-preview.tsx, apps/library/components/markdown-preview.tsx, apps/library/components/file-card.tsx, apps/library/app/globals.css
FileTypeIcon maps MIME/extension to colored Lucide icons. FilePreview dispatches to PdfView, OfficePreview, MarkdownPreview, TextPreview, or Placeholder. MarkdownPreview supports mdx/plain/source toggle modes. FileCard gains theme-aware dual-image thumbnails and a float-based title layout. Global CSS adds thumbnail theme-switching selectors and .markdown-preview typography.
PDF download API route and file detail page integration
apps/library/app/api/pdf/route.ts, apps/library/app/file/[id]/page.tsx
GET /api/pdf validates fileId/style/variant, fast-paths stored PDF blobs, and falls back to on-demand Thumbnailer Service conversion. The file detail page renders FilePreview full-width, computes pdfKind, and shows PdfLink variant download buttons (office, dark, MDX, plain) with hover thumbnail previews.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant NextApp as Next.js App
  participant ConvexDB as Convex (files)
  participant ThumbSvc as Thumbnailer Service
  participant Gotenberg
  participant Storage as Convex Storage

  Browser->>NextApp: Upload file
  NextApp->>ConvexDB: files.create (thumbnailStatus=pending)
  ConvexDB->>ConvexDB: scheduler.runAfter(thumbnails.generate)
  ConvexDB->>ThumbSvc: POST /render {url, contentType}
  alt Office document
    ThumbSvc->>Gotenberg: LibreOffice convert → PDF
    Gotenberg-->>ThumbSvc: PDF bytes
  else Markdown/MDX
    ThumbSvc->>NextApp: GET /internal/md-thumb?src=&token=
    NextApp-->>ThumbSvc: rendered HTML screenshot
  end
  ThumbSvc-->>ConvexDB: JPEG + PDF buffers
  ConvexDB->>Storage: store thumbnails
  ConvexDB->>ConvexDB: attachThumbnail (thumbnailStatus=completed)
  Browser->>NextApp: GET /api/pdf?fileId=&style=mdx
  NextApp->>ConvexDB: files.get + downloadUrl
  ConvexDB-->>NextApp: file metadata
  NextApp->>ThumbSvc: POST /pdf {url, contentType, style}
  ThumbSvc-->>NextApp: PDF bytes
  NextApp-->>Browser: application/pdf
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Wikipefia/wikipefia-ck#2: Both PRs modify apps/library/app/file/[id]/page.tsx — the earlier PR established the FileDetailPage baseline that this PR extends with FilePreview and PDF conversion link variants.

Poem

🐰 Hop hop, the rabbit clicks a file,
A thumbnail blooms in thumbnail style!
Gotenberg spins, pdftoppm hums,
Dark mode inverts — oh, the magic comes.
From MDX prose to JPEG art,
Sharp pixels spring from every part. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 and concisely summarizes the main changes: adding inline file previews and PDF export functionality to library files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/fa841013

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (6)
packages/backend/convex/schema.ts-291-294 (1)

291-294: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the lifecycle comment for Office files.

thumbnailKind() and the generator both support Office thumbnails, so listing office under none contradicts the new pipeline and can mislead future schema/API changes.

Proposed fix
-     *   none       — file type isn't thumbnailable (office/video/text/…)
+     *   none       — file type isn't thumbnailable (video/audio/unsupported text/…)
🤖 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 `@packages/backend/convex/schema.ts` around lines 291 - 294, The comment
describing the thumbnail lifecycle states that office files are not
thumbnailable (listed under the `none` state), but this contradicts the current
implementation where `thumbnailKind()` and the generator both support Office
thumbnails. Remove "office" from the list of file types shown in the `none`
lifecycle state comment to accurately reflect that Office files are now
supported by the thumbnail pipeline.
apps/library/app/file/[id]/page.tsx-346-346 (1)

346-346: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove shadow-xl from the PDF preview tooltip.

This new tooltip uses the only forbidden shadow utility outside the Search dialog exception. Keep it border-based (or a lighter allowed treatment) instead.

As per coding guidelines, "Search dialog: use fixed inset-0 z-50 with rgba(0,0,0,0.5) backdrop, max-w-2xl, border, and shadow-xl (only allowed shadow exception)."

🤖 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 `@apps/library/app/file/`[id]/page.tsx at line 346, The span element that
creates the PDF preview tooltip contains the shadow-xl utility class on line 346
of apps/library/app/file/[id]/page.tsx, which violates the coding guidelines
that restrict shadow-xl usage to the Search dialog only. Remove the shadow-xl
class from the className attribute of this tooltip span while keeping the other
styling (border, border-line-soft, bg-surface, and other utilities) intact.

Source: Coding guidelines

apps/library/components/file-type-icon.tsx-50-62 (1)

50-62: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace hardcoded icon colors with theme variables.

The COLORS map hardcodes multiple hex values, which breaks the project styling contract for TSX components. Move these to theme variables (or subject palette constants) and consume via @/lib/theme.

As per coding guidelines, "Use CSS variables (C.bg, C.bgWhite, C.text, C.textMuted, C.accent, C.border, C.borderLight, C.headerBg, C.headerText) imported from @/lib/theme for all styling; never hardcode colors except for subject palette colors."

🤖 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 `@apps/library/components/file-type-icon.tsx` around lines 50 - 62, The COLORS
constant in file-type-icon.tsx contains hardcoded hex color values which
violates the styling guidelines. Replace all hardcoded hex values in the COLORS
map with CSS variables imported from `@/lib/theme` (such as C.bg, C.bgWhite,
C.text, C.textMuted, C.accent, C.border, C.borderLight, C.headerBg,
C.headerText, or other appropriate theme variables). First add the necessary
import statement for the theme variables at the top of the file, then update
each color entry in the COLORS object to reference the corresponding theme
variable instead of its hardcoded hex equivalent.

Source: Coding guidelines

apps/library/components/file-preview.tsx-11-11 (1)

11-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Swap hardcoded white/black color utilities for theme variables.

The new preview surfaces use hardcoded white/black color classes; these should be expressed via theme variables/tokens from @/lib/theme to keep light/dark theming and visual consistency aligned with the design system.

As per coding guidelines, "Use CSS variables (C.bg, C.bgWhite, C.text, C.textMuted, C.accent, C.border, C.borderLight, C.headerBg, C.headerText) imported from @/lib/theme for all styling; never hardcode colors except for subject palette colors."

Also applies to: 60-60, 73-73, 113-113

🤖 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 `@apps/library/components/file-preview.tsx` at line 11, Replace hardcoded color
utility classes with theme color variables from `@/lib/theme` throughout the file.
In the FRAME constant definition, replace the hardcoded bg-white class with the
appropriate theme variable such as C.bgWhite. Apply the same approach to
identify and replace any other hardcoded white or black color utilities
elsewhere in the file with their corresponding theme color tokens (e.g., C.bg,
C.text, C.textMuted, C.border, C.borderLight, etc.) to ensure consistency with
the design system and proper light/dark theme support.

Source: Coding guidelines

apps/library/lib/preview.ts-75-77 (1)

75-77: ⚠️ Potential issue | 🟡 Minor

Use own-property lookup for extension mapping.

ext in KIND_BY_EXT matches prototype-chain keys. With a crafted filename (e.g., file.__proto__), the extracted extension could be __proto__, bypassing the intended map and returning non-PreviewKind values at runtime. Use an own-property check before indexing.

Suggested fix
-  if (ext && ext in KIND_BY_EXT) {
+  if (ext && Object.prototype.hasOwnProperty.call(KIND_BY_EXT, ext)) {
     if (ext === "txt" && ct.startsWith("text/")) return "text";
     return KIND_BY_EXT[ext];
   }
🤖 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 `@apps/library/lib/preview.ts` around lines 75 - 77, The condition using the
`in` operator for extension lookup in the KIND_BY_EXT mapping checks the entire
prototype chain, not just own properties, which creates a security vulnerability
where a crafted filename like `file.__proto__` could return non-PreviewKind
values. Replace the `ext in KIND_BY_EXT` check with an own-property lookup using
Object.hasOwn(KIND_BY_EXT, ext) or
Object.prototype.hasOwnProperty.call(KIND_BY_EXT, ext) to ensure only direct
properties of the KIND_BY_EXT object are considered.
services/thumbnailer/README.md-7-14 (1)

7-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify a fenced code language for the flow diagram block.

Line 7 opens a fenced block without a language, which trips markdownlint MD040. Use a neutral language like text for the ASCII diagram.

🤖 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 `@services/thumbnailer/README.md` around lines 7 - 14, The fenced code block in
the README.md file containing the ASCII flow diagram (at line 7) is missing a
language specifier, which violates markdownlint rule MD040. Add the language
identifier `text` immediately after the opening triple backticks to specify that
this is a text-based ASCII diagram, making the fenced block properly formatted.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
apps/library/components/mdx-view.tsx (1)

55-77: ⚡ Quick win

Align component styling with required theme token usage (C.*).

This TSX component styles preview surfaces via utility classes, but the required color token source is C.* from @/lib/theme for component styling. Please switch surface/border/text color bindings to C.bg, C.textMuted, C.borderLight, etc., for compliance and consistency.

As per coding guidelines, "Use CSS variables (C.bg, C.bgWhite, C.text, C.textMuted, C.accent, C.border, C.borderLight, C.headerBg, C.headerText) imported from @/lib/theme for all styling; never hardcode colors except for subject palette colors."

🤖 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 `@apps/library/components/mdx-view.tsx` around lines 55 - 77, The mdx-view
component is using Tailwind utility classes (bg-surface, border-line-soft,
text-muted) for styling preview surfaces instead of CSS variables from
`@/lib/theme`. Replace the className utilities with inline style objects using C.*
tokens: replace bg-surface with C.bg, border-line-soft with C.borderLight,
text-muted with C.textMuted, and border styling with appropriate C.* values.
Apply these changes to all three div elements in the component (the loading
state div, error state div, and content render div) to ensure consistent theming
compliance throughout the file.

Source: Coding guidelines

🤖 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 `@apps/library/app/api/pdf/route.ts`:
- Around line 56-113: Wrap the entire code block in a try/catch statement to
handle any thrown errors gracefully. For the Promise.all containing the
convex.query calls for api.library.files.get and
api.library.files.getDownloadUrl, add an AbortSignal with a timeout (e.g., 5-10
seconds) to both queries to prevent hanging. For both fetch calls (the one
accessing the stored PDF URL and the one calling the Gotenberg PDF service), add
an AbortSignal with explicit timeouts to the fetch requests. In the catch block,
return a Response with an appropriate error message and status code (e.g., 500
for internal errors or 504 for timeout errors) to prevent unhandled exceptions
from propagating.

In `@apps/library/app/globals.css`:
- Around line 56-79: The `.markdown-preview h1`, `.markdown-preview h2`,
`.markdown-preview h3`, and related heading styles do not comply with the
required MDX header specification. Update these selectors to include:
text-transform: uppercase on all headers, adjust font-size values to match the
mandated MDX scale (h1 should use text-3xl with md:text-4xl equivalent, h2 and
h3 with their respective sizes), apply letter-spacing equivalent to
tracking-tighter, set margin to 0 0 1.5rem 0 (mb-6), and ensure font-weight is
set to bold (700). Remove or adjust the border-bottom and padding-bottom
properties on h1 and h2 as needed to match the specification requirements.

In `@apps/library/app/internal/md-thumb/page.tsx`:
- Around line 79-81: The fetch call that retrieves src does not include a
timeout mechanism, which can cause requests to hang indefinitely if the upstream
server stalls. Modify the fetch invocation to include an AbortSignal with a
timeout by passing a signal option to the fetch call using
AbortSignal.timeout(...). This ensures the request will fail after a reasonable
duration, allowing the error to be caught by the existing catch block and
preventing resource exhaustion.
- Around line 68-73: Remove the `token` parameter from the searchParams
destructuring and validation logic in the page component. Instead, implement
header-based authentication by accessing the token from the request headers
(through Next.js request context or middleware) and validating it there. This
prevents the authentication secret from being exposed through URL query
parameters, which can be logged, cached by proxies, and retained in browser
history. Ensure the client-side code that calls this page also sends the token
via an HTTP header (such as Authorization) rather than as a query parameter.

In `@apps/library/components/markdown-preview.tsx`:
- Around line 99-101: The className in the markdown-preview.tsx component at the
button styling section hardcodes text-white for the active variant state instead
of using theme tokens. Replace the hardcoded text-white with an appropriate CSS
variable token imported from `@/lib/theme` (such as C.text or a similar foreground
color token). Ensure the active state styling when variant === v uses only
theme-driven color tokens from the required CSS variable imports, following the
coding guideline to never hardcode colors except for subject palette colors.

In `@apps/library/components/mdx-view.tsx`:
- Around line 26-33: The compileMdxAction(source) call at line 27 can be
rejected as a promise, but this rejection bypasses the "error" in result branch
check, leaving the component stuck in a loading state with an unhandled
rejection. Wrap the entire async operation (the await compileMdxAction(source)
call and the subsequent if block that checks for "error" in result) in a
try/catch block, and in the catch handler, set the error state with an
appropriate error message to ensure all failure modes are properly handled and
the component reflects the error state.
- Around line 34-37: The `run()` function call at line 34-37 in mdx-view.tsx
executes compiled MDX directly in the browser, creating a stored-XSS
vulnerability for user-uploaded content. To fix this, implement one of the
following: (1) Add authorization checks to ensure only trusted authors can
preview files before the `run()` call executes, (2) Sanitize the
`result.compiled` output by validating and removing any potentially dangerous
code patterns before passing it to `run()`, or (3) Replace the `run()` execution
with a trusted Markdown-only renderer that does not evaluate arbitrary code.
Verify that whichever approach is chosen prevents arbitrary code execution from
untrusted MDX sources.

In `@packages/backend/convex/library/files.ts`:
- Around line 225-234: The detail-page artifact URLs (plainThumbnailUrl, pdfUrl,
and darkPdfUrl) are being returned whenever their corresponding storage IDs
exist, without checking if the thumbnail regeneration is complete. This creates
a state mismatch where stale PDFs can be shown to users even after a backfill or
regeneration sets the status to pending or processing. Gate each of these three
URL fields on the same thumbnailStatus === "completed" condition used by
thumbnailUrlsFor() so they are only returned when the new render is actually
complete, preventing stale artifacts from being served during thumbnail
regeneration.
- Around line 461-475: The enqueueAllThumbnails function currently uses
collect() to load all libraryFiles at once, which can exceed Convex mutation and
read limits as the table grows. Replace the collect() call with take() using a
fixed batch size of 100-200 records (consistent with the codebase pattern), and
modify the handler to process only that batch. This allows the operation to
resume across multiple invocations instead of attempting to process the entire
table in a single mutation. The existing by_thumbnail_status index can
optionally be used to target only pending items more precisely if needed.

In `@packages/backend/convex/library/thumbnail_kind.ts`:
- Around line 27-33: The thumbnailKind function converts the contentType to
lowercase but does not remove MIME type parameters (the part after semicolon).
This causes MIME types with parameters like "text/markdown; charset=utf-8" to
fail the exact match checks. After converting contentType to lowercase in the ct
variable, split on the semicolon character and use only the first part (before
any parameters) for all subsequent MIME type classification checks including the
comparisons with "image/svg+xml", "text/markdown", "application/pdf", and the
startsWith checks.

In `@packages/backend/convex/library/thumbnails.ts`:
- Around line 49-56: The fetch call in the `callService` function (lines 49-56)
lacks a timeout mechanism, which could cause the Convex action to block
indefinitely if the render service stalls after accepting the connection. Add an
`AbortController` with a configurable timeout deadline by creating a new
AbortController instance before the fetch call, setting up a timeout that aborts
the controller after a specified duration (in milliseconds), and passing the
controller's signal to the fetch options object. This ensures the request fails
fast if the service is unresponsive, allowing it to reach the
`markThumbnailFailed` handler. Reference the existing timeout pattern used in
`agent_action.ts` as an example of how to implement this in the codebase.

In `@services/thumbnailer/docker-compose.yml`:
- Around line 30-31: The `depends_on` directive in the thumbnailer service only
enforces start order and does not wait for the gotenberg service to be fully
ready, causing early render failures. Add a healthcheck configuration to the
gotenberg service definition (around lines 36-45) that validates the
LibreOffice/Chromium endpoints are accessible, then modify the thumbnailer's
`depends_on` section (lines 30-31) to change the gotenberg dependency from a
simple list format to an object format with a `condition: service_healthy`
constraint to ensure the thumbnailer only starts after gotenberg has passed its
health check.

In `@services/thumbnailer/Dockerfile`:
- Around line 19-22: The Dockerfile runs the thumbnailer service as root by
default because there is no USER directive specified before the CMD instruction.
Add a USER directive before the CMD ["node", "server.mjs"] line to switch to a
non-root user for better security. You can use an existing `node` user or create
a dedicated non-root user, ensuring proper file ownership if needed.

In `@services/thumbnailer/server.mjs`:
- Around line 25-27: The PORT, TOKEN, and GOTENBERG_URL configuration at the
start of the file currently allows TOKEN to default to an empty string when
THUMBNAIL_TOKEN environment variable is not set. This disables authentication
check, creating a security vulnerability if the service becomes reachable
outside a private network. Instead of defaulting TOKEN to an empty string, the
service should fail closed by requiring THUMBNAIL_TOKEN to be set—either throw
an error or call process.exit() if the environment variable is missing. Apply
the same fail-closed pattern at the other two affected locations (around lines
276-279 and 344-347) where token validation likely occurs, ensuring that
authentication is always enforced and the service cannot operate without a valid
token configured.
- Around line 167-169: The .sort() method on the filtered page files array uses
lexicographic (alphabetical) sorting, which incorrectly orders filenames like
"p-10.png" before "p-2.png". Replace the default .sort() call with a custom
comparator function that extracts the numeric page number from each filename
(the digits between "p-" and ".png") and performs a numeric comparison on those
extracted numbers to ensure correct ordering regardless of page count.
- Line 23: The execFile calls for pdftoppm and img2pdf lack timeout and
maxBuffer protection, allowing malformed or expensive files to block worker
threads indefinitely. Create a shared wrapper function that wraps execFileP with
configurable timeout and maxBuffer options to protect against resource
exhaustion and hanging processes. Apply this wrapper function to all conversion
command invocations: both pdftoppm calls and the img2pdf call throughout the
file, ensuring each execution respects the timeout and maxBuffer limits.
- Around line 291-298: The code buffers entire responses into memory using
`Buffer.from(await fileRes.arrayBuffer())` before checking if they exceed
MAX_BYTES, allowing oversized fetches to consume memory before rejection.
Replace all instances where the response is fully buffered into memory with a
streaming approach that accumulates bytes incrementally from the response body
and aborts as soon as the accumulated size exceeds MAX_BYTES, preventing memory
exhaustion on large responses. This fix applies to both user-provided URL fetch
paths mentioned in the comment where `fileRes.arrayBuffer()` is called.
- Around line 68-72: The fetch() function in fetchWithTimeout() at
services/thumbnailer/server.mjs lines 68-72 follows HTTP redirects by default,
which bypasses the SSRF allowlist check performed by isAllowedUrl(). To fix
this, add redirect: 'manual' to the fetch options in the fetchWithTimeout
function to prevent following redirects. This same fix must also be applied at
services/thumbnailer/server.mjs lines 286-291 and
services/thumbnailer/server.mjs lines 352-357 where similar fetch calls occur
without redirect blocking.

---

Minor comments:
In `@apps/library/app/file/`[id]/page.tsx:
- Line 346: The span element that creates the PDF preview tooltip contains the
shadow-xl utility class on line 346 of apps/library/app/file/[id]/page.tsx,
which violates the coding guidelines that restrict shadow-xl usage to the Search
dialog only. Remove the shadow-xl class from the className attribute of this
tooltip span while keeping the other styling (border, border-line-soft,
bg-surface, and other utilities) intact.

In `@apps/library/components/file-preview.tsx`:
- Line 11: Replace hardcoded color utility classes with theme color variables
from `@/lib/theme` throughout the file. In the FRAME constant definition, replace
the hardcoded bg-white class with the appropriate theme variable such as
C.bgWhite. Apply the same approach to identify and replace any other hardcoded
white or black color utilities elsewhere in the file with their corresponding
theme color tokens (e.g., C.bg, C.text, C.textMuted, C.border, C.borderLight,
etc.) to ensure consistency with the design system and proper light/dark theme
support.

In `@apps/library/components/file-type-icon.tsx`:
- Around line 50-62: The COLORS constant in file-type-icon.tsx contains
hardcoded hex color values which violates the styling guidelines. Replace all
hardcoded hex values in the COLORS map with CSS variables imported from
`@/lib/theme` (such as C.bg, C.bgWhite, C.text, C.textMuted, C.accent, C.border,
C.borderLight, C.headerBg, C.headerText, or other appropriate theme variables).
First add the necessary import statement for the theme variables at the top of
the file, then update each color entry in the COLORS object to reference the
corresponding theme variable instead of its hardcoded hex equivalent.

In `@apps/library/lib/preview.ts`:
- Around line 75-77: The condition using the `in` operator for extension lookup
in the KIND_BY_EXT mapping checks the entire prototype chain, not just own
properties, which creates a security vulnerability where a crafted filename like
`file.__proto__` could return non-PreviewKind values. Replace the `ext in
KIND_BY_EXT` check with an own-property lookup using Object.hasOwn(KIND_BY_EXT,
ext) or Object.prototype.hasOwnProperty.call(KIND_BY_EXT, ext) to ensure only
direct properties of the KIND_BY_EXT object are considered.

In `@packages/backend/convex/schema.ts`:
- Around line 291-294: The comment describing the thumbnail lifecycle states
that office files are not thumbnailable (listed under the `none` state), but
this contradicts the current implementation where `thumbnailKind()` and the
generator both support Office thumbnails. Remove "office" from the list of file
types shown in the `none` lifecycle state comment to accurately reflect that
Office files are now supported by the thumbnail pipeline.

In `@services/thumbnailer/README.md`:
- Around line 7-14: The fenced code block in the README.md file containing the
ASCII flow diagram (at line 7) is missing a language specifier, which violates
markdownlint rule MD040. Add the language identifier `text` immediately after
the opening triple backticks to specify that this is a text-based ASCII diagram,
making the fenced block properly formatted.

---

Nitpick comments:
In `@apps/library/components/mdx-view.tsx`:
- Around line 55-77: The mdx-view component is using Tailwind utility classes
(bg-surface, border-line-soft, text-muted) for styling preview surfaces instead
of CSS variables from `@/lib/theme`. Replace the className utilities with inline
style objects using C.* tokens: replace bg-surface with C.bg, border-line-soft
with C.borderLight, text-muted with C.textMuted, and border styling with
appropriate C.* values. Apply these changes to all three div elements in the
component (the loading state div, error state div, and content render div) to
ensure consistent theming compliance throughout the file.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a53c1bdb-d7df-4ee5-ab94-167898637c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 7f8916a and c3e6bc0.

⛔ Files ignored due to path filters (2)
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • apps/library/app/actions/compile-mdx.ts
  • apps/library/app/api/pdf/route.ts
  • apps/library/app/file/[id]/page.tsx
  • apps/library/app/globals.css
  • apps/library/app/internal/md-thumb/page.tsx
  • apps/library/app/layout.tsx
  • apps/library/components/file-card.tsx
  • apps/library/components/file-preview.tsx
  • apps/library/components/file-type-icon.tsx
  • apps/library/components/markdown-preview.tsx
  • apps/library/components/mdx-view.tsx
  • apps/library/lib/mdx-components.tsx
  • apps/library/lib/preview.ts
  • apps/library/next.config.ts
  • apps/library/package.json
  • packages/backend/convex.json
  • packages/backend/convex/library/files.ts
  • packages/backend/convex/library/thumbnail_kind.ts
  • packages/backend/convex/library/thumbnails.ts
  • packages/backend/convex/schema.ts
  • packages/backend/package.json
  • services/thumbnailer/.dockerignore
  • services/thumbnailer/.env.example
  • services/thumbnailer/Dockerfile
  • services/thumbnailer/README.md
  • services/thumbnailer/docker-compose.yml
  • services/thumbnailer/package.json
  • services/thumbnailer/server.mjs

Comment thread apps/library/app/api/pdf/route.ts Outdated
Comment on lines +56 to +113
const [file, url] = await Promise.all([
convex.query(api.library.files.get, { fileId: id }),
convex.query(api.library.files.getDownloadUrl, { fileId: id }),
]);
if (!file || !url) {
return new Response("File not found", { status: 404 });
}

const ct = file.contentType.toLowerCase();
const isOffice = OFFICE_MIMES.has(ct);
const isMarkdown = ct === "text/markdown";
const isPdf = ct === "application/pdf";
// PDF files only support the dark variant (the original is the file itself).
if (!isOffice && !isMarkdown && !(isPdf && dark)) {
return new Response("This file type can't be converted to PDF", {
status: 415,
});
}

const base = pdfName(file.originalName).replace(/\.pdf$/, "");
const headers = {
"content-type": "application/pdf",
"content-disposition": contentDisposition(
`${base}${dark ? "-dark" : ""}.pdf`,
),
"cache-control": "no-store",
};

// Fast path: stream the pre-rendered blob (dark PDF, or styled/Office light PDF).
const stored = dark
? file.darkPdfUrl
: style !== "plain"
? file.pdfUrl
: null;
if (stored) {
const res = await fetch(stored);
if (res.ok) return new Response(res.body, { headers });
}

// Fallback: convert on demand via the render service (Gotenberg).
const res = await fetch(`${serviceUrl.replace(/\/+$/, "")}/pdf`, {
method: "POST",
headers: {
"content-type": "application/json",
...(token ? { "x-thumbnail-token": token } : {}),
},
body: JSON.stringify({
url,
contentType: file.contentType,
style,
theme: dark ? "dark" : "light",
}),
});
if (!res.ok) {
return new Response(`Conversion failed (${res.status})`, { status: 502 });
}
return new Response(res.body, { headers });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "route.ts" | grep -i pdf

Repository: Wikipefia/wikipefia-ck

Length of output: 237


🏁 Script executed:

cat -n apps/library/app/api/pdf/route.ts | head -150

Repository: Wikipefia/wikipefia-ck

Length of output: 4733


🏁 Script executed:

rg "fetchWithTimeout|AbortController" --type ts --type tsx

Repository: Wikipefia/wikipefia-ck

Length of output: 94


🏁 Script executed:

rg "api/.*route\.ts" -l | head -5

Repository: Wikipefia/wikipefia-ck

Length of output: 48


🏁 Script executed:

rg "try.*await.*fetch|fetch.*catch" -A 3 -B 1 --type ts --type tsx | head -50

Repository: Wikipefia/wikipefia-ck

Length of output: 94


🏁 Script executed:

find apps -name "route.ts" -o -name "route.js"

Repository: Wikipefia/wikipefia-ck

Length of output: 142


🏁 Script executed:

rg "export async function GET|export async function POST" --type ts -B 2 -A 15 | head -100

Repository: Wikipefia/wikipefia-ck

Length of output: 1287


🏁 Script executed:

rg "AbortController|timeout" --type ts | head -20

Repository: Wikipefia/wikipefia-ck

Length of output: 162


🏁 Script executed:

cat -n apps/library/app/api/uploadthing/route.ts

Repository: Wikipefia/wikipefia-ck

Length of output: 365


🏁 Script executed:

rg "try\s*\{" apps --type ts -B 2 -A 8 | head -60

Repository: Wikipefia/wikipefia-ck

Length of output: 3811


Harden upstream calls with explicit error handling and timeouts.

Convex queries (lines 56–59) and both fetch calls (lines 91, 96–108) can throw or hang without error handling. Wrap upstream calls in try/catch and apply bounded timeouts to prevent unhandled 500 errors and request exhaustion.

Suggested hardening pattern
+async function fetchWithTimeout(
+  input: RequestInfo | URL,
+  init: RequestInit = {},
+  timeoutMs = 15_000,
+) {
+  const controller = new AbortController();
+  const timer = setTimeout(() => controller.abort(), timeoutMs);
+  try {
+    return await fetch(input, { ...init, signal: controller.signal });
+  } finally {
+    clearTimeout(timer);
+  }
+}
+
 export async function GET(req: NextRequest) {
+  try {
     const params = req.nextUrl.searchParams;
     // ...
-    const [file, url] = await Promise.all([
+    const [file, url] = await Promise.all([
       convex.query(api.library.files.get, { fileId: id }),
       convex.query(api.library.files.getDownloadUrl, { fileId: id }),
     ]);
     // ...
     if (stored) {
-      const res = await fetch(stored);
+      const res = await fetchWithTimeout(stored);
       if (res.ok) return new Response(res.body, { headers });
     }
-    const res = await fetch(`${serviceUrl.replace(/\/+$/, "")}/pdf`, {
+    const res = await fetchWithTimeout(`${serviceUrl.replace(/\/+$/, "")}/pdf`, {
       method: "POST",
       headers: {
         "content-type": "application/json",
         ...(token ? { "x-thumbnail-token": token } : {}),
       },
       body: JSON.stringify({ url, contentType: file.contentType, style, theme: dark ? "dark" : "light" }),
     });
     if (!res.ok) {
       return new Response(`Conversion failed (${res.status})`, { status: 502 });
     }
     return new Response(res.body, { headers });
+  } catch {
+    return new Response("Upstream PDF service failure", { status: 502 });
+  }
 }
🤖 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 `@apps/library/app/api/pdf/route.ts` around lines 56 - 113, Wrap the entire
code block in a try/catch statement to handle any thrown errors gracefully. For
the Promise.all containing the convex.query calls for api.library.files.get and
api.library.files.getDownloadUrl, add an AbortSignal with a timeout (e.g., 5-10
seconds) to both queries to prevent hanging. For both fetch calls (the one
accessing the stored PDF URL and the one calling the Gotenberg PDF service), add
an AbortSignal with explicit timeouts to the fetch requests. In the catch block,
return a Response with an appropriate error message and status code (e.g., 500
for internal errors or 504 for timeout errors) to prevent unhandled exceptions
from propagating.

Comment on lines +56 to +79
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
font-family: var(--font-mono), sans-serif;
font-weight: 600;
line-height: 1.25;
margin: 1.6em 0 0.6em;
}
.markdown-preview h1 {
font-size: 1.7em;
border-bottom: 1px solid var(--c-border-light);
padding-bottom: 0.2em;
}
.markdown-preview h2 {
font-size: 1.4em;
border-bottom: 1px solid var(--c-border-light);
padding-bottom: 0.2em;
}
.markdown-preview h3 {
font-size: 1.2em;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

MDX heading typography does not match the required header spec.

Lines 56–79 define h1/h2/h3 styles that miss the mandated uppercase + exact MDX header scale/spacing/tracking rules. Please align these selectors to the required h1h4 style contract. As per coding guidelines, “MDX h1 headers: use text-3xl md:text-4xl tracking-tighter mb-6 uppercase font-bold…”, “MDX h2…”, “MDX h3…”, and “MDX h4…”, plus “Apply uppercase styling to all headers…”.

🤖 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 `@apps/library/app/globals.css` around lines 56 - 79, The `.markdown-preview
h1`, `.markdown-preview h2`, `.markdown-preview h3`, and related heading styles
do not comply with the required MDX header specification. Update these selectors
to include: text-transform: uppercase on all headers, adjust font-size values to
match the mandated MDX scale (h1 should use text-3xl with md:text-4xl
equivalent, h2 and h3 with their respective sizes), apply letter-spacing
equivalent to tracking-tighter, set margin to 0 0 1.5rem 0 (mb-6), and ensure
font-weight is set to bold (700). Remove or adjust the border-bottom and
padding-bottom properties on h1 and h2 as needed to match the specification
requirements.

Source: Coding guidelines

Comment thread apps/library/app/internal/md-thumb/page.tsx Outdated
Comment thread apps/library/app/internal/md-thumb/page.tsx Outdated
Comment on lines +99 to +101
className={`px-3 py-1 transition-colors ${
variant === v ? "bg-accent text-white" : "text-muted hover:text-fg"
}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hardcoded text-white; use theme color tokens for state styling.

Line 100 hardcodes text-white for the active button state, which bypasses the required CSS-variable theme contract. Switch to token-driven foreground/background classes or inline CSS vars from the theme map. As per coding guidelines, “Use CSS variables (C.bg, C.bgWhite, C.text, C.textMuted, C.accent, C.border, C.borderLight, C.headerBg, C.headerText) imported from @/lib/theme for all styling; never hardcode colors except for subject palette colors.”

🤖 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 `@apps/library/components/markdown-preview.tsx` around lines 99 - 101, The
className in the markdown-preview.tsx component at the button styling section
hardcodes text-white for the active variant state instead of using theme tokens.
Replace the hardcoded text-white with an appropriate CSS variable token imported
from `@/lib/theme` (such as C.text or a similar foreground color token). Ensure
the active state styling when variant === v uses only theme-driven color tokens
from the required CSS variable imports, following the coding guideline to never
hardcode colors except for subject palette colors.

Source: Coding guidelines

Comment thread services/thumbnailer/server.mjs
Comment thread services/thumbnailer/server.mjs
Comment on lines +68 to +72
async function fetchWithTimeout(url, opts = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
return await fetch(url, { ...opts, signal: controller.signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only check: inspect service docs/env for expected storage host/redirect behavior assumptions.
fd -a 'README.md|\.env.example|docker-compose.yml' \
  | xargs -r sed -n '/ALLOWED_HOSTS/p;/THUMBNAIL/p;/convex/p;/redirect/p'

Repository: Wikipefia/wikipefia-ck

Length of output: 2104


🏁 Script executed:

# Inspect the actual fetchWithTimeout implementation and surrounding context
head -100 services/thumbnailer/server.mjs | tail -50

Repository: Wikipefia/wikipefia-ck

Length of output: 1557


🏁 Script executed:

# Find and read the isAllowedUrl function
grep -n "isAllowedUrl" services/thumbnailer/server.mjs | head -5

Repository: Wikipefia/wikipefia-ck

Length of output: 162


🏁 Script executed:

# Check the full structure of server.mjs to understand URL validation flow
wc -l services/thumbnailer/server.mjs

Repository: Wikipefia/wikipefia-ck

Length of output: 102


🌐 Web query:

Convex Storage API redirect behavior files download

💡 Result:

In Convex, file storage retrieval behavior depends on the method used to access the file [1][2]. 1. Using storage.getUrl: When you call ctx.storage.getUrl(storageId), Convex generates a URL that provides direct access to the file [1][3]. While the exact underlying mechanism can involve internal redirects to highly available storage, this is the standard, optimized method for serving files to clients (e.g., in img tags or for direct downloads) [1][3][4]. It is recommended for most use cases, including large files, as it offloads the serving process [1][5]. 2. Using HTTP Actions: If you require custom access control, you can serve files via HTTP actions using ctx.storage.get(storageId) [1][5]. - In this pattern, the action fetches the file content (returning a Blob) and serves it directly in the HTTP response [1][6]. - Important Limitation: HTTP action responses have a size limit (currently 20MB) [1][5]. For files exceeding this size, you should use storage.getUrl() to generate a direct URL and, if necessary, perform a 302 redirect from your HTTP action to that URL [1][5]. 3. Redirect Behavior in Ecosystem Tools: Some community-built packages, such as convex-fs or static-hosting, specifically implement redirect-based patterns [7][8]. For example, these tools may serve a 302 redirect to a CDN edge URL (such as Bunny.net) to cache and accelerate file delivery [7][8]. This behavior is a feature of those specific integrations and not the default behavior of the core storage.getUrl() method [1][7][8]. In summary, for standard file serving, prefer storage.getUrl(). For custom access-controlled endpoints, use HTTP actions, but be mindful of the 20MB response limit and redirect to a getUrl() result for larger files [1][5].

Citations:


🏁 Script executed:

# Check how Convex Storage URLs are used in the codebase
rg "convex\.cloud|ALLOWED_HOSTS" -A 3 -B 3

Repository: Wikipefia/wikipefia-ck

Length of output: 3270


🏁 Script executed:

# Look for any actual file fetches in render/pdf endpoint handlers
grep -n "fetchWithTimeout\|fetch" services/thumbnailer/server.mjs | head -20

Repository: Wikipefia/wikipefia-ck

Length of output: 1134


🏁 Script executed:

# Check if there are any examples or tests showing ALLOWED_HOSTS configuration with redirect-using domains
rg "ALLOWED_HOSTS|convex\.cloud" services/thumbnailer/ -B 2 -A 2

Repository: Wikipefia/wikipefia-ck

Length of output: 1612


🏁 Script executed:

# Check if there's any indication that redirect-based storage or CDN is used
grep -r "redirect\|cdn\|edge" services/thumbnailer/ --include="*.md" --include="*.mjs" --include="*.yml"

Repository: Wikipefia/wikipefia-ck

Length of output: 631


Block fetch redirects to prevent SSRF bypass.

The isAllowedUrl() validation checks only the initial URL, but fetch() follows 3xx redirects by default. If an allowed host redirects to an internal or disallowed URL, the SSRF allowlist is bypassed. For default configurations using convex.cloud, this risk is low since Convex Storage URLs do not redirect, but custom ALLOWED_HOSTS configurations with redirect-based storage or CDN providers would be vulnerable.

Proposed fix
 async function fetchWithTimeout(url, opts = {}) {
   const controller = new AbortController();
   const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
   try {
-    return await fetch(url, { ...opts, signal: controller.signal });
+    const res = await fetch(url, {
+      ...opts,
+      redirect: "manual",
+      signal: controller.signal,
+    });
+    if (res.status >= 300 && res.status < 400) {
+      const location = res.headers.get("location");
+      throw new Error(`redirects are not allowed${location ? `: ${location}` : ""}`);
+    }
+    return res;
   } finally {
     clearTimeout(timer);
   }
 }

Also applies to: 286-291, 352-357

🤖 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 `@services/thumbnailer/server.mjs` around lines 68 - 72, The fetch() function
in fetchWithTimeout() at services/thumbnailer/server.mjs lines 68-72 follows
HTTP redirects by default, which bypasses the SSRF allowlist check performed by
isAllowedUrl(). To fix this, add redirect: 'manual' to the fetch options in the
fetchWithTimeout function to prevent following redirects. This same fix must
also be applied at services/thumbnailer/server.mjs lines 286-291 and
services/thumbnailer/server.mjs lines 352-357 where similar fetch calls occur
without redirect blocking.

Comment thread services/thumbnailer/server.mjs Outdated
Comment thread services/thumbnailer/server.mjs
- Add timeouts, auth hardening, and capped fetches for render services
- Paginate thumbnail backfills and hide stale generated artifacts until ready
- Improve MDX thumbnail auth and FileCard title/rating layout handling

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/thumbnailer/server.mjs (1)

116-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redirect following still allows SSRF bypass.

The past comment about blocking redirects remains unaddressed. fetch() follows 3xx redirects by default, so an allowed host that issues a redirect to an internal/disallowed URL bypasses isAllowedUrl(). While Convex storage URLs typically don't redirect, custom ALLOWED_HOSTS configurations could be vulnerable.

Proposed fix
 async function fetchWithTimeout(url, opts = {}) {
   const controller = new AbortController();
   const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
   try {
-    return await fetch(url, { ...opts, signal: controller.signal });
+    const res = await fetch(url, {
+      ...opts,
+      redirect: "manual",
+      signal: controller.signal,
+    });
+    if (res.status >= 300 && res.status < 400) {
+      const location = res.headers.get("location");
+      throw new Error(`redirects are not allowed${location ? `: ${location}` : ""}`);
+    }
+    return res;
   } finally {
     clearTimeout(timer);
   }
 }
🤖 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 `@services/thumbnailer/server.mjs` around lines 116 - 124, The fetchWithTimeout
function does not prevent HTTP redirects, which allows an SSRF bypass where an
allowed URL can redirect to a disallowed internal URL. To fix this, add a
redirect option to the fetch call within the fetchWithTimeout function to
disable automatic redirect following. Set the redirect property to 'error' in
the fetch options (merge it with the existing opts spread) so that any 3xx
redirect response will be treated as an error rather than followed
automatically, ensuring that only the validated URL is actually accessed.
🤖 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.

Outside diff comments:
In `@services/thumbnailer/server.mjs`:
- Around line 116-124: The fetchWithTimeout function does not prevent HTTP
redirects, which allows an SSRF bypass where an allowed URL can redirect to a
disallowed internal URL. To fix this, add a redirect option to the fetch call
within the fetchWithTimeout function to disable automatic redirect following.
Set the redirect property to 'error' in the fetch options (merge it with the
existing opts spread) so that any 3xx redirect response will be treated as an
error rather than followed automatically, ensuring that only the validated URL
is actually accessed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 104e2110-cf32-4687-8c8f-2be70fdab098

📥 Commits

Reviewing files that changed from the base of the PR and between c3e6bc0 and d213c02.

📒 Files selected for processing (10)
  • apps/library/app/api/pdf/route.ts
  • apps/library/app/internal/md-thumb/page.tsx
  • apps/library/components/file-card.tsx
  • apps/library/components/mdx-view.tsx
  • packages/backend/convex/library/files.ts
  • packages/backend/convex/library/thumbnail_kind.ts
  • packages/backend/convex/library/thumbnails.ts
  • services/thumbnailer/Dockerfile
  • services/thumbnailer/docker-compose.yml
  • services/thumbnailer/server.mjs
✅ Files skipped from review due to trivial changes (1)
  • services/thumbnailer/Dockerfile
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/library/components/mdx-view.tsx
  • services/thumbnailer/docker-compose.yml
  • apps/library/app/internal/md-thumb/page.tsx
  • apps/library/components/file-card.tsx
  • packages/backend/convex/library/files.ts
  • apps/library/app/api/pdf/route.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant