feat(static): add opt-in directory listing via dirListing - #277
Conversation
Serve a minimal HTML directory listing when a request resolves to a directory with no index file. Off by default (it exposes the directory structure); the CLI enables it in dev mode only. The listing re-asserts the same boundaries a file request gets — lexical and symlink-resolved containment, plus the dot-path deny check on both the requested path and each entry — so it never names anything a direct request would refuse to serve. Entry hrefs are absolute, so links resolve identically whether the directory was requested with a trailing slash or without. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughDirectory listing support is added to static serving with secure symlink and dotfile filtering, escaped HTML output, CLI controls, production-aware defaults, documentation, and coverage for routing, headers, HEAD requests, and fallback behavior. ChangesDirectory listing
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
commit: |
A plain `prefers-color-scheme: dark` palette swap on the listing page — no toggle, no stored preference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refresh the listing style — card-style rows with hover, a monospace heading, and folder/file icons — driven by CSS variables so the dark-theme swap is a single palette override. Add a `noindex, nofollow` robots meta tag and matching `X-Robots-Tag` header, since a generated listing is not content to index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The listing interpolates filenames into HTML, so add defense-in-depth headers. The page is fully self-contained (inline CSS, no scripts, images, or fonts), so a strict CSP pins it to exactly that — even a hypothetical escaping slip could neither run a script nor reach an external origin. Add `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer` alongside. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `dirListing` option to the serveStatic reference with a paragraph covering behaviour, dotfile hiding, absolute links, dark mode, and the noindex/CSP headers. Note the dev-only listing in the CLI static-files section and cross-link the two. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/static.ts`:
- Around line 364-366: Update the directory-entry mapping in the static listing
flow to resolve each child’s canonical target, verify it remains under root, and
apply dot-path filtering to the resolved path before including it. Classify
included entries using the resolved target rather than the symlink’s directory
flag, and add coverage for symlinks escaping root or resolving to denied dot
paths.
- Line 693: Update the URL generation around the base-path normalization in the
static request handling flow so extension-less nested paths such as /docs/api
resolve the parent URL to /docs/ rather than /. Derive an absolute parent path
from the normalized base and apply the same correction to the corresponding
logic also used around lines 748-751.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 52b8d831-f879-45d2-8659-66e96cd7c33a
📒 Files selected for processing (3)
src/cli/serve.tssrc/static.tstest/static.test.ts
Make the directory listing explicitly controllable: `--dir-listing` forces it on (e.g. under --prod) and `--no-dir-listing` forces it off (e.g. in dev), overriding the dev-on/prod-off default. `parseArgs` has no native negation, so the opt-out is its own flag; the two collapse into a tri-state `dirListing` that falls back to the default when unset. Document both flags in the usage help and CLI guide, and cover the prod opt-in with an end-to-end test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address two review findings on the directory listing: - Resolve each symlink entry's canonical target and re-check it against the root and dot-path rules before listing it, classifying by the target. A symlink escaping the root or aliasing a denied dot path is now hidden — matching what a direct request would refuse — and a contained symlink to a directory lists as one. - Build the parent link as an absolute path derived from the base, so an extension-less nested request served without a trailing slash (`/docs/api`) points `../` to `/docs/` rather than `/`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The listing no longer pre-empts the rest of the app: `next()` runs first, and only a 404 response for a path naming a listable directory is replaced by the listing. A real route beats a listing even when a same-name directory exists in the static dir, and a custom 404 page keeps working for any path that is not a listable directory. Static-only CLI mode now answers a miss with an ordinary 404 instead of a 501 "Server Entry Not Found" page — the correct status for that mode, and the one the fallback keys on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… listings The per-entry symlink containment check compared the canonical target against the root without the trailing separator treatment the directory check gets, so a link whose target is exactly the served root (`self -> .`) vanished from listings while still serving. Apply `asPrefix` before the prefix test, matching the directory-level check. Also send `Cache-Control: no-store` on listings — they mirror live directory state, and heuristic caching could otherwise show a stale listing after files change. Pin the request-path dot-deny with a test (`/.secret-dir/` stays unlistable) and correct "dev mode only" wording to "by default" now that `--dir-listing` can force it on in prod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds a
dirListingoption toserveStaticthat serves a minimal HTML directory listing as a 404 fallback when a request names a directory with no index file (index.html). Off by default; thesrvxCLI enables it in dev mode only.Behaviour
The resolution order is static files → your handler → listing:
next()runs first, and only a404response is replaced by the listing. A server route named like a directory in the static dir (e.g./api) keeps working.index.htmlserves the index, never a listing (the index candidate is already probed by the existing lookup loop)./files/), or an extension-less path (/files)./. A../parent link appears everywhere but the root.HEADreturns the headers without a body.Security
The listing re-asserts the same boundaries a file request gets, so it never names anything a direct request would refuse to serve:
dircould point the directory out of the root).dotfiles) on both the requested path and each entry — so.env/.gitare hidden while.well-knownshows, matching what is actually servable.encodeURIComponent-encoded then HTML-escaped before interpolation.CLI
Enabled automatically in dev, off in prod:
Static-only mode (no server entry) now answers a miss with an ordinary
404instead of a501"Server Entry Not Found" page — the correct status for that mode, and the one the fallback keys on.Tests
New
directory listing (dirListing)suite covering: off-by-default fall-through, non-404 downstream responses winning over the listing, custom-404 passthrough for non-directory paths, listing render, dot-segment hiding, index precedence (including root), extension-less routes,HEAD, symlink-escape denial, and missing directories. Full suite (1253 tests) passes; lint, format, and typecheck clean.Verified end-to-end via the CLI: a
/apiroute beats a same-name static directory, an entry 404 falls back to the listing, a custom 404 page passes through, and prod keeps the listing off.🤖 Generated with Claude Code
Summary by CodeRabbit
indexexists.--dir-listing/--no-dir-listing, supporting dev defaults and explicit production override.HEADhandling.