// ============================================================ // SafeAIScan — API Layer v2.0 // Standardized responses, plan-aware gating, retry logic // ============================================================ const BASE_URL = "https://rathious-safeaiscan.hf.space"; const MAX_RETRIES = 2; const RETRY_DELAY_MS = 900; // ---- AUTH HELPERS ---- const getToken = () => localStorage.getItem("access_token"); const getApiKey = () => localStorage.getItem("api_key"); function clearAuth() { localStorage.clear(); window.location.replace("login.html"); } // ── Plan helpers ─────────────────────────────────────────── // Source of truth: localStorage values set by getMe() after every page load. // pro_trial counts as full Pro access. let _userPlan = localStorage.getItem("user_plan") || "free"; let _userLimits = null; try { const s = localStorage.getItem("user_limits"); if (s) _userLimits = JSON.parse(s); } catch {} function isProUser() { if (localStorage.getItem("is_pro") === "true") return true; if (localStorage.getItem("trial_active") === "true") return true; const p = localStorage.getItem("user_plan") || "free"; return p === "pro_trial" || p === "pro" || p === "enterprise"; } function isTrialUser() { const p = localStorage.getItem("user_plan") || "free"; return p === "pro_trial" && localStorage.getItem("trial_active") === "true"; } function getTrialDaysLeft() { return parseInt(localStorage.getItem("trial_days_left") || "0"); } function getUserPlan() { return _userPlan; } function getUserLimits() { return _userLimits; } function cachePlanData(plan, limits) { _userPlan = plan; _userLimits = limits; localStorage.setItem("user_plan", plan); if (limits) localStorage.setItem("user_limits", JSON.stringify(limits)); } function canAccessFeature(feature) { if (isProUser()) return true; // Free plan has limited access const freeFeatures = { repo_scan: true, basic_scan: true }; return !!freeFeatures[feature]; } // ---- RETRY HELPER ---- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ---- CORE REQUEST ---- async function apiRequest(endpoint, options = {}, retries = MAX_RETRIES) { const token = getToken(); const apiKey = getApiKey(); // FIX: only attach x-api-key when the caller explicitly opts in via options.useApiKey // Sending a stale/rotated api_key on every browser request caused 403 "Invalid API key" // which was previously misclassified as a PlanError, breaking all dashboard calls. const sendApiKey = options.useApiKey && apiKey && apiKey !== "undefined" && apiKey !== "null"; const headers = { "Content-Type": "application/json", ...(token && token !== "undefined" && token !== "null" && { "Authorization": `Bearer ${token}` }), ...(sendApiKey && { "x-api-key": apiKey }), ...(options.headers || {}) }; // Strip internal flag before passing to fetch const { useApiKey: _omit, ...fetchOptions } = options; try { const res = await fetch(BASE_URL + endpoint, { ...fetchOptions, headers }); if (res.status === 401) { clearAuth(); throw new Error("Session expired. Please log in again."); } if (res.status === 403) { const body = await res.json().catch(() => ({})); const msg = body?.detail?.error || body?.error || "Access denied"; // FIX: "Invalid API key" is an auth/credential error, NOT a plan restriction. // Strip the bad key from storage and retry the request without it so the // JWT-only flow takes over — no upgrade modal, no crash. if (msg === "Invalid API key") { console.warn("[SafeAIScan] Stale API key detected — removing from storage and retrying."); localStorage.removeItem("api_key"); _userLimits = null; localStorage.removeItem("user_limits"); // Retry once without the api key if (retries > 0) { return apiRequest(endpoint, { ...fetchOptions, headers: options.headers }, retries - 1); } throw new Error("API key invalid. Please rotate your key in settings."); } throw new PlanError(msg); } if (res.status === 429) { const body = await res.json().catch(() => ({})); const msg = body?.detail?.error || body?.error || "Usage limit reached"; throw new LimitError(msg); } if (!res.ok) { const body = await res.json().catch(() => null); const msg = body?.detail?.error || body?.error || res.statusText || `HTTP ${res.status}`; throw new Error(msg); } return res; } catch (err) { if (err instanceof PlanError || err instanceof LimitError) throw err; if (err.message.includes("Session expired")) throw err; if (err.message.includes("API key invalid")) throw err; if (retries > 0) { await sleep(RETRY_DELAY_MS); return apiRequest(endpoint, fetchOptions, retries - 1); } throw err; } } // ---- CUSTOM ERROR TYPES ---- class PlanError extends Error { constructor(message) { super(message); this.name = "PlanError"; } } class LimitError extends Error { constructor(message) { super(message); this.name = "LimitError"; } } // ---- SAFE JSON PARSE ---- async function safeJson(res) { const text = await res.text(); try { const parsed = JSON.parse(text); // Unwrap standardized {success, data} envelope if (parsed && typeof parsed === "object" && "success" in parsed) { if (!parsed.success) throw new Error(parsed.error || "Request failed"); return parsed.data ?? parsed; } return parsed; } catch (e) { if (e.message !== "Request failed") { console.error("Non-JSON response:", text.substring(0, 200)); } throw e; } } // ============================================================ // API METHODS // ============================================================ async function analyzeCode(text) { const res = await apiRequest("/api/analyze", { method: "POST", body: JSON.stringify({ text }) }); const data = await safeJson(res); // Cache plan data from response for usage counters if (data.plan && data.usage_limit) { cachePlanData(data.plan, null); document.dispatchEvent(new CustomEvent("planUpdated", { detail: data })); } return data; } async function scanRepoAPI(repoUrl) { if (!canAccessFeature("repo_scan")) { throw new PlanError("Repo scanning requires Pro or Enterprise. Upgrade to unlock."); } const res = await apiRequest("/api/scan-repo", { method: "POST", body: JSON.stringify({ repo_url: repoUrl }) }); return safeJson(res); } async function getTaskStatus(taskId) { const res = await apiRequest(`/api/task/${taskId}`); return safeJson(res); } async function getUsage() { const res = await apiRequest("/api/usage"); return safeJson(res); } async function getHistory() { const res = await apiRequest("/api/history"); return safeJson(res); } async function getMe() { // Backend route: GET /api/me — returns full subscription + trial info try { const res = await apiRequest("/api/me"); const data = await safeJson(res); if (data?.plan) localStorage.setItem("user_plan", data.plan); if (data?.is_pro !== undefined) localStorage.setItem("is_pro", data.is_pro ? "true" : "false"); if (data?.trial_active !== undefined) localStorage.setItem("trial_active", data.trial_active ? "true" : "false"); if (data?.days_left !== undefined) localStorage.setItem("trial_days_left", String(data.days_left)); cachePlanData(data.plan, data.limits || null); return data; } catch (err) { if (err.message.includes("404")) { return { plan: localStorage.getItem("user_plan") || "free", is_pro: localStorage.getItem("is_pro") === "true", trial_active: localStorage.getItem("trial_active") === "true", days_left: parseInt(localStorage.getItem("trial_days_left") || "0"), user_id: localStorage.getItem("user_id") || "", email: localStorage.getItem("user_email") || "" }; } throw err; } } async function getTrialStatus() { try { const res = await apiRequest("/api/trial/status"); return safeJson(res); } catch (err) { console.warn("[SecretScan] trial status unavailable:", err.message); return { plan: localStorage.getItem("user_plan") || "free", is_pro: localStorage.getItem("is_pro") === "true", trial_active: localStorage.getItem("trial_active") === "true", days_left: parseInt(localStorage.getItem("trial_days_left") || "0"), trial_expired: false, days_left: 0 }; } } function rotateApiKey() { // FIX: key rotation now calls the backend, then refreshes UI via initApiKey if available apiRequest("/api/auth/rotate-key", { method: "POST" }) .then(res => safeJson(res)) .then(data => { const newKey = data?.api_key || data?.data?.api_key; if (newKey) { localStorage.setItem("api_key", newKey); // initApiKey lives in app.js — call only if loaded if (typeof window.initApiKey === "function") window.initApiKey(); } showToast("API key rotated successfully", "success"); }) .catch(err => showToast("Key rotation failed: " + err.message, "error")); } async function fetchCVE() { const input = document.getElementById("cveInput")?.value?.trim(); if (!input) return showToast("Enter a CVE ID or keyword", "warning"); const panel = document.getElementById("cvePanel"); if (!panel) return; panel.innerHTML = `
`; try { const res = await apiRequest(`/api/cve/search?query=${encodeURIComponent(input)}`); const data = await safeJson(res); const cves = data?.cves || data?.data?.cves || []; if (!cves.length) { panel.innerHTML = `' + escHtml(message) + '
' + featureBlock + '