From eb3234d6eaaa21c1d0d26071ec51360dad5f0a0c Mon Sep 17 00:00:00 2001 From: thakurakanksha288 Date: Sun, 7 Jun 2026 10:00:29 +0530 Subject: [PATCH 1/5] feat: resolve runtime compilation errors and optimize SWR cache headers --- app/api/streak/route.ts | 223 +++++++++++----------------------------- 1 file changed, 58 insertions(+), 165 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 905f21898..90b6a9534 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -1,6 +1,5 @@ // app/api/streak/route.ts -import crypto from 'crypto'; import { NextResponse } from 'next/server'; import { fetchGitHubContributions, getOrgDashboardData } from '@/lib/github'; import { calculateStreak, calculateMonthlyStats, aggregateCalendars } from '@/lib/calculate'; @@ -17,7 +16,6 @@ import { import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time'; import type { BadgeParams, - ContributionCalendar, RepoContribution, ExtendedContributionData, } from '@/types'; @@ -43,24 +41,27 @@ function getMonthlyReferenceDate(year: string | undefined, timezone: string): Da return selectedYear < currentYear ? new Date(`${year}-12-15T12:00:00Z`) : undefined; } +async function generateETag(content: string): Promise { + const msgUint8 = new TextEncoder().encode(content); + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} + export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const parseResult = streakParamsSchema.safeParse(Object.fromEntries(searchParams.entries())); + try { if (!parseResult.success) { const fieldErrors = parseResult.error.flatten(); - return NextResponse.json( - { - error: 'Invalid parameters', - details: fieldErrors, - }, + { error: 'Invalid parameters', details: fieldErrors }, { status: 400, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', + 'Cache-Control': 'no-store, no-cache, must-revalidate', }, } ); @@ -110,14 +111,14 @@ export async function GET(request: Request) { badges, entrance, } = parseResult.data; + const normalizedView = view as 'default' | 'monthly' | 'heatmap' | 'pulse' | 'languages'; const themeName = theme || 'dark'; let timezone = 'UTC'; if (tzParam) { try { - timezone = new Intl.DateTimeFormat(undefined, { timeZone: tzParam }).resolvedOptions() - .timeZone; + timezone = new Intl.DateTimeFormat(undefined, { timeZone: tzParam }).resolvedOptions().timeZone; } catch (error) { if (error instanceof RangeError) { const validationErr = new Error(`Invalid timezone: ${tzParam}`); @@ -141,9 +142,7 @@ export async function GET(request: Request) { if (normalizedView === 'monthly') { const referenceDate = getMonthlyReferenceDate(year, timezone) || new Date(); - const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format( - referenceDate - ); + const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(referenceDate); const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); const currentYearNum = parseInt(currentYearStr, 10); const currentMonthNum = parseInt(currentMonthStr, 10); @@ -182,7 +181,6 @@ export async function GET(request: Request) { return themes[theme] || themes.dark; })(); - // If 'org' is provided, we use it as the display user const targetEntity = org || (user.includes(',') @@ -192,9 +190,11 @@ export async function GET(request: Request) { .slice(0, 2) .join(' + ') : user); + const borderParam = searchParams.get('border'); const sanitizedBorder = borderParam ? borderParam.replace(/[^a-fA-F0-9]/g, '') : undefined; const animate = searchParams.get('animate') !== 'false'; + const params: BadgeParams = { user: targetEntity, bg: isAutoTheme ? selectedTheme.bg : bg || selectedTheme.bg, @@ -215,12 +215,7 @@ export async function GET(request: Request) { width, height, size, - - grace: Math.max( - 0, - Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10)) - ), - + grace: Math.max(0, Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10))), mode, repo, org, @@ -231,12 +226,7 @@ export async function GET(request: Request) { gradient, gradient_stops, gradient_dir, - - opacity: Math.max( - 0.1, - Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0))) - ), - + opacity: Math.max(0.1, Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0)))), disable_particles, glow, animate, @@ -249,25 +239,14 @@ export async function GET(request: Request) { let versusCalendar; let repoContributions: RepoContribution[] = []; - // Fetch Organization Mega-City Data OR Single User Data if (org) { - const orgData = await getOrgDashboardData(org, { - bypassCache: refresh, - from, - to, - }); + const orgData = await getOrgDashboardData(org, { bypassCache: refresh, from, to }); calendar = orgData.calendar; repoContributions = normalizedView === 'languages' ? orgData.repoContributions || [] : []; } else if (user.includes(',')) { - const users = user - .split(',') - .map((u) => u.trim()) - .filter(Boolean); - + const users = user.split(',').map((u) => u.trim()).filter(Boolean); if (users.length > 2) { - throw new Error( - 'ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.' - ); + throw new Error('ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.'); } let lastError: unknown = null; @@ -275,14 +254,8 @@ export async function GET(request: Request) { const fetchedCalendars = await Promise.all( users.map(async (u) => { try { - const userData = await fetchGitHubContributions(u, { - bypassCache: refresh, - from, - to, - }); - if (userData.isOfflineFallback) { - hasOfflineFallback = true; - } + const userData = await fetchGitHubContributions(u, { bypassCache: refresh, from, to }); + if (userData.isOfflineFallback) hasOfflineFallback = true; return userData; } catch (err) { lastError = err; @@ -290,100 +263,62 @@ export async function GET(request: Request) { } }) ); - const successfulData = fetchedCalendars.filter( - (d): d is ExtendedContributionData => d !== null - ); - if (successfulData.length === 0) { - throw lastError || new Error('No successful data fetched'); - } + + const successfulData = fetchedCalendars.filter((d): d is ExtendedContributionData => d !== null); + if (successfulData.length === 0) throw lastError || new Error('No successful data fetched'); + calendar = aggregateCalendars(successfulData.map((d) => d.calendar)); - repoContributions = - normalizedView === 'languages' - ? successfulData.flatMap((d) => d.repoContributions || []) - : []; - if (hasOfflineFallback) { - params.isOfflineFallback = true; - } + repoContributions = normalizedView === 'languages' ? successfulData.flatMap((d) => d.repoContributions || []) : []; + if (hasOfflineFallback) params.isOfflineFallback = true; } else { - const userData = await fetchGitHubContributions(user, { - bypassCache: refresh, - from, - to, - }); + const userData = await fetchGitHubContributions(user, { bypassCache: refresh, from, to }); calendar = userData.calendar; repoContributions = normalizedView === 'languages' ? userData.repoContributions || [] : []; - if (userData.isOfflineFallback) { - params.isOfflineFallback = true; - } + if (userData.isOfflineFallback) params.isOfflineFallback = true; if (versus) { - const versusData = await fetchGitHubContributions(versus, { - bypassCache: refresh, - from, - to, - }); + const versusData = await fetchGitHubContributions(versus, { bypassCache: refresh, from, to }); versusCalendar = versusData.calendar; - if (versusData.isOfflineFallback) { - params.isOfflineFallback = true; - } + if (versusData.isOfflineFallback) params.isOfflineFallback = true; } } if (days && normalizedView !== 'monthly') { const allDays = calendar.weeks.flatMap((w) => w.contributionDays); - const filteredDays = allDays.slice(-days); - calendar = { totalContributions: filteredDays.reduce((sum, d) => sum + d.contributionCount, 0), - weeks: [ - { - contributionDays: filteredDays, - }, - ], + weeks: [{ contributionDays: filteredDays }], }; } + const secondsToMidnight = tzParam ? getSecondsUntilMidnightInTimezone(timezone) : getSecondsUntilUTCMidnight(); + const cacheControl = refresh + ? 'no-store, no-cache, must-revalidate, proxy-revalidate' + : isHistoricalYear + ? 'public, s-maxage=31536000, immutable' + : `public, max-age=3600, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400, must-revalidate`; + // ─── JSON output mode ────────────────────────────────────────────────── if (format === 'json') { const stats = calculateStreak(calendar, timezone, undefined, grace); - const monthlyStats = calculateMonthlyStats( - calendar, - timezone, - getMonthlyReferenceDate(year, timezone) - ); - - const secondsToMidnight = tzParam - ? getSecondsUntilMidnightInTimezone(timezone) - : getSecondsUntilUTCMidnight(); - const cacheControl = refresh - ? 'no-cache, no-store, must-revalidate' - : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; + const monthlyStats = calculateMonthlyStats(calendar, timezone, getMonthlyReferenceDate(year, timezone)); const jsonPayload = JSON.stringify({ user: targetEntity, stats, monthlyStats, - calendar: { - totalContributions: calendar.totalContributions, - weeks: calendar.weeks, - }, + calendar: { totalContributions: calendar.totalContributions, weeks: calendar.weeks }, }); - const etag = crypto.createHash('sha1').update(jsonPayload).digest('hex'); + const etag = await generateETag(jsonPayload); const weakEtag = `W/"${etag}"`; const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch) { const etags = ifNoneMatch.split(',').map((e) => e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { - return new NextResponse(null, { - status: 304, - headers: { - 'Cache-Control': cacheControl, - ETag: weakEtag, - }, - }); + return new NextResponse(null, { status: 304, headers: { 'Cache-Control': cacheControl, ETag: weakEtag } }); } } @@ -400,11 +335,7 @@ export async function GET(request: Request) { // ─── SVG output mode (default) ────────────────────────────────────────── let svg = ''; if (normalizedView === 'monthly') { - const stats = calculateMonthlyStats( - calendar, - timezone, - getMonthlyReferenceDate(year, timezone) - ); + const stats = calculateMonthlyStats(calendar, timezone, getMonthlyReferenceDate(year, timezone)); svg = generateMonthlySVG(stats, params); } else if (normalizedView === 'languages') { const stats = calculateStreak(calendar, timezone, undefined, grace); @@ -413,8 +344,6 @@ export async function GET(request: Request) { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateHeatmapSVG(stats, params, calendar); } else if (normalizedView === 'pulse') { - // We still use calculateStreak here to efficiently parse totalContributions for the stat display, - // even though the sparkline generator will extract its own daily 30-day timeline below. const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generatePulseSVG(stats, params, calendar); } else if (versus && versusCalendar) { @@ -426,29 +355,14 @@ export async function GET(request: Request) { svg = generateSVG(stats, params, calendar); } - const secondsToMidnight = tzParam - ? getSecondsUntilMidnightInTimezone(timezone) - : getSecondsUntilUTCMidnight(); - const cacheControl = refresh - ? 'no-cache, no-store, must-revalidate' - : isHistoricalYear - ? 'public, s-maxage=31536000, immutable' - : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; - - const etag = crypto.createHash('sha1').update(svg).digest('hex'); + const etag = await generateETag(svg); const weakEtag = `W/"${etag}"`; const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch) { const etags = ifNoneMatch.split(',').map((e) => e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { - return new NextResponse(null, { - status: 304, - headers: { - 'Cache-Control': cacheControl, - ETag: weakEtag, - }, - }); + return new NextResponse(null, { status: 304, headers: { 'Cache-Control': cacheControl, ETag: weakEtag } }); } } @@ -470,45 +384,31 @@ type ParseResult = ReturnType; function buildErrorResponse(error: unknown, parseResult: ParseResult): NextResponse { const message = error instanceof Error ? error.message : String(error); + function buildInlineErrorSVG(text: string): string { const MAX_LINE = 48; const truncated = text.length > MAX_LINE * 2 ? text.slice(0, MAX_LINE * 2 - 1) + '…' : text; - const line1 = escapeSVGText(truncated.slice(0, MAX_LINE)); const line2 = truncated.length > MAX_LINE ? escapeSVGText(truncated.slice(MAX_LINE)) : null; - const textY = line2 ? '62' : '75'; return ` ${line1}${ - line2 - ? ` - ${line2}` - : '' + line2 ? `${line2}` : '' } `; } - const isNotFound = - message.toLowerCase().includes('not found') || - message.toLowerCase().includes('could not resolve'); + const isNotFound = message.toLowerCase().includes('not found') || message.toLowerCase().includes('could not resolve'); const isRateLimit = message.toLowerCase().includes('rate limit'); - - // 2. Safely detect if the error was a validation/client error - const isValidationError = - (error instanceof Error && error.name === 'ValidationError') || + const isValidationError = (error instanceof Error && error.name === 'ValidationError') || message.toLowerCase().includes('invalid') || message.toLowerCase().includes('validation') || message.toLowerCase().includes('strictly for organizations'); const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`; - const errAccentRaw = - (parseResult.success && - (Array.isArray(parseResult.data.accent) - ? parseResult.data.accent[parseResult.data.accent.length - 1] - : parseResult.data.accent)) || - undefined; + const errAccentRaw = (parseResult.success && (Array.isArray(parseResult.data.accent) ? parseResult.data.accent[parseResult.data.accent.length - 1] : parseResult.data.accent)) || undefined; const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`; const errText = `#${sanitizeHexColor(parseResult.success ? parseResult.data.text : undefined, 'c9d1d9')}`; const errRadius = sanitizeRadius(parseResult.success ? parseResult.data.radius : undefined, 8); @@ -520,7 +420,7 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo status: 429, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); @@ -528,9 +428,7 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo if (isNotFound) { const match = message.match(/"([^"]+)"|login of '([^']+)'/); - const fallbackTarget = parseResult.success - ? parseResult.data.org || parseResult.data.user - : 'unknown'; + const fallbackTarget = parseResult.success ? parseResult.data.org || parseResult.data.user : 'unknown'; const badUsername = match?.[1] ?? match?.[2] ?? fallbackTarget; const svg = generateNotFoundSVG(badUsername, errBg, errAccent, errText, errRadius, errSpeed); @@ -538,37 +436,32 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo status: 404, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache', + 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=7200, must-revalidate', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); } - // 3. Return a 400 Bad Request for Validation Errors if (isValidationError) { const validationSvg = buildInlineErrorSVG(message); - return new NextResponse(validationSvg, { status: 400, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store', + 'Cache-Control': 'no-store, no-cache, must-revalidate', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); } - // 4. Return a 500 Internal Server Error for real crashes console.error('[streak] Unhandled error:', message); - const errorSvg = buildInlineErrorSVG('Something went wrong. Please try again later.'); - return new NextResponse(errorSvg, { status: 500, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store', + 'Cache-Control': 'no-store, no-cache, must-revalidate', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); -} +} \ No newline at end of file From db5df252f6343968d6514b3d70414436cdfca481 Mon Sep 17 00:00:00 2001 From: thakurakanksha288 Date: Sun, 7 Jun 2026 10:15:59 +0530 Subject: [PATCH 2/5] style: conform streak route to project prettier rules --- app/api/streak/route.ts | 101 +++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 90b6a9534..315f3692b 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -14,11 +14,7 @@ import { generateLanguagesSVG, } from '@/lib/svg/generator'; import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time'; -import type { - BadgeParams, - RepoContribution, - ExtendedContributionData, -} from '@/types'; +import type { BadgeParams, RepoContribution, ExtendedContributionData } from '@/types'; import { themes } from '@/lib/svg/themes'; import { streakParamsSchema } from '@/lib/validations'; import { sanitizeHexColor, sanitizeRadius } from '@/lib/svg/sanitizer'; @@ -45,7 +41,7 @@ async function generateETag(content: string): Promise { const msgUint8 = new TextEncoder().encode(content); const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } export async function GET(request: Request) { @@ -118,7 +114,8 @@ export async function GET(request: Request) { let timezone = 'UTC'; if (tzParam) { try { - timezone = new Intl.DateTimeFormat(undefined, { timeZone: tzParam }).resolvedOptions().timeZone; + timezone = new Intl.DateTimeFormat(undefined, { timeZone: tzParam }).resolvedOptions() + .timeZone; } catch (error) { if (error instanceof RangeError) { const validationErr = new Error(`Invalid timezone: ${tzParam}`); @@ -142,7 +139,9 @@ export async function GET(request: Request) { if (normalizedView === 'monthly') { const referenceDate = getMonthlyReferenceDate(year, timezone) || new Date(); - const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(referenceDate); + const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format( + referenceDate + ); const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); const currentYearNum = parseInt(currentYearStr, 10); const currentMonthNum = parseInt(currentMonthStr, 10); @@ -215,7 +214,10 @@ export async function GET(request: Request) { width, height, size, - grace: Math.max(0, Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10))), + grace: Math.max( + 0, + Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10)) + ), mode, repo, org, @@ -226,7 +228,10 @@ export async function GET(request: Request) { gradient, gradient_stops, gradient_dir, - opacity: Math.max(0.1, Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0)))), + opacity: Math.max( + 0.1, + Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0))) + ), disable_particles, glow, animate, @@ -244,9 +249,14 @@ export async function GET(request: Request) { calendar = orgData.calendar; repoContributions = normalizedView === 'languages' ? orgData.repoContributions || [] : []; } else if (user.includes(',')) { - const users = user.split(',').map((u) => u.trim()).filter(Boolean); + const users = user + .split(',') + .map((u) => u.trim()) + .filter(Boolean); if (users.length > 2) { - throw new Error('ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.'); + throw new Error( + 'ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.' + ); } let lastError: unknown = null; @@ -264,11 +274,16 @@ export async function GET(request: Request) { }) ); - const successfulData = fetchedCalendars.filter((d): d is ExtendedContributionData => d !== null); + const successfulData = fetchedCalendars.filter( + (d): d is ExtendedContributionData => d !== null + ); if (successfulData.length === 0) throw lastError || new Error('No successful data fetched'); calendar = aggregateCalendars(successfulData.map((d) => d.calendar)); - repoContributions = normalizedView === 'languages' ? successfulData.flatMap((d) => d.repoContributions || []) : []; + repoContributions = + normalizedView === 'languages' + ? successfulData.flatMap((d) => d.repoContributions || []) + : []; if (hasOfflineFallback) params.isOfflineFallback = true; } else { const userData = await fetchGitHubContributions(user, { bypassCache: refresh, from, to }); @@ -277,7 +292,11 @@ export async function GET(request: Request) { if (userData.isOfflineFallback) params.isOfflineFallback = true; if (versus) { - const versusData = await fetchGitHubContributions(versus, { bypassCache: refresh, from, to }); + const versusData = await fetchGitHubContributions(versus, { + bypassCache: refresh, + from, + to, + }); versusCalendar = versusData.calendar; if (versusData.isOfflineFallback) params.isOfflineFallback = true; } @@ -292,7 +311,9 @@ export async function GET(request: Request) { }; } - const secondsToMidnight = tzParam ? getSecondsUntilMidnightInTimezone(timezone) : getSecondsUntilUTCMidnight(); + const secondsToMidnight = tzParam + ? getSecondsUntilMidnightInTimezone(timezone) + : getSecondsUntilUTCMidnight(); const cacheControl = refresh ? 'no-store, no-cache, must-revalidate, proxy-revalidate' : isHistoricalYear @@ -302,7 +323,11 @@ export async function GET(request: Request) { // ─── JSON output mode ────────────────────────────────────────────────── if (format === 'json') { const stats = calculateStreak(calendar, timezone, undefined, grace); - const monthlyStats = calculateMonthlyStats(calendar, timezone, getMonthlyReferenceDate(year, timezone)); + const monthlyStats = calculateMonthlyStats( + calendar, + timezone, + getMonthlyReferenceDate(year, timezone) + ); const jsonPayload = JSON.stringify({ user: targetEntity, @@ -318,7 +343,10 @@ export async function GET(request: Request) { if (ifNoneMatch) { const etags = ifNoneMatch.split(',').map((e) => e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { - return new NextResponse(null, { status: 304, headers: { 'Cache-Control': cacheControl, ETag: weakEtag } }); + return new NextResponse(null, { + status: 304, + headers: { 'Cache-Control': cacheControl, ETag: weakEtag }, + }); } } @@ -335,7 +363,11 @@ export async function GET(request: Request) { // ─── SVG output mode (default) ────────────────────────────────────────── let svg = ''; if (normalizedView === 'monthly') { - const stats = calculateMonthlyStats(calendar, timezone, getMonthlyReferenceDate(year, timezone)); + const stats = calculateMonthlyStats( + calendar, + timezone, + getMonthlyReferenceDate(year, timezone) + ); svg = generateMonthlySVG(stats, params); } else if (normalizedView === 'languages') { const stats = calculateStreak(calendar, timezone, undefined, grace); @@ -362,7 +394,10 @@ export async function GET(request: Request) { if (ifNoneMatch) { const etags = ifNoneMatch.split(',').map((e) => e.trim()); if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { - return new NextResponse(null, { status: 304, headers: { 'Cache-Control': cacheControl, ETag: weakEtag } }); + return new NextResponse(null, { + status: 304, + headers: { 'Cache-Control': cacheControl, ETag: weakEtag }, + }); } } @@ -384,7 +419,7 @@ type ParseResult = ReturnType; function buildErrorResponse(error: unknown, parseResult: ParseResult): NextResponse { const message = error instanceof Error ? error.message : String(error); - + function buildInlineErrorSVG(text: string): string { const MAX_LINE = 48; const truncated = text.length > MAX_LINE * 2 ? text.slice(0, MAX_LINE * 2 - 1) + '…' : text; @@ -395,20 +430,30 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo return ` ${line1}${ - line2 ? `${line2}` : '' + line2 + ? `${line2}` + : '' } `; } - const isNotFound = message.toLowerCase().includes('not found') || message.toLowerCase().includes('could not resolve'); + const isNotFound = + message.toLowerCase().includes('not found') || + message.toLowerCase().includes('could not resolve'); const isRateLimit = message.toLowerCase().includes('rate limit'); - const isValidationError = (error instanceof Error && error.name === 'ValidationError') || + const isValidationError = + (error instanceof Error && error.name === 'ValidationError') || message.toLowerCase().includes('invalid') || message.toLowerCase().includes('validation') || message.toLowerCase().includes('strictly for organizations'); const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`; - const errAccentRaw = (parseResult.success && (Array.isArray(parseResult.data.accent) ? parseResult.data.accent[parseResult.data.accent.length - 1] : parseResult.data.accent)) || undefined; + const errAccentRaw = + (parseResult.success && + (Array.isArray(parseResult.data.accent) + ? parseResult.data.accent[parseResult.data.accent.length - 1] + : parseResult.data.accent)) || + undefined; const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`; const errText = `#${sanitizeHexColor(parseResult.success ? parseResult.data.text : undefined, 'c9d1d9')}`; const errRadius = sanitizeRadius(parseResult.success ? parseResult.data.radius : undefined, 8); @@ -428,7 +473,9 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo if (isNotFound) { const match = message.match(/"([^"]+)"|login of '([^']+)'/); - const fallbackTarget = parseResult.success ? parseResult.data.org || parseResult.data.user : 'unknown'; + const fallbackTarget = parseResult.success + ? parseResult.data.org || parseResult.data.user + : 'unknown'; const badUsername = match?.[1] ?? match?.[2] ?? fallbackTarget; const svg = generateNotFoundSVG(badUsername, errBg, errAccent, errText, errRadius, errSpeed); @@ -464,4 +511,4 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo 'Content-Security-Policy': SVG_CSP_HEADER, }, }); -} \ No newline at end of file +} From 93c5eda5d0772d020c88ccabaa61ea18db7393e6 Mon Sep 17 00:00:00 2001 From: thakurakanksha288 Date: Sun, 7 Jun 2026 10:36:16 +0530 Subject: [PATCH 3/5] fix: resolve prettier formatting and test cache headers in streak route --- app/api/streak/route.ts | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 315f3692b..93c24edf4 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -37,11 +37,27 @@ function getMonthlyReferenceDate(year: string | undefined, timezone: string): Da return selectedYear < currentYear ? new Date(`${year}-12-15T12:00:00Z`) : undefined; } +// Fixed ETag engine with safe fallback for test mock environments async function generateETag(content: string): Promise { - const msgUint8 = new TextEncoder().encode(content); - const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + if (typeof crypto !== 'undefined' && crypto.subtle && typeof crypto.subtle.digest === 'function') { + try { + const msgUint8 = new TextEncoder().encode(content); + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } catch { + // Fall through to fallback hash if subtle crypto fails in test containers + } + } + + // Safe internal quick string hash fallback if environment lacks Web Crypto + let hash = 0; + for (let i = 0; i < content.length; i++) { + const chr = content.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return Math.abs(hash).toString(16); } export async function GET(request: Request) { @@ -57,7 +73,7 @@ export async function GET(request: Request) { status: 400, headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Cache-Control': 'no-store', }, } ); @@ -314,11 +330,12 @@ export async function GET(request: Request) { const secondsToMidnight = tzParam ? getSecondsUntilMidnightInTimezone(timezone) : getSecondsUntilUTCMidnight(); + const cacheControl = refresh - ? 'no-store, no-cache, must-revalidate, proxy-revalidate' + ? 'no-cache, no-store, must-revalidate' : isHistoricalYear ? 'public, s-maxage=31536000, immutable' - : `public, max-age=3600, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400, must-revalidate`; + : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; // ─── JSON output mode ────────────────────────────────────────────────── if (format === 'json') { @@ -465,7 +482,7 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo status: 429, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); @@ -495,7 +512,7 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo status: 400, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Cache-Control': 'no-store', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); @@ -507,8 +524,8 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo status: 500, headers: { 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Cache-Control': 'no-store', 'Content-Security-Policy': SVG_CSP_HEADER, }, }); -} +} \ No newline at end of file From dee6336dec2281042a95b30ad803b7609e1f1468 Mon Sep 17 00:00:00 2001 From: thakurakanksha288 Date: Sun, 7 Jun 2026 11:15:29 +0530 Subject: [PATCH 4/5] fix: resolve prettier formatting and test cache headers in streak route --- app/api/streak/route.ts | 425 +++++++++++++++++++++++++++------------- 1 file changed, 292 insertions(+), 133 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 93c24edf4..10488d3e2 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -1,8 +1,15 @@ // app/api/streak/route.ts -import { NextResponse } from 'next/server'; -import { fetchGitHubContributions, getOrgDashboardData } from '@/lib/github'; -import { calculateStreak, calculateMonthlyStats, aggregateCalendars } from '@/lib/calculate'; +import { NextResponse } from "next/server"; +import { + fetchGitHubContributions, + getOrgDashboardData, +} from "@/lib/github"; +import { + calculateStreak, + calculateMonthlyStats, + aggregateCalendars, +} from "@/lib/calculate"; import { generateNotFoundSVG, generateRateLimitSVG, @@ -12,39 +19,66 @@ import { generateHeatmapSVG, generatePulseSVG, generateLanguagesSVG, -} from '@/lib/svg/generator'; -import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time'; -import type { BadgeParams, RepoContribution, ExtendedContributionData } from '@/types'; -import { themes } from '@/lib/svg/themes'; -import { streakParamsSchema } from '@/lib/validations'; -import { sanitizeHexColor, sanitizeRadius } from '@/lib/svg/sanitizer'; +} from "@/lib/svg/generator"; +import { + getSecondsUntilUTCMidnight, + getSecondsUntilMidnightInTimezone, +} from "@/utils/time"; +import type { + BadgeParams, + RepoContribution, + ExtendedContributionData, +} from "@/types"; +import { themes } from "@/lib/svg/themes"; +import { streakParamsSchema } from "@/lib/validations"; +import { + sanitizeHexColor, + sanitizeRadius, +} from "@/lib/svg/sanitizer"; const SVG_CSP_HEADER = "default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;"; function escapeSVGText(value: string): string { - return value.replace(/&/g, '&').replace(//g, '>'); + return value + .replace(/&/g, "&") + .replace(//g, ">"); } -function getMonthlyReferenceDate(year: string | undefined, timezone: string): Date | undefined { +function getMonthlyReferenceDate( + year: string | undefined, + timezone: string, +): Date | undefined { if (!year) return undefined; const selectedYear = Number(year); const currentYear = Number( - new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric' }).format(new Date()) + new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + year: "numeric", + }).format(new Date()), ); - return selectedYear < currentYear ? new Date(`${year}-12-15T12:00:00Z`) : undefined; + return selectedYear < currentYear + ? new Date(`${year}-12-15T12:00:00Z`) + : undefined; } // Fixed ETag engine with safe fallback for test mock environments async function generateETag(content: string): Promise { - if (typeof crypto !== 'undefined' && crypto.subtle && typeof crypto.subtle.digest === 'function') { + if ( + typeof crypto !== "undefined" && + crypto.subtle && + typeof crypto.subtle.digest === "function" + ) { try { const msgUint8 = new TextEncoder().encode(content); - const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); + const hashBuffer = await crypto.subtle.digest("SHA-1", msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + return hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } catch { // Fall through to fallback hash if subtle crypto fails in test containers } @@ -62,20 +96,22 @@ async function generateETag(content: string): Promise { export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const parseResult = streakParamsSchema.safeParse(Object.fromEntries(searchParams.entries())); + const parseResult = streakParamsSchema.safeParse( + Object.fromEntries(searchParams.entries()), + ); try { if (!parseResult.success) { const fieldErrors = parseResult.error.flatten(); return NextResponse.json( - { error: 'Invalid parameters', details: fieldErrors }, + { error: "Invalid parameters", details: fieldErrors }, { status: 400, headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', + "Content-Type": "application/json", + "Cache-Control": "no-store", }, - } + }, ); } @@ -124,18 +160,24 @@ export async function GET(request: Request) { entrance, } = parseResult.data; - const normalizedView = view as 'default' | 'monthly' | 'heatmap' | 'pulse' | 'languages'; - const themeName = theme || 'dark'; + const normalizedView = view as + | "default" + | "monthly" + | "heatmap" + | "pulse" + | "languages"; + const themeName = theme || "dark"; - let timezone = 'UTC'; + let timezone = "UTC"; if (tzParam) { try { - timezone = new Intl.DateTimeFormat(undefined, { timeZone: tzParam }).resolvedOptions() - .timeZone; + timezone = new Intl.DateTimeFormat(undefined, { + timeZone: tzParam, + }).resolvedOptions().timeZone; } catch (error) { if (error instanceof RangeError) { const validationErr = new Error(`Invalid timezone: ${tzParam}`); - validationErr.name = 'ValidationError'; + validationErr.name = "ValidationError"; throw validationErr; } throw error; @@ -153,12 +195,13 @@ export async function GET(request: Request) { ? `${year}-12-31T23:59:59Z` : undefined; - if (normalizedView === 'monthly') { - const referenceDate = getMonthlyReferenceDate(year, timezone) || new Date(); - const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format( - referenceDate - ); - const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); + if (normalizedView === "monthly") { + const referenceDate = + getMonthlyReferenceDate(year, timezone) || new Date(); + const localTodayStr = new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + }).format(referenceDate); + const [currentYearStr, currentMonthStr] = localTodayStr.split("-"); const currentYearNum = parseInt(currentYearStr, 10); const currentMonthNum = parseInt(currentMonthStr, 10); @@ -169,7 +212,9 @@ export async function GET(request: Request) { prevYear -= 1; } - const calculatedFromStr = `${prevYear}-${prevMonth.toString().padStart(2, '0')}-01T00:00:00Z`; + const calculatedFromStr = `${prevYear}-${prevMonth + .toString() + .padStart(2, "0")}-01T00:00:00Z`; if (!from || new Date(from) > new Date(calculatedFromStr)) { from = calculatedFromStr; } @@ -183,13 +228,15 @@ export async function GET(request: Request) { const currentYear = new Date().getUTCFullYear(); const isHistoricalYear = !!year && Number(year) < currentYear; - const isAutoTheme = themeName === 'auto'; - const isRandomTheme = themeName === 'random'; + const isAutoTheme = themeName === "auto"; + const isRandomTheme = themeName === "random"; const selectedTheme = (() => { if (isAutoTheme) return themes.light; if (isRandomTheme) { const keys = Object.keys(themes); - const hash = user.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); + const hash = user + .split("") + .reduce((acc, c) => acc + c.charCodeAt(0), 0); const stableKey = keys[hash % keys.length]; return themes[stableKey] || themes.dark; } @@ -198,26 +245,31 @@ export async function GET(request: Request) { const targetEntity = org || - (user.includes(',') + (user.includes(",") ? user - .split(',') + .split(",") .map((u) => u.trim()) .slice(0, 2) - .join(' + ') + .join(" + ") : user); - const borderParam = searchParams.get('border'); - const sanitizedBorder = borderParam ? borderParam.replace(/[^a-fA-F0-9]/g, '') : undefined; - const animate = searchParams.get('animate') !== 'false'; + const borderParam = searchParams.get("border"); + const sanitizedBorder = borderParam + ? borderParam.replace(/[^a-fA-F0-9]/g, "") + : undefined; + const animate = searchParams.get("animate") !== "false"; const params: BadgeParams = { user: targetEntity, bg: isAutoTheme ? selectedTheme.bg : bg || selectedTheme.bg, text: isAutoTheme ? selectedTheme.text : text || selectedTheme.text, - accent: isAutoTheme ? selectedTheme.accent : accent || selectedTheme.accent, + accent: isAutoTheme + ? selectedTheme.accent + : accent || selectedTheme.accent, border: sanitizedBorder, radius, - speed: speed && /^(?:[2-9]|1\d|20)s$/.test(speed) ? speed : '8s', + speed: + speed && /^(?:[2-9]|1\d|20)s$/.test(speed) ? speed : "8s", scale, font, autoTheme: isAutoTheme, @@ -232,7 +284,12 @@ export async function GET(request: Request) { size, grace: Math.max( 0, - Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10)) + Math.min( + 7, + typeof grace === "number" + ? grace + : parseInt(String(grace || 1), 10), + ), ), mode, repo, @@ -246,7 +303,12 @@ export async function GET(request: Request) { gradient_dir, opacity: Math.max( 0.1, - Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0))) + Math.min( + 1.0, + typeof opacity === "number" + ? opacity + : parseFloat(String(opacity || 1.0)), + ), ), disable_particles, glow, @@ -261,17 +323,24 @@ export async function GET(request: Request) { let repoContributions: RepoContribution[] = []; if (org) { - const orgData = await getOrgDashboardData(org, { bypassCache: refresh, from, to }); + const orgData = await getOrgDashboardData(org, { + bypassCache: refresh, + from, + to, + }); calendar = orgData.calendar; - repoContributions = normalizedView === 'languages' ? orgData.repoContributions || [] : []; - } else if (user.includes(',')) { + repoContributions = + normalizedView === "languages" + ? orgData.repoContributions || [] + : []; + } else if (user.includes(",")) { const users = user - .split(',') + .split(",") .map((u) => u.trim()) .filter(Boolean); if (users.length > 2) { throw new Error( - 'ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.' + "ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.", ); } @@ -280,31 +349,45 @@ export async function GET(request: Request) { const fetchedCalendars = await Promise.all( users.map(async (u) => { try { - const userData = await fetchGitHubContributions(u, { bypassCache: refresh, from, to }); + const userData = await fetchGitHubContributions(u, { + bypassCache: refresh, + from, + to, + }); if (userData.isOfflineFallback) hasOfflineFallback = true; return userData; } catch (err) { lastError = err; return null; } - }) + }), ); const successfulData = fetchedCalendars.filter( - (d): d is ExtendedContributionData => d !== null + (d): d is ExtendedContributionData => d !== null, ); - if (successfulData.length === 0) throw lastError || new Error('No successful data fetched'); + if (successfulData.length === 0) + throw lastError || new Error("No successful data fetched"); - calendar = aggregateCalendars(successfulData.map((d) => d.calendar)); + calendar = aggregateCalendars( + successfulData.map((d) => d.calendar), + ); repoContributions = - normalizedView === 'languages' + normalizedView === "languages" ? successfulData.flatMap((d) => d.repoContributions || []) : []; if (hasOfflineFallback) params.isOfflineFallback = true; } else { - const userData = await fetchGitHubContributions(user, { bypassCache: refresh, from, to }); + const userData = await fetchGitHubContributions(user, { + bypassCache: refresh, + from, + to, + }); calendar = userData.calendar; - repoContributions = normalizedView === 'languages' ? userData.repoContributions || [] : []; + repoContributions = + normalizedView === "languages" + ? userData.repoContributions || [] + : []; if (userData.isOfflineFallback) params.isOfflineFallback = true; if (versus) { @@ -318,11 +401,14 @@ export async function GET(request: Request) { } } - if (days && normalizedView !== 'monthly') { + if (days && normalizedView !== "monthly") { const allDays = calendar.weeks.flatMap((w) => w.contributionDays); const filteredDays = allDays.slice(-days); calendar = { - totalContributions: filteredDays.reduce((sum, d) => sum + d.contributionCount, 0), + totalContributions: filteredDays.reduce( + (sum, d) => sum + d.contributionCount, + 0, + ), weeks: [{ contributionDays: filteredDays }], }; } @@ -332,73 +418,95 @@ export async function GET(request: Request) { : getSecondsUntilUTCMidnight(); const cacheControl = refresh - ? 'no-cache, no-store, must-revalidate' + ? "no-cache, no-store, must-revalidate" : isHistoricalYear - ? 'public, s-maxage=31536000, immutable' + ? "public, s-maxage=31536000, immutable" : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; // ─── JSON output mode ────────────────────────────────────────────────── - if (format === 'json') { + if (format === "json") { const stats = calculateStreak(calendar, timezone, undefined, grace); const monthlyStats = calculateMonthlyStats( calendar, timezone, - getMonthlyReferenceDate(year, timezone) + getMonthlyReferenceDate(year, timezone), ); const jsonPayload = JSON.stringify({ user: targetEntity, stats, monthlyStats, - calendar: { totalContributions: calendar.totalContributions, weeks: calendar.weeks }, + calendar: { + totalContributions: calendar.totalContributions, + weeks: calendar.weeks, + }, }); const etag = await generateETag(jsonPayload); const weakEtag = `W/"${etag}"`; - const ifNoneMatch = request.headers.get('if-none-match'); + const ifNoneMatch = request.headers.get("if-none-match"); if (ifNoneMatch) { - const etags = ifNoneMatch.split(',').map((e) => e.trim()); - if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { + const etags = ifNoneMatch.split(",").map((e) => e.trim()); + if ( + etags.includes(weakEtag) || + etags.includes(`"${etag}"`) + ) { return new NextResponse(null, { status: 304, - headers: { 'Cache-Control': cacheControl, ETag: weakEtag }, + headers: { + "Cache-Control": cacheControl, + ETag: weakEtag, + }, }); } } return new NextResponse(jsonPayload, { headers: { - 'Content-Type': 'application/json', - 'Cache-Control': cacheControl, + "Content-Type": "application/json", + "Cache-Control": cacheControl, ETag: weakEtag, - 'X-Cache-Status': refresh ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', + "X-Cache-Status": refresh + ? `BYPASS, fetched=${new Date().toISOString()}` + : "HIT", }, }); } // ─── SVG output mode (default) ────────────────────────────────────────── - let svg = ''; - if (normalizedView === 'monthly') { + let svg = ""; + if (normalizedView === "monthly") { const stats = calculateMonthlyStats( calendar, timezone, - getMonthlyReferenceDate(year, timezone) + getMonthlyReferenceDate(year, timezone), ); svg = generateMonthlySVG(stats, params); - } else if (normalizedView === 'languages') { + } else if (normalizedView === "languages") { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateLanguagesSVG(stats, params, repoContributions); - } else if (normalizedView === 'heatmap') { + } else if (normalizedView === "heatmap") { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateHeatmapSVG(stats, params, calendar); - } else if (normalizedView === 'pulse') { + } else if (normalizedView === "pulse") { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generatePulseSVG(stats, params, calendar); } else if (versus && versusCalendar) { const stats1 = calculateStreak(calendar, timezone, undefined, grace); - const stats2 = calculateStreak(versusCalendar, timezone, undefined, grace); - svg = generateVersusSVG(stats1, stats2, params, calendar, versusCalendar); + const stats2 = calculateStreak( + versusCalendar, + timezone, + undefined, + grace, + ); + svg = generateVersusSVG( + stats1, + stats2, + params, + calendar, + versusCalendar, + ); } else { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateSVG(stats, params, calendar); @@ -406,25 +514,33 @@ export async function GET(request: Request) { const etag = await generateETag(svg); const weakEtag = `W/"${etag}"`; - const ifNoneMatch = request.headers.get('if-none-match'); + const ifNoneMatch = request.headers.get("if-none-match"); if (ifNoneMatch) { - const etags = ifNoneMatch.split(',').map((e) => e.trim()); - if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { + const etags = ifNoneMatch.split(",").map((e) => e.trim()); + if ( + etags.includes(weakEtag) || + etags.includes(`"${etag}"`) + ) { return new NextResponse(null, { status: 304, - headers: { 'Cache-Control': cacheControl, ETag: weakEtag }, + headers: { + "Cache-Control": cacheControl, + ETag: weakEtag, + }, }); } } return new NextResponse(svg, { headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': cacheControl, - 'Content-Security-Policy': SVG_CSP_HEADER, + "Content-Type": "image/svg+xml", + "Cache-Control": cacheControl, + "Content-Security-Policy": SVG_CSP_HEADER, ETag: weakEtag, - 'X-Cache-Status': refresh ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', + "X-Cache-Status": refresh + ? `BYPASS, fetched=${new Date().toISOString()}` + : "HIT", }, }); } catch (error: unknown) { @@ -434,74 +550,115 @@ export async function GET(request: Request) { type ParseResult = ReturnType; -function buildErrorResponse(error: unknown, parseResult: ParseResult): NextResponse { - const message = error instanceof Error ? error.message : String(error); +function buildErrorResponse( + error: unknown, + parseResult: ParseResult, +): NextResponse { + const message = + error instanceof Error ? error.message : String(error); function buildInlineErrorSVG(text: string): string { const MAX_LINE = 48; - const truncated = text.length > MAX_LINE * 2 ? text.slice(0, MAX_LINE * 2 - 1) + '…' : text; + const truncated = + text.length > MAX_LINE * 2 + ? text.slice(0, MAX_LINE * 2 - 1) + "…" + : text; const line1 = escapeSVGText(truncated.slice(0, MAX_LINE)); - const line2 = truncated.length > MAX_LINE ? escapeSVGText(truncated.slice(MAX_LINE)) : null; - const textY = line2 ? '62' : '75'; + const line2 = + truncated.length > MAX_LINE + ? escapeSVGText(truncated.slice(MAX_LINE)) + : null; + const textY = line2 ? "62" : "75"; return ` ${line1}${ - line2 - ? `${line2}` - : '' - } + line2 + ? `${line2}` + : "" + } `; } const isNotFound = - message.toLowerCase().includes('not found') || - message.toLowerCase().includes('could not resolve'); - const isRateLimit = message.toLowerCase().includes('rate limit'); + message.toLowerCase().includes("not found") || + message.toLowerCase().includes("could not resolve"); + const isRateLimit = + message.toLowerCase().includes("rate limit"); const isValidationError = - (error instanceof Error && error.name === 'ValidationError') || - message.toLowerCase().includes('invalid') || - message.toLowerCase().includes('validation') || - message.toLowerCase().includes('strictly for organizations'); - - const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`; + (error instanceof Error && + error.name === "ValidationError") || + message.toLowerCase().includes("invalid") || + message.toLowerCase().includes("validation") || + message.toLowerCase().includes("strictly for organizations"); + + const errBg = `#${sanitizeHexColor( + parseResult.success ? parseResult.data.bg : undefined, + "0d1117", + )}`; const errAccentRaw = (parseResult.success && (Array.isArray(parseResult.data.accent) - ? parseResult.data.accent[parseResult.data.accent.length - 1] + ? parseResult.data.accent[ + parseResult.data.accent.length - 1 + ] : parseResult.data.accent)) || undefined; - const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`; - const errText = `#${sanitizeHexColor(parseResult.success ? parseResult.data.text : undefined, 'c9d1d9')}`; - const errRadius = sanitizeRadius(parseResult.success ? parseResult.data.radius : undefined, 8); - const errSpeed = (parseResult.success && parseResult.data.speed) || '8s'; + const errAccent = `#${sanitizeHexColor(errAccentRaw, "58a6ff")}`; + const errText = `#${sanitizeHexColor( + parseResult.success ? parseResult.data.text : undefined, + "c9d1d9", + )}`; + const errRadius = sanitizeRadius( + parseResult.success ? parseResult.data.radius : undefined, + 8, + ); + const errSpeed = + (parseResult.success && parseResult.data.speed) || "8s"; if (isRateLimit) { - const svg = generateRateLimitSVG(errBg, errAccent, errText, errRadius, errSpeed); + const svg = generateRateLimitSVG( + errBg, + errAccent, + errText, + errRadius, + errSpeed, + ); return new NextResponse(svg, { status: 429, headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Content-Security-Policy': SVG_CSP_HEADER, + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Content-Security-Policy": SVG_CSP_HEADER, }, }); } if (isNotFound) { - const match = message.match(/"([^"]+)"|login of '([^']+)'/); + const match = message.match( + /"([^"]+)"|login of '([^']+)'/, + ); const fallbackTarget = parseResult.success ? parseResult.data.org || parseResult.data.user - : 'unknown'; - const badUsername = match?.[1] ?? match?.[2] ?? fallbackTarget; - - const svg = generateNotFoundSVG(badUsername, errBg, errAccent, errText, errRadius, errSpeed); + : "unknown"; + const badUsername = + match?.[1] ?? match?.[2] ?? fallbackTarget; + + const svg = generateNotFoundSVG( + badUsername, + errBg, + errAccent, + errText, + errRadius, + errSpeed, + ); return new NextResponse(svg, { status: 404, headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=7200, must-revalidate', - 'Content-Security-Policy': SVG_CSP_HEADER, + "Content-Type": "image/svg+xml", + "Cache-Control": + "public, max-age=3600, stale-while-revalidate=7200, must-revalidate", + "Content-Security-Policy": SVG_CSP_HEADER, }, }); } @@ -511,21 +668,23 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo return new NextResponse(validationSvg, { status: 400, headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store', - 'Content-Security-Policy': SVG_CSP_HEADER, + "Content-Type": "image/svg+xml", + "Cache-Control": "no-store", + "Content-Security-Policy": SVG_CSP_HEADER, }, }); } - console.error('[streak] Unhandled error:', message); - const errorSvg = buildInlineErrorSVG('Something went wrong. Please try again later.'); + console.error("[streak] Unhandled error:", message); + const errorSvg = buildInlineErrorSVG( + "Something went wrong. Please try again later.", + ); return new NextResponse(errorSvg, { status: 500, headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-store', - 'Content-Security-Policy': SVG_CSP_HEADER, + "Content-Type": "image/svg+xml", + "Cache-Control": "no-store", + "Content-Security-Policy": SVG_CSP_HEADER, }, }); } \ No newline at end of file From 4a0d9ca3bd4b551e2ea0657bae11183b8edf43e2 Mon Sep 17 00:00:00 2001 From: thakurakanksha288 Date: Sun, 7 Jun 2026 11:19:31 +0530 Subject: [PATCH 5/5] fix: resolve prettier formatting and test cache headers in streak route --- app/api/streak/route.ts | 380 ++++++++++++++-------------------------- 1 file changed, 128 insertions(+), 252 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 10488d3e2..d912e6d45 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -1,15 +1,8 @@ // app/api/streak/route.ts -import { NextResponse } from "next/server"; -import { - fetchGitHubContributions, - getOrgDashboardData, -} from "@/lib/github"; -import { - calculateStreak, - calculateMonthlyStats, - aggregateCalendars, -} from "@/lib/calculate"; +import { NextResponse } from 'next/server'; +import { fetchGitHubContributions, getOrgDashboardData } from '@/lib/github'; +import { calculateStreak, calculateMonthlyStats, aggregateCalendars } from '@/lib/calculate'; import { generateNotFoundSVG, generateRateLimitSVG, @@ -19,66 +12,46 @@ import { generateHeatmapSVG, generatePulseSVG, generateLanguagesSVG, -} from "@/lib/svg/generator"; -import { - getSecondsUntilUTCMidnight, - getSecondsUntilMidnightInTimezone, -} from "@/utils/time"; -import type { - BadgeParams, - RepoContribution, - ExtendedContributionData, -} from "@/types"; -import { themes } from "@/lib/svg/themes"; -import { streakParamsSchema } from "@/lib/validations"; -import { - sanitizeHexColor, - sanitizeRadius, -} from "@/lib/svg/sanitizer"; +} from '@/lib/svg/generator'; +import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time'; +import type { BadgeParams, RepoContribution, ExtendedContributionData } from '@/types'; +import { themes } from '@/lib/svg/themes'; +import { streakParamsSchema } from '@/lib/validations'; +import { sanitizeHexColor, sanitizeRadius } from '@/lib/svg/sanitizer'; const SVG_CSP_HEADER = "default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;"; function escapeSVGText(value: string): string { - return value - .replace(/&/g, "&") - .replace(//g, ">"); + return value.replace(/&/g, '&').replace(//g, '>'); } -function getMonthlyReferenceDate( - year: string | undefined, - timezone: string, -): Date | undefined { +function getMonthlyReferenceDate(year: string | undefined, timezone: string): Date | undefined { if (!year) return undefined; const selectedYear = Number(year); const currentYear = Number( - new Intl.DateTimeFormat("en-CA", { + new Intl.DateTimeFormat('en-CA', { timeZone: timezone, - year: "numeric", - }).format(new Date()), + year: 'numeric', + }).format(new Date()) ); - return selectedYear < currentYear - ? new Date(`${year}-12-15T12:00:00Z`) - : undefined; + return selectedYear < currentYear ? new Date(`${year}-12-15T12:00:00Z`) : undefined; } // Fixed ETag engine with safe fallback for test mock environments async function generateETag(content: string): Promise { if ( - typeof crypto !== "undefined" && + typeof crypto !== 'undefined' && crypto.subtle && - typeof crypto.subtle.digest === "function" + typeof crypto.subtle.digest === 'function' ) { try { const msgUint8 = new TextEncoder().encode(content); - const hashBuffer = await crypto.subtle.digest("SHA-1", msgUint8); + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } catch { // Fall through to fallback hash if subtle crypto fails in test containers } @@ -96,22 +69,20 @@ async function generateETag(content: string): Promise { export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const parseResult = streakParamsSchema.safeParse( - Object.fromEntries(searchParams.entries()), - ); + const parseResult = streakParamsSchema.safeParse(Object.fromEntries(searchParams.entries())); try { if (!parseResult.success) { const fieldErrors = parseResult.error.flatten(); return NextResponse.json( - { error: "Invalid parameters", details: fieldErrors }, + { error: 'Invalid parameters', details: fieldErrors }, { status: 400, headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store", + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', }, - }, + } ); } @@ -160,15 +131,10 @@ export async function GET(request: Request) { entrance, } = parseResult.data; - const normalizedView = view as - | "default" - | "monthly" - | "heatmap" - | "pulse" - | "languages"; - const themeName = theme || "dark"; + const normalizedView = view as 'default' | 'monthly' | 'heatmap' | 'pulse' | 'languages'; + const themeName = theme || 'dark'; - let timezone = "UTC"; + let timezone = 'UTC'; if (tzParam) { try { timezone = new Intl.DateTimeFormat(undefined, { @@ -177,7 +143,7 @@ export async function GET(request: Request) { } catch (error) { if (error instanceof RangeError) { const validationErr = new Error(`Invalid timezone: ${tzParam}`); - validationErr.name = "ValidationError"; + validationErr.name = 'ValidationError'; throw validationErr; } throw error; @@ -195,13 +161,12 @@ export async function GET(request: Request) { ? `${year}-12-31T23:59:59Z` : undefined; - if (normalizedView === "monthly") { - const referenceDate = - getMonthlyReferenceDate(year, timezone) || new Date(); - const localTodayStr = new Intl.DateTimeFormat("en-CA", { + if (normalizedView === 'monthly') { + const referenceDate = getMonthlyReferenceDate(year, timezone) || new Date(); + const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, }).format(referenceDate); - const [currentYearStr, currentMonthStr] = localTodayStr.split("-"); + const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); const currentYearNum = parseInt(currentYearStr, 10); const currentMonthNum = parseInt(currentMonthStr, 10); @@ -212,9 +177,7 @@ export async function GET(request: Request) { prevYear -= 1; } - const calculatedFromStr = `${prevYear}-${prevMonth - .toString() - .padStart(2, "0")}-01T00:00:00Z`; + const calculatedFromStr = `${prevYear}-${prevMonth.toString().padStart(2, '0')}-01T00:00:00Z`; if (!from || new Date(from) > new Date(calculatedFromStr)) { from = calculatedFromStr; } @@ -228,15 +191,13 @@ export async function GET(request: Request) { const currentYear = new Date().getUTCFullYear(); const isHistoricalYear = !!year && Number(year) < currentYear; - const isAutoTheme = themeName === "auto"; - const isRandomTheme = themeName === "random"; + const isAutoTheme = themeName === 'auto'; + const isRandomTheme = themeName === 'random'; const selectedTheme = (() => { if (isAutoTheme) return themes.light; if (isRandomTheme) { const keys = Object.keys(themes); - const hash = user - .split("") - .reduce((acc, c) => acc + c.charCodeAt(0), 0); + const hash = user.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); const stableKey = keys[hash % keys.length]; return themes[stableKey] || themes.dark; } @@ -245,31 +206,26 @@ export async function GET(request: Request) { const targetEntity = org || - (user.includes(",") + (user.includes(',') ? user - .split(",") + .split(',') .map((u) => u.trim()) .slice(0, 2) - .join(" + ") + .join(' + ') : user); - const borderParam = searchParams.get("border"); - const sanitizedBorder = borderParam - ? borderParam.replace(/[^a-fA-F0-9]/g, "") - : undefined; - const animate = searchParams.get("animate") !== "false"; + const borderParam = searchParams.get('border'); + const sanitizedBorder = borderParam ? borderParam.replace(/[^a-fA-F0-9]/g, '') : undefined; + const animate = searchParams.get('animate') !== 'false'; const params: BadgeParams = { user: targetEntity, bg: isAutoTheme ? selectedTheme.bg : bg || selectedTheme.bg, text: isAutoTheme ? selectedTheme.text : text || selectedTheme.text, - accent: isAutoTheme - ? selectedTheme.accent - : accent || selectedTheme.accent, + accent: isAutoTheme ? selectedTheme.accent : accent || selectedTheme.accent, border: sanitizedBorder, radius, - speed: - speed && /^(?:[2-9]|1\d|20)s$/.test(speed) ? speed : "8s", + speed: speed && /^(?:[2-9]|1\d|20)s$/.test(speed) ? speed : '8s', scale, font, autoTheme: isAutoTheme, @@ -284,12 +240,7 @@ export async function GET(request: Request) { size, grace: Math.max( 0, - Math.min( - 7, - typeof grace === "number" - ? grace - : parseInt(String(grace || 1), 10), - ), + Math.min(7, typeof grace === 'number' ? grace : parseInt(String(grace || 1), 10)) ), mode, repo, @@ -303,12 +254,7 @@ export async function GET(request: Request) { gradient_dir, opacity: Math.max( 0.1, - Math.min( - 1.0, - typeof opacity === "number" - ? opacity - : parseFloat(String(opacity || 1.0)), - ), + Math.min(1.0, typeof opacity === 'number' ? opacity : parseFloat(String(opacity || 1.0))) ), disable_particles, glow, @@ -329,18 +275,15 @@ export async function GET(request: Request) { to, }); calendar = orgData.calendar; - repoContributions = - normalizedView === "languages" - ? orgData.repoContributions || [] - : []; - } else if (user.includes(",")) { + repoContributions = normalizedView === 'languages' ? orgData.repoContributions || [] : []; + } else if (user.includes(',')) { const users = user - .split(",") + .split(',') .map((u) => u.trim()) .filter(Boolean); if (users.length > 2) { throw new Error( - "ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.", + 'ValidationError: The streak comparison generator strictly accepts a maximum of 2 usernames.' ); } @@ -360,20 +303,17 @@ export async function GET(request: Request) { lastError = err; return null; } - }), + }) ); const successfulData = fetchedCalendars.filter( - (d): d is ExtendedContributionData => d !== null, + (d): d is ExtendedContributionData => d !== null ); - if (successfulData.length === 0) - throw lastError || new Error("No successful data fetched"); + if (successfulData.length === 0) throw lastError || new Error('No successful data fetched'); - calendar = aggregateCalendars( - successfulData.map((d) => d.calendar), - ); + calendar = aggregateCalendars(successfulData.map((d) => d.calendar)); repoContributions = - normalizedView === "languages" + normalizedView === 'languages' ? successfulData.flatMap((d) => d.repoContributions || []) : []; if (hasOfflineFallback) params.isOfflineFallback = true; @@ -384,10 +324,7 @@ export async function GET(request: Request) { to, }); calendar = userData.calendar; - repoContributions = - normalizedView === "languages" - ? userData.repoContributions || [] - : []; + repoContributions = normalizedView === 'languages' ? userData.repoContributions || [] : []; if (userData.isOfflineFallback) params.isOfflineFallback = true; if (versus) { @@ -401,14 +338,11 @@ export async function GET(request: Request) { } } - if (days && normalizedView !== "monthly") { + if (days && normalizedView !== 'monthly') { const allDays = calendar.weeks.flatMap((w) => w.contributionDays); const filteredDays = allDays.slice(-days); calendar = { - totalContributions: filteredDays.reduce( - (sum, d) => sum + d.contributionCount, - 0, - ), + totalContributions: filteredDays.reduce((sum, d) => sum + d.contributionCount, 0), weeks: [{ contributionDays: filteredDays }], }; } @@ -418,18 +352,18 @@ export async function GET(request: Request) { : getSecondsUntilUTCMidnight(); const cacheControl = refresh - ? "no-cache, no-store, must-revalidate" + ? 'no-cache, no-store, must-revalidate' : isHistoricalYear - ? "public, s-maxage=31536000, immutable" + ? 'public, s-maxage=31536000, immutable' : `public, s-maxage=${secondsToMidnight}, stale-while-revalidate=86400`; // ─── JSON output mode ────────────────────────────────────────────────── - if (format === "json") { + if (format === 'json') { const stats = calculateStreak(calendar, timezone, undefined, grace); const monthlyStats = calculateMonthlyStats( calendar, timezone, - getMonthlyReferenceDate(year, timezone), + getMonthlyReferenceDate(year, timezone) ); const jsonPayload = JSON.stringify({ @@ -444,18 +378,15 @@ export async function GET(request: Request) { const etag = await generateETag(jsonPayload); const weakEtag = `W/"${etag}"`; - const ifNoneMatch = request.headers.get("if-none-match"); + const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch) { - const etags = ifNoneMatch.split(",").map((e) => e.trim()); - if ( - etags.includes(weakEtag) || - etags.includes(`"${etag}"`) - ) { + const etags = ifNoneMatch.split(',').map((e) => e.trim()); + if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { return new NextResponse(null, { status: 304, headers: { - "Cache-Control": cacheControl, + 'Cache-Control': cacheControl, ETag: weakEtag, }, }); @@ -464,49 +395,36 @@ export async function GET(request: Request) { return new NextResponse(jsonPayload, { headers: { - "Content-Type": "application/json", - "Cache-Control": cacheControl, + 'Content-Type': 'application/json', + 'Cache-Control': cacheControl, ETag: weakEtag, - "X-Cache-Status": refresh - ? `BYPASS, fetched=${new Date().toISOString()}` - : "HIT", + 'X-Cache-Status': refresh ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', }, }); } // ─── SVG output mode (default) ────────────────────────────────────────── - let svg = ""; - if (normalizedView === "monthly") { + let svg = ''; + if (normalizedView === 'monthly') { const stats = calculateMonthlyStats( calendar, timezone, - getMonthlyReferenceDate(year, timezone), + getMonthlyReferenceDate(year, timezone) ); svg = generateMonthlySVG(stats, params); - } else if (normalizedView === "languages") { + } else if (normalizedView === 'languages') { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateLanguagesSVG(stats, params, repoContributions); - } else if (normalizedView === "heatmap") { + } else if (normalizedView === 'heatmap') { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateHeatmapSVG(stats, params, calendar); - } else if (normalizedView === "pulse") { + } else if (normalizedView === 'pulse') { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generatePulseSVG(stats, params, calendar); } else if (versus && versusCalendar) { const stats1 = calculateStreak(calendar, timezone, undefined, grace); - const stats2 = calculateStreak( - versusCalendar, - timezone, - undefined, - grace, - ); - svg = generateVersusSVG( - stats1, - stats2, - params, - calendar, - versusCalendar, - ); + const stats2 = calculateStreak(versusCalendar, timezone, undefined, grace); + svg = generateVersusSVG(stats1, stats2, params, calendar, versusCalendar); } else { const stats = calculateStreak(calendar, timezone, undefined, grace); svg = generateSVG(stats, params, calendar); @@ -514,18 +432,15 @@ export async function GET(request: Request) { const etag = await generateETag(svg); const weakEtag = `W/"${etag}"`; - const ifNoneMatch = request.headers.get("if-none-match"); + const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch) { - const etags = ifNoneMatch.split(",").map((e) => e.trim()); - if ( - etags.includes(weakEtag) || - etags.includes(`"${etag}"`) - ) { + const etags = ifNoneMatch.split(',').map((e) => e.trim()); + if (etags.includes(weakEtag) || etags.includes(`"${etag}"`)) { return new NextResponse(null, { status: 304, headers: { - "Cache-Control": cacheControl, + 'Cache-Control': cacheControl, ETag: weakEtag, }, }); @@ -534,13 +449,11 @@ export async function GET(request: Request) { return new NextResponse(svg, { headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": cacheControl, - "Content-Security-Policy": SVG_CSP_HEADER, + 'Content-Type': 'image/svg+xml', + 'Cache-Control': cacheControl, + 'Content-Security-Policy': SVG_CSP_HEADER, ETag: weakEtag, - "X-Cache-Status": refresh - ? `BYPASS, fetched=${new Date().toISOString()}` - : "HIT", + 'X-Cache-Status': refresh ? `BYPASS, fetched=${new Date().toISOString()}` : 'HIT', }, }); } catch (error: unknown) { @@ -550,115 +463,80 @@ export async function GET(request: Request) { type ParseResult = ReturnType; -function buildErrorResponse( - error: unknown, - parseResult: ParseResult, -): NextResponse { - const message = - error instanceof Error ? error.message : String(error); +function buildErrorResponse(error: unknown, parseResult: ParseResult): NextResponse { + const message = error instanceof Error ? error.message : String(error); function buildInlineErrorSVG(text: string): string { const MAX_LINE = 48; - const truncated = - text.length > MAX_LINE * 2 - ? text.slice(0, MAX_LINE * 2 - 1) + "…" - : text; + const truncated = text.length > MAX_LINE * 2 ? text.slice(0, MAX_LINE * 2 - 1) + '…' : text; const line1 = escapeSVGText(truncated.slice(0, MAX_LINE)); - const line2 = - truncated.length > MAX_LINE - ? escapeSVGText(truncated.slice(MAX_LINE)) - : null; - const textY = line2 ? "62" : "75"; + const line2 = truncated.length > MAX_LINE ? escapeSVGText(truncated.slice(MAX_LINE)) : null; + const textY = line2 ? '62' : '75'; return ` ${line1}${ - line2 - ? `${line2}` - : "" - } + line2 + ? `${line2}` + : '' + } `; } const isNotFound = - message.toLowerCase().includes("not found") || - message.toLowerCase().includes("could not resolve"); - const isRateLimit = - message.toLowerCase().includes("rate limit"); + message.toLowerCase().includes('not found') || + message.toLowerCase().includes('could not resolve'); + const isRateLimit = message.toLowerCase().includes('rate limit'); const isValidationError = - (error instanceof Error && - error.name === "ValidationError") || - message.toLowerCase().includes("invalid") || - message.toLowerCase().includes("validation") || - message.toLowerCase().includes("strictly for organizations"); + (error instanceof Error && error.name === 'ValidationError') || + message.toLowerCase().includes('invalid') || + message.toLowerCase().includes('validation') || + message.toLowerCase().includes('strictly for organizations'); const errBg = `#${sanitizeHexColor( parseResult.success ? parseResult.data.bg : undefined, - "0d1117", + '0d1117' )}`; const errAccentRaw = (parseResult.success && (Array.isArray(parseResult.data.accent) - ? parseResult.data.accent[ - parseResult.data.accent.length - 1 - ] + ? parseResult.data.accent[parseResult.data.accent.length - 1] : parseResult.data.accent)) || undefined; - const errAccent = `#${sanitizeHexColor(errAccentRaw, "58a6ff")}`; + const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`; const errText = `#${sanitizeHexColor( parseResult.success ? parseResult.data.text : undefined, - "c9d1d9", + 'c9d1d9' )}`; - const errRadius = sanitizeRadius( - parseResult.success ? parseResult.data.radius : undefined, - 8, - ); - const errSpeed = - (parseResult.success && parseResult.data.speed) || "8s"; + const errRadius = sanitizeRadius(parseResult.success ? parseResult.data.radius : undefined, 8); + const errSpeed = (parseResult.success && parseResult.data.speed) || '8s'; if (isRateLimit) { - const svg = generateRateLimitSVG( - errBg, - errAccent, - errText, - errRadius, - errSpeed, - ); + const svg = generateRateLimitSVG(errBg, errAccent, errText, errRadius, errSpeed); return new NextResponse(svg, { status: 429, headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Content-Security-Policy": SVG_CSP_HEADER, + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Content-Security-Policy': SVG_CSP_HEADER, }, }); } if (isNotFound) { - const match = message.match( - /"([^"]+)"|login of '([^']+)'/, - ); + const match = message.match(/"([^"]+)"|login of '([^']+)'/); const fallbackTarget = parseResult.success ? parseResult.data.org || parseResult.data.user - : "unknown"; - const badUsername = - match?.[1] ?? match?.[2] ?? fallbackTarget; - - const svg = generateNotFoundSVG( - badUsername, - errBg, - errAccent, - errText, - errRadius, - errSpeed, - ); + : 'unknown'; + const badUsername = match?.[1] ?? match?.[2] ?? fallbackTarget; + + const svg = generateNotFoundSVG(badUsername, errBg, errAccent, errText, errRadius, errSpeed); return new NextResponse(svg, { status: 404, headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": - "public, max-age=3600, stale-while-revalidate=7200, must-revalidate", - "Content-Security-Policy": SVG_CSP_HEADER, + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=7200, must-revalidate', + 'Content-Security-Policy': SVG_CSP_HEADER, }, }); } @@ -668,23 +546,21 @@ function buildErrorResponse( return new NextResponse(validationSvg, { status: 400, headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-store", - "Content-Security-Policy": SVG_CSP_HEADER, + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'no-store', + 'Content-Security-Policy': SVG_CSP_HEADER, }, }); } - console.error("[streak] Unhandled error:", message); - const errorSvg = buildInlineErrorSVG( - "Something went wrong. Please try again later.", - ); + console.error('[streak] Unhandled error:', message); + const errorSvg = buildInlineErrorSVG('Something went wrong. Please try again later.'); return new NextResponse(errorSvg, { status: 500, headers: { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-store", - "Content-Security-Policy": SVG_CSP_HEADER, + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'no-store', + 'Content-Security-Policy': SVG_CSP_HEADER, }, }); -} \ No newline at end of file +}