fix: TypeScript strict mode, security hardening, and bug fixes#545
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
There was a problem hiding this comment.
Pull request overview
This PR tightens correctness and security across the Next.js docs site by enabling TypeScript strictness, hardening server-side inputs/rate limits, and cleaning up build/lint/test tooling and assets.
Changes:
- Enable TypeScript strict mode and disallow shipping type errors; add missing type packages/declarations.
- Add security hardening: payload validation + rate limiting for feedback and newsletter subscribe/unsubscribe; sanitize Scarf pixel env usage; disable
X-Powered-By. - Tooling/cleanup: merge global styles, fix repo links, update CI workflow, add Playwright e2e coverage and eslint config updates.
Reviewed changes
Copilot reviewed 37 out of 44 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Enables strict type-checking and removes stale includes. |
| next.config.mjs | Disables X-Powered-By and enforces TS build correctness (ignoreBuildErrors: false). |
| package.json | Updates postbuild script extension, adds type deps, fixes lint-staged invocation. |
| pnpm-lock.yaml | Locks newly added @types/* packages. |
| global.d.ts | Adds Window typing tightening and local module types for @glidejs/glide. |
| lib/supabase.ts | Centralizes Supabase client creation + email helpers. |
| app/api/subscribe/route.ts | Adds validation + rate limiting and uses shared Supabase/email helpers. |
| app/api/unsubscribe/route.ts | Adds validation + rate limiting and uses shared Supabase/email helpers. |
| app/actions/feedback.ts | Adds input validation + rate limiting before posting to GitHub/Discord. |
| app/layout.tsx | Sanitizes Scarf pixel ID before injecting script. |
| app/global.css | Consolidates styles previously duplicated in style.css. |
| style.css | Removes duplicate Tailwind/style definitions (merged into app/global.css). |
| tailwind.config.js | Updates comment to reflect global CSS source. |
| components.json | Points shadcn/tailwind CSS reference to app/global.css. |
| components/page-actions.tsx | Fixes “Open in GitHub” targeting and supports per-page GitHub URLs. |
| app/docs/[[...slug]]/page.tsx | Passes correct GitHub file URL for the current MDX page. |
| components/screenshot.tsx | Standardizes cn import to @/lib/utils. |
| components/features/index.tsx | Standardizes cn import to @/lib/utils. |
| components/features/themes-animation.tsx | Adds proper SVG props typing under strict TS. |
| components/feature.tsx | Adds prop typing for image source and children. |
| components/cards.tsx | Adds prop typing and standardizes cn import. |
| components/carousel/Carousel.tsx | Adds component prop typing and ref typing for strict TS. |
| components/home/index.tsx | Marks unused component as deprecated and removes unused content. |
| components/Newsletter/SubscribeForm.tsx | Types form submit event and uses validator.isEmail. |
| components/Newsletter/UnsubscribeForm.tsx | Types form submit event and uses validator.isEmail. |
| eslint.config.mjs | Adds e2e/test-file ESLint override. |
| playwright.config.ts | Adds Playwright config targeting ./e2e with local dev webServer. |
| e2e/yaml-cards-check.spec.ts | Adds UI verification for YAML docs cards section. |
| e2e/user-guides-verify.spec.ts | Adds user guides page audit screenshots/logging. |
| e2e/toolkit-fullpage.spec.ts | Adds toolkit page screenshot + basic assertions. |
| e2e/toolkit-debug.spec.ts | Adds debug-oriented toolkit DOM logging. |
| e2e/toolkit-debug2.spec.ts | Adds additional debug-oriented toolkit logging. |
| e2e/stats-section-check.spec.ts | Adds layout/value checks for homepage stats section. |
| e2e/mobile-features-check.spec.ts | Adds mobile overflow/layout checks and screenshots. |
| e2e/google-search-verify.spec.ts | Adds content/screenshot audit for Google Search docs page. |
| e2e/docker-page-verify.spec.ts | Adds screenshot/content checks for Docker docs page. |
| e2e/custom-endpoints-verify.spec.ts | Adds callout/tabs/cards checks for custom endpoints docs page. |
| e2e/config-overview-verify.spec.ts | Adds FileTree/callout/cards verification for configuration overview. |
| e2e/check-redirect.js | Adds standalone redirect-check script (non-test). |
| scripts/clean-cache.cjs | Replaces TS script with CJS for postbuild cache cleanup. |
| .github/workflows/generate_embeddings.yml | Updates actions versions/node/pnpm setup and moves Supabase URL to secrets. |
| .gitignore | Tracks Playwright config, ignores bun lockfile, and tidies tooling artifact ignores. |
| proxy.ts | Switches to default export and adds explicit NextResponse return type. |
| bun.lockb | Removed from repo (and now ignored). |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
components/page-actions.tsx:110
fullMarkdownUrlis set to the literal string'loading'during server render (client components are still SSR’d), which means the generatedq(and the external link hrefs) will differ between SSR and hydration. Prefer buildingqwithout readingwindowin render (e.g., usemarkdownUrldirectly, a known base URL, or compute the absolute URL in an effect and store it in state) to avoid hydration inconsistencies.
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window === 'undefined' ? 'loading' : new URL(markdownUrl, window.location.origin)
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Screenshot the Reference/Cards area | ||
| const box = await refHeading.boundingBox() | ||
| await page.screenshot({ | ||
| path: '/home/berry13/librechat.ai/e2e/screenshots/yaml-light-reference-cards.png', | ||
| clip: { | ||
| x: box!.x - 20, | ||
| y: box!.y - 10, |
There was a problem hiding this comment.
boundingBox() can return null if the element isn’t rendered/visible yet, and the current code non-null asserts box! before the visibility expectation runs (so the test can crash instead of failing with a clear assertion). Move await expect(refHeading).toBeVisible() before calling boundingBox(), and handle the null case explicitly.
| const screenshotPath = path.resolve('/home/berry13/librechat.ai/e2e/redirect-check.png') | ||
| await page.screenshot({ path: screenshotPath, fullPage: false }) | ||
| console.log('Screenshot saved to:', screenshotPath) |
There was a problem hiding this comment.
This script writes its screenshot to an absolute, developer-specific path (/home/berry13/...), which will fail on other machines. Prefer writing under test-results/ (or using a relative path resolved from process.cwd()), and consider keeping this kind of ad-hoc script out of the Playwright test directory to avoid confusion.
| // Full-page screenshot for overview | ||
| await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })) | ||
| await page.waitForTimeout(300) |
There was a problem hiding this comment.
Same issue here: behavior: 'instant' isn’t a valid ScrollBehavior type and will fail TypeScript checking under strict: true. Use 'auto' or omit the behavior option.
| export async function submitFeedback(raw: FeedbackPayload): Promise<{ success: boolean }> { | ||
| try { | ||
| // Rate limit by URL to prevent spamming the same page | ||
| if (isRateLimited(raw.url ?? 'unknown')) { | ||
| return { success: false } | ||
| } | ||
|
|
||
| const payload = validatePayload(raw) | ||
| if (!payload) { | ||
| return { success: false } |
There was a problem hiding this comment.
This rate limiter runs before payload validation and uses raw.url directly as the key. A malformed request with many unique/large url values can grow rateLimitMap without bound even though the payload is rejected later. Validate/sanitize first (and cap key length) before using it for rate limiting, and add eviction/cleanup to prevent unbounded memory growth.
| const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown' | ||
| if (isRateLimited(ip)) { | ||
| return NextResponse.json({ message: 'Too many requests' }, { status: 429 }) | ||
| } |
There was a problem hiding this comment.
ip falls back to 'unknown' when x-forwarded-for is missing, which will cause unrelated clients to share the same bucket and get rate-limited together in environments that don’t set that header. Consider checking other common headers (e.g., x-real-ip, cf-connecting-ip) and/or skipping rate limiting when the client IP can’t be determined reliably.
| const SHOTS = '/home/berry13/librechat.ai/test-results' | ||
|
|
||
| test.describe('user_guides page verification', () => { | ||
| test('light mode - verify sections and cards', async ({ page }) => { | ||
| await page.emulateMedia({ colorScheme: 'light' }) | ||
| await page.goto('/docs/user_guides', { waitUntil: 'networkidle' }) | ||
|
|
||
| await page.waitForSelector('main', { timeout: 15000 }) | ||
|
|
||
| await page.screenshot({ | ||
| path: `${SHOTS}/user-guides-light.png`, | ||
| fullPage: true, | ||
| }) |
There was a problem hiding this comment.
This test writes screenshots to an absolute, developer-specific directory (/home/berry13/...), which will fail on CI/other machines. Use testInfo.outputPath(...) / testInfo.outputDir or a repo-relative test-results/ path instead.
| const URL = 'http://localhost:3333/docs/configuration/tools/google_search' | ||
| const SCREENSHOT_DIR = '/home/berry13/librechat.ai/test-results/google-search-verify' | ||
|
|
There was a problem hiding this comment.
SCREENSHOT_DIR is set to an absolute, developer-specific path (/home/berry13/...), which will break on CI/other machines. Use Playwright’s output directory (testInfo.outputPath) or a repo-relative test-results/ path instead.
| const SCREENSHOT_DIR = '/tmp/toolkit-final' | ||
|
|
There was a problem hiding this comment.
These screenshots are written to a fixed /tmp/... location. This is OS/environment-specific and can also cause collisions when tests run in parallel. Prefer using Playwright’s per-test output path (testInfo.outputPath(...)) so artifacts are isolated and portable.
| /** Simple in-memory rate limiter: max 5 requests per IP per 60 seconds. */ | ||
| const rateLimitMap = new Map<string, { count: number; resetAt: number }>() | ||
| const RATE_LIMIT_WINDOW_MS = 60_000 | ||
| const RATE_LIMIT_MAX = 5 |
There was a problem hiding this comment.
This in-memory rateLimitMap has no eviction and will grow unbounded with unique IPs, and it won’t enforce limits across multiple server instances/serverless invocations. Consider using a shared store (Redis/KV) or middleware-based rate limiting, and add cleanup/bounds if keeping an in-memory fallback.
| await page.screenshot({ | ||
| path: '/home/berry13/librechat.ai/e2e/screenshots/yaml-light-reference-cards.png', | ||
| clip: { |
There was a problem hiding this comment.
This test writes screenshots to an absolute, developer-specific path (/home/berry13/...), which will fail on CI and other machines. Use Playwright’s testInfo.outputPath(...) / testInfo.outputDir (or a repo-relative test-results/ path) so artifacts are portable.
ff1e724 to
9453115
Compare
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
0700d6f to
9453115
Compare
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
1 similar comment
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 48 changed files in this pull request and generated 15 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
9453115 to
d81dbde
Compare
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
… code removal
- Enable strict: true in tsconfig.json and ignoreBuildErrors: false in next.config.mjs
- Fix all implicit-any errors; install @types/validator and @types/js-yaml
- Add type declarations for @glidejs/glide; fix nullable searchParams in CategoryFilter
- Remove stale tsconfig includes (pages/, utils/)
- Merge duplicate style.css into app/global.css; remove dead font-mono on h1
- Add rate limiting and strict email validation to subscribe/unsubscribe routes
- Extract shared Supabase client (cached singleton) to lib/supabase.ts
- Extract shared rate limiter with bounded eviction to lib/rate-limit.ts
- getClientIp: multi-header detection with 'unknown' fallback (never skips limiting)
- Replace import cn from 'clsx' with import { cn } from '@/lib/utils' in 3 components
- Fix all ESLint errors in e2e tests; add ESLint config section for e2e/ files
- Fix hardcoded paths in e2e tests to relative paths; fix /tmp/ → test-results/
- Fix behavior: 'instant' to 'auto' in scroll calls (invalid ScrollBehavior type)
- Fix boundingBox null checks and clamp negative clip coordinates
- Mark debug-only e2e tests as test.skip; add assertions to dark mode tests
- Move ad-hoc check-redirect.js to scripts/check-redirect.cjs
- Fix lint-staged: remove broken ESLINT_USE_FLAT_CONFIG=false
- Rename scripts/clean-cache.ts to .cjs (uses require(), not ESM)
- Remove dead Companies import from components/home/index.tsx
- Update generate_embeddings workflow to actions v4, Node 20, pnpm/action-setup@v4
- Move hardcoded Supabase URL to ${{ secrets.SUPABASE_URL }}
- Delete bun.lockb, 8 orphaned root PNGs; clean up .gitignore
- Disable X-Powered-By header (poweredByHeader: false)
The dropdown 'Open in GitHub' button was hardcoded to danny-avila/LibreChat (the app repo, repo root). Now points to LibreChat-AI/librechat.ai and links directly to the source MDX file for the current page.
- Feedback action: validate opinion ('good'|'bad' only), enforce relative URL
(no open redirects), cap message at 2000 chars
- Use shared createRateLimiter from lib/rate-limit.ts (3/URL/60s)
- Scarf pixel: sanitize NEXT_PUBLIC_SCARF_PIXEL_ID with /^[\w-]+$/ before
interpolating into dangerouslySetInnerHTML to prevent XSS
d81dbde to
176a6b6
Compare
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
Summary
TypeScript Safety
strict: trueandignoreBuildErrors: false. We were silently shipping type errors to prod@glidejs/glide,@types/validator,@types/js-yaml)tsconfig.jsonincludes pointing at deleted filesSecurity Hardening
dangerouslySetInnerHTMLso we're not just trusting it blindlyvalidatorfor RFC 5322 compliancegenerate_embeddings.yml, moved to${{ secrets.SUPABASE_URL }}X-Powered-Byheader (poweredByHeader: false)Bug Fixes
proxy.ts: switched toexport defaultfor Next.js 16 compatlint-staged: removedESLINT_USE_FLAT_CONFIG=falsewhich was blowing up pre-commitLocator.tagName()call in e2e testsCode Quality
style.cssintoapp/global.cssand deleted the extra copylib/supabase.tscnimports to@/lib/utils(unblockstailwind-merge)e2e/scripts/clean-cache.ts→.cjssince it's actually CommonJSCI & Cleanup
generate_embeddings.ymltoactions/checkout@v4, Node 20,pnpm/action-setup@v4bun.lockb(490 KB) and 8 orphaned PNGs (~1.2 MB).gitignore