mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
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:
commit
e6cec80c9d
16 changed files with 226 additions and 194 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" });`
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue