diff --git a/app/console/attack-pipeline/page.tsx b/app/console/attack-pipeline/page.tsx index f2b37fc..d0c4ce5 100644 --- a/app/console/attack-pipeline/page.tsx +++ b/app/console/attack-pipeline/page.tsx @@ -231,7 +231,7 @@ export default function AttackPipelinePage() { const [useAi, setUseAi] = useState(true); const [includePerf, setIncludePerf] = useState(false); const [scanning, setScanning] = useState(false); - const [logs, setLogs] = useState([]); + const [report, setReport] = useState(null); const [error, setError] = useState(null); const [runs, setRuns] = useState([]); @@ -291,8 +291,7 @@ export default function AttackPipelinePage() { const appendLog = (text: string) => { const id = `log-${++logCounterRef.current}`; setLogLines((prev) => [...prev, { id, text }]); - // Keep legacy `logs` in sync for anything that reads it - setLogs((prev) => [...prev, text]); + }; const loadReport = async (runId: string) => { @@ -302,7 +301,7 @@ export default function AttackPipelinePage() { if (data.ok && data.report) { setReport(data.report as Report); setLogLines([]); - setLogs([]); + setError(null); } } catch { /* ignore */ } @@ -330,7 +329,7 @@ export default function AttackPipelinePage() { setError(null); setReport(null); setLogLines([]); - setLogs([]); + setScanning(true); currentRunIdRef.current = null; abortRef.current = new AbortController(); diff --git a/app/console/audit-logs/page.tsx b/app/console/audit-logs/page.tsx index 93a0562..22216d4 100644 --- a/app/console/audit-logs/page.tsx +++ b/app/console/audit-logs/page.tsx @@ -10,7 +10,7 @@ const MOCK_LOGS: AuditEvent[] = [ { id: "3", timestamp: Date.now() - 1000 * 60 * 60 * 2, action: "Failed Deployment: nextjs-template", severity: "critical", details: "Build timeout exceeded 300s." }, ]; -const SEVERITY_CONFIG: Record = { +const SEVERITY_CONFIG: Record, styles: string }> = { info: { icon: Info, styles: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/20" }, warning: { icon: AlertTriangle, styles: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20" }, critical: { icon: ShieldAlert, styles: "bg-red-50 text-red-700 border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/20" }, diff --git a/app/console/secrets/page.tsx b/app/console/secrets/page.tsx index 1bd662e..24e3da4 100644 --- a/app/console/secrets/page.tsx +++ b/app/console/secrets/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Key, ShieldCheck, Eye, EyeOff, Save, Check, RefreshCw, AlertCircle } from "lucide-react"; +import { Key, Eye, EyeOff, Save, Check, RefreshCw, AlertCircle } from "lucide-react"; export default function SecretsPage() { // Visibility States for sensitive keys (Fixes #50) diff --git a/app/console/testing/page.tsx b/app/console/testing/page.tsx index 98ebf05..73997b4 100644 --- a/app/console/testing/page.tsx +++ b/app/console/testing/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState, useCallback, useRef } from "react"; +import React, { useEffect, useState, useCallback, useMemo, useRef } from "react"; import Link from "next/link"; import { Globe, Play, RefreshCw, CheckCircle2, XCircle, AlertTriangle, @@ -542,7 +542,7 @@ export default function Page() { const [layerStartTimes, setLayerStartTimes] = useState>({}); const abortRef = useRef(false); - const selectedDeployment = deployments.find((d) => d.sandboxId === selectedSandbox); + const selectedDeployment = useMemo(() => deployments.find((d) => d.sandboxId === selectedSandbox), [deployments, selectedSandbox]); const [announcement, setAnnouncement] = useState(""); const announcementTimeoutRef = useRef(null); @@ -692,10 +692,10 @@ export default function Page() { fetchHistory(); }, [selectedSandbox, selectedDeployment, runLayer, fetchHistory]); - const handleStop = () => { + const handleStop = useCallback(() => { abortRef.current = true; setRunning(false); - }; + }, []); /* ── AI report generation ── */ const handleGenerateReport = async () => { diff --git a/app/login/page.tsx b/app/login/page.tsx index d086a30..1dafa14 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -25,7 +25,6 @@ export default function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); - const [keepSignedIn, setKeepSignedIn] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const router = useRouter(); diff --git a/app/register/page.tsx b/app/register/page.tsx index 725bc6a..a33b196 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -40,26 +40,25 @@ export default function RegisterPage() { setLoading(true); try { - const result = await signIn("credentials", { - email: trimmedEmail, - password, - redirect: false, - callbackUrl: "/console/dashboard", + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: trimmedEmail, password }), }); - const payload = (await response.json()) as { ok?: boolean; error?: string; detail?: string }; + const payload = (await response.json()) as { ok?: boolean; error?: string; detail?: string }; - if (!response.ok || !payload.ok) { - throw new Error(payload.error ?? payload.detail ?? "Registration failed"); - } - - router.push("/login"); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Registration failed"); - } finally { - setLoading(false); + if (!response.ok || !payload.ok) { + throw new Error(payload.error ?? payload.detail ?? "Registration failed"); } + + router.push("/login"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Registration failed"); + } finally { + setLoading(false); } +} return (
diff --git a/components/console/repository-list.tsx b/components/console/repository-list.tsx index 34d5b3f..381bc84 100644 --- a/components/console/repository-list.tsx +++ b/components/console/repository-list.tsx @@ -251,12 +251,16 @@ export function RepositoryList({ setIsDragActive(false); }, []); - const handleDrop = useCallback(async (e: React.DragEvent | any) => { + const handleDrop = useCallback(async (e: React.DragEvent | React.ChangeEvent) => { e.preventDefault(); setIsDragActive(false); setFileError(null); - const files = Array.from(e.dataTransfer?.files || e.target?.files || []) as File[]; + const files = Array.from( + ('dataTransfer' in e && e.dataTransfer ? e.dataTransfer.files : null) || + ('target' in e && e.target ? (e.target as HTMLInputElement).files : null) || + [] + ) as File[]; const validFiles: File[] = []; // Validation Checkpoint @@ -294,6 +298,10 @@ export function RepositoryList({ const filteredNew = processedFiles.filter(f => !existingNames.has(f.name)); return [...prev, ...filteredNew]; }); + + if (e.target) { + (e.target as HTMLInputElement).value = ""; + } }, []); const removeFile = (id: string) => { @@ -315,8 +323,7 @@ export function RepositoryList({ const sortedRepos = useMemo(() => { return [...filteredRepos].sort((a, b) => { if (sortBy === "name") return a.name.localeCompare(b.name); - // Fixed: Using `as any` bypasses strict interface checking for the optional stargazers_count - if (sortBy === "stars") return ((b as any).stargazers_count ?? 0) - ((a as any).stargazers_count ?? 0); + if (sortBy === "stars") return (b.stars ?? 0) - (a.stars ?? 0); return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); }); }, [filteredRepos, sortBy]); @@ -335,13 +342,6 @@ export function RepositoryList({

Connect your GitHub Identity

Link your GitHub profile to fetch repositories, analyze codebases, and enable one-click deployments. -

- -

- Connect your GitHub account -

-

- Connect GitHub to access your repositories and deploy both public and private projects from your account.

- @@ -516,32 +479,6 @@ export function RepositoryList({
)} - {/* Loading skeletons */} - {loading && ( - view === "grid" ? ( -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
- ) : ( -
- - - - {["Repository", "Visibility", "Language", "Updated", "Actions"].map((h) => ( - - ))} - - - - {Array.from({ length: 6 }).map((_, i) => ( - - ))} - -
- {h} -
)} diff --git a/lib/attack-pipeline/parsers/routeParser.ts b/lib/attack-pipeline/parsers/routeParser.ts index ae92db0..4a2d49a 100644 --- a/lib/attack-pipeline/parsers/routeParser.ts +++ b/lib/attack-pipeline/parsers/routeParser.ts @@ -3,6 +3,187 @@ * Fixes #53: Implements explicit Fetch API request timeout controls via AbortController */ +import { httpRequest } from "../utils/httpClient"; + +export type RouteCategory = "login" | "register" | "auth" | "admin" | "api" | "page"; + +export interface DiscoveredRoute { + path: string; + methods: string[]; + category: RouteCategory; + type: "api" | "page"; + priority: number; // 1 = highest (login/register tested first) +} + +export interface ParseResult { + baseUrl: string; + routes: DiscoveredRoute[]; + loginRoutes: DiscoveredRoute[]; + registerRoutes: DiscoveredRoute[]; + apiRoutes: DiscoveredRoute[]; + framework: string; +} + +// High-value attack targets to probe +const PROBE_API_PATHS = [ + "/api/login", "/api/auth/login", "/api/auth/signin", "/api/signin", + "/api/register", "/api/auth/register", "/api/auth/signup", "/api/signup", + "/api/user", "/api/users", "/api/me", "/api/profile", + "/api/admin", "/api/admin/users", "/api/admin/config", + "/api/reset-password", "/api/forgot-password", "/api/change-password", + "/api/auth/callback", "/api/auth/session", "/api/auth/token", + "/api/health", "/api/status", "/api/version", "/api/config", + "/api/deploy", "/api/test", "/api/tests/run", + "/api/env-vars", "/api/sandboxes", + "/api/upload", "/api/export", "/api/import", + "/api/search", "/api/query", +]; + +const PROBE_PAGE_PATHS = [ + "/", "/login", "/register", "/signup", "/signin", + "/dashboard", "/profile", "/settings", "/account", + "/admin", "/admin/dashboard", "/admin/users", + "/forgot-password", "/reset-password", + "/about", "/docs", +]; + +const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const; + +const PRIORITY: Record = { + login: 1, register: 2, auth: 3, admin: 4, api: 5, page: 6, +}; + +function categorize(path: string): RouteCategory { + const p = path.toLowerCase(); + if (p.includes("login") || p.includes("signin")) return "login"; + if (p.includes("register") || p.includes("signup")) return "register"; + if (p.includes("auth") || p.includes("token") || p.includes("session") || + p.includes("password") || p.includes("forgot")) return "auth"; + if (p.includes("admin")) return "admin"; + if (p.startsWith("/api/")) return "api"; + return "page"; +} + +/** + * Probe a single path — returns accepted HTTP methods or null if unreachable. + */ +async function probeEndpoint(baseUrl: string, path: string): Promise { + const url = `${baseUrl}${path}`; + const initial = await httpRequest(url, { method: "GET", timeoutMs: 6_000 }); + // 0 = network error, treat as not found + if (initial.status === 0 || initial.status === 404) return null; + + const accepted = new Set(); + + // GET successfully verifies if the status is not 405 Method Not Allowed + if (initial.status !== 405) { + accepted.add("GET"); + } + + // Check the Allow header of the response to parse accepted methods safely + const allowHeader = Object.entries(initial.headers).find( + ([k]) => k.toLowerCase() === "allow" + )?.[1]; + + if (allowHeader) { + const parsed = allowHeader + .split(",") + .map((m) => m.trim().toUpperCase()) + .filter((m) => (HTTP_METHODS as readonly string[]).includes(m)); + for (const m of parsed) { + accepted.add(m); + } + } + + return Array.from(accepted); +} + +/** + * Discover routes by probing well-known paths against the live target. + */ +async function probeCommonPaths(baseUrl: string): Promise { + const routes: DiscoveredRoute[] = []; + + // Run all probes concurrently (batched to 20 at a time) + const allPaths = [...PROBE_API_PATHS, ...PROBE_PAGE_PATHS]; + const BATCH = 20; + + for (let i = 0; i < allPaths.length; i += BATCH) { + const batch = allPaths.slice(i, i + BATCH); + const results = await Promise.all( + batch.map(async (path) => { + const methods = await probeEndpoint(baseUrl, path); + if (!methods) return null; + const category = categorize(path); + return { + path, + methods, + category, + type: (path.startsWith("/api/") ? "api" : "page") as "api" | "page", + priority: PRIORITY[category], + }; + }) + ); + for (const r of results) if (r) routes.push(r); + } + + return routes.sort((a, b) => a.priority - b.priority); +} + +/** + * Optionally parse routes from an E2B sandbox filesystem. + */ +async function parseFromSandbox(sandboxId: string): Promise<{ paths: string[]; framework: string }> { + try { + const { parseRoutes, parseApiRoutes } = await import("@/lib/route-parser"); + const [parsed, apiPaths] = await Promise.all([ + parseRoutes(sandboxId), + parseApiRoutes(sandboxId), + ]); + return { paths: [...parsed.routes, ...apiPaths], framework: parsed.framework }; + } catch { + return { paths: [], framework: "unknown" }; + } +} + +/** + * Main entry point — discover all routes for a deployment. + */ +export async function discoverRoutes(baseUrl: string, sandboxId?: string): Promise { + const cleanBase = baseUrl.replace(/\/$/, ""); + + const [probedRoutes, sandboxData] = await Promise.all([ + probeCommonPaths(cleanBase), + sandboxId ? parseFromSandbox(sandboxId) : Promise.resolve({ paths: [], framework: "unknown" }), + ]); + + // Merge sandbox-only paths that weren't found by probing + const knownPaths = new Set(probedRoutes.map((r) => r.path)); + const sandboxExtras: DiscoveredRoute[] = sandboxData.paths + .filter((p) => !knownPaths.has(p)) + .map((path) => { + const category = categorize(path); + return { + path, + methods: ["GET"], + category, + type: (path.startsWith("/api/") ? "api" : "page") as "api" | "page", + priority: PRIORITY[category], + }; + }); + + const allRoutes = [...probedRoutes, ...sandboxExtras].sort((a, b) => a.priority - b.priority); + + return { + baseUrl: cleanBase, + routes: allRoutes, + loginRoutes: allRoutes.filter((r) => r.category === "login"), + registerRoutes: allRoutes.filter((r) => r.category === "register"), + apiRoutes: allRoutes.filter((r) => r.type === "api"), + framework: sandboxData.framework || "unknown", + }; +} + interface RouteDiscoveryResult { url: string; status: number; @@ -65,14 +246,17 @@ export async function parseTargetRoute(targetUrl: string, timeoutMs = DEFAULT_TI } return result; - } catch (error: any) { + } catch (error: unknown) { + const errName = error instanceof Error ? error.name : ""; + const errMsg = error instanceof Error ? error.message : String(error); + // Check if network pipeline error matches the Abort signal trigger - if (error.name === "AbortError") { + if (errName === "AbortError") { console.error(`NETWORK TIMEOUT CRITICAL EXCEPTION: Connection to ${targetUrl} aborted after exceeding ${timeoutMs}ms restriction.`); throw new Error(`Pipeline socket termination: Endpoint [${targetUrl}] failed to respond within ${timeoutMs}ms.`); } - console.error(`Route Discovery Endpoint Connectivity Failure [${targetUrl}]:`, error.message); + console.error(`Route Discovery Endpoint Connectivity Failure [${targetUrl}]:`, errMsg); return result; } finally { // Clear out active timing handles to prevent process execution leaks diff --git a/lib/crypto.ts b/lib/crypto.ts index 3013fd7..9335e51 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -5,7 +5,6 @@ import crypto from "crypto"; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 12; // Standard IV size for GCM -const AUTH_TAG_LENGTH = 16; /** * Validates and retrieves the operational encryption key buffer. diff --git a/lib/hooks/useDebounce.ts b/lib/hooks/useDebounce.ts new file mode 100644 index 0000000..ba49720 --- /dev/null +++ b/lib/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/lib/utils/auditLogger.ts b/lib/utils/auditLogger.ts index bacd328..d669edd 100644 --- a/lib/utils/auditLogger.ts +++ b/lib/utils/auditLogger.ts @@ -26,8 +26,8 @@ export const auditLogger = { // In a production environment, this would dispatch to a secure backend API. // For now, we standardize the console output format for the sandbox environment. const logPrefix = `[AUDIT - ${severity.toUpperCase()}]`; - if (severity === "critical") console.error(logPrefix, action, details || ""); - else if (severity === "warning") console.warn(logPrefix, action, details || ""); - else console.info(logPrefix, action, details || ""); + if (severity === "critical") console.error(logPrefix, event); + else if (severity === "warning") console.warn(logPrefix, event); + else console.info(logPrefix, event); } }; \ No newline at end of file diff --git a/lib/utils/httpClient.ts b/lib/utils/httpClient.ts index c87cc56..bacf979 100644 --- a/lib/utils/httpClient.ts +++ b/lib/utils/httpClient.ts @@ -39,9 +39,12 @@ export async function fetchWithRetry(url: string, options: FetchRetryOptions = { console.warn(`[HTTP Client] 5xx Server Error on ${url}.`); - } catch (error: any) { + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + const errName = error instanceof Error ? error.name : ""; + // If the user or a system explicitly aborted the request (like Issue #53), DO NOT retry. - if (error.name === "AbortError") { + if (errName === "AbortError") { throw error; } @@ -50,7 +53,7 @@ export async function fetchWithRetry(url: string, options: FetchRetryOptions = { throw new Error(`[HTTP Client] Network failure. Exhausted all ${retries} retry attempts for ${url}.`); } - console.warn(`[HTTP Client] Network drop on ${url}: ${error.message}`); + console.warn(`[HTTP Client] Network drop on ${url}: ${errMsg}`); } // --- EXPONENTIAL BACKOFF MATH --- @@ -71,7 +74,7 @@ export async function fetchWithRetry(url: string, options: FetchRetryOptions = { export const httpClient = { get: (url: string, options?: FetchRetryOptions) => fetchWithRetry(url, { ...options, method: 'GET' }), - post: (url: string, body: any, options?: FetchRetryOptions) => fetchWithRetry(url, { + post: (url: string, body: unknown, options?: FetchRetryOptions) => fetchWithRetry(url, { ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, diff --git a/package.json b/package.json index c88a860..723bc05 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,8 @@ "lint": "eslint", "test": "jest", "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint", - "test": "jest", - "test:coverage": "jest --coverage" -}, "dependencies": { "@neondatabase/serverless": "^1.0.2", "@radix-ui/react-label": "^2.1.8",