fix: bump next 16.0.0 → 16.0.11 (CVE-2026-23864 / GHSA-h25m-26qc-wcjf DoS)#1
fix: bump next 16.0.0 → 16.0.11 (CVE-2026-23864 / GHSA-h25m-26qc-wcjf DoS)#1Leathal1 wants to merge 128 commits into
Conversation
…m-26qc-wcjf) Next.js HTTP request deserialization DoS vulnerability affects App Router server function endpoints. SMRY uses App Router with server components (app/proxy/page.tsx uses headers(), generateMetadata, async data fetching) and API routes — all reachable via HTTP requests. Affected version: next 16.0.0 (range: 16.0.0-beta.0 to 16.0.11) Fixed version: next 16.0.11 Also removes deprecated pnpm.overrides field from package.json (pnpm 10+ ignores it with a warning; overrides should go in pnpm-workspace.yaml).
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe SMRY.ai codebase is overhauled from a Next.js 14 edge-runtime streaming architecture to Next.js 16 with parallel multi-source article fetching. Old KV/Resend/proxy/direct/wayback routes are removed and replaced with consolidated ChangesArticle Fetching, Caching, and Summarization Pipeline
Sequence Diagram(s)sequenceDiagram
participant Browser
participant ProxyPage as app/proxy/page.tsx
participant ArticleRoute as GET /api/article
participant JinaRoute as GET /api/jina
participant Redis as Upstash Redis
participant Diffbot as Diffbot API
participant JinaAI as r.jina.ai
participant SummaryRoute as POST /api/summary
participant OpenAI as OpenAI gpt-5-nano
Browser->>ProxyPage: GET /proxy?url=
ProxyPage-->>Browser: render ProxyContent + ArrowTabs
rect rgba(59, 130, 246, 0.5)
note over Browser,Diffbot: Parallel client-side article fetching
Browser->>ArticleRoute: GET ?source=smry-fast
Browser->>ArticleRoute: GET ?source=smry-slow
Browser->>ArticleRoute: GET ?source=wayback
Browser->>JinaRoute: GET ?url= (cache check)
ArticleRoute->>Redis: cache lookup
Redis-->>ArticleRoute: miss
ArticleRoute->>Diffbot: fetchArticleWithDiffbot
Diffbot-->>ArticleRoute: structured article + DOM
ArticleRoute->>Redis: write cache
ArticleRoute-->>Browser: ArticleResponse
JinaRoute-->>Browser: CACHE_MISS 404
Browser->>JinaAI: fetchJinaArticle (client-side)
JinaAI-->>Browser: markdown article
Browser->>JinaRoute: POST /api/jina (background cache update)
end
rect rgba(16, 185, 129, 0.5)
note over Browser,OpenAI: Summary generation (on demand)
Browser->>SummaryRoute: POST /api/summary {content, language, ip}
SummaryRoute->>SummaryRoute: rate limit check (Upstash)
SummaryRoute->>Redis: cache key lookup
Redis-->>SummaryRoute: miss
SummaryRoute->>OpenAI: chat.completions.create
OpenAI-->>SummaryRoute: summary text
SummaryRoute->>Redis: write summary cache
SummaryRoute-->>Browser: {summary, cached: false}
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
app/[...slug]/page.tsxESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. app/api/article/route.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. app/api/jina/route.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
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 |
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/page.tsx (1)
71-122:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd accessible names for the URL input and icon-only submit button.
The primary form is missing an explicit accessible label for the input and an accessible name for the submit control, which blocks assistive-tech users from reliably completing the flow.
♿ Suggested fix
<form onSubmit={handleSubmit} className="mt-6 w-full"> + <label htmlFor="url-input" className="sr-only"> + Article URL + </label> <div className={`${ urlError ? "border-red-500" : "" } flex overflow-hidden rounded-lg border border-[`#E5E5E5`] bg-white shadow-sm focus-within:border-purple-500 focus-within:ring-4 focus-within:ring-purple-200 focus-within:ring-offset-0`} > <input + id="url-input" className="w-full rounded-l-lg bg-transparent p-4 py-3 shadow-lg focus:outline-none" autoFocus autoComplete="off" placeholder="https://example.com/page" + aria-invalid={urlError} name="url" value={url} onChange={(e) => { setUrl(e.target.value); if (urlError) setUrlError(false); }} /> <button className="cursor-pointer rounded-r-lg px-4 py-2 font-mono transition-all duration-300 ease-in-out" type="submit" + aria-label="Generate summary" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > + <span className="sr-only">Generate summary</span> {/* Icon here */}🤖 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 `@app/page.tsx` around lines 71 - 122, The URL input field and the submit button are missing accessible names that screen readers need. Add an aria-label attribute to the input element with name="url" that describes its purpose (e.g., "Enter website URL"), and add an aria-label attribute to the button element with type="submit" that describes the submit action (e.g., "Submit URL"). This ensures assistive technology users can understand the form's purpose and functionality.
🟠 Major comments (21)
VALIDATION_SUMMARY.md-227-233 (1)
227-233:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove machine-specific absolute paths from repository docs.
These lines expose a local username/path and make the doc non-portable. Use repo-relative paths instead.
Suggested doc patch
-1. `/Users/michaelryaboy/projects/13ft/lib/api/diffbot.ts` +1. `lib/api/diffbot.ts` ... -2. `/Users/michaelryaboy/projects/13ft/app/api/article/route.ts` +2. `app/api/article/route.ts`🤖 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 `@VALIDATION_SUMMARY.md` around lines 227 - 233, In VALIDATION_SUMMARY.md, replace the absolute paths that include the full machine path (starting with /Users/michaelryaboy/projects/13ft) with repository-relative paths. For example, change /Users/michaelryaboy/projects/13ft/lib/api/diffbot.ts to lib/api/diffbot.ts and /Users/michaelryaboy/projects/13ft/app/api/article/route.ts to app/api/article/route.ts. This makes the documentation portable across different machines and users.package.json-70-87 (1)
70-87:⚠️ Potential issue | 🟠 MajorDeclare
eslint-plugin-react-hooksexplicitly in devDependencies to avoid lint bootstrap failures.
eslint-plugin-react-hooksis imported on line 3 ofeslint.config.mjsbut not declared inpackage.json. Under pnpm, relying on transitive dependency resolution is brittle and can breaknpm run lint/CI when the transitive path changes.🤖 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 `@package.json` around lines 70 - 87, The eslint-plugin-react-hooks package is imported in eslint.config.mjs but is missing from the devDependencies section in package.json. Add eslint-plugin-react-hooks as an explicit entry in the devDependencies object in package.json with an appropriate version number to ensure pnpm can properly resolve this dependency and prevent lint command failures when transitive dependency paths change.components/shared/error-display.tsx-98-114 (1)
98-114:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate outbound link protocols before rendering user-derived URLs.
Line 102 and Line 114 render
displayUrldirectly intohref. Since this value can originate from request-driven inputs, non-HTTP schemes (for examplejavascript:) can be exposed as clickable links. Whitelisthttp:/https:before rendering either link.Suggested fix
+ const sanitizeHttpUrl = (value?: string): string | undefined => { + if (!value) return undefined; + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:" + ? parsed.toString() + : undefined; + } catch { + return undefined; + } + }; + - const displayUrl = originalUrl || errorUrl; + const displayUrl = sanitizeHttpUrl(originalUrl || errorUrl); ... - {showProxyLink && source && displayUrl && getExternalUrl(source, displayUrl) && ( + {showProxyLink && source && displayUrl && getExternalUrl(source, displayUrl) && (🤖 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 `@components/shared/error-display.tsx` around lines 98 - 114, Add URL protocol validation to prevent injection of malicious schemes like javascript: into the href attributes. Create a helper function that validates URLs to ensure they only start with http: or https: protocols. Apply this validation before rendering both the displayUrl link (around line 102) and the external URL returned by getExternalUrl(source, displayUrl) (around line 114). Only render each link if the URL passes the protocol whitelist validation, ensuring that user-derived inputs cannot be exploited for XSS attacks through URL schemes.components/shared/debug-panel.tsx-159-161 (1)
159-161:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDebug payload is unreadable due to white-on-white text.
Line 159 applies
text-whiteinside a white details container, so expanded JSON data is effectively invisible.Proposed fix
- <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-white"> + <pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-zinc-800"> {JSON.stringify(step.data, null, 2)} </pre>🤖 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 `@components/shared/debug-panel.tsx` around lines 159 - 161, The pre element displaying the debug JSON payload has text-white class applied, which creates white-on-white text that is invisible against the white details container background. In the debug-panel.tsx file where the JSON.stringify output is rendered within the pre tag, replace the text-white class with a dark text color class (such as text-gray-900 or text-black) to ensure the debug data is readable against the white background.components/shared/debug-panel.tsx-178-180 (1)
178-180:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnsafe URL parsing can crash render for malformed debug URLs.
Line 179 assumes
debugContext.urlis always an absolute valid URL. If it is malformed/relative,new URL(...)throws during render.Proposed fix
+ const safeHostname = (() => { + try { + return new URL(debugContext.url).hostname; + } catch { + return debugContext.url; + } + })(); ... - <span className="max-w-xs truncate font-mono text-gray-500" title={debugContext.url}> - {new URL(debugContext.url).hostname} + <span className="max-w-xs truncate font-mono text-gray-500" title={debugContext.url}> + {safeHostname} </span>🤖 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 `@components/shared/debug-panel.tsx` around lines 178 - 180, The code directly calls new URL(debugContext.url).hostname without handling potential errors. If debugContext.url is malformed or relative, the new URL() constructor will throw an exception during render, crashing the component. Wrap the URL parsing logic in a try-catch block or add validation to safely handle the hostname extraction, providing a fallback value (such as the raw URL or a dash) when URL parsing fails.components/shared/debug-panel.tsx-115-118 (1)
115-118:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStep expansion is mouse-only; keyboard users can’t toggle rows.
Line 115 uses a clickable
<div>withonClickbut no keyboard semantics. This blocks keyboard-only navigation of the debug details.Proposed fix
- <div - className="cursor-pointer bg-white px-4 py-3 transition-colors hover:bg-gray-50/50" - onClick={() => toggleStep(index)} - > + <button + type="button" + className="w-full cursor-pointer bg-white px-4 py-3 text-left transition-colors hover:bg-gray-50/50" + onClick={() => toggleStep(index)} + aria-expanded={isExpanded} + > ... - </div> + </button>🤖 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 `@components/shared/debug-panel.tsx` around lines 115 - 118, The expandable step row uses a clickable `<div>` element with an onClick handler but lacks keyboard accessibility semantics, preventing keyboard-only users from toggling steps. Replace the `<div>` element at line 115 with a `<button>` element to provide native keyboard support (Enter and Space keys will automatically trigger the toggleStep function), and ensure the button maintains the same styling and onClick behavior as the current implementation.app/[...slug]/page.tsx-12-13 (1)
12-13:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse segment-safe route guards for internal paths.
Line 12 currently matches any slug starting with
/api, so inputs like/api.example.comreturn early and never redirect. Guard only true internal segments (/apiand/api/..., same for/_next).Suggested fix
- if (pathname.startsWith('/_next') || pathname.startsWith('/api')) { + const isNextInternal = pathname === "/_next" || pathname.startsWith("/_next/"); + const isApiRoute = pathname === "/api" || pathname.startsWith("/api/"); + if (isNextInternal || isApiRoute) { return; }🤖 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 `@app/`[...slug]/page.tsx around lines 12 - 13, The pathname checks in the if statement are too broad and will match partial segment names like `/api.example.com`. Replace the startsWith checks for both `/_next` and `/api` with segment-safe guards that check for either an exact match to the segment or the segment followed by a forward slash, ensuring only true internal paths like `/api` or `/api/...` (and `/_next` or `/_next/...`) trigger the early return, not partial matches.app/api/article/route.ts-136-144 (1)
136-144:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd a hard timeout to the direct upstream fetch.
Line 136 performs a blocking external call with no timeout. Slow or hanging upstreams can pin server resources and stall requests.
Proposed fix
const response = await fetch(url, { headers: { "User-Agent": "smry.ai bot/1.0 (+https://smry.ai)", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", }, cache: "no-store", redirect: "follow", + signal: AbortSignal.timeout(15_000), });🤖 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 `@app/api/article/route.ts` around lines 136 - 144, The fetch call to the upstream URL in the response object assignment has no timeout specified, which can cause requests to hang indefinitely if the upstream server is slow or unresponsive. Add a hard timeout to this fetch call by creating an AbortController instance, setting a timeout that aborts the controller after a reasonable duration (such as 10-15 seconds), and passing the abort signal to the fetch options alongside the existing headers, cache, and redirect settings.types/api.ts-64-71 (1)
64-71:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAlign shared summary request schema with the server validator.
Line 65 currently allows 100 chars, but
app/api/summary/route.tsenforces 2000 chars. This contract mismatch causes avoidable 400s for payloads that pass shared typing/validation (and UI paths that allow much shorter content).Proposed fix
+export const SUMMARY_MIN_CONTENT_CHARS = 2000; + export const SummaryRequestSchema = z.object({ - content: z.string().min(100, "Content must be at least 100 characters"), + content: z.string().min( + SUMMARY_MIN_CONTENT_CHARS, + `Content must be at least ${SUMMARY_MIN_CONTENT_CHARS} characters` + ), title: z.string().optional(), url: z.string().optional(), ip: z.string().optional(), language: z.string().optional().default("en"), });🤖 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 `@types/api.ts` around lines 64 - 71, Update the minimum character validation for the content field in the SummaryRequestSchema object in types/api.ts. Change the z.string().min(100) constraint to z.string().min(2000) to align with the server-side validator in app/api/summary/route.ts. This ensures that payloads passing client-side validation will not encounter avoidable 400 errors from the server.lib/api/client.ts-36-42 (1)
36-42:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHarden non-OK error parsing to always throw
ArticleFetchError.Line 37 assumes JSON on every error response. If the server/proxy returns HTML or plain text, this throws a
SyntaxErrorand skips your custom error path.Proposed fix
if (!response.ok) { - const errorData: ErrorResponse = await response.json(); + let errorData: ErrorResponse | undefined; + try { + errorData = (await response.json()) as ErrorResponse; + } catch { + errorData = { + error: `HTTP error! status: ${response.status}`, + details: { status: response.status }, + }; + } // Throw custom error that preserves debug context throw new ArticleFetchError( - errorData.error || `HTTP error! status: ${response.status}`, + errorData.error || `HTTP error! status: ${response.status}`, errorData ); }🤖 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 `@lib/api/client.ts` around lines 36 - 42, The response.json() call on line 37 assumes the error response is always valid JSON. If the server returns HTML or plain text, this will throw a SyntaxError and bypass the ArticleFetchError path. Wrap the response.json() call in a try-catch block, and in the catch handler, still throw an ArticleFetchError with a fallback error message (such as including the response status code). This ensures ArticleFetchError is always thrown for non-OK responses regardless of the response format.app/api/summary/route.ts-159-162 (1)
159-162:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse a full-content hash for cache keys to avoid summary collisions.
The fallback key uses only a truncated prefix (
content.substring(0, 500)), so different articles with similar starts can share a key and receive the wrong cached summary.Proposed fix
+import { createHash } from "node:crypto"; @@ - const cacheKey = url - ? `summary:${language}:${url}` - : `summary:${language}:${Buffer.from(content.substring(0, 500)).toString('base64').substring(0, 50)}`; + const contentHash = createHash("sha256").update(content).digest("base64url"); + const cacheKey = url + ? `summary:${language}:${url}` + : `summary:${language}:${contentHash}`;🤖 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 `@app/api/summary/route.ts` around lines 159 - 162, The cache key generation in the fallback case (when url is not provided) uses only a truncated substring prefix of the content converted to base64, which can cause different articles with similar beginnings to collide and share the same cache key. Instead of using content.substring(0, 500) and then truncating the base64 result, generate a full-content hash using a cryptographic hash function like crypto.createHash with the entire content value to ensure unique cache keys for different content and prevent incorrect cached summary retrieval.app/api/summary/route.ts-181-193 (1)
181-193:⚠️ Potential issue | 🟠 MajorConfigure explicit timeout and maxRetries for the OpenAI API client.
The OpenAI client initialized at line 13-15 lacks explicit timeout and retry configuration, relying on SDK defaults (10-minute timeout, 2 retries). In a serverless environment, slow upstream responses can hold function capacity and degrade availability under load.
Set
timeout(in milliseconds) andmaxRetriesduring client initialization. For example:Configuration example
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, timeout: 30000, // 30 seconds maxRetries: 1, // Adjust based on your requirements });🤖 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 `@app/api/summary/route.ts` around lines 181 - 193, The OpenAI client initialization lacks explicit timeout and maxRetries configuration, relying on SDK defaults which can cause performance issues in serverless environments. Locate the OpenAI client initialization (where "new OpenAI()" is called with apiKey configuration) and add two parameters: set timeout to an appropriate value in milliseconds such as 30000 for 30 seconds, and set maxRetries to a lower value like 1 to prevent long wait times that could impact function capacity. This ensures the client has explicit, optimized configuration suitable for your serverless environment instead of relying on SDK defaults.app/api/jina/route.ts-134-145 (1)
134-145:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftUse an atomic update for “only if longer” cache writes.
The current
get→ compare →setsequence is race-prone; concurrent requests can overwrite a longer article with a shorter one, violating your own update rule.🤖 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 `@app/api/jina/route.ts` around lines 134 - 145, The current implementation uses a separate redis.get followed by a conditional redis.set, which creates a race condition where concurrent requests can overwrite a longer cached article with a shorter one. Replace this non-atomic get-compare-set pattern with an atomic Redis operation, such as a Lua script or Redis transaction that performs the length comparison and conditional update atomically on the server side. This ensures that between reading the existing article value and deciding whether to update it, no other request can modify the cache and invalidate your length comparison check.lib/api/diffbot.ts-336-344 (1)
336-344:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd an explicit timeout to Diffbot fetch calls.
This request path calls a third-party API without timeout control. A stuck upstream can hold server resources and degrade availability under load.
Suggested fix
- const response = await fetch(apiUrl.toString()); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15000); + const response = await fetch(apiUrl.toString(), { signal: controller.signal }); + clearTimeout(timeout);🤖 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 `@lib/api/diffbot.ts` around lines 336 - 344, The fetch call in the Diffbot API request lacks timeout protection, which can cause requests to hang indefinitely if the upstream service is unresponsive. Add an AbortController with a timeout to the fetch call that instantiates the controller before the fetch invocation and passes it to the fetch options along with a reasonable timeout duration (typically 30 seconds for API calls). Ensure the abort signal is properly handled in the error path so that timeout errors are caught and logged appropriately along with the existing HTTP error handling.lib/api/diffbot.ts-324-584 (1)
324-584:⚠️ Potential issue | 🟠 MajorRemove async Promise executor at line 324.
new Promise(async (resolve, reject) => ...)is an anti-pattern that risks unhandled rejections and violates linting standards. Refactor to use non-async executor orPromise.resolve()to perform async work after promise construction.🤖 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 `@lib/api/diffbot.ts` around lines 324 - 584, The Promise constructor at the start of this function block uses `async (resolve, reject) =>` which is an anti-pattern that can lead to unhandled rejections. Remove the `async` keyword from the Promise executor parameter and instead wrap all the try-catch logic in an immediately-invoked async function (IIFE) inside the executor, or extract the entire async logic into a separate named async function that gets invoked within the Promise constructor. The key is to ensure the executor itself is not async while still maintaining all the async operations (fetch, response parsing, etc.) in the work that happens within the Promise.Source: Linters/SAST tools
types/diffbot.d.ts-73-81 (1)
73-81:⚠️ Potential issue | 🟠 MajorMake Diffbot callback response optional to match actual error-first callback behavior.
The current
articleandfrontpagesignatures declareresponseas required, but the library passesundefinedfor response whenerris non-null. This masks real undefined-response paths and prevents TypeScript from catching unsafe access in error branches.Suggested fix
article( options: DiffbotArticleOptions, - callback: (err: Error | null, response: DiffbotArticleResponse) => void + callback: (err: Error | null, response?: DiffbotArticleResponse) => void ): void; frontpage( options: DiffbotFrontpageOptions, - callback: (err: Error | null, response: any) => void + callback: (err: Error | null, response?: any) => void ): void;🤖 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 `@types/diffbot.d.ts` around lines 73 - 81, The response parameter in the callback signatures for both the article and frontpage methods should be optional to match the actual library behavior where response is undefined when an error occurs. In the article method callback, change the response type from DiffbotArticleResponse to DiffbotArticleResponse | undefined to indicate the response may not be present. Similarly, in the frontpage method callback, change the response type from any to any | undefined. This will enable TypeScript to properly enforce null-safety checks and catch unsafe access to the response object in error-handling branches.components/article/content.tsx-73-73 (1)
73-73:⚠️ Potential issue | 🟠 MajorRemove
allow-same-originfrom iframe sandbox policies for external content.All iframe instances (lines 73, 126, 148, 208) embed external URLs from third-party services (Wayback Machine, jina.ai). The combination of
allow-same-originwithallow-scriptsweakens sandbox isolation by allowing scripts to bypass CORS restrictions. This should be removed since the iframes load untrusted external content.Suggested fix
- sandbox="allow-same-origin allow-scripts allow-popups allow-forms" + sandbox="allow-scripts allow-popups allow-forms" + referrerPolicy="no-referrer"Apply to lines 73, 126, 148, and 208.
🤖 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 `@components/article/content.tsx` at line 73, Remove `allow-same-origin` from the sandbox attribute on all four iframe instances that load external third-party content (at lines 73, 126, 148, and 208 in components/article/content.tsx). The combination of `allow-same-origin` with `allow-scripts` creates a security vulnerability by allowing scripts to bypass CORS restrictions on untrusted external content from Wayback Machine and jina.ai. Keep the remaining sandbox policies (`allow-scripts`, `allow-popups`, `allow-forms`) but remove only the `allow-same-origin` attribute from each iframe's sandbox attribute.Source: Linters/SAST tools
app/proxy/page.tsx-142-144 (1)
142-144:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize
searchParams.urlbefore use.
urlis currently asserted asstring, but the type allowsstring[] | undefined. This can flow invalid values into metadata fetch and page rendering logic.Suggested fix
+function getSingleParam(value: string | string[] | undefined): string | undefined { + if (typeof value === "string") return value; + if (Array.isArray(value)) return value[0]; + return undefined; +} @@ - const url = resolvedSearchParams?.url as string; + const url = getSingleParam(resolvedSearchParams?.url); @@ - const url = resolvedSearchParams?.url as string; + const url = getSingleParam(resolvedSearchParams?.url);Also applies to: 239-240
🤖 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 `@app/proxy/page.tsx` around lines 142 - 144, The url variable is being unsafely type-asserted as a string using as string, but searchParams.url can actually be string[] or undefined, which allows invalid values to flow into metadata and rendering logic. Remove the unsafe type assertion and instead add proper validation logic to normalize the value: check if url is actually a string before using it, handle the case where it might be an array by extracting the first element if needed, and handle the undefined case appropriately. This validation pattern needs to be applied in both locations where this issue occurs around lines 142-144 and 239-240 in the resolvedSearchParams variable assignment.lib/hooks/use-local-storage.ts-9-17 (1)
9-17:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDeferred + unguarded localStorage hydration is causing avoidable failures and extra work.
This reads persisted state on a later tick and parses without protection. In current usage, that can trigger duplicate summary requests when stored language differs from default, and malformed storage can crash rendering.
Suggested fix
- const [storedValue, setStoredValue] = useState(initialValue); + const [storedValue, setStoredValue] = useState<T>(() => { + if (typeof window === "undefined") return initialValue; + try { + const item = window.localStorage.getItem(key); + return item ? (JSON.parse(item) as T) : initialValue; + } catch { + return initialValue; + } + }); useEffect(() => { - // Use a timeout to avoid setting state synchronously - const timer = setTimeout(() => { - const item = window.localStorage.getItem(key); - if (item) { - setStoredValue(JSON.parse(item)); - } - }, 0); - return () => clearTimeout(timer); - }, [key]); + try { + const item = window.localStorage.getItem(key); + if (item) setStoredValue(JSON.parse(item) as T); + } catch { + setStoredValue(initialValue); + } + }, [key, initialValue]);🤖 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 `@lib/hooks/use-local-storage.ts` around lines 9 - 17, The useEffect hook in use-local-storage.ts reads from localStorage and parses JSON without error protection, causing crashes on malformed data. Additionally, the deferred setTimeout approach causes unnecessary duplicate work and requests when stored values differ from defaults. Wrap the JSON.parse call in a try-catch block to safely handle invalid stored data, remove the deferred setTimeout to synchronously hydrate the initial state during mount, and ensure the stored value is properly validated before calling setStoredValue.app/proxy/page.tsx-226-232 (1)
226-232:⚠️ Potential issue | 🟠 Major | ⚡ Quick winIP extraction is too narrow for proxied deployments.
Only checking
x-real-ipfrequently falls back to"default_ip", which can collapse unrelated users into one identity for downstream rate limiting and analytics flows.Suggested fix
- if (headersList && typeof headersList.get === 'function') { - ip = headersList.get("x-real-ip") || "default_ip"; + if (headersList && typeof headersList.get === 'function') { + const forwardedFor = headersList.get("x-forwarded-for"); + const realIp = headersList.get("x-real-ip"); + const cfIp = headersList.get("cf-connecting-ip"); + ip = + forwardedFor?.split(",")[0]?.trim() || + realIp || + cfIp || + "default_ip";🤖 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 `@app/proxy/page.tsx` around lines 226 - 232, The IP extraction logic in the headersList processing block only checks the x-real-ip header before falling back to "default_ip", which causes multiple users to be grouped together in proxied deployments. Modify the code to check multiple common headers that contain the real IP address in priority order, such as x-forwarded-for, cf-connecting-ip, x-client-ip, and true-client-ip, before falling back to a default value. When a header like x-forwarded-for contains multiple IPs, extract the first one (the client IP). This ensures more accurate IP identification across different proxy configurations.components/features/summary-form.tsx-34-43 (1)
34-43:⚠️ Potential issue | 🟠 MajorAuto-summary triggers multiple API calls as source ranking changes during parallel article loads.
longestAvailableSourcerecalculates asarticleResultsupdates, causingselectedSourceand subsequentlysummaryParamsto change. SinceuseAutoSummarykeys bysource(line 50 inlib/hooks/use-summary.ts), each change to the source triggers a new query with a different queryKey, resulting in multiple/api/summarycalls during initial load.Defer enabling the summary query until all article sources have settled:
Suggested fix
+ const allSourcesSettled = useMemo( + () => Object.values(articleResults).every((r) => r.isSuccess || r.isError), + [articleResults] + ); @@ - const autoSummary = useAutoSummary(summaryParams, true); + const autoSummary = useAutoSummary(summaryParams, allSourcesSettled);🤖 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 `@components/features/summary-form.tsx` around lines 34 - 43, The `longestAvailableSource` memo recalculates whenever `articleResults` changes during parallel article loading, causing `selectedSource` to change multiple times and trigger new API calls to `/api/summary` via `useAutoSummary` (which keys queries by source). Instead of allowing the summary query to trigger on every source change, implement a check to defer enabling the summary query until all article sources have completed loading. This could involve tracking whether all expected sources have loaded data or checking if the `articleResults` object has settled (no longer being updated), then only then allowing `selectedSource` to be used and the automatic summary to be triggered.
🟡 Minor comments (10)
VALIDATION_SUMMARY.md-245-245 (1)
245-245:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse “public APIs” instead of “public API interfaces.”
This is redundant wording and reads awkwardly.
🤖 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 `@VALIDATION_SUMMARY.md` at line 245, The phrase "public API interfaces" contains redundant wording where "interfaces" is unnecessary. In the VALIDATION_SUMMARY.md file, replace the text "public API interfaces" with "public APIs" to improve clarity and readability.Source: Linters/SAST tools
lib/README.md-7-24 (1)
7-24:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language identifier to the fenced code block.
This trips markdown lint (
MD040) and is easy to fix (e.g., use ```text for the tree block).🤖 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 `@lib/README.md` around lines 7 - 24, The fenced code block containing the directory structure tree in the README.md file is missing a language identifier after the opening triple backticks, which violates markdown lint rule MD040. Add a language identifier (such as `text`) immediately after the opening triple backticks to specify the type of code block content, making the syntax valid for markdown linting tools.Source: Linters/SAST tools
lib/README.md-97-100 (1)
97-100:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate
use-articles.tssource list to match current 4-source pipeline.The doc still describes 3 sources. Current architecture uses
smry-fast,smry-slow,wayback, andjina.ai.🤖 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 `@lib/README.md` around lines 97 - 100, Update the documentation for use-articles.ts in the README to accurately reflect the current 4-source pipeline instead of the 3-source pipeline. Replace the reference to fetching from direct, wayback, and jina.ai sources with the actual current sources: smry-fast, smry-slow, wayback, and jina.ai. Ensure the description of how these sources are fetched (parallel, client-side vs server-side, caching strategy) matches the current implementation in use-articles.ts.VALIDATION_SUMMARY.md-124-130 (1)
124-130:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate cache terminology from “KV” to “Redis” for accuracy.
This section says “KV cache”, but the route implementation uses Upstash Redis (
redis.get/redis.set). Align wording to avoid confusion.🤖 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 `@VALIDATION_SUMMARY.md` around lines 124 - 130, In the Cache Read Validation section of the documentation, update all references from "KV cache" to "Redis" to accurately reflect that the implementation uses Upstash Redis (via redis.get/redis.set operations) rather than generic key-value cache terminology. Specifically, change the phrase "Validates data retrieved from KV cache" to reference Redis instead, ensuring consistency between the documentation and the actual redis.get/redis.set method calls used in the route implementation.components/marketing/banner.tsx-96-97 (1)
96-97:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a fallback when
NEXT_PUBLIC_LOGODEV_TOKENis missing.Line 97 always appends
token=${logoDevToken}. If the env var is unset, the homepage emits broken logo requests (token=undefined).Proposed fix
const logoDevToken = process.env.NEXT_PUBLIC_LOGODEV_TOKEN; - const logoUrl = `https://img.logo.dev/${url}?token=${logoDevToken}&size=64&greyscale=true&format=png`; + const logoUrl = logoDevToken + ? `https://img.logo.dev/${url}?token=${logoDevToken}&size=64&greyscale=true&format=png` + : `https://www.google.com/s2/favicons?domain=${url}&sz=64`;🤖 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 `@components/marketing/banner.tsx` around lines 96 - 97, The logoUrl construction in the banner component always includes the token parameter even when NEXT_PUBLIC_LOGODEV_TOKEN is undefined, resulting in broken requests with `token=undefined`. Add a conditional check to verify that logoDevToken is defined and non-empty before appending the token parameter to the logoUrl string. If the token is missing, construct the URL without the token parameter or provide an appropriate fallback mechanism.components/ui/shadcn-io/sliding-number/index.tsx-138-141 (1)
138-141:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNormalize
numberto a finite numeric value before animation math.
numberacceptsstring; non-numeric values becomeNaNand leak into rolling-digit calculations.🧮 Suggested fix
+ const numericValue = Number(number); const effectiveNumber = React.useMemo( - () => (!isInView ? 0 : Math.abs(Number(number))), - [number, isInView], + () => (!isInView ? 0 : Number.isFinite(numericValue) ? Math.abs(numericValue) : 0), + [numericValue, isInView], ); @@ - {isInView && Number(number) < 0 && <span className="mr-1">-</span>} + {isInView && Number.isFinite(numericValue) && numericValue < 0 && ( + <span className="mr-1">-</span> + )}Also applies to: 209-209
🤖 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 `@components/ui/shadcn-io/sliding-number/index.tsx` around lines 138 - 141, The effectiveNumber useMemo hook does not validate that the converted number is a finite numeric value. Since the number prop accepts strings, non-numeric input values become NaN when passed to Math.abs and this NaN leaks into rolling-digit calculations. Fix this by ensuring the result of Number(number) is finite using Number.isFinite(), and provide a fallback value (such as 0) when the number is not finite or NaN. Apply this normalization before passing the value to Math.abs within the effectiveNumber useMemo hook.components/ui/shadcn-io/github-stars-button/index.tsx-63-71 (1)
63-71:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAvoid caching invalid GitHub API responses as star counts.
If GitHub returns a non-OK payload (e.g., rate-limited),
stargazers_countis absent and an invalid value is cached for 5 days.🛠️ Suggested fix
- const response = await fetch(`https://api.github.com/repos/${username}/${repo}`); - const data = await response.json(); - const starCount = data.stargazers_count as number; + const response = await fetch(`https://api.github.com/repos/${username}/${repo}`); + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + const data = await response.json(); + const starCount = Number(data?.stargazers_count); + if (!Number.isFinite(starCount)) { + throw new Error('Invalid stargazers_count in GitHub API response'); + } // Save to localStorage localStorage.setItem( `github-stars-${username}-${repo}`, JSON.stringify({ stars: starCount, timestamp: Date.now() }) );🤖 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 `@components/ui/shadcn-io/github-stars-button/index.tsx` around lines 63 - 71, The code is caching the GitHub API response without validating that the request was successful, which means invalid responses (such as rate-limit errors) will be cached with undefined or missing stargazers_count values. Add a check for response.ok immediately after the fetch call to the GitHub API endpoint, and only proceed with parsing the JSON data and caching via localStorage.setItem if the response status is successful. If the response is not OK, handle the error appropriately (such as throwing an error or returning early) to prevent invalid data from being stored in the cache.components/marketing/ad.tsx-20-27 (1)
20-27:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd an accessible name to the close button.
Line 20 renders an icon-only control without an accessible label, so assistive tech users won’t know what the button does.
Suggested fix
- <button + <button + type="button" + aria-label="Dismiss advertisement" onClick={() => { // track('close banner'); setShowAd(false); }} className="absolute right-1 top-[8px]" >🤖 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 `@components/marketing/ad.tsx` around lines 20 - 27, The close button in the ad component that contains only the XIcon lacks an accessible label for assistive technology users. Add an aria-label attribute to the button element (the one with className "absolute right-1 top-[8px]" that contains the XIcon) with a descriptive label such as "Close ad" or "Close banner" to provide an accessible name that screen readers can announce.lib/errors/types.ts-131-230 (1)
131-230:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNormalize optional error payload fields in factory helpers.
String(originalError)converts missing values into the literal"undefined"in multiple factories, andcreateParseErrorcannot passdebugContexteven thoughParseErrorsupports it. This produces noisy technical details and drops parse-step debugging context.Suggested fix
+const normalizeOriginalError = (originalError?: unknown): string | undefined => + originalError instanceof Error + ? originalError.message + : originalError != null + ? String(originalError) + : undefined; export const createNetworkError = ( message: string, url: string, statusCode?: number, originalError?: unknown, debugContext?: DebugContext ): NetworkError => ({ type: "NETWORK_ERROR", message, url, statusCode, - originalError: originalError instanceof Error ? originalError.message : String(originalError), + originalError: normalizeOriginalError(originalError), debugContext, }); // ...apply same normalization in createProxyError/createDiffbotError/createCacheError/createValidationError/createUnknownError export const createParseError = ( message: string, source: string, - originalError?: unknown + originalError?: unknown, + debugContext?: DebugContext ): ParseError => ({ type: "PARSE_ERROR", message, source, - originalError: originalError instanceof Error ? originalError.message : String(originalError), + originalError: normalizeOriginalError(originalError), + debugContext, });🤖 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 `@lib/errors/types.ts` around lines 131 - 230, The factory functions are inconsistently handling optional originalError fields, producing the literal string "undefined" instead of actual undefined values, and createParseError is missing the debugContext parameter that ParseError supports. Normalize all error factory functions (createNetworkError, createProxyError, createDiffbotError, createParseError, createTimeoutError, createRateLimitError, createCacheError, createValidationError, createUnknownError) to return undefined when originalError is falsy rather than converting it to the string "undefined". Additionally, add debugContext as an optional parameter to createParseError and include it in the returned ParseError object to preserve debugging context throughout the error handling chain.components/article/length.tsx-37-39 (1)
37-39:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHandle zero-word articles explicitly.
Line 37 uses a truthy check, so
0is treated as “no value” and nothing is rendered. This hides a valid state.Suggested fix
- if (data?.article?.length) { + if (data?.article?.length != null) { return <>{" · " + data.article.length + " words"}</>; }🤖 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 `@components/article/length.tsx` around lines 37 - 39, The truthy check on data?.article?.length treats 0 as falsy, so articles with zero words will not render any output. Replace the truthy check with an explicit check for null or undefined (for example, using !== null or !== undefined, or checking typeof) to ensure that a length value of 0 is treated as a valid state and properly displayed as "0 words".
🧹 Nitpick comments (2)
README.md (1)
1-394: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueAdd language identifiers to fenced code blocks for markdown best practices.
Several code blocks lack explicit language specifications (
typescript,bash, etc.). While this doesn't affect rendering, it improves linter compliance and clarity for documentation tools.Affected blocks (estimated ranges):
- Lines ~124–127: Cache key format
- Lines ~147–176: Directory structure
- Lines ~181–199: Request flow diagram
- Lines ~202–214: Summary flow diagram
- Lines ~267–280: URL examples
📝 Example: Adding language identifiers
- ``` + ```text ┌─────────────────────────────────────────────────────────────────┐ │ API Request to /api/article │ └────────────────────────────┬────────────────────────────────────┘or for directory trees:
- ``` + ``` app/ ├── api/ │ ├── article/route.tsor for cache keys:
- ``` + ``` summary:en:https://example.com summary:es:https://example.com🤖 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 `@README.md` around lines 1 - 394, Several fenced code blocks in the README.md file lack explicit language specifications, which impacts markdown linting compliance and documentation tool clarity. Add appropriate language identifiers to all code blocks throughout the file: use `typescript` for TypeScript code snippets, `bash` for shell commands, `text` for diagrams and flow charts, and similar language identifiers for other code blocks. Target the affected sections including cache key format examples, directory structure listings, request flow diagrams, summary flow diagrams, and URL usage examples.VALIDATION_FLOW.md (1)
3-101: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueSpecify language identifier for ASCII diagram fenced code block.
Line 3 opens a code block without a language designation. For ASCII diagrams, use
```textor```asciito improve markdown compliance.🎨 Proposed fix
- ``` + ```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 `@VALIDATION_FLOW.md` around lines 3 - 101, The ASCII diagram code block in VALIDATION_FLOW.md is missing a language identifier after the opening triple backticks. Add a language designation such as "text" or "ascii" immediately after the opening ``` on line 3 to improve markdown compliance and enable proper syntax highlighting. This applies to the entire fenced code block containing the validation flow diagram.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e80cdd8a-360f-4174-b018-bd0b33ac7314
⛔ Files ignored due to path filters (7)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/bg.pngis excluded by!**/*.pngpublic/book.pngis excluded by!**/*.pngpublic/grid.svgis excluded by!**/*.svgpublic/next.svgis excluded by!**/*.svgpublic/og-image.pngis excluded by!**/*.pngpublic/vercel.svgis excluded by!**/*.svg
📒 Files selected for processing (84)
.env.example.env.production.eslintrc.jsonREADME.mdVALIDATION_FLOW.mdVALIDATION_SUMMARY.mdapp/[...slug]/page.tsxapp/actions.tsapp/api/article/route.tsapp/api/direct/route.tsapp/api/jina/route.tsapp/api/proxy/route.tsapp/api/summary/route.tsapp/api/wayback.ts/route.tsapp/feedback/page.tsxapp/layout.tsxapp/page.tsxapp/proxy/error.tsxapp/proxy/layout.tsxapp/proxy/loading.tsxapp/proxy/page.tsxapp/read/page.tsxapp/robots.txtcomponents/arrow-tabs.tsxcomponents/article-content.tsxcomponents/article/content.tsxcomponents/article/length.tsxcomponents/article/tabs.tsxcomponents/bookmarklet.tsxcomponents/dashboard-wrapper.tsxcomponents/dashboard.tsxcomponents/email-template.tsxcomponents/features/proxy-content.tsxcomponents/features/responsive-drawer.tsxcomponents/features/share-button.tsxcomponents/features/summary-form.tsxcomponents/layout/nav.tsxcomponents/layout/scroll-progress.tsxcomponents/layout/site-footer.tsxcomponents/layout/top-bar.tsxcomponents/marketing/ad.tsxcomponents/marketing/banner.tsxcomponents/marketing/bookmarklet.tsxcomponents/marketing/card-spotlight.tsxcomponents/marketing/faq.tsxcomponents/marketing/github.tsxcomponents/shared/client-only.tsxcomponents/shared/debug-panel.tsxcomponents/shared/error-boundary.tsxcomponents/shared/error-display.tsxcomponents/shared/query-provider.tsxcomponents/shared/underline-link.tsxcomponents/ui/accordion.tsxcomponents/ui/button.tsxcomponents/ui/dialog.tsxcomponents/ui/drawer.tsxcomponents/ui/popover.tsxcomponents/ui/select.tsxcomponents/ui/shadcn-io/github-stars-button/index.tsxcomponents/ui/shadcn-io/sliding-number/index.tsxcomponents/ui/sheet.tsxeslint.config.mjslib/README.mdlib/api/client.tslib/api/diffbot.tslib/api/jina.tslib/data.tslib/errors/index.tslib/errors/safe-error.tslib/errors/types.tslib/fetch-with-timeout.tslib/format-error.tslib/hooks/use-articles.tslib/hooks/use-local-storage.tslib/hooks/use-media-query.tslib/hooks/use-scroll.tslib/hooks/use-summary.tslib/logger.tsnext.config.jspackage.jsontsconfig.jsontypes/api.tstypes/diffbot.d.tsvercel.json
💤 Files with no reviewable changes (17)
- .eslintrc.json
- app/actions.ts
- components/bookmarklet.tsx
- app/feedback/page.tsx
- .env.production
- components/article-content.tsx
- app/api/direct/route.ts
- components/dashboard-wrapper.tsx
- app/api/proxy/route.ts
- app/read/page.tsx
- components/arrow-tabs.tsx
- components/email-template.tsx
- lib/fetch-with-timeout.ts
- lib/format-error.ts
- app/api/wayback.ts/route.ts
- components/dashboard.tsx
- lib/data.ts
| const validationResult = ArticleRequestSchema.safeParse({ url, source }); | ||
|
|
||
| if (!validationResult.success) { | ||
| const error = fromError(validationResult.error); | ||
| const debugSmryUrl = url ? buildSmryUrl(url, source ?? "smry-fast") : undefined; | ||
| logger.error({ error: error.toString(), smryUrl: debugSmryUrl, url, source }, 'Validation error - Full URL for debugging'); | ||
| return NextResponse.json( | ||
| ErrorResponseSchema.parse({ | ||
| error: error.toString(), | ||
| type: "VALIDATION_ERROR", | ||
| }), | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const { url: validatedUrl, source: validatedSource } = validationResult.data; | ||
|
|
||
| // Construct the full smry.ai URL for debugging | ||
| const smryUrl = buildSmryUrl(validatedUrl, validatedSource); | ||
|
|
||
| // Jina.ai is handled by a separate endpoint (/api/jina) for client-side fetching | ||
| if (validatedSource === "jina.ai") { | ||
| logger.warn({ source: validatedSource, smryUrl }, 'Jina.ai source not supported in this endpoint'); | ||
| return NextResponse.json( | ||
| ErrorResponseSchema.parse({ | ||
| error: "Jina.ai source is handled client-side. Use /api/jina endpoint instead.", | ||
| type: "VALIDATION_ERROR", | ||
| }), | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| logger.info({ source: validatedSource, hostname: new URL(validatedUrl).hostname, smryUrl }, 'API Request'); | ||
|
|
||
| const urlWithSource = getUrlWithSource(validatedSource, validatedUrl); | ||
| const cacheKey = `${validatedSource}:${validatedUrl}`; |
There was a problem hiding this comment.
Block private/internal destinations before server-side fetch.
The URL validation path (Lines 317–352) only checks format, then smry-fast fetches it directly (Line 136). This allows SSRF against internal/metadata endpoints.
Please enforce outbound URL policy before any fetch: allow only http/https, resolve DNS, and reject loopback/link-local/private ranges (IPv4 + IPv6), localhost-style hostnames, and non-public targets.
Also applies to: 130-144
🤖 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 `@app/api/article/route.ts` around lines 317 - 352, The current validation
using ArticleRequestSchema.safeParse only checks URL format but does not
validate whether the URL targets a safe, public destination before server-side
fetching occurs. Add SSRF (Server-Side Request Forgery) protection after the
ArticleRequestSchema validation succeeds: validate that the URL scheme is http
or https, resolve the hostname to its IP address, and reject any URLs pointing
to loopback addresses (127.0.0.1, ::1), link-local ranges (169.254.0.0/16,
fe80::/10), private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
fc00::/7), or localhost-style hostnames. Return a 400 error with an appropriate
message if the URL fails these checks, ensuring this validation happens before
validatedUrl is used in getUrlWithSource or any subsequent fetch operations.
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const body = await request.json(); | ||
|
|
||
| const validationResult = JinaCacheUpdateSchema.safeParse(body); | ||
|
|
||
| if (!validationResult.success) { | ||
| const error = fromError(validationResult.error); | ||
| logger.error({ error: error.toString() }, 'Validation error'); | ||
| return NextResponse.json( | ||
| ErrorResponseSchema.parse({ | ||
| error: error.toString(), | ||
| type: "VALIDATION_ERROR", | ||
| }), | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| const { url, article } = validationResult.data; | ||
| const cacheKey = `jina.ai:${url}`; | ||
|
|
||
| logger.info({ hostname: new URL(url).hostname, length: article.length }, 'Updating Jina cache'); | ||
|
|
||
| try { | ||
| const existingArticle = await redis.get<z.infer<typeof CachedArticleSchema>>(cacheKey); | ||
|
|
||
| const validatedExisting = existingArticle | ||
| ? CachedArticleSchema.parse(existingArticle) | ||
| : null; | ||
|
|
||
| // Only update if new article is longer or doesn't exist | ||
| if (!validatedExisting || article.length > validatedExisting.length) { | ||
| await redis.set(cacheKey, article); | ||
| logger.info({ hostname: new URL(url).hostname, length: article.length }, 'Jina cache updated'); | ||
|
|
||
| const response = ArticleResponseSchema.parse({ | ||
| source: "jina.ai", | ||
| cacheURL: `https://r.jina.ai/${url}`, | ||
| article: { | ||
| ...article, | ||
| byline: "", | ||
| dir: "", | ||
| lang: "", | ||
| }, | ||
| status: "success", | ||
| }); | ||
|
|
||
| return NextResponse.json(response); | ||
| } else { | ||
| logger.debug({ hostname: new URL(url).hostname, existingLength: validatedExisting.length, newLength: article.length }, 'Keeping existing Jina cache'); | ||
|
|
||
| const response = ArticleResponseSchema.parse({ | ||
| source: "jina.ai", | ||
| cacheURL: `https://r.jina.ai/${url}`, | ||
| article: { | ||
| ...validatedExisting, | ||
| byline: "", | ||
| dir: "", | ||
| lang: "", | ||
| }, | ||
| status: "success", | ||
| }); | ||
|
|
||
| return NextResponse.json(response); | ||
| } | ||
| } catch (error) { | ||
| logger.warn({ error: error instanceof Error ? error.message : String(error) }, 'Jina cache update error'); | ||
|
|
||
| // Return the article even if caching fails | ||
| const response = ArticleResponseSchema.parse({ | ||
| source: "jina.ai", | ||
| cacheURL: `https://r.jina.ai/${url}`, | ||
| article: { | ||
| ...article, | ||
| byline: "", | ||
| dir: "", | ||
| lang: "", | ||
| }, | ||
| status: "success", | ||
| }); | ||
|
|
||
| return NextResponse.json(response); | ||
| } |
There was a problem hiding this comment.
Lock down POST /api/jina to prevent cache poisoning (and downstream XSS risk).
This endpoint accepts unauthenticated user-controlled article payloads and persists them globally. Since cached content is later rendered as HTML in the UI flow, an attacker can poison cache entries for shared URLs.
🤖 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 `@app/api/jina/route.ts` around lines 111 - 193, The POST function in route.ts
currently accepts unauthenticated requests and directly caches user-provided
article content that can later be rendered as HTML, creating a cache poisoning
and XSS vulnerability. Add authentication and authorization checks at the
beginning of the POST function before accepting or processing the request body,
ensuring only authenticated and authorized users can invoke the cache update
functionality. Verify the user's identity and permissions before proceeding with
the validation and cache operations.
| const { content, title, url, ip, language } = validationResult.data; | ||
| const clientIp = ip || request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for") || "unknown"; | ||
|
|
There was a problem hiding this comment.
Do not trust ip from request body for rate limiting.
Line 112 lets callers choose clientIp via body input, so rate limits are trivially bypassed by rotating arbitrary ip values.
Use only trusted proxy/network headers (or platform-provided request IP), and ignore body-provided IP for enforcement.
🤖 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 `@app/api/summary/route.ts` around lines 111 - 113, The clientIp assignment on
line 112 trusts the ip value from the request body (validationResult.data),
which allows attackers to bypass rate limiting by sending arbitrary ip values.
Remove the ip parameter from the rate limiting logic and only use trusted proxy
headers (x-real-ip, x-forwarded-for) or the platform-provided request IP. The
validation schema may still accept the ip parameter for other purposes, but it
should not be used for determining the client IP in the clientIp variable.
| <div | ||
| className="overflow-wrap mt-10 max-w-full break-words" | ||
| dangerouslySetInnerHTML={{ __html: content.article.content }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n --type=ts --type=tsx -C3 'dangerouslySetInnerHTML|article\.content|DOMPurify|sanitize-html|sanitize\('Repository: caffo/SMRY
Length of output: 82
🏁 Script executed:
#!/bin/bash
# First, locate the content.tsx file
find . -name "content.tsx" -path "*/article/*" 2>/dev/nullRepository: caffo/SMRY
Length of output: 87
🏁 Script executed:
#!/bin/bash
# Try a broader search for the file
find . -name "content.tsx" 2>/dev/null | head -20Repository: caffo/SMRY
Length of output: 87
🏁 Script executed:
#!/bin/bash
# Search for dangerouslySetInnerHTML in the codebase with simpler approach
rg -n "dangerouslySetInnerHTML" 2>/dev/nullRepository: caffo/SMRY
Length of output: 163
🏁 Script executed:
#!/bin/bash
# Read the content.tsx file to examine the code context
cat -n ./components/article/content.tsxRepository: caffo/SMRY
Length of output: 10665
🏁 Script executed:
#!/bin/bash
# Check package.json for available security/sanitization libraries
grep -E "dompurify|sanitize|xss" package.jsonRepository: caffo/SMRY
Length of output: 36
🏁 Script executed:
#!/bin/bash
# Check the ArticleResponse type definition
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "ArticleResponse" | head -5Repository: caffo/SMRY
Length of output: 200
🏁 Script executed:
#!/bin/bash
# Search for all dangerouslySetInnerHTML uses in the codebase
rg "dangerouslySetInnerHTML" -A2 -B2Repository: caffo/SMRY
Length of output: 392
🏁 Script executed:
#!/bin/bash
# Find and read the types/api file
find . -path "*/types/api*" -type f 2>/dev/nullRepository: caffo/SMRY
Length of output: 69
🏁 Script executed:
#!/bin/bash
# Check the API route that returns article content
cat -n ./app/api/article/route.ts | head -100Repository: caffo/SMRY
Length of output: 3982
🏁 Script executed:
#!/bin/bash
# Continue reading the API route
cat -n ./app/api/article/route.ts | sed -n '100,200p'Repository: caffo/SMRY
Length of output: 4386
🏁 Script executed:
#!/bin/bash
# Read types/api.ts to see ArticleResponse type
cat -n ./types/api.tsRepository: caffo/SMRY
Length of output: 4289
🏁 Script executed:
#!/bin/bash
# Read package.json to see dependencies
cat package.json | head -50Repository: caffo/SMRY
Length of output: 1560
Unsanitized HTML injection enables XSS vulnerability.
Line 238 injects external article content directly into the DOM without sanitization. Article content originates from third-party web pages extracted via Mozilla Readability, which preserves all HTML markup including potentially malicious scripts. No sanitization libraries (DOMPurify, sanitize-html) are present in project dependencies, and no sanitization occurs at any point in the pipeline: fetch → Readability extraction → Redis cache → API response → client rendering.
Suggested fix
import React from "react";
+import DOMPurify from "dompurify";
@@
const content = data;
+ const sanitizedArticleHtml = React.useMemo(
+ () => DOMPurify.sanitize(content.article?.content ?? ""),
+ [content.article?.content]
+ );
@@
- <div
- className="overflow-wrap mt-10 max-w-full break-words"
- dangerouslySetInnerHTML={{ __html: content.article.content }}
- />
+ <div
+ className="overflow-wrap mt-10 max-w-full break-words"
+ dangerouslySetInnerHTML={{ __html: sanitizedArticleHtml }}
+ />Note: Add DOMPurify to dependencies: npm install dompurify and npm install -D @types/dompurify``
📝 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.
| <div | |
| className="overflow-wrap mt-10 max-w-full break-words" | |
| dangerouslySetInnerHTML={{ __html: content.article.content }} | |
| /> | |
| import React from "react"; | |
| import DOMPurify from "dompurify"; | |
| // ... other imports and code ... | |
| export default function YourComponent() { | |
| // ... other code ... | |
| const content = data; | |
| const sanitizedArticleHtml = React.useMemo( | |
| () => DOMPurify.sanitize(content.article?.content ?? ""), | |
| [content.article?.content] | |
| ); | |
| // ... other code ... | |
| return ( | |
| <div> | |
| {/* ... other JSX ... */} | |
| <div | |
| className="overflow-wrap mt-10 max-w-full break-words" | |
| dangerouslySetInnerHTML={{ __html: sanitizedArticleHtml }} | |
| /> | |
| {/* ... other JSX ... */} | |
| </div> | |
| ); | |
| } |
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] 237-237: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation
(react-unsafe-html-injection)
🪛 React Doctor (0.5.6)
[error] 238-238: dangerouslySetInnerHTML is an XSS hole that runs attacker-controlled HTML in your users' browsers.
Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.
(no-danger)
🤖 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 `@components/article/content.tsx` around lines 236 - 239, The div element using
dangerouslySetInnerHTML with content.article.content is vulnerable to XSS
attacks because the HTML from third-party sources (extracted via Mozilla
Readability) is injected without sanitization. Install DOMPurify as a
dependency, import it at the top of the content.tsx file, and then wrap the
content.article.content value with DOMPurify.sanitize() before passing it to the
dangerouslySetInnerHTML prop to remove any potentially malicious scripts or
harmful HTML elements.
Source: Linters/SAST tools
| const contentHtml = converter.makeHtml(mainContent); | ||
|
|
||
| const article: JinaArticle = { | ||
| title: title, | ||
| content: contentHtml, | ||
| textContent: mainContent, | ||
| length: mainContent.length, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check if the files exist and examine the code
find . -type f -name "jina.ts" | head -5
find . -type f -name "content.tsx" | head -5Repository: caffo/SMRY
Length of output: 105
🏁 Script executed:
# Once we confirm file locations, examine the jina.ts file around lines 84-90
if [ -f "lib/api/jina.ts" ]; then
echo "=== lib/api/jina.ts lines 80-95 ==="
sed -n '80,95p' lib/api/jina.ts
fiRepository: caffo/SMRY
Length of output: 544
🏁 Script executed:
# Check components/article/content.tsx for dangerouslySetInnerHTML usage
if [ -f "components/article/content.tsx" ]; then
echo "=== Checking components/article/content.tsx ==="
cat -n components/article/content.tsx | grep -A 5 -B 5 "dangerouslySetInnerHTML\|contentHtml\|content"
fiRepository: caffo/SMRY
Length of output: 3884
🏁 Script executed:
#!/bin/bash
# Check imports and full context of jina.ts
head -20 lib/api/jina.ts
echo "---"
# Check if DOMPurify or any sanitization is already imported
grep -n "import\|DOMPurify\|sanitize" lib/api/jina.tsRepository: caffo/SMRY
Length of output: 392
🏁 Script executed:
# Check package.json to see if dompurify is available
if [ -f "package.json" ]; then
grep -A 5 -B 5 "dompurify" package.json
fiRepository: caffo/SMRY
Length of output: 36
🏁 Script executed:
# Check the full package.json for dependencies
if [ -f "package.json" ]; then
cat package.json | jq '.dependencies' 2>/dev/null || cat package.json | grep -A 30 '"dependencies"'
fiRepository: caffo/SMRY
Length of output: 1781
Sanitize converted HTML before returning it to the UI.
Line 84 converts untrusted remote markdown to HTML without sanitization, and line 88 returns it unsanitized in the article object. This HTML is later rendered with dangerouslySetInnerHTML (components/article/content.tsx:238), creating an XSS vulnerability.
The proposed fix using DOMPurify is appropriate, but note that dompurify is not currently in the project's dependencies and will need to be added via npm install dompurify.
Proposed fix
import showdown from "showdown";
+import DOMPurify from "dompurify";
@@
- const contentHtml = converter.makeHtml(mainContent);
+ const contentHtml = DOMPurify.sanitize(
+ converter.makeHtml(mainContent),
+ { USE_PROFILES: { html: true } }
+ );🤖 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 `@lib/api/jina.ts` around lines 84 - 90, The HTML content generated by
converter.makeHtml() from untrusted remote markdown is not being sanitized
before being assigned to contentHtml and returned in the JinaArticle object's
content property, creating an XSS vulnerability since it's rendered with
dangerouslySetInnerHTML. Import DOMPurify (install it via npm install dompurify
if not already present), then sanitize the contentHtml by passing it through
DOMPurify.sanitize() before assigning it to the content property of the article
object. This ensures malicious scripts are removed from the converted HTML
before it reaches the UI.
| const mutation = useMutation<SummaryResponse, Error, SummaryParams>({ | ||
| mutationFn: generateSummary, | ||
| }); | ||
|
|
||
| return { | ||
| regenerate: mutation.mutate, | ||
| isRegenerating: mutation.isPending, | ||
| error: mutation.error, | ||
| preferredLanguage, | ||
| setPreferredLanguage, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/hooks/use-summary.tsRepository: caffo/SMRY
Length of output: 2605
🏁 Script executed:
cat -n components/features/summary-form.tsxRepository: caffo/SMRY
Length of output: 10498
Manual regenerate does not update the rendered summary state.
useRegenerateSummary fires the mutation, but it never updates/invalidates the ["summary", url, language, source] query cache. The UI reads exclusively from useAutoSummary (line 71 of summary-form.tsx), so regeneration can complete without visible change.
Suggested fix
-import { useQuery, useMutation } from "`@tanstack/react-query`";
+import { useQuery, useMutation, useQueryClient } from "`@tanstack/react-query`";
@@
export function useRegenerateSummary() {
+ const queryClient = useQueryClient();
@@
const mutation = useMutation<SummaryResponse, Error, SummaryParams>({
mutationFn: generateSummary,
+ onSuccess: (data, variables) => {
+ queryClient.setQueryData(
+ ["summary", variables.url, variables.language, variables.source],
+ data
+ );
+ },
});📝 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.
| const mutation = useMutation<SummaryResponse, Error, SummaryParams>({ | |
| mutationFn: generateSummary, | |
| }); | |
| return { | |
| regenerate: mutation.mutate, | |
| isRegenerating: mutation.isPending, | |
| error: mutation.error, | |
| preferredLanguage, | |
| setPreferredLanguage, | |
| }; | |
| export function useRegenerateSummary() { | |
| const queryClient = useQueryClient(); | |
| const mutation = useMutation<SummaryResponse, Error, SummaryParams>({ | |
| mutationFn: generateSummary, | |
| onSuccess: (data, variables) => { | |
| queryClient.setQueryData( | |
| ["summary", variables.url, variables.language, variables.source], | |
| data | |
| ); | |
| }, | |
| }); | |
| return { | |
| regenerate: mutation.mutate, | |
| isRegenerating: mutation.isPending, | |
| error: mutation.error, | |
| preferredLanguage, | |
| setPreferredLanguage, | |
| }; | |
| } |
🤖 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 `@lib/hooks/use-summary.ts` around lines 69 - 79, The useMutation in
useRegenerateSummary needs to invalidate the query cache after successful
regeneration so that useAutoSummary reads the updated summary data. Import
useQueryClient from React Query, then add an onSuccess callback to the
useMutation configuration that calls invalidateQueries with the query key
["summary", url, language, source] to force a refetch of the summary data after
the generateSummary mutation completes successfully.
Security Fix: CVE-2026-23864 / GHSA-h25m-26qc-wcjf
Vulnerability: Next.js HTTP request deserialization can lead to DoS when using insecure React Server Components. A specially crafted HTTP request to any App Router Server Function endpoint can trigger excessive CPU usage and memory exhaustion.
Affected: next 16.0.0 (range: 16.0.0-beta.0 – 16.0.11)
Fixed in: next 16.0.11
Reachability Analysis
SMRY is vulnerable:
Changes
Verification
Build fails both before and after the change due to pre-existing Turbopack issues (thread-stream test files picked up by bundler — unrelated to this fix). The version bump is a patch-level upgrade with no API changes.
References