// ============================================================ // 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 = `
No CVEs found for "${escHtml(input)}"
`; return; } panel.innerHTML = cves.map(cve => `
${escHtml(cve.id || "")} ${cve.cvss ?? "N/A"}
${escHtml((cve.description || "").substring(0, 180))}
`).join(""); } catch (err) { console.error(err); panel.innerHTML = `
${escHtml(err.message)}
`; } } // FIX: guard with typeof check — app.js and minisky.js also define these helpers if (typeof window.cvssSev === "undefined") { window.cvssSev = function cvssSev(score) { if (score == null) return "low"; if (score >= 9) return "critical"; if (score >= 7) return "high"; if (score >= 4) return "medium"; return "low"; }; } function cvssSev(score) { return window.cvssSev(score); } if (typeof window.escHtml === "undefined") { window.escHtml = function escHtml(str) { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); }; } function escHtml(str) { return window.escHtml(str); } // ============================================================ // UPGRADE PROMPT (shown on PlanError / LimitError) // ============================================================ function showUpgradePrompt(message) { // Remove any existing upgrade modal document.getElementById("_upgradeModal")?.remove(); const plan = localStorage.getItem("user_plan") || "free"; const alreadyTrial = plan === "pro_trial"; const alreadyPro = plan === "pro" || plan === "enterprise"; // Show trial offer to free users who haven't tried it yet const offerTrial = !alreadyTrial && !alreadyPro; const modal = document.createElement("div"); modal.id = "_upgradeModal"; modal.setAttribute("style", "position:fixed;inset:0;z-index:99998;display:flex;align-items:center;" + "justify-content:center;background:rgba(0,0,0,0.7);backdrop-filter:blur(6px);padding:20px;" ); const featureList = offerTrial ? ["Unlimited scans every day", "All findings revealed", "GitHub repo scanning", "Deep AI analysis & fix suggestions", "PDF report download", "CVE enrichment & lookup"] .map(f => '
' + '' + f + '
') .join("") : ""; const headerHTML = offerTrial ? '
' + '🎁' + '
Try Pro Free — 30 Days
' + '
' : '
' + ' ' + (alreadyTrial ? "Keep Your Pro Access" : "Upgrade to Pro") + '
'; const featureBlock = featureList ? '
' + featureList + '
' : ""; const primaryBtn = offerTrial ? '' + 'Start Free 30-Day Trial — No Card Needed' + '
After trial: $1.99/mo · Cancel anytime
' : ''; modal.innerHTML = '
' + headerHTML + '

' + escHtml(message) + '

' + featureBlock + '
' + primaryBtn + '' + '
'; document.body.appendChild(modal); modal.addEventListener("click", e => { if (e.target === modal) modal.remove(); }); } // ============================================================ // TOAST NOTIFICATIONS // FIX: guarded — app.js also defines showToast; only define once // ============================================================ if (typeof window.showToast === "undefined") function showToast(message, type = "info") { const colors = { info: "rgba(91,123,254,0.15)", success: "rgba(52,211,153,0.12)", warning: "rgba(251,146,60,0.12)", error: "rgba(244,63,94,0.12)" }; const textColors = { info: "#93aaff", success: "#6ee7b7", warning: "#fdba74", error: "#fb7185" }; const icons = { info: "bi-info-circle", success: "bi-check-circle", warning: "bi-exclamation-triangle", error: "bi-x-circle" }; if (!document.querySelector("#toastStyle")) { const s = document.createElement("style"); s.id = "toastStyle"; s.textContent = ` @keyframes toastIn { from { opacity:0; transform:translateY(12px) scale(0.95); } to { opacity:1; transform:translateY(0) scale(1); } } @keyframes toastOut { from { opacity:1; transform:scale(1); } to { opacity:0; transform:translateY(6px) scale(0.95); } } .toast-container { position:fixed;bottom:20px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px;align-items:flex-end; } `; document.head.appendChild(s); } let container = document.querySelector(".toast-container"); if (!container) { container = document.createElement("div"); container.className = "toast-container"; document.body.appendChild(container); } const toast = document.createElement("div"); toast.style.cssText = ` background:${colors[type] || colors.info}; color:${textColors[type] || textColors.info}; border:1px solid ${textColors[type] || textColors.info}33; border-radius:10px;padding:11px 16px;font-size:13px; font-family:'DM Sans',sans-serif;font-weight:500; backdrop-filter:blur(12px); box-shadow:0 8px 32px rgba(0,0,0,0.4); animation:toastIn 0.25s ease both; max-width:320px;display:flex;align-items:center;gap:8px;cursor:pointer; `; toast.innerHTML = `${escHtml(message)}`; toast.addEventListener("click", () => dismiss()); container.appendChild(toast); const dismiss = () => { toast.style.animation = "toastOut 0.22s ease forwards"; setTimeout(() => toast.remove(), 230); }; setTimeout(dismiss, 3800); } // Global plan-error handler window.addEventListener("unhandledrejection", e => { if (e.reason instanceof PlanError) { showUpgradePrompt(e.reason.message); e.preventDefault(); } else if (e.reason instanceof LimitError) { showToast(e.reason.message, "warning"); e.preventDefault(); } else { console.error("Unhandled rejection:", e.reason); } }); // Expose for dashboard window.PlanError = PlanError; window.LimitError = LimitError; window.showUpgradePrompt = showUpgradePrompt;