Merge pull request #2606 from codeflash-ai/fix/cf-webapp-security-hardening

fix: harden cf-webapp security across auth, XSS, and headers
This commit is contained in:
Kevin Turcios 2026-04-14 17:04:44 -05:00 committed by GitHub
commit e6cec80c9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 226 additions and 194 deletions

View file

@ -21,8 +21,8 @@ STRIPE_PRO_PRICE_YEARLY_ID=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
# Codeflash # Codeflash
NEXT_PUBLIC_CF_API_KEY=
API_TOKEN_LIMIT=4000 API_TOKEN_LIMIT=4000
JWT_SECRET=
# Sentry (omit NEXT_PUBLIC_SENTRY_DISABLED to enable) # Sentry (omit NEXT_PUBLIC_SENTRY_DISABLED to enable)
NEXT_PUBLIC_SENTRY_DISABLED=true NEXT_PUBLIC_SENTRY_DISABLED=true

View file

@ -10,6 +10,37 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const nextConfig = { const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://widget.intercom.io https://js.intercomcdn.com https://client.crisp.chat https://settings.crisp.chat",
"style-src 'self' 'unsafe-inline' https://client.crisp.chat",
"img-src 'self' data: blob: https://avatars.githubusercontent.com https://github.com https://*.intercomcdn.com https://*.crisp.chat https://image.crisp.chat",
"font-src 'self' data: https://client.crisp.chat",
"connect-src 'self' https://*.intercom.io https://api-iam.intercom.io wss://*.intercom.io https://*.crisp.chat wss://*.crisp.chat https://*.sentry.io https://*.ingest.us.sentry.io https://us.i.posthog.com https://us.posthog.com",
"frame-src 'self' https://intercom-sheets.com https://game.crisp.chat",
"media-src 'self' https://*.intercomcdn.com",
"worker-src 'self' blob:",
].join("; "),
},
],
},
]
},
cacheComponents: true, cacheComponents: true,
cacheLife: { cacheLife: {
dashboard: { dashboard: {

View file

@ -51,6 +51,7 @@
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"diff": "^8.0.2", "diff": "^8.0.2",
"dompurify": "^3.3.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"marked": "^18.0.0", "marked": "^18.0.0",
@ -83,6 +84,7 @@
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^16.2.2", "@next/bundle-analyzer": "^16.2.2",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@types/dompurify": "^3.2.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/papaparse": "^5.5.2", "@types/papaparse": "^5.5.2",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",

View file

@ -7,16 +7,19 @@ import jwt from "jsonwebtoken"
import { CacheContainer } from "node-ts-cache" import { CacheContainer } from "node-ts-cache"
import { MemoryStorage } from "node-ts-cache-storage-memory" import { MemoryStorage } from "node-ts-cache-storage-memory"
import { organizationMemberRepository } from "@codeflash-ai/common" import { organizationMemberRepository } from "@codeflash-ai/common"
import { cookies } from "next/headers"
const RATE_LIMIT = 5 const RATE_LIMIT = 5
const RATE_LIMIT_WINDOW_MS = 60 * 1000 const RATE_LIMIT_WINDOW_MS = 60 * 1000
const rateLimitCache = new CacheContainer(new MemoryStorage()) const rateLimitCache = new CacheContainer(new MemoryStorage())
// TODO:: Find a way to save it in Session function getJwtSecret(): string {
const JWT_SECRET = process.env.JWT_SECRET || "abrakadabra-codeflash-jwt-secret" const secret = process.env.JWT_SECRET
if (!secret) {
if (!JWT_SECRET) { throw new Error("JWT_SECRET environment variable is required")
throw new Error("JWT_SECRET is not defined in environment variables") }
return secret
} }
const JWT_SECRET: string = getJwtSecret()
interface OAuthStatePayload { interface OAuthStatePayload {
userId: string userId: string
@ -72,8 +75,7 @@ export async function fetchUserInfo(): Promise<{
avatarUrl: session.user.picture, avatarUrl: session.user.picture,
}, },
} }
} catch (error) { } catch {
console.error("Error fetching user info:", error)
return { error: "Failed to fetch user info" } return { error: "Failed to fetch user info" }
} }
} }
@ -94,12 +96,84 @@ export async function fetchUserOrganizations(): Promise<{
} }
return { organizations: result.organizations } return { organizations: result.organizations }
} catch (error) { } catch {
console.error("Error fetching user organizations:", error)
return { error: "Failed to fetch organizations" } return { error: "Failed to fetch organizations" }
} }
} }
const OAUTH_COOKIE_NAME = "oauth_params"
interface OAuthParams {
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
vscodeState: string
}
export async function storeOAuthParams(params: OAuthParams): Promise<{ error?: string }> {
try {
const userId = await getUserId()
if (!userId) {
return { error: "Unauthorized" }
}
const signed = jwt.sign({ ...params, type: "oauth_params" }, JWT_SECRET, {
expiresIn: "10m",
})
const cookieStore = await cookies()
cookieStore.set(OAUTH_COOKIE_NAME, signed, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/codeflash/auth",
maxAge: 600,
})
return {}
} catch {
return { error: "Failed to store OAuth parameters" }
}
}
export async function getStoredOAuthParams(): Promise<{
params?: OAuthParams
error?: string
}> {
try {
const cookieStore = await cookies()
const cookie = cookieStore.get(OAUTH_COOKIE_NAME)
if (!cookie?.value) {
return { error: "Session expired. Please refresh the page and try again." }
}
const payload = jwt.verify(cookie.value, JWT_SECRET) as unknown as OAuthParams & {
type: string
}
if (payload.type !== "oauth_params") {
return { error: "Invalid session" }
}
return {
params: {
redirectUri: payload.redirectUri,
codeChallenge: payload.codeChallenge,
codeChallengeMethod: payload.codeChallengeMethod,
clientId: payload.clientId,
vscodeState: payload.vscodeState,
},
}
} catch {
return { error: "Session expired. Please refresh the page and try again." }
}
}
async function clearOAuthCookie() {
const cookieStore = await cookies()
cookieStore.delete(OAUTH_COOKIE_NAME)
}
export async function isRateLimited(userId: string): Promise<boolean> { export async function isRateLimited(userId: string): Promise<boolean> {
const cacheKey = `rate_limit_vsc_signin_${userId}` const cacheKey = `rate_limit_vsc_signin_${userId}`
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey) const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
@ -111,12 +185,10 @@ export async function isRateLimited(userId: string): Promise<boolean> {
{ count: 1, startTime: now }, { count: 1, startTime: now },
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 }, { ttl: RATE_LIMIT_WINDOW_MS / 1000 },
) )
console.log(`Rate limit initialized for user: ${userId}`)
return false return false
} }
if (record.count >= RATE_LIMIT) { if (record.count >= RATE_LIMIT) {
console.warn(`Rate limit exceeded for user: ${userId}, count: ${record.count}`)
return true return true
} }
@ -124,7 +196,6 @@ export async function isRateLimited(userId: string): Promise<boolean> {
await rateLimitCache.setItem(cacheKey, record, { await rateLimitCache.setItem(cacheKey, record, {
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000, ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
}) })
console.log(`Rate limit check passed for user: ${userId}, count: ${record.count}`)
return false return false
} }
@ -136,19 +207,9 @@ export async function createOAuthState(params: {
clientId: string clientId: string
orgId?: string orgId?: string
}): Promise<{ state: string; error?: string }> { }): Promise<{ state: string; error?: string }> {
console.log("=== Creating OAuth State (JWT) ===")
console.log("Params:", {
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge.substring(0, 10) + "...",
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
orgId: params.orgId,
})
try { try {
const userId = await getUserId() const userId = await getUserId()
if (!userId) { if (!userId) {
console.error("No user ID found - unauthorized")
return { state: "", error: "Unauthorized" } return { state: "", error: "Unauthorized" }
} }
if (params.orgId) { if (params.orgId) {
@ -158,11 +219,8 @@ export async function createOAuthState(params: {
} }
} }
console.log("User ID:", userId)
const limited = await isRateLimited(userId) const limited = await isRateLimited(userId)
if (limited) { if (limited) {
console.error("Rate limit exceeded for user:", userId)
return { state: "", error: "Rate limit exceeded" } return { state: "", error: "Rate limit exceeded" }
} }
@ -181,11 +239,8 @@ export async function createOAuthState(params: {
jwtid: crypto.randomBytes(16).toString("hex"), jwtid: crypto.randomBytes(16).toString("hex"),
}) })
console.log("OAuth state JWT created successfully")
return { state } return { state }
} catch (error) { } catch {
console.error("Error creating OAuth state:", error)
return { state: "", error: "Internal server error" } return { state: "", error: "Internal server error" }
} }
} }
@ -195,34 +250,24 @@ export async function authorizeOAuth(state: string): Promise<{
redirectUri?: string redirectUri?: string
error?: string error?: string
}> { }> {
console.log("=== Authorizing OAuth (JWT) ===")
try { try {
const userId = await getUserId() const userId = await getUserId()
if (!userId) { if (!userId) {
console.error("No user ID found - unauthorized")
return { error: "Unauthorized" } return { error: "Unauthorized" }
} }
console.log("User ID:", userId)
let oauthState: OAuthStatePayload let oauthState: OAuthStatePayload
try { try {
oauthState = jwt.verify(state, JWT_SECRET) as OAuthStatePayload oauthState = jwt.verify(state, JWT_SECRET) as unknown as OAuthStatePayload
} catch (error) { } catch {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired state" } return { error: "Invalid or expired state" }
} }
if (oauthState.type !== "oauth_state") { if (oauthState.type !== "oauth_state") {
console.error("Invalid token type:", oauthState.type)
return { error: "Invalid state token" } return { error: "Invalid state token" }
} }
console.log("OAuth state JWT verified successfully")
if (oauthState.userId !== userId) { if (oauthState.userId !== userId) {
console.error("User mismatch:", { expected: oauthState.userId, actual: userId })
return { error: "User mismatch" } return { error: "User mismatch" }
} }
@ -241,14 +286,13 @@ export async function authorizeOAuth(state: string): Promise<{
jwtid: crypto.randomBytes(16).toString("hex"), jwtid: crypto.randomBytes(16).toString("hex"),
}) })
console.log("Authorization code JWT created successfully") await clearOAuthCookie()
return { return {
code, code,
redirectUri: oauthState.redirectUri, redirectUri: oauthState.redirectUri,
} }
} catch (error) { } catch {
console.error("Error authorizing OAuth:", error)
return { error: "Internal server error" } return { error: "Internal server error" }
} }
} }
@ -263,80 +307,45 @@ interface TokenExchangeParams {
export async function exchangeCodeForToken( export async function exchangeCodeForToken(
params: TokenExchangeParams, params: TokenExchangeParams,
): Promise<{ accessToken?: string; error?: string }> { ): Promise<{ accessToken?: string; error?: string }> {
console.log("=== Exchanging Code for Token (JWT) ===")
console.log("Params:", {
codeVerifier: params.codeVerifier.substring(0, 10) + "...",
redirectUri: params.redirectUri,
clientId: params.clientId,
})
try { try {
let codeData: AuthCodePayload let codeData: AuthCodePayload
try { try {
codeData = jwt.verify(params.code, JWT_SECRET) as AuthCodePayload codeData = jwt.verify(params.code, JWT_SECRET) as unknown as AuthCodePayload
} catch (error) { } catch {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired authorization code" } return { error: "Invalid or expired authorization code" }
} }
if (codeData.type !== "auth_code") { if (codeData.type !== "auth_code") {
console.error("Invalid token type:", codeData.type)
return { error: "Invalid authorization code" } return { error: "Invalid authorization code" }
} }
console.log("✓ Authorization code JWT verified successfully!")
console.log("Code data:", {
userId: codeData.userId,
redirectUri: codeData.redirectUri,
clientId: codeData.clientId,
})
if (codeData.clientId !== params.clientId) { if (codeData.clientId !== params.clientId) {
console.error("Client ID mismatch:", { expected: codeData.clientId, actual: params.clientId })
return { error: "Client ID mismatch" } return { error: "Client ID mismatch" }
} }
if (codeData.redirectUri !== params.redirectUri) { if (codeData.redirectUri !== params.redirectUri) {
console.error("Redirect URI mismatch:", {
expected: codeData.redirectUri,
actual: params.redirectUri,
})
return { error: "Redirect URI mismatch" } return { error: "Redirect URI mismatch" }
} }
console.log("Computing code challenge...")
const computedChallenge = crypto const computedChallenge = crypto
.createHash(codeData.codeChallengeMethod) .createHash(codeData.codeChallengeMethod)
.update(params.codeVerifier) .update(params.codeVerifier)
.digest("base64url") .digest("base64url")
if (computedChallenge !== codeData.codeChallenge) { if (computedChallenge !== codeData.codeChallenge) {
console.error("Code verifier validation failed")
return { error: "Code verifier validation failed" } return { error: "Code verifier validation failed" }
} }
console.log("✓ PKCE validation successful")
console.log("Generating API token for userId:", codeData.userId, "orgId:", codeData.orgId)
try { try {
const apiKey = await generateTokenForVsCode(codeData.userId, codeData.orgId) const apiKey = await generateTokenForVsCode(codeData.userId, codeData.orgId)
console.log("API token generated successfully")
console.log("=== Token Exchange Completed Successfully ===")
return { accessToken: apiKey.token } return { accessToken: apiKey.token }
} catch (tokenError: unknown) { } catch (tokenError: unknown) {
if (tokenError instanceof Error && tokenError.message === "NEXT_REDIRECT") { if (tokenError instanceof Error && tokenError.message === "NEXT_REDIRECT") {
console.error("Caught redirect error during token generation")
return { error: "Authentication required" } return { error: "Authentication required" }
} }
console.error("Error generating token:", tokenError)
return { error: "Failed to generate API token" } return { error: "Failed to generate API token" }
} }
} catch (error) { } catch {
console.error("=== Token Exchange Failed ===")
console.error("Error:", error)
return { error: "Internal server error" } return { error: "Internal server error" }
} }
} }

View file

@ -10,6 +10,8 @@ import {
createOAuthState, createOAuthState,
fetchUserOrganizations, fetchUserOrganizations,
fetchUserInfo, fetchUserInfo,
storeOAuthParams,
getStoredOAuthParams,
Organization, Organization,
UserInfo, UserInfo,
} from "./action" } from "./action"
@ -59,15 +61,6 @@ export default function CodeFlashAuthContent() {
const hasCheckedAuth = useRef(false) const hasCheckedAuth = useRef(false)
useEffect(() => { useEffect(() => {
// Check if user already authenticated in this session
const authenticated = sessionStorage.getItem("oauth_authenticated")
if (authenticated === "true") {
setHasAuthenticated(true)
setStep("waiting")
setIsCheckingAuth(false)
return
}
// Guard against duplicate runs (React Strict Mode, Suspense remounts) // Guard against duplicate runs (React Strict Mode, Suspense remounts)
if (hasCheckedAuth.current) return if (hasCheckedAuth.current) return
hasCheckedAuth.current = true hasCheckedAuth.current = true
@ -121,16 +114,22 @@ export default function CodeFlashAuthContent() {
setOrganizations(orgsResult.organizations) setOrganizations(orgsResult.organizations)
} }
// Store OAuth params for later use when user clicks authenticate // Store OAuth params in server-side HttpOnly cookie
sessionStorage.setItem("oauth_redirect_uri", redirectUri) const storeResult = await storeOAuthParams({
sessionStorage.setItem("oauth_code_challenge", codeChallenge) redirectUri,
sessionStorage.setItem("oauth_code_challenge_method", codeChallengeMethod) codeChallenge,
sessionStorage.setItem("oauth_client_id", clientId) codeChallengeMethod,
sessionStorage.setItem("oauth_vscode_state", state) clientId,
vscodeState: state,
})
if (storeResult.error) {
setError(storeResult.error)
return
}
setStep("ready") setStep("ready")
} catch (err) { } catch {
console.error("Error checking authentication:", err)
setError("An unexpected error occurred. Please try again.") setError("An unexpected error occurred. Please try again.")
} finally { } finally {
setIsCheckingAuth(false) setIsCheckingAuth(false)
@ -152,19 +151,18 @@ export default function CodeFlashAuthContent() {
setStep("authorizing") setStep("authorizing")
try { try {
const redirectUri = sessionStorage.getItem("oauth_redirect_uri") // Retrieve OAuth params from server-side HttpOnly cookie
const codeChallenge = sessionStorage.getItem("oauth_code_challenge") const stored = await getStoredOAuthParams()
const codeChallengeMethod = sessionStorage.getItem("oauth_code_challenge_method") if (stored.error || !stored.params) {
const clientId = sessionStorage.getItem("oauth_client_id") setError(stored.error || "Session expired. Please refresh the page and try again.")
const vscodeState = sessionStorage.getItem("oauth_vscode_state")
if (!redirectUri || !codeChallenge || !codeChallengeMethod || !clientId || !vscodeState) {
setError("Session expired. Please refresh the page and try again.")
setIsLoading(false) setIsLoading(false)
setStep("ready") setStep("ready")
return return
} }
const { redirectUri, codeChallenge, codeChallengeMethod, clientId, vscodeState } =
stored.params
// Create OAuth state with selected org // Create OAuth state with selected org
const stateResult = await createOAuthState({ const stateResult = await createOAuthState({
redirectUri, redirectUri,
@ -205,30 +203,19 @@ export default function CodeFlashAuthContent() {
return return
} }
// Mark as authenticated
sessionStorage.setItem("oauth_authenticated", "true")
setHasAuthenticated(true) setHasAuthenticated(true)
// Clean up OAuth state
sessionStorage.removeItem("oauth_redirect_uri")
sessionStorage.removeItem("oauth_code_challenge")
sessionStorage.removeItem("oauth_code_challenge_method")
sessionStorage.removeItem("oauth_client_id")
sessionStorage.removeItem("oauth_vscode_state")
// Redirect back to VS Code with code, state, and theme // Redirect back to VS Code with code, state, and theme
const redirectUrl = new URL(result.redirectUri) const redirectUrl = new URL(result.redirectUri)
redirectUrl.searchParams.set("code", result.code) redirectUrl.searchParams.set("code", result.code)
redirectUrl.searchParams.set("state", vscodeState) redirectUrl.searchParams.set("state", vscodeState)
redirectUrl.searchParams.set("theme", theme) // Add theme parameter redirectUrl.searchParams.set("theme", theme)
setStep("waiting") setStep("waiting")
setIsLoading(false) setIsLoading(false)
// Redirect immediately
window.location.href = redirectUrl.toString() window.location.href = redirectUrl.toString()
} catch (err) { } catch {
console.error("Error authorizing:", err)
setError("An error occurred. Please try again.") setError("An error occurred. Please try again.")
setIsLoading(false) setIsLoading(false)
setStep("ready") setStep("ready")

View file

@ -384,10 +384,6 @@ export async function createPullRequest({
const authorizedRepository = ( const authorizedRepository = (
authorizedEvent.event as { repository?: { full_name?: string | null } | null } authorizedEvent.event as { repository?: { full_name?: string | null } | null }
).repository ).repository
if (
full_repo_name &&
authorizedRepository?.full_name &&
authorizedRepository.full_name !== full_repo_name
if (full_repo_name && !authorizedRepository?.full_name) { if (full_repo_name && !authorizedRepository?.full_name) {
return createErrorResponse("Repository not found for this optimization event") return createErrorResponse("Repository not found for this optimization event")
} }
@ -398,8 +394,6 @@ export async function createPullRequest({
) { ) {
return createErrorResponse("Repository mismatch for optimization event") return createErrorResponse("Repository mismatch for optimization event")
} }
return createErrorResponse("Repository mismatch for optimization event")
}
const cfapiUrl = process.env.CODEFLASH_CFAPI_URL const cfapiUrl = process.env.CODEFLASH_CFAPI_URL
const session = await auth0.getAccessToken() const session = await auth0.getAccessToken()

View file

@ -132,6 +132,12 @@ function baseParams(systemPrompt: string, conversationMessages: Anthropic.Messag
} }
export async function POST(request: NextRequest): Promise<Response> { export async function POST(request: NextRequest): Promise<Response> {
const { auth0 } = await import("@/lib/auth0")
const session = await auth0.getSession()
if (!session?.user?.sub) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
let client: Anthropic let client: Anthropic
try { try {
client = getClient() client = getClient()

View file

@ -1,7 +1,13 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { auth0 } from "@/lib/auth0"
export async function GET(request: NextRequest): Promise<NextResponse> { export async function GET(request: NextRequest): Promise<NextResponse> {
const session = await auth0.getSession()
if (!session?.user?.sub) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const callId = request.nextUrl.searchParams.get("callId") const callId = request.nextUrl.searchParams.get("callId")
if (!callId) { if (!callId) {

View file

@ -12,8 +12,11 @@ export async function GET(request: NextRequest) {
} }
const { searchParams } = request.nextUrl const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get("page") || "1", 10) const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1)
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10) const pageSize = Math.min(
100,
Math.max(1, parseInt(searchParams.get("pageSize") || "10", 10) || 10),
)
const search = searchParams.get("search") || "" const search = searchParams.get("search") || ""
const repositoryId = searchParams.get("repositoryId") || undefined const repositoryId = searchParams.get("repositoryId") || undefined
const status = searchParams.get("status") || "all" const status = searchParams.get("status") || "all"
@ -38,8 +41,16 @@ export async function GET(request: NextRequest) {
filter.review_quality = reviewQuality filter.review_quality = reviewQuality
} }
// Build sort object // Build sort object with allowlisted fields
const [sortField, sortDirection] = sortBy.split("_").reduce( const ALLOWED_SORT_FIELDS = new Set([
"created_at",
"updated_at",
"status",
"event_type",
"function_name",
"repository_id",
])
const [parsedField, parsedDirection] = sortBy.split("_").reduce(
(acc, part, index, arr) => { (acc, part, index, arr) => {
if (index === arr.length - 1 && (part === "asc" || part === "desc")) { if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
return [acc[0], part] return [acc[0], part]
@ -48,8 +59,11 @@ export async function GET(request: NextRequest) {
}, },
["", "desc"] as [string, string], ["", "desc"] as [string, string],
) )
const sortField = ALLOWED_SORT_FIELDS.has(parsedField) ? parsedField : "created_at"
const sortDirection =
parsedDirection === "asc" || parsedDirection === "desc" ? parsedDirection : "desc"
const sort: Record<string, "asc" | "desc"> = { const sort: Record<string, "asc" | "desc"> = {
[sortField]: sortDirection as "asc" | "desc", [sortField]: sortDirection,
} }
const data = await getAllOptimizationEvents({ const data = await getAllOptimizationEvents({
@ -66,9 +80,6 @@ export async function GET(request: NextRequest) {
}) })
} catch (error) { } catch (error) {
console.error("Failed to fetch optimization events:", error) console.error("Failed to fetch optimization events:", error)
return NextResponse.json( return NextResponse.json({ error: "Internal server error" }, { status: 500 })
{ error: error instanceof Error ? error.message : "Internal server error" },
{ status: 500 },
)
} }
} }

View file

@ -31,8 +31,11 @@ export async function GET(request: NextRequest) {
} }
const { searchParams } = request.nextUrl const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get("page") || "1", 10) const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1)
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10) const pageSize = Math.min(
100,
Math.max(1, parseInt(searchParams.get("pageSize") || "10", 10) || 10),
)
const eventTypeFilter = searchParams.get("eventTypeFilter") || "all" const eventTypeFilter = searchParams.get("eventTypeFilter") || "all"
const repositoryId = searchParams.get("repositoryId") || undefined const repositoryId = searchParams.get("repositoryId") || undefined
@ -42,9 +45,6 @@ export async function GET(request: NextRequest) {
}) })
} catch (error) { } catch (error) {
console.error("Failed to fetch optimization PRs:", error) console.error("Failed to fetch optimization PRs:", error)
return NextResponse.json( return NextResponse.json({ error: "Internal server error" }, { status: 500 })
{ error: error instanceof Error ? error.message : "Internal server error" },
{ status: 500 },
)
} }
} }

View file

@ -58,10 +58,10 @@ async function IntercomScript() {
const bootSnippet = session const bootSnippet = session
? `window.Intercom('boot', { ? `window.Intercom('boot', {
app_id: "ljxo1nzr", app_id: "ljxo1nzr",
name: "${session.user.name}", name: ${JSON.stringify(session.user.name ?? "")},
nickname: "${session.user.nickname}", nickname: ${JSON.stringify(session.user.nickname ?? "")},
picture: "${session.user.picture}", picture: ${JSON.stringify(session.user.picture ?? "")},
user_id: "${session.user.sub}", user_id: ${JSON.stringify(session.user.sub ?? "")},
email: null, email: null,
});` });`
: `window.Intercom('boot', { app_id: "ljxo1nzr" });` : `window.Intercom('boot', { app_id: "ljxo1nzr" });`

View file

@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from "next/server" import { type NextRequest, NextResponse } from "next/server"
import { auth0 } from "@/lib/auth0"
import { getTraceData, type TraceData } from "@/app/observability/lib/get-trace-data" import { getTraceData, type TraceData } from "@/app/observability/lib/get-trace-data"
import { transformToTimelineSections } from "@/app/observability/components/timeline-types" import { transformToTimelineSections } from "@/app/observability/components/timeline-types"
import { formatTimelineForLLM } from "@/app/observability/components/format-llm-export" import { formatTimelineForLLM } from "@/app/observability/components/format-llm-export"
@ -21,6 +22,11 @@ function getFilePath(
} }
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const session = await auth0.getSession()
if (!session?.user?.sub) {
return new NextResponse("Unauthorized", { status: 401 })
}
const traceId = request.nextUrl.searchParams.get("trace_id")?.trim() const traceId = request.nextUrl.searchParams.get("trace_id")?.trim()
if (!traceId) { if (!traceId) {

View file

@ -3,6 +3,7 @@
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
import { marked } from "marked" import { marked } from "marked"
import DOMPurify from "dompurify"
interface MarkdownViewerProps { interface MarkdownViewerProps {
content: string content: string
@ -20,9 +21,9 @@ export function MarkdownViewer({ content, className = "" }: MarkdownViewerProps)
gfm: true, gfm: true,
}) })
const html = await marked(content) const rawHtml = await marked(content)
if (containerRef.current) { if (containerRef.current) {
containerRef.current.innerHTML = html containerRef.current.innerHTML = DOMPurify.sanitize(rawHtml)
const codeBlocks = containerRef.current.querySelectorAll("pre code") const codeBlocks = containerRef.current.querySelectorAll("pre code")
codeBlocks.forEach(block => { codeBlocks.forEach(block => {
block.classList.add("bg-muted", "p-2", "rounded", "text-sm") block.classList.add("bg-muted", "p-2", "rounded", "text-sm")

View file

@ -1,22 +0,0 @@
export async function fetchFromAPI(endpoint: string, options: RequestInit = {}) {
const apiKey = process.env.NEXT_PUBLIC_CF_API_KEY || localStorage.getItem("cf_api_key")
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"}${endpoint}`,
{
...options,
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey || "",
...options.headers,
},
},
)
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || "API request failed")
}
return response.json()
}

View file

@ -57,16 +57,14 @@ function getAuth0Client(): Auth0Client {
}, },
async onCallback(error, context, session) { async onCallback(error, context, session) {
if (error) { if (error) {
console.error("[Auth] Error in callback:", error)
const errorMessage = error.message || "" const errorMessage = error.message || ""
if (errorMessage.includes("allowlist-fail")) { if (errorMessage.includes("allowlist-fail")) {
const re = /allowlist-fail\s(.*)\s(.*)\)/ const re = /allowlist-fail\s(.*)\s(.*)\)/
const match = errorMessage.match(re) const match = errorMessage.match(re)
if (match != null) { if (match != null) {
const userId = match[1]
const userNickname = match[2] const userNickname = match[2]
return redirectTo(`/waitlist?username=${userNickname}&userid=${userId}`) return redirectTo(`/waitlist?username=${userNickname}`)
} }
} }
@ -78,22 +76,14 @@ function getAuth0Client(): Auth0Client {
} }
const user = session.user const user = session.user
console.log(`[Auth] Processing login for user: ${user.sub}`)
if (!user.sub || !user.nickname) { if (!user.sub || !user.nickname) {
console.error("[Auth] Missing required user fields")
return redirectTo(context.returnTo || APP_ROUTES.BASE) return redirectTo(context.returnTo || APP_ROUTES.BASE)
} }
try { try {
// Save user to database (must complete before checking onboarding)
console.log("[Auth] Saving user to database...")
await createOrUpdateUser(user.sub, user.nickname, user.email ?? null, user.name ?? null) await createOrUpdateUser(user.sub, user.nickname, user.email ?? null, user.name ?? null)
console.log("[Auth] User saved successfully")
// Track login and check onboarding in parallel — both only need the
// user to exist (which createOrUpdateUser ensures), and neither
// depends on the other's result.
const [, completedOnboarding] = await Promise.all([ const [, completedOnboarding] = await Promise.all([
trackUserLogin({ trackUserLogin({
userId: user.sub, userId: user.sub,
@ -103,11 +93,9 @@ function getAuth0Client(): Auth0Client {
}), }),
hasCompletedOnboarding(user.sub), hasCompletedOnboarding(user.sub),
]) ])
console.log(`[Auth] Onboarding completed: ${completedOnboarding}`)
const intendedDestination = context.returnTo || APP_ROUTES.BASE const intendedDestination = context.returnTo || APP_ROUTES.BASE
// Check if the path is codeflash/auth/[token]
const isAuthPath = const isAuthPath =
intendedDestination.startsWith("/codeflash/auth") || intendedDestination.startsWith("/codeflash/auth") ||
intendedDestination.includes("/codeflash/auth") intendedDestination.includes("/codeflash/auth")
@ -117,8 +105,7 @@ function getAuth0Client(): Auth0Client {
} }
return redirectTo(intendedDestination) return redirectTo(intendedDestination)
} catch (err) { } catch {
console.error("[Auth] Error in onCallback:", err)
return redirectTo(context.returnTo || APP_ROUTES.BASE) return redirectTo(context.returnTo || APP_ROUTES.BASE)
} }
}, },

View file

@ -58,7 +58,7 @@ importers:
version: 2.6.1(@opentelemetry/api@1.9.1) version: 2.6.1(@opentelemetry/api@1.9.1)
'@prisma/client': '@prisma/client':
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@sentry/node': '@sentry/node':
specifier: ^10.48.0 specifier: ^10.48.0
version: 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.1)) version: 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.1))
@ -200,7 +200,7 @@ importers:
version: 0.214.0(@opentelemetry/api@1.9.1) version: 0.214.0(@opentelemetry/api@1.9.1)
'@prisma/client': '@prisma/client':
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@prisma/instrumentation': '@prisma/instrumentation':
specifier: ^7.6.0 specifier: ^7.6.0
version: 7.7.0(@opentelemetry/api@1.9.1) version: 7.7.0(@opentelemetry/api@1.9.1)
@ -270,6 +270,9 @@ importers:
diff: diff:
specifier: ^8.0.2 specifier: ^8.0.2
version: 8.0.4 version: 8.0.4
dompurify:
specifier: ^3.3.3
version: 3.3.3
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.3 version: 9.0.3
@ -361,6 +364,9 @@ importers:
'@testing-library/react': '@testing-library/react':
specifier: ^16.0.0 specifier: ^16.0.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/dompurify':
specifier: ^3.2.0
version: 3.2.0
'@types/jsonwebtoken': '@types/jsonwebtoken':
specifier: ^9.0.10 specifier: ^9.0.10
version: 9.0.10 version: 9.0.10
@ -430,7 +436,7 @@ importers:
version: 7.7.0 version: 7.7.0
'@prisma/client': '@prisma/client':
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@prisma/sqlcommenter-query-insights': '@prisma/sqlcommenter-query-insights':
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.7.0 version: 7.7.0
@ -3178,6 +3184,10 @@ packages:
'@types/deep-eql@4.0.2': '@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/eslint-scope@3.7.7': '@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@ -9839,7 +9849,7 @@ snapshots:
'@prisma/client-runtime-utils@7.7.0': {} '@prisma/client-runtime-utils@7.7.0': {}
'@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)': '@prisma/client@7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)':
dependencies: dependencies:
'@prisma/client-runtime-utils': 7.7.0 '@prisma/client-runtime-utils': 7.7.0
optionalDependencies: optionalDependencies:
@ -10816,6 +10826,10 @@ snapshots:
'@types/deep-eql@4.0.2': {} '@types/deep-eql@4.0.2': {}
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.3
'@types/eslint-scope@3.7.7': '@types/eslint-scope@3.7.7':
dependencies: dependencies:
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
@ -12341,7 +12355,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.3 '@next/eslint-plugin-next': 16.2.3
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
@ -12376,7 +12390,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@ -12391,13 +12405,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)): eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -12428,7 +12442,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3