diff --git a/.github/workflows/railway-deploy.yml b/.github/workflows/railway-deploy.yml index 4ba4ad0..b66329c 100644 --- a/.github/workflows/railway-deploy.yml +++ b/.github/workflows/railway-deploy.yml @@ -70,6 +70,8 @@ jobs: - name: Build Next.js application run: npm run build + env: + SKIP_ENV_VALIDATION: true - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/middleware.ts b/middleware.ts index 34cae11..dc103a6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,10 +1,13 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; /** * ============================================================================ - * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION + * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION & SECURITY * ============================================================================ * This middleware protects routes and handles authentication across the app. + * Also adds security headers to all responses. * * Route Protection: * - Public routes: Landing page, sign-in, sign-up, public API endpoints @@ -14,6 +17,7 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; * 1. Public routes are accessible to everyone * 2. Protected routes redirect to sign-in if not authenticated * 3. API routes return 401 if not authenticated (handled in route handlers) + * 4. Security headers are added to all responses */ // Define public routes that don't require authentication @@ -22,13 +26,71 @@ const isPublicRoute = createRouteMatcher([ "/sign-in(.*)", // Sign in page and sub-routes "/sign-up(.*)", // Sign up page and sub-routes "/api/webhooks/(.*)", // Webhook endpoints (verified by webhook secret) + "/api/health", // Health check endpoint ]); -export default clerkMiddleware(async (auth, request) => { +/** + * Security headers to protect against common attacks + */ +const securityHeaders = { + // Prevent clickjacking + "X-Frame-Options": "DENY", + // Prevent MIME type sniffing + "X-Content-Type-Options": "nosniff", + // Control referrer information + "Referrer-Policy": "strict-origin-when-cross-origin", + // Restrict browser features + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + // XSS protection (legacy, but still useful) + "X-XSS-Protection": "1; mode=block", +}; + +/** + * Content Security Policy - adjust as needed for your specific requirements + */ +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + img-src 'self' blob: data: https: http:; + font-src 'self' https://fonts.gstatic.com data:; + connect-src 'self' https://*.clerk.accounts.dev https://clerk.com https://api.clerk.com https://*.github.com https://api.github.com wss://*.clerk.accounts.dev https://prod.spline.design https://*.firebase.com https://*.firebaseio.com https://*.googleapis.com; + frame-src 'self' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + worker-src 'self' blob:; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; +`.replace(/\s{2,}/g, ' ').trim(); + +export default clerkMiddleware(async (auth, request: NextRequest) => { // Protect all routes except public ones if (!isPublicRoute(request)) { await auth.protect(); } + + // Get response (will be created by next middleware/handler) + const response = NextResponse.next(); + + // Add security headers + for (const [key, value] of Object.entries(securityHeaders)) { + response.headers.set(key, value); + } + + // Add CSP header (only in production to avoid dev issues) + if (process.env.NODE_ENV === "production") { + response.headers.set("Content-Security-Policy", cspHeader); + } + + // Add HSTS header (only in production over HTTPS) + if (process.env.NODE_ENV === "production") { + response.headers.set( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains; preload" + ); + } + + return response; }); export const config = { @@ -39,3 +101,4 @@ export const config = { "/(api|trpc)(.*)", ], }; + diff --git a/next.config.ts b/next.config.ts index 2636543..091532c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,12 +15,28 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_SEPOLIA_RPC_URL: process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL, NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS: process.env.NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS, }, - // Optimize image handling + // Optimize image handling with specific domains images: { remotePatterns: [ { protocol: 'https', - hostname: '**', + hostname: 'images.unsplash.com', + }, + { + protocol: 'https', + hostname: 'assets.aceternity.com', + }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + { + protocol: 'https', + hostname: 'img.clerk.com', + }, + { + protocol: 'https', + hostname: 'firebasestorage.googleapis.com', }, ], }, diff --git a/nixpacks.toml b/nixpacks.toml index cb00d07..396d648 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,10 +2,18 @@ nixPkgs = ["nodejs_20"] [phases.install] -cmds = ["npm ci --legacy-peer-deps"] +cmds = ["npm install --legacy-peer-deps"] [phases.build] -cmds = ["npm run build"] +cmds = [ + "rm -rf .next/cache", + "rm -rf node_modules/.cache", + "npm run build" +] + +[variables] +SKIP_ENV_VALIDATION = "true" +NODE_ENV = "production" [start] cmd = "npm start" diff --git a/package.json b/package.json index 382f5c9..88d56aa 100644 --- a/package.json +++ b/package.json @@ -100,4 +100,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/src/app/LandingPageClient.tsx b/src/app/LandingPageClient.tsx new file mode 100644 index 0000000..aa5ee5a --- /dev/null +++ b/src/app/LandingPageClient.tsx @@ -0,0 +1,112 @@ +"use client"; + +import nextDynamic from 'next/dynamic'; +import { Suspense } from 'react'; +import { Header, HeroSection, HeroHighlightSection, StickyScrollRevealDemo } from '@/components/layout'; +import { StickyFooter } from "@/components/ui/sticky-footer"; + +// Loading skeleton for heavy sections +const SectionSkeleton = ({ height = "h-[500px]" }: { height?: string }) => ( +
+
+
+); + +// Dynamic imports with loading states for heavy components +const FeaturesSectionDemo = nextDynamic( + () => import("@/components/ui/features-section-demo-3"), + { + loading: () => , + ssr: true, + } +); + +const AnimatedTestimonialsDemo = nextDynamic( + () => import("@/components/ui/animated-testimonials-demo"), + { + loading: () => , + ssr: false, // Disable SSR for client-only animations + } +); + +const SplineSceneDemo = nextDynamic( + () => import("@/components/ui/spline-scene-demo").then(mod => ({ default: mod.SplineSceneDemo })), + { + loading: () => , + ssr: false, // Spline is client-only + } +); + +const TextHoverEffect = nextDynamic( + () => import("@/components/ui/text-hover-effect").then(mod => ({ default: mod.TextHoverEffect })), + { + loading: () => , + ssr: false, + } +); + +const CallToAction = nextDynamic( + () => import("@/components/ui/cta").then(mod => ({ default: mod.CallToAction })), + { + loading: () => , + ssr: true, + } +); + +export default function LandingPageClient() { + return ( +
+
+ + {/* Hero Section with Background Ripple Effect */} + + + {/* Text Highlight Section */} + + + {/* Interactive 3D Spline Scene - Lazy loaded */} +
+ }> + + +
+ + {/* Sticky Scroll Features Section */} + + + {/* Features Section - Contains heavy Globe component */} + }> + + + + {/* GHOSTFOUNDER Text Effect */} +
+ }> + + +
+ + {/* Animated Testimonials Section */} + }> + + + + {/* CTA Section */} +
+ }> + + +
+ + {/* Sticky Footer Reveal */} + +
+ ); +} diff --git a/src/app/api/code-police/analyze/route.ts b/src/app/api/code-police/analyze/route.ts index b5af941..694d21c 100644 --- a/src/app/api/code-police/analyze/route.ts +++ b/src/app/api/code-police/analyze/route.ts @@ -20,17 +20,6 @@ import type { DocumentData, QueryDocumentSnapshot, Firestore } from "firebase-ad * Analyzes code from a GitHub repository and optionally sends email report. */ -// Helper to remove undefined values for Firestore -const sanitizeForFirestore = >(obj: T): T => { - const result: Record = {}; - for (const [key, value] of Object.entries(obj)) { - if (value !== undefined) { - result[key] = value; - } - } - return result as T; -}; - export async function POST(request: NextRequest) { try { const { userId } = await auth(); @@ -125,38 +114,8 @@ export async function POST(request: NextRequest) { createdAt: now, }); - // Fetch commit details - if no SHA provided, get latest commit - let commit; - let actualCommitSha = commitSha; - - if (!commitSha || commitSha === "latest") { - console.log("[Analyze] No commit SHA provided, fetching latest commit from main branch..."); - // Fetch the list of commits to get the latest SHA - const commitsResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`, - { - headers: { - Authorization: `Bearer ${githubToken}`, - Accept: "application/vnd.github.v3+json", - }, - } - ); - - if (!commitsResponse.ok) { - const errorData = await commitsResponse.json().catch(() => ({})); - throw new Error(`Failed to fetch commits: ${commitsResponse.status} ${errorData.message || commitsResponse.statusText}`); - } - - const commits = await commitsResponse.json(); - if (!commits || commits.length === 0) { - throw new Error("No commits found in repository"); - } - - actualCommitSha = commits[0].sha; - console.log("[Analyze] Using latest commit:", actualCommitSha); - } - - commit = await fetchCommit(githubToken, owner, repo, actualCommitSha); + // Fetch commit details + const commit = await fetchCommit(githubToken, owner, repo, commitSha); // ======================================================================== // FILE FILTERING - Exclude non-source files from analysis @@ -240,7 +199,7 @@ export async function POST(request: NextRequest) { // Ensure commit.files exists const commitFiles = commit.files || []; - console.log(`[Analyze] Commit ${actualCommitSha}: ${commitFiles.length} files changed`); + console.log(`[Analyze] Commit ${commitSha}: ${commitFiles.length} files changed`); if (commitFiles.length === 0) { console.log(`[Analyze] ⚠️ No files in commit to analyze`); @@ -263,7 +222,7 @@ export async function POST(request: NextRequest) { owner, repo, file.filename, - actualCommitSha + commitSha ); // Skip very large files (> 50KB) to avoid token limits @@ -317,13 +276,13 @@ export async function POST(request: NextRequest) { const batch = adminDb.batch(); for (const issue of fullIssues) { const issueRef = analysisRef.collection("issues").doc(issue.id); - batch.set(issueRef, sanitizeForFirestore(issue as unknown as Record)); + batch.set(issueRef, issue); } // Generate summary const summary = await generateAnalysisSummary({ repoName: `${owner}/${repo}`, - commitSha: actualCommitSha, + commitSha, branch: "main", issues: fullIssues, }); @@ -369,7 +328,7 @@ export async function POST(request: NextRequest) { issues: fullIssues, summary, repoName: `${owner}/${repo}`, - commitUrl: `https://github.com/${owner}/${repo}/commit/${actualCommitSha}`, + commitUrl: `https://github.com/${owner}/${repo}/commit/${commitSha || 'HEAD'}`, }); await analysisRef.update({ emailStatus: "sent", emailSentTo: emailTo }); diff --git a/src/app/api/code-police/projects/[id]/route.ts b/src/app/api/code-police/projects/[id]/route.ts index 607b9f3..670bf0a 100644 --- a/src/app/api/code-police/projects/[id]/route.ts +++ b/src/app/api/code-police/projects/[id]/route.ts @@ -31,19 +31,19 @@ export async function GET( hasClientEmail: !!process.env.FIREBASE_CLIENT_EMAIL, hasPrivateKey: !!process.env.FIREBASE_PRIVATE_KEY, }); - return NextResponse.json({ - error: "Database not configured. Please check server logs for Firebase Admin initialization errors." + return NextResponse.json({ + error: "Database not configured. Please check server logs for Firebase Admin initialization errors." }, { status: 503 }); } const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } - const project = { id: projectDoc.id, ...projectDoc.data() } as { id: string; userId?: string;[key: string]: unknown }; - + const project = { id: projectDoc.id, ...projectDoc.data() } as { id: string; userId?: string; [key: string]: unknown }; + // Verify ownership if (project.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -68,7 +68,7 @@ export async function PATCH( const { id } = await params; const body = await request.json(); - const { status, customRules, ownerEmail, notificationPrefs, autoFixEnabled } = body; + const { status, customRules, ownerEmail, notificationPrefs } = body; const adminDb = getAdminDb(); if (!adminDb) { @@ -77,13 +77,13 @@ export async function PATCH( // Fetch existing project const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const existingProject = projectDoc.data(); - + // Verify ownership if (existingProject?.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -101,7 +101,7 @@ export async function PATCH( return NextResponse.json({ error: "Invalid status" }, { status: 400 }); } updateData.status = status; - + // If status is changing to 'stopped', also set isActive to false for backwards compat if (status === 'stopped') { updateData.isActive = false; @@ -131,19 +131,14 @@ export async function PATCH( }; } - // Set auto-fix enabled - if (autoFixEnabled !== undefined) { - updateData.autoFixEnabled = Boolean(autoFixEnabled); - } - await adminDb.collection("projects").doc(id).update(updateData); // Fetch updated project const updatedDoc = await adminDb.collection("projects").doc(id).get(); const updatedProject = { id: updatedDoc.id, ...updatedDoc.data() }; - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, project: updatedProject, message: `Project updated successfully`, }); @@ -171,13 +166,13 @@ export async function DELETE( // Fetch existing project const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const project = projectDoc.data(); - + // Verify ownership if (project?.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -189,7 +184,7 @@ export async function DELETE( // Get user's GitHub token const userDoc = await adminDb.collection("users").doc(userId).get(); const githubToken = userDoc.data()?.githubAccessToken; - + if (githubToken) { await deleteWebhook( githubToken, @@ -213,16 +208,16 @@ export async function DELETE( .collection("analysis_runs") .where("projectId", "==", id) .get(); - + const batch = adminDb.batch(); runsSnapshot.docs.forEach(doc => { batch.delete(doc.ref); }); await batch.commit(); - return NextResponse.json({ - success: true, - message: "Project deleted successfully" + return NextResponse.json({ + success: true, + message: "Project deleted successfully" }); } catch (error) { console.error("Error deleting project:", error); diff --git a/src/app/api/equity/projects/route.ts b/src/app/api/equity/projects/route.ts index cc91e8c..316af6a 100644 --- a/src/app/api/equity/projects/route.ts +++ b/src/app/api/equity/projects/route.ts @@ -92,10 +92,15 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } + // Normalize githubRepoId to number for consistent querying + const normalizedRepoId = typeof githubRepoId === 'string' + ? parseInt(githubRepoId, 10) + : githubRepoId; + // Check if tokens have already been minted for this repository const existingProjectSnapshot = await adminDb .collection("equity_projects") - .where("githubRepoId", "==", githubRepoId) + .where("githubRepoId", "==", normalizedRepoId) .limit(1) .get(); @@ -114,7 +119,7 @@ export async function POST(request: Request) { symbol, contractAddress, totalSupply: totalSupply || "1000000", - githubRepoId, + githubRepoId: normalizedRepoId, // Always store as number githubRepoFullName, githubRepoOwner, ownerWalletAddress: ownerWalletAddress || null, diff --git a/src/app/api/equity/verify-owner/route.ts b/src/app/api/equity/verify-owner/route.ts index 210395c..09ad6a9 100644 --- a/src/app/api/equity/verify-owner/route.ts +++ b/src/app/api/equity/verify-owner/route.ts @@ -30,16 +30,25 @@ export async function GET(request: Request) { let githubToken: string | null = null; try { - const tokens = await clerk.users.getUserOauthAccessToken(userId, "github"); + // Clerk uses "oauth_github" as the provider name + const tokens = await clerk.users.getUserOauthAccessToken(userId, "oauth_github"); if (tokens.data && tokens.data.length > 0) { githubToken = tokens.data[0].token; } } catch { - return NextResponse.json({ - isOwner: false, - error: "GitHub not connected", - message: "Please connect your GitHub account to verify repository ownership.", - }); + // Also try legacy "github" provider name for backwards compatibility + try { + const tokens = await clerk.users.getUserOauthAccessToken(userId, "github"); + if (tokens.data && tokens.data.length > 0) { + githubToken = tokens.data[0].token; + } + } catch { + return NextResponse.json({ + isOwner: false, + error: "GitHub not connected", + message: "Please connect your GitHub account to verify repository ownership.", + }); + } } if (!githubToken) { diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index e83ad5a..1375085 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -7,7 +7,6 @@ import { generateAnalysisSummary, } from "@/lib/agents/code-police/analyzer"; import { sendAnalysisReport } from "@/lib/agents/code-police/email"; -import { generateAndCreateFixPR } from "@/lib/agents/code-police/auto-fix"; import { fetchCommit, fetchFileContent, postPRComment, formatPRComment, getDependentFiles } from "@/lib/agents/code-police/github"; import type { CodeIssue, IssueSeverity, ProjectStatus } from "@/types"; @@ -67,17 +66,17 @@ function verifyWebhookSignature( secret: string ): boolean { if (!signature) return false; - + const hmac = crypto.createHmac("sha256", secret); const digest = `sha256=${hmac.update(payload).digest("hex")}`; - + return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature)); } export async function POST(request: NextRequest) { try { console.log("[GitHub Webhook] Received webhook event"); - + const rawBody = await request.text(); const signature = request.headers.get("x-hub-signature-256"); const event = request.headers.get("x-github-event"); @@ -126,9 +125,9 @@ export async function POST(request: NextRequest) { } const projectDoc = projectsSnapshot.docs[0]; - const project = { id: projectDoc.id, ...projectDoc.data() } as { - id: string; - userId: string; + const project = { id: projectDoc.id, ...projectDoc.data() } as { + id: string; + userId: string; webhookSecret?: string; status?: ProjectStatus; customRules?: string[]; @@ -161,7 +160,7 @@ export async function POST(request: NextRequest) { // Get user's GitHub token from Clerk (OAuth tokens are stored in Clerk, not Firestore) let githubToken: string | null = null; - + try { // Fetch GitHub OAuth token from Clerk const clerkResponse = await fetch( @@ -202,7 +201,7 @@ export async function POST(request: NextRequest) { console.log("[GitHub Webhook] Handling push event"); await handlePushEvent( payload as GitHubPushPayload, - project as { id: string; userId: string;[key: string]: unknown }, + project as { id: string; userId: string; [key: string]: unknown }, githubToken ); } else if (event === "pull_request") { @@ -211,7 +210,7 @@ export async function POST(request: NextRequest) { if (["opened", "synchronize"].includes(prPayload.action)) { await handlePREvent( prPayload, - project as { id: string; userId: string;[key: string]: unknown }, + project as { id: string; userId: string; [key: string]: unknown }, githubToken ); } @@ -232,7 +231,7 @@ export async function POST(request: NextRequest) { */ async function handlePushEvent( payload: GitHubPushPayload, - project: { id: string; userId: string;[key: string]: unknown }, + project: { id: string; userId: string; [key: string]: unknown }, githubToken: string ) { const { repository, after: commitSha, commits } = payload; @@ -253,9 +252,9 @@ async function handlePushEvent( // Create analysis run const analysisRef = adminDb.collection("analysis_runs").doc(); - + console.log("[Push Event] Creating analysis run:", analysisRef.id); - + await analysisRef.set({ id: analysisRef.id, userId: project.userId, @@ -272,109 +271,48 @@ async function handlePushEvent( console.log("[Push Event] Fetching commit details..."); // Fetch commit and analyze const commit = await fetchCommit(githubToken, owner, repo, commitSha); - const commitFiles = commit.files || []; - console.log("[Push Event] Commit has", commitFiles.length, "files changed"); - - // ======================================================================== - // FILE FILTERING - Same as analyze route - // ======================================================================== - const EXCLUDED_PATTERNS = [ - /^node_modules\//, - /^\.git\//, - /^dist\//, - /^build\//, - /^\.next\//, - /^out\//, - /^coverage\//, - /package-lock\.json$/, - /yarn\.lock$/, - /pnpm-lock\.yaml$/, - /\.min\.(js|css)$/, - /\.map$/, - /\.d\.ts$/, - ]; - - const ANALYZABLE_EXTENSIONS = [ - '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', - '.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift', - '.c', '.cpp', '.h', '.hpp', '.cs', - '.php', '.sql', '.sol', '.vue', '.svelte', - ]; - - function shouldAnalyzeFile(filename: string): boolean { - for (const pattern of EXCLUDED_PATTERNS) { - if (pattern.test(filename)) return false; - } - const ext = '.' + (filename.split('.').pop()?.toLowerCase() || ''); - return ANALYZABLE_EXTENSIONS.includes(ext); - } - + console.log("[Push Event] Commit has", commit.files.length, "files changed"); + const allIssues: Omit[] = []; - const analyzedFiles: string[] = []; - const skippedFiles: string[] = []; // Get custom rules from project settings const customRules = (project.customRules as string[] | undefined) || []; - for (const file of commitFiles) { + for (const file of commit.files) { if (file.status === "removed") { console.log("[Push Event] Skipping removed file:", file.filename); continue; } - // Apply file filtering - if (!shouldAnalyzeFile(file.filename)) { - console.log("[Push Event] Skipping (filtered):", file.filename); - skippedFiles.push(file.filename); - continue; - } - console.log("[Push Event] Analyzing file:", file.filename); + const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); + const language = detectLanguage(file.filename); + // Get dependent files for graph-aware analysis (optional, may fail due to rate limits) + let dependentContext = ''; try { - const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); - - // Skip large files - if (content.length > 50000) { - console.log("[Push Event] Skipping (too large):", file.filename); - skippedFiles.push(file.filename); - continue; - } - - const language = detectLanguage(file.filename); - - // Get dependent files for graph-aware analysis (optional, may fail due to rate limits) - let dependentContext = ''; - try { - const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); - if (dependentFiles.length > 0) { - dependentContext = dependentFiles - .map(df => `- ${df.path}:\n${df.snippet}`) - .join('\n\n'); - } - } catch (err) { - console.warn("Graph-aware analysis skipped:", err); + const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); + if (dependentFiles.length > 0) { + dependentContext = dependentFiles + .map(df => `- ${df.path}:\n${df.snippet}`) + .join('\n\n'); } + } catch (err) { + console.warn("Graph-aware analysis skipped:", err); + } - const issues = await analyzeCode({ - code: content, - filePath: file.filename, - language, - commitMessage: commit.commit.message, - customRules, - dependentContext: dependentContext || undefined, - }); + const issues = await analyzeCode({ + code: content, + filePath: file.filename, + language, + commitMessage: commit.commit.message, + customRules, + dependentContext: dependentContext || undefined, + }); - allIssues.push(...issues); - analyzedFiles.push(file.filename); - } catch (fileError) { - console.warn("[Push Event] Failed to analyze file:", file.filename, fileError); - skippedFiles.push(file.filename); - } + allIssues.push(...issues); } - console.log(`[Push Event] Analyzed ${analyzedFiles.length} files, skipped ${skippedFiles.length}`); - // Calculate counts const issueCounts: Record = { critical: allIssues.filter((i) => i.severity === "critical").length, @@ -387,7 +325,7 @@ async function handlePushEvent( console.log("[Push Event] Issue counts:", issueCounts); console.log("[Push Event] Total issues found:", allIssues.length); - // Store issues in Firestore SUBCOLLECTION (matching the GET API) + // Store issues in Firestore const fullIssues: CodeIssue[] = allIssues.map((issue, idx) => ({ ...issue, id: `${analysisRef.id}-${idx}`, @@ -396,19 +334,18 @@ async function handlePushEvent( isMuted: false, })); - // Store in SUBCOLLECTION: analysis_runs/{runId}/issues + // Actually store the issues in the issues collection if (fullIssues.length > 0) { const issuesBatch = adminDb.batch(); for (const issue of fullIssues) { - // FIX: Store in subcollection, not top-level collection - const issueRef = analysisRef.collection("issues").doc(issue.id); + const issueRef = adminDb.collection("issues").doc(issue.id); issuesBatch.set(issueRef, { ...issue, createdAt: new Date(), }); } await issuesBatch.commit(); - console.log("[Push Event] Stored", fullIssues.length, "issues in subcollection"); + console.log("[Push Event] Stored", fullIssues.length, "issues in Firestore"); } // Generate summary @@ -465,50 +402,6 @@ async function handlePushEvent( await analysisRef.update({ emailStatus: "sent" }); } - - // Auto-fix: Generate fixes and create PR if enabled - if ((project.autoFixEnabled as boolean | undefined) && fullIssues.length > 0) { - console.log("[Push Event] Auto-fix enabled, generating fixes..."); - - try { - const autoFixResult = await generateAndCreateFixPR({ - githubToken, - owner, - repo, - branch, - commitSha, - issues: fullIssues, - analysisRunId: analysisRef.id, - severityFilter: ["critical", "high", "medium"], - }); - - if (autoFixResult.success) { - console.log(`[Push Event] Auto-fix PR created: ${autoFixResult.prUrl}`); - await analysisRef.update({ - autoFixPrUrl: autoFixResult.prUrl, - autoFixPrNumber: autoFixResult.prNumber, - autoFixBranch: autoFixResult.branchName, - autoFixesGenerated: autoFixResult.fixesGenerated, - autoFixFilesChanged: autoFixResult.filesChanged, - }); - } else { - console.log(`[Push Event] Auto-fix did not create PR: ${autoFixResult.error}`); - if (autoFixResult.fixesGenerated > 0) { - await analysisRef.update({ - autoFixAttempted: true, - autoFixError: autoFixResult.error, - autoFixesGenerated: autoFixResult.fixesGenerated, - }); - } - } - } catch (autoFixError) { - console.error("[Push Event] Auto-fix error:", autoFixError); - await analysisRef.update({ - autoFixAttempted: true, - autoFixError: autoFixError instanceof Error ? autoFixError.message : "Auto-fix failed", - }); - } - } } catch (error) { console.error("Push event analysis failed:", error); await analysisRef.update({ @@ -523,7 +416,7 @@ async function handlePushEvent( */ async function handlePREvent( payload: GitHubPRPayload, - project: { id: string; userId: string;[key: string]: unknown }, + project: { id: string; userId: string; [key: string]: unknown }, githubToken: string ) { const { repository, pull_request: pr } = payload; @@ -541,7 +434,7 @@ async function handlePREvent( // Create analysis run const analysisRef = adminDb.collection("analysis_runs").doc(); - + await analysisRef.set({ id: analysisRef.id, userId: project.userId, @@ -562,60 +455,40 @@ async function handlePREvent( try { // Similar analysis as push event but with PR comment output const commit = await fetchCommit(githubToken, owner, repo, commitSha); - const commitFiles = commit.files || []; - - // File filtering (same as push event) - const EXCLUDED_PATTERNS = [/^node_modules\//, /^\.git\//, /^dist\//, /^build\//, /^\.next\//, /package-lock\.json$/, /yarn\.lock$/, /\.min\.(js|css)$/, /\.map$/, /\.d\.ts$/]; - const ANALYZABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.java', '.vue', '.svelte']; - function shouldAnalyzeFile(filename: string): boolean { - for (const pattern of EXCLUDED_PATTERNS) { - if (pattern.test(filename)) return false; - } - const ext = '.' + (filename.split('.').pop()?.toLowerCase() || ''); - return ANALYZABLE_EXTENSIONS.includes(ext); - } - const allIssues: Omit[] = []; // Get custom rules from project settings const customRules = (project.customRules as string[] | undefined) || []; - for (const file of commitFiles) { + for (const file of commit.files) { if (file.status === "removed") continue; - if (!shouldAnalyzeFile(file.filename)) continue; + const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); + const language = detectLanguage(file.filename); + + // Get dependent files for graph-aware analysis + let dependentContext = ''; try { - const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); - if (content.length > 50000) continue; // Skip large files - - const language = detectLanguage(file.filename); - - // Get dependent files for graph-aware analysis - let dependentContext = ''; - try { - const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); - if (dependentFiles.length > 0) { - dependentContext = dependentFiles - .map(df => `- ${df.path}:\n${df.snippet}`) - .join('\n\n'); - } - } catch (err) { - console.warn("Graph-aware analysis skipped:", err); + const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); + if (dependentFiles.length > 0) { + dependentContext = dependentFiles + .map(df => `- ${df.path}:\n${df.snippet}`) + .join('\n\n'); } + } catch (err) { + console.warn("Graph-aware analysis skipped:", err); + } - const issues = await analyzeCode({ - code: content, - filePath: file.filename, - language, - commitMessage: pr.title, - customRules, - dependentContext: dependentContext || undefined, - }); + const issues = await analyzeCode({ + code: content, + filePath: file.filename, + language, + commitMessage: pr.title, + customRules, + dependentContext: dependentContext || undefined, + }); - allIssues.push(...issues); - } catch (fileError) { - console.warn("[PR Event] Failed to analyze file:", file.filename); - } + allIssues.push(...issues); } const issueCounts: Record = { @@ -637,18 +510,18 @@ async function handlePREvent( isMuted: false, })); - // Store issues in SUBCOLLECTION (matching GET API) + // Store issues in Firestore if (fullIssues.length > 0) { const issuesBatch = adminDb.batch(); for (const issue of fullIssues) { - const issueRef = analysisRef.collection("issues").doc(issue.id); + const issueRef = adminDb.collection("issues").doc(issue.id); issuesBatch.set(issueRef, { ...issue, createdAt: new Date(), }); } await issuesBatch.commit(); - console.log("[PR Event] Stored", fullIssues.length, "issues in subcollection"); + console.log("[PR Event] Stored", fullIssues.length, "issues in Firestore"); } const summary = await generateAnalysisSummary({ diff --git a/src/app/dashboard/code-police/[id]/page.tsx b/src/app/dashboard/code-police/[id]/page.tsx index de8e653..972e4c4 100644 --- a/src/app/dashboard/code-police/[id]/page.tsx +++ b/src/app/dashboard/code-police/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, use } from "react"; +import { useState, useEffect, use, useCallback } from "react"; import Link from "next/link"; import { Shield, @@ -23,7 +23,6 @@ import { GitPullRequest, Wrench, ExternalLink, - Code, } from "lucide-react"; import { ProjectSettings } from "@/components/code-police/ProjectSettings"; @@ -106,7 +105,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st const [isCreatingPR, setIsCreatingPR] = useState(null); // Fetch project and analysis runs - const fetchData = async (showRefresh = false) => { + const fetchData = useCallback(async (showRefresh = false) => { if (showRefresh) setIsRefreshing(true); try { // Fetch project @@ -126,8 +125,8 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st if (runsRes.ok && runsData.runs) { setRuns(runsData.runs); // Expand first run by default - if (runsData.runs.length > 0 && !expandedRun) { - setExpandedRun(runsData.runs[0].id); + if (runsData.runs.length > 0) { + setExpandedRun((prev) => prev || runsData.runs[0].id); } } } catch (err) { @@ -136,11 +135,11 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st setIsLoading(false); setIsRefreshing(false); } - }; + }, [projectId]); useEffect(() => { fetchData(); - }, [projectId]); + }, [fetchData]); // Update project settings const handleUpdateProject = async (updates: Partial) => { @@ -346,7 +345,8 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st } }; - const formatDate = (dateStr: string) => { + // Helper function to format relative time + const _formatDate = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); diff --git a/src/app/dashboard/database/connect/page.tsx b/src/app/dashboard/database/connect/page.tsx index 550366e..e89aa97 100644 --- a/src/app/dashboard/database/connect/page.tsx +++ b/src/app/dashboard/database/connect/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { @@ -30,29 +30,29 @@ const databaseTypes = [ // Auto-detect database type from connection string function detectDatabaseType(connectionString: string): DatabaseType | null { const trimmed = connectionString.trim().toLowerCase(); - + if (!trimmed) return null; - + // MongoDB detection if (trimmed.startsWith("mongodb://") || trimmed.startsWith("mongodb+srv://")) { return "mongodb"; } - + // Supabase detection if (trimmed.includes("supabase.co") || trimmed.includes("supabase.com")) { return "supabase"; } - + // PostgreSQL detection if (trimmed.startsWith("postgres://") || trimmed.startsWith("postgresql://")) { return "postgresql"; } - + // MySQL detection if (trimmed.startsWith("mysql://")) { return "mysql"; } - + return null; } @@ -61,18 +61,18 @@ export default function ConnectDatabasePage() { const [selectedType, setSelectedType] = useState("postgresql"); const [connectionMode, setConnectionMode] = useState("string"); const [connectionName, setConnectionName] = useState(""); - + // Connection string mode const [connectionString, setConnectionString] = useState(""); const [detectedType, setDetectedType] = useState(null); - + // Form mode const [host, setHost] = useState(""); const [port, setPort] = useState("5432"); const [database, setDatabase] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - + const [showPassword, setShowPassword] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isTesting, setIsTesting] = useState(false); @@ -125,13 +125,13 @@ export default function ConnectDatabasePage() { const body = connectionMode === "string" ? { connectionString } : { - type: selectedType, - host, - port: parseInt(port), - database, - username, - password, - }; + type: selectedType, + host, + port: parseInt(port), + database, + username, + password, + }; const response = await fetch("/api/database/test", { method: "POST", @@ -163,14 +163,14 @@ export default function ConnectDatabasePage() { const body = connectionMode === "string" ? { name: connectionName, type: detectedType || selectedType, connectionString } : { - name: connectionName, - type: selectedType, - host, - port: parseInt(port), - database, - username, - password, - }; + name: connectionName, + type: selectedType, + host, + port: parseInt(port), + database, + username, + password, + }; const response = await fetch("/api/database/connections", { method: "POST", @@ -191,23 +191,23 @@ export default function ConnectDatabasePage() { } }; - const isFormValid = connectionMode === "string" + const isFormValid = connectionMode === "string" ? connectionName && connectionString : connectionName && host && port && database && username && password; // Generate detection badge based on type const getDetectionBadge = () => { if (!detectedType) return null; - + const badges: Record = { supabase: { label: "Supabase Detected", icon: "⚡", color: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30" }, mongodb: { label: "MongoDB Detected", icon: "🍃", color: "bg-green-500/20 text-green-400 border-green-500/30" }, postgresql: { label: "PostgreSQL Detected", icon: "🐘", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" }, mysql: { label: "MySQL Detected", icon: "🐬", color: "bg-orange-500/20 text-orange-400 border-orange-500/30" }, }; - + const badge = badges[detectedType]; - + return (
@@ -246,22 +246,20 @@ export default function ConnectDatabasePage() {
diff --git a/src/app/dashboard/equity/portfolio/page.tsx b/src/app/dashboard/equity/portfolio/page.tsx index b36750e..56d1af1 100644 --- a/src/app/dashboard/equity/portfolio/page.tsx +++ b/src/app/dashboard/equity/portfolio/page.tsx @@ -12,7 +12,6 @@ import { ArrowUpRight, ExternalLink, Briefcase, - ArrowDownLeft, RefreshCw, AlertCircle, } from "lucide-react"; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9c72228..71d2058 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -9,7 +9,6 @@ import { Coins, Database, ArrowRight, - Clock, Activity, } from "lucide-react"; diff --git a/src/app/dashboard/pitch-deck/[id]/page.tsx b/src/app/dashboard/pitch-deck/[id]/page.tsx index 994d7c5..9a5ab2c 100644 --- a/src/app/dashboard/pitch-deck/[id]/page.tsx +++ b/src/app/dashboard/pitch-deck/[id]/page.tsx @@ -4,7 +4,6 @@ import { useState, useEffect, use } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { - Presentation, ArrowLeft, ChevronLeft, ChevronRight, @@ -15,7 +14,6 @@ import { Loader2, Plus, Trash2, - GripVertical, } from "lucide-react"; interface Slide { @@ -145,13 +143,12 @@ export default function PitchDeckDetailPage({ params }: { params: Promise<{ id:

${slide.title}

${slide.subtitle ? `

${slide.subtitle}

` : ""} - ${ - slide.bullets + ${slide.bullets ? `
    ${slide.bullets.map((b) => `
  • ${b}
  • `).join("")}
` : "" - } + } ${slide.content ? `

${slide.content}

` : ""}
` @@ -240,16 +237,14 @@ export default function PitchDeckDetailPage({ params }: { params: Promise<{ id: setCurrentSlideIndex(index); setIsEditing(false); }} - className={`w-full p-2 rounded-lg mb-2 transition-all ${ - currentSlideIndex === index + className={`w-full p-2 rounded-lg mb-2 transition-all ${currentSlideIndex === index ? "bg-blue-500/20 border border-blue-500/30" : "bg-zinc-800/50 hover:bg-zinc-800 border border-transparent" - }`} + }`} >

{slide.title}

@@ -262,9 +257,8 @@ export default function PitchDeckDetailPage({ params }: { params: Promise<{ id:
{/* Slide Content */}
{isEditing && editedSlide ? ( /* Edit Mode */ diff --git a/src/app/dashboard/pitch-deck/page.tsx b/src/app/dashboard/pitch-deck/page.tsx index 7e2ad12..7a698b6 100644 --- a/src/app/dashboard/pitch-deck/page.tsx +++ b/src/app/dashboard/pitch-deck/page.tsx @@ -1,15 +1,19 @@ import Link from "next/link"; import { auth } from "@clerk/nextjs/server"; import { getAdminDb } from "@/lib/firebase/admin"; -import { DeckList } from "@/components/pitch-deck"; // Force dynamic rendering - requires Clerk auth at runtime export const dynamic = 'force-dynamic'; import { Presentation, Plus, + FileText, + Clock, + Download, + ArrowRight, Sparkles, Wand2, + PenTool, } from "lucide-react"; /** @@ -25,17 +29,17 @@ interface PitchDeck { tagline: string; status: "draft" | "completed"; slidesCount: number; - createdAt: string; + createdAt: Date; } export default async function PitchDeckPage() { // Get authenticated user const { userId } = await auth(); - + // Fetch decks from Firestore let decks: PitchDeck[] = []; const db = getAdminDb(); - + if (db && userId) { try { const decksSnapshot = await db @@ -52,7 +56,7 @@ export default async function PitchDeckPage() { tagline: data.tagline || "", status: data.status || "draft", slidesCount: data.slides?.length || 0, - createdAt: (data.createdAt?.toDate?.() || new Date()).toISOString(), + createdAt: data.createdAt?.toDate?.() || new Date(), }; }); } catch (error) { @@ -60,6 +64,14 @@ export default async function PitchDeckPage() { } } + const formatDate = (date: Date) => { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + return (
{/* Header */} @@ -95,7 +107,11 @@ export default async function PitchDeckPage() { {decks.length === 0 ? ( ) : ( - +
+ {decks.map((deck) => ( + + ))} +
)}
); @@ -124,3 +140,60 @@ function EmptyState() { ); } +function DeckCard({ + deck, + formatDate, +}: { + deck: PitchDeck; + formatDate: (date: Date) => string; +}) { + return ( +
+ {/* Preview placeholder */} + +
+ +
+ + +
+
+

+ {deck.projectName} +

+ + {deck.status} + +
+

{deck.tagline || "No tagline"}

+
+ {deck.slidesCount} slides +
+ + {formatDate(deck.createdAt)} +
+
+
+ +
+ + + + Edit in Studio + +
+
+ ); +} diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index d16a6b4..5f9e354 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState, useEffect, Suspense } from "react"; +import { useState, Suspense } from "react"; import { useSearchParams } from "next/navigation"; +import Image from "next/image"; import { UserProfile, useUser } from "@clerk/nextjs"; import { Settings, @@ -47,7 +48,7 @@ function SettingsContent() { const { user, isLoaded } = useUser(); const [showAccountModal, setShowAccountModal] = useState(false); const justConnected = searchParams.get("github_connected") === "true"; - + // Check if GitHub is connected via Clerk const githubAccount = user?.externalAccounts?.find( (account) => account.provider === "github" @@ -90,9 +91,11 @@ function SettingsContent() {
{user?.imageUrl ? ( - Profile ) : ( @@ -170,7 +173,7 @@ function SettingsContent() { {!isGithubConnected && (

- Note: GitHub connection is required for Code Police (code review) + Note: GitHub connection is required for Code Police (code review) and Pitch Deck Generator (repository analysis). Click "Connect GitHub" above to get started.

@@ -276,7 +279,7 @@ function SettingsContent() { > ✕ - - {/* Interactive 3D Spline Scene */} -
- -
- {/* Sticky Scroll Features Section */} {/* Features Section */} - {/* GHOSTFOUNDER Text Effect */} -
- -
- {/* Animated Testimonials Section */} + {/* Text Hover Effect Section */} +
+ +
+ {/* CTA Section */} -
- +
+
+

+ Ready to get started? +

+

+ Join thousands of teams already using GhostHunter to build better products. +

+
+ + Start Free Trial + + + Contact Sales + +
+
- {/* Sticky Footer Reveal */} - +
); } diff --git a/src/components/pitch-deck/index.ts b/src/components/pitch-deck/index.ts index 9e5c275..a4aabc4 100644 --- a/src/components/pitch-deck/index.ts +++ b/src/components/pitch-deck/index.ts @@ -9,5 +9,3 @@ export { SlideCanvas } from "./SlideCanvas"; export { SlideList } from "./SlideList"; export { PropertiesPanel } from "./PropertiesPanel"; export { EditorToolbar } from "./EditorToolbar"; -export { DeleteDeckButton } from "./DeleteDeckButton"; -export { DeckList } from "./DeckList"; diff --git a/src/components/ui/Button.tsx b/src/components/ui/button.tsx similarity index 100% rename from src/components/ui/Button.tsx rename to src/components/ui/button.tsx diff --git a/src/components/ui/features-section-demo-3.tsx b/src/components/ui/features-section-demo-3.tsx index ef986a0..16dfb90 100644 --- a/src/components/ui/features-section-demo-3.tsx +++ b/src/components/ui/features-section-demo-3.tsx @@ -1,5 +1,6 @@ "use client"; import React from "react"; +import Image from "next/image"; import { cn } from "@/lib/utils"; import createGlobe from "cobe"; import { useEffect, useRef } from "react"; @@ -107,7 +108,7 @@ const FeatureDescription = ({ children }: { children?: React.ReactNode }) => { import { CodeBlock } from "@/components/ui/code-block"; export const SkeletonOne = () => { - const code = `async function processSecurePayment(userId, amount) { + const code = `async function processSecurePayment(userId, amount) { // 🚨 CRITICAL VULNERABILITY: Hardcoded API Key const STRIPE_SECRET = "sk_live_51M..."; @@ -131,16 +132,16 @@ export const SkeletonOne = () => {
- +
@@ -158,9 +159,9 @@ export const SkeletonThree = () => {
{/* TODO */} - header { whileTap="whileTap" className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 shrink-0 overflow-hidden" > - bali images diff --git a/src/components/ui/sticky-footer.tsx b/src/components/ui/sticky-footer.tsx index 974400a..2aac4ae 100644 --- a/src/components/ui/sticky-footer.tsx +++ b/src/components/ui/sticky-footer.tsx @@ -10,8 +10,8 @@ import { LinkedInLogoIcon, VideoIcon, } from '@radix-ui/react-icons'; -import { Button } from './button'; -import { TextHoverEffect } from './text-hover-effect'; +import { Button } from '@/components/ui/button'; +import { TextHoverEffect } from '@/components/ui/text-hover-effect'; interface FooterLink { diff --git a/src/lib/agents/equity/contract.ts b/src/lib/agents/equity/contract.ts index 0e9aa6d..60cbcc3 100644 --- a/src/lib/agents/equity/contract.ts +++ b/src/lib/agents/equity/contract.ts @@ -96,9 +96,38 @@ export async function hasUserMinted(signer: JsonRpcSigner, address: string): Pro */ export async function mintInitialTokens(signer: JsonRpcSigner): Promise { const contract = getContract(signer); - const tx = await contract.mintInitialTokens(); - await tx.wait(); - return tx.hash; + + try { + // First, estimate gas to catch any revert reasons + const gasEstimate = await contract.mintInitialTokens.estimateGas(); + console.log("[Contract] Gas estimate:", gasEstimate.toString()); + + // Execute transaction with extra gas buffer + const tx = await contract.mintInitialTokens({ + gasLimit: gasEstimate * BigInt(120) / BigInt(100), // 20% buffer + }); + + console.log("[Contract] Transaction sent:", tx.hash); + const receipt = await tx.wait(); + console.log("[Contract] Transaction mined:", receipt.hash); + + return tx.hash; + } catch (error: any) { + console.error("[Contract] Mint failed:", error); + + // Parse common error messages + if (error.message?.includes("Already minted")) { + throw new Error("You have already minted your initial tokens"); + } + if (error.message?.includes("user rejected")) { + throw new Error("Transaction was rejected"); + } + if (error.code === 'CALL_EXCEPTION') { + throw new Error("Contract call failed. Please ensure the contract is deployed on Sepolia testnet."); + } + + throw error; + } } /** diff --git a/src/lib/pitch-deck/ai-generator.ts b/src/lib/pitch-deck/ai-generator.ts index 27fd159..3225bab 100644 --- a/src/lib/pitch-deck/ai-generator.ts +++ b/src/lib/pitch-deck/ai-generator.ts @@ -173,15 +173,15 @@ const STYLE_PRIORITIES: Record = { function formatProfile(profile?: Partial): string { if (!profile) return "Not provided"; - + const sections: string[] = []; - + if (profile.companyName) sections.push(`Company: ${profile.companyName}`); if (profile.oneLiner) sections.push(`One-liner: ${profile.oneLiner}`); if (profile.targetCustomer) sections.push(`Target Customer: ${profile.targetCustomer}`); if (profile.problemStatement) sections.push(`Problem: ${profile.problemStatement}`); if (profile.solutionDescription) sections.push(`Solution: ${profile.solutionDescription}`); - + if (profile.metrics) { const m = profile.metrics; const metricLines: string[] = []; @@ -192,44 +192,44 @@ function formatProfile(profile?: Partial): string { if (m.retention) metricLines.push(`Retention: ${m.retention}`); if (metricLines.length) sections.push(`Metrics:\n${metricLines.join("\n")}`); } - + if (profile.marketSize) { const ms = profile.marketSize; if (ms.tam || ms.sam || ms.som) { sections.push(`Market Size: TAM ${ms.tam || "?"}, SAM ${ms.sam || "?"}, SOM ${ms.som || "?"}`); } } - + if (profile.competitors?.length) { sections.push(`Competitors: ${profile.competitors.join(", ")}`); } - + if (profile.competitiveAdvantage) { sections.push(`Competitive Advantage: ${profile.competitiveAdvantage}`); } - + if (profile.team?.length) { sections.push(`Team: ${profile.team.map(t => `${t.name} (${t.role})`).join(", ")}`); } - + if (profile.fundingAsk) { sections.push(`Funding Ask: ${profile.fundingAsk.amount} (${profile.fundingAsk.type})`); if (profile.fundingAsk.useOfFunds?.length) { sections.push(`Use of Funds: ${profile.fundingAsk.useOfFunds.join(", ")}`); } } - + return sections.length ? sections.join("\n\n") : "Not provided"; } function formatGithubMeta(meta?: { stars?: number; forks?: number; contributors?: number }): string { if (!meta) return "Not available"; - + const lines: string[] = []; if (meta.stars) lines.push(`Stars: ${meta.stars}`); if (meta.forks) lines.push(`Forks: ${meta.forks}`); if (meta.contributors) lines.push(`Contributors: ${meta.contributors}`); - + return lines.length ? lines.join(", ") : "Not available"; } @@ -255,10 +255,10 @@ function createSlideFromContent( const slideType = content.type as SlideType; const layout = getDefaultLayout(slideType); const layoutId = layout?.id || `${slideType}-default`; - + const elements: SlideElement[] = []; let elementOrder = 0; - + // Create headline element if (content.headline) { elements.push({ @@ -282,7 +282,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create subheadline element if (content.subheadline) { elements.push({ @@ -306,7 +306,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create bullets element if (content.bullets?.length) { elements.push({ @@ -330,7 +330,7 @@ function createSlideFromContent( itemSpacing: 16, } as BulletListElement); } - + // Create body text element if (content.bodyText && !content.bullets?.length) { elements.push({ @@ -353,7 +353,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create metric elements if (content.metrics?.length) { const metricWidth = Math.min(300, (SLIDE_WIDTH - 120 - (content.metrics.length - 1) * 40) / content.metrics.length); @@ -389,10 +389,10 @@ function createSlideFromContent( } as MetricElement); }); } - + // Create warnings const warnings: SlideWarning[] = []; - + content.warnings?.forEach((warning, index) => { warnings.push({ id: `warn-${slideId}-${index}`, @@ -401,7 +401,7 @@ function createSlideFromContent( message: warning, }); }); - + content.placeholders?.forEach((placeholder, index) => { warnings.push({ id: `placeholder-${slideId}-${index}`, @@ -411,7 +411,7 @@ function createSlideFromContent( suggestion: "Add this information to strengthen your deck", }); }); - + const slide: Slide = { id: slideId, type: slideType, @@ -423,7 +423,7 @@ function createSlideFromContent( contentScore: content.contentScore, warnings, }; - + // Only add notes if there are suggestions if (content.suggestions?.length) { slide.notes = { @@ -431,7 +431,7 @@ function createSlideFromContent( aiSuggestions: content.suggestions, }; } - + return slide; } @@ -444,37 +444,37 @@ export async function generateDeckFromSources( ): Promise { const genId = `gen-${Date.now()}-${Math.random().toString(36).substring(7)}`; const startTime = Date.now(); - + console.log(`${LOG_PREFIX} ----------------------------------------`); console.log(`${LOG_PREFIX} [${genId}] Starting enhanced deck generation`); console.log(`${LOG_PREFIX} [${genId}] Style: ${request.style}, Tone: ${request.tone}`); - + // Validate API key if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + // Initialize model const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.7, }); - + // Prepare prompt const essentialSlides = getEssentialSlides(request.style); - + const promptTemplate = new PromptTemplate({ template: DECK_GENERATION_PROMPT, inputVariables: [ - "readme", "profile", "githubMeta", "deckStyle", + "readme", "profile", "githubMeta", "deckStyle", "tone", "essentialSlides", "toneGuidelines", "priorities" ], partialVariables: { format_instructions: parser.getFormatInstructions(), }, }); - + const formattedPrompt = await promptTemplate.format({ readme: request.readme || "No README provided", profile: formatProfile(request.profile), @@ -485,18 +485,18 @@ export async function generateDeckFromSources( toneGuidelines: TONE_GUIDELINES[request.tone], priorities: STYLE_PRIORITIES[request.style], }); - + console.log(`${LOG_PREFIX} [${genId}] Prompt prepared, invoking AI...`); - + // Invoke AI const response = await model.invoke(formattedPrompt); const content = response.content as string; - + console.log(`${LOG_PREFIX} [${genId}] AI response received, parsing...`); - + // Parse response let parsedOutput: DeckGenerationOutput; - + try { parsedOutput = await parser.parse(content); } catch { @@ -508,17 +508,17 @@ export async function generateDeckFromSources( throw new Error("Failed to parse AI response"); } } - + console.log(`${LOG_PREFIX} [${genId}] Parsed ${parsedOutput.slides.length} slides`); - + // Build deck const deckId = uuidv4(); const defaultTheme = getDefaultTheme(); - - const slides: Slide[] = parsedOutput.slides.map((slideContent, index) => + + const slides: Slide[] = parsedOutput.slides.map((slideContent, index) => createSlideFromContent(slideContent, index) ); - + const deck: Deck = { id: deckId, userId: "", // Will be set by API route @@ -541,7 +541,7 @@ export async function generateDeckFromSources( repoName: request.repoName, repoOwner: request.repoOwner, }; - + // Collect all warnings const allWarnings: SlideWarning[] = []; slides.forEach(slide => { @@ -549,7 +549,7 @@ export async function generateDeckFromSources( allWarnings.push(...slide.warnings); } }); - + // Add missing slides warnings parsedOutput.missingSlides.forEach((slideType, index) => { allWarnings.push({ @@ -560,11 +560,11 @@ export async function generateDeckFromSources( suggestion: `Add a ${slideType} slide to strengthen your pitch`, }); }); - + const duration = Date.now() - startTime; console.log(`${LOG_PREFIX} [${genId}] Generation completed in ${duration}ms`); console.log(`${LOG_PREFIX} ----------------------------------------`); - + return { deck, warnings: allWarnings, @@ -607,34 +607,34 @@ export async function improveText( deckContext?: { projectName: string; tagline: string; tone: ContentTone } ): Promise { console.log(`${LOG_PREFIX} Improving text: action=${action}, slideType=${slideType}`); - + if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.7, }); - + const promptTemplate = new PromptTemplate({ template: TEXT_IMPROVEMENT_PROMPT, inputVariables: ["slideType", "deckContext", "text", "action"], }); - + const formattedPrompt = await promptTemplate.format({ slideType, - deckContext: deckContext + deckContext: deckContext ? `Project: ${deckContext.projectName}, Tagline: ${deckContext.tagline}, Tone: ${deckContext.tone}` : "General pitch deck", text, action, }); - + const response = await model.invoke(formattedPrompt); const improvedText = (response.content as string).trim(); - + // Clean up any markdown formatting return improvedText .replace(/^["']|["']$/g, "") @@ -709,17 +709,17 @@ export async function checkDeckHealth( profile?: StartupProfile ): Promise { console.log(`${LOG_PREFIX} Running health check for deck: ${deck.id}`); - + if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.3, // Lower for more consistent analysis }); - + // Prepare slides summary for the AI const slidesSummary = deck.slides.map(slide => { const textContent = slide.elements @@ -731,38 +731,38 @@ export async function checkDeckHealth( return (el as TextElement).content; }) .join("\n"); - + return { id: slide.id, type: slide.type, content: textContent || "[No text content]", }; }); - + const promptTemplate = new PromptTemplate({ template: HEALTH_CHECK_PROMPT, inputVariables: ["projectName", "tagline", "slidesJson", "profile"], }); - + const formattedPrompt = await promptTemplate.format({ projectName: deck.projectName, tagline: deck.tagline, slidesJson: JSON.stringify(slidesSummary, null, 2), profile: formatProfile(profile), }); - + const response = await model.invoke(formattedPrompt); const content = response.content as string; - + // Parse response const jsonMatch = content.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error("Failed to parse health check response"); } - + const result: HealthCheckResult = JSON.parse(jsonMatch[0]); - + console.log(`${LOG_PREFIX} Health check complete: score=${result.overallScore}`); - + return result; } diff --git a/src/lib/security/rate-limit.ts b/src/lib/security/rate-limit.ts new file mode 100644 index 0000000..cc14df0 --- /dev/null +++ b/src/lib/security/rate-limit.ts @@ -0,0 +1,120 @@ +/** + * ============================================================================ + * RATE LIMITING UTILITY + * ============================================================================ + * Simple in-memory rate limiting for API protection. + * For production scale, consider Redis-based rate limiting. + */ + +interface RateLimitEntry { + count: number; + resetTime: number; +} + +// In-memory store for rate limits +const rateLimitStore = new Map(); + +// Cleanup old entries every 5 minutes +const CLEANUP_INTERVAL = 5 * 60 * 1000; + +let cleanupInitialized = false; + +function initCleanup() { + if (cleanupInitialized) return; + cleanupInitialized = true; + + setInterval(() => { + const now = Date.now(); + for (const [key, entry] of rateLimitStore.entries()) { + if (now > entry.resetTime) { + rateLimitStore.delete(key); + } + } + }, CLEANUP_INTERVAL); +} + +export interface RateLimitConfig { + /** Maximum number of requests allowed in the window */ + limit: number; + /** Time window in seconds */ + windowSeconds: number; +} + +export interface RateLimitResult { + success: boolean; + remaining: number; + resetIn: number; +} + +/** + * Check if a request should be rate limited + * @param identifier - Unique identifier for the client (e.g., IP, userId) + * @param config - Rate limit configuration + * @returns RateLimitResult with success status and remaining requests + */ +export function checkRateLimit( + identifier: string, + config: RateLimitConfig +): RateLimitResult { + initCleanup(); + + const now = Date.now(); + const windowMs = config.windowSeconds * 1000; + const entry = rateLimitStore.get(identifier); + + // No existing entry or window expired + if (!entry || now > entry.resetTime) { + rateLimitStore.set(identifier, { + count: 1, + resetTime: now + windowMs, + }); + return { + success: true, + remaining: config.limit - 1, + resetIn: config.windowSeconds, + }; + } + + // Within window, check count + if (entry.count >= config.limit) { + return { + success: false, + remaining: 0, + resetIn: Math.ceil((entry.resetTime - now) / 1000), + }; + } + + // Increment count + entry.count++; + return { + success: true, + remaining: config.limit - entry.count, + resetIn: Math.ceil((entry.resetTime - now) / 1000), + }; +} + +/** + * Get rate limit headers for response + */ +export function getRateLimitHeaders( + result: RateLimitResult, + config: RateLimitConfig +): Record { + return { + "X-RateLimit-Limit": config.limit.toString(), + "X-RateLimit-Remaining": result.remaining.toString(), + "X-RateLimit-Reset": result.resetIn.toString(), + }; +} + +// Preset configurations for common use cases +export const RATE_LIMITS = { + /** Standard API endpoint: 60 requests per minute */ + standard: { limit: 60, windowSeconds: 60 }, + /** Strict for auth/sensitive: 10 requests per minute */ + strict: { limit: 10, windowSeconds: 60 }, + /** Relaxed for read-only: 120 requests per minute */ + relaxed: { limit: 120, windowSeconds: 60 }, + /** Webhook endpoints: 100 requests per minute */ + webhook: { limit: 100, windowSeconds: 60 }, +} as const;