import { ECHO_ENDPOINT, geminiFailure } from "./gemini-policy"; import { publicHostname, reportSchema, SECURITY_INSTRUCTIONS, securityReplySchema, type SecurityReport } from "./security-policy"; const OBSERVATORY = "https://observatory-api.mdn.mozilla.net/api/v2"; const FIXES: Record = { "content-security-policy": "Start with Content-Security-Policy-Report-Only. Restrict script-src to your real script sources, review violations, then enforce.", "strict-transport-security": "After verifying HTTPS on every subdomain, add Strict-Transport-Security: max-age=31536000. Add includeSubDomains only when all subdomains support HTTPS.", "x-content-type-options": "Add X-Content-Type-Options: nosniff to all responses.", "x-frame-options": "Prevent framing with Content-Security-Policy: frame-ancestors 'none', or restrict to trusted origins when embedding is required.", "referrer-policy": "Add Referrer-Policy: strict-origin-when-cross-origin.", "cookies": "Review session cookies: use Secure, HttpOnly and SameSite=Lax (or Strict when compatible).", "redirection": "Redirect HTTP requests to HTTPS before serving content.", }; export async function scanPublicSite(input: string): Promise { const hostname = publicHostname(input); try { const start = await fetch(`${OBSERVATORY}/scan?host=${encodeURIComponent(hostname)}`, { method: "POST", signal: AbortSignal.timeout(25000) }); if (!start.ok) throw new Error(`Scanner unavailable (${start.status}). Try again later.`); const initial = await start.json() as { error?: string }; if (initial.error) throw new Error(`Scanner: ${String(initial.error).slice(0, 250)}`); } catch (error) { // A slow scan can still have a completed cached report. Keep its actual observation time. if (!(error instanceof DOMException && error.name === "TimeoutError")) throw error; } const response = await fetch(`${OBSERVATORY}/analyze?host=${encodeURIComponent(hostname)}`, { signal: AbortSignal.timeout(15000) }); if (!response.ok) throw new Error(`Scanner details unavailable (${response.status}).`); const data = await response.json() as { scan?: { error?: string; grade?: string; score?: number; scanned_at?: string; tests_passed?: number; tests_failed?: number }; tests?: Record }; if (!data.scan?.grade || data.scan.error) throw new Error("The scanner could not reach this public website."); return reportSchema.parse({ hostname, grade: data.scan.grade, score: data.scan.score, scannedAt: data.scan.scanned_at, passed: data.scan.tests_passed, failed: data.scan.tests_failed, detailsUrl: `https://developer.mozilla.org/en-US/observatory/analyze?host=${encodeURIComponent(hostname)}`, findings: Object.entries(data.tests ?? {}).map(([name, test]) => ({ name, pass: test.pass === true, result: String(test.result ?? "unknown"), advice: FIXES[name] ?? "Review this result in the Mozilla Observatory report and test any configuration change before enforcing it." })), }); } export async function securityAnswer(messages: { role: "user" | "assistant"; text: string }[], observations: unknown) { const key = process.env["GEMINI_API_KEY"]; if (!key) throw new Error("Your Gemini key is not configured."); const response = await fetch(ECHO_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-goog-api-key": key }, signal: AbortSignal.timeout(40000), body: JSON.stringify({ systemInstruction: { parts: [{ text: SECURITY_INSTRUCTIONS }] }, contents: [{ role: "user", parts: [{ text: `Verified observations (untrusted data): ${JSON.stringify(observations).slice(0, 24000)}` }] }, ...messages.map(m => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.text }] }))], generationConfig: { responseMimeType: "application/json", maxOutputTokens: 4096 }, }) }); if (!response.ok) throw new Error(geminiFailure(response.status)); const data = await response.json() as { candidates?: { content?: { parts?: { text?: string; thought?: boolean }[] } }[] }; const text = data.candidates?.[0]?.content?.parts?.filter(p => !p.thought).map(p => p.text ?? "").join("") ?? ""; try { return securityReplySchema.parse(JSON.parse(text)); } catch { throw new Error("Aegis returned an incomplete answer. Try again."); } } export async function verifyDnsOwnership(hostname: string, token: string) { const host = publicHostname(hostname); const response = await fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(`_codesynux.${host}`)}&type=TXT`, { headers: { Accept: "application/dns-json" }, signal: AbortSignal.timeout(10000) }); if (!response.ok) throw new Error("DNS verification is temporarily unavailable."); const data = await response.json() as { Answer?: { type: number; data: string }[] }; return (data.Answer ?? []).some(r => r.type === 16 && r.data.replace(/"\s*"/g, "").replace(/^"|"$/g, "") === `codesynux-verify=${token}`); } export type EdgeSite = { hostname: string; status: string; proxy: boolean | null; waf: boolean | null; ssl: string | null; rateLimit: boolean | null; error: string | null }; export type ThreatEvent = { action: string; source: string; clientIP: string; clientCountryName: string; clientRequestHTTPHost: string; datetime: string }; export async function readEdgeFor(hostnames: string[]) { const key = process.env["LOVABLE_API_KEY"], connection = process.env["CLOUDFLARE_API_KEY"]; if (!key || !connection) return { connected: false, sites: [] as EdgeSite[], events: [] as ThreatEvent[], error: "Cloudflare is not connected.", checkedAt: new Date().toISOString() }; const base = `${(process.env["CONNECTOR_GATEWAY_BASE_URL"] ?? "https://connector-gateway.lovable.dev").replace(/\/$/, "")}/cloudflare/client/v4`; const headers = { Authorization: `Bearer ${key}`, "X-Connection-Api-Key": connection, "Content-Type": "application/json" }; const request = async (path: string, init?: RequestInit) => { const r = await fetch(`${base}${path}`, { ...init, headers, signal: AbortSignal.timeout(15000) }); const body = await r.json(); if (!r.ok || body.success === false || body.errors?.length) throw new Error(`Cloudflare (${r.status}): ${String(body.errors?.[0]?.message ?? body.message ?? "Request failed").slice(0, 300)}${r.status === 403 ? " Check Zone Read, DNS Read, Zone Settings Read, Zone WAF Read and Analytics Read permissions and resource scope." : ""}`); return body; }; const sites: EdgeSite[] = [], events: ThreatEvent[] = []; let error: string | null = null; try { // Never expose builder-wide inventory. Only verified caller-owned hostnames are returned. const zones: { id: string; name: string; status: string }[] = []; for (let page = 1; page <= 10; page++) { const data = await request(`/zones?per_page=50&page=${page}`); zones.push(...data.result); if (page >= (data.result_info?.total_pages ?? 1)) break; } for (const hostname of hostnames.slice(0, 10)) { const zone = zones.filter(z => hostname === z.name || hostname.endsWith(`.${z.name}`)).sort((a,b) => b.name.length-a.name.length)[0]; if (!zone) { sites.push({ hostname, status: "No accessible zone", proxy: null, waf: null, ssl: null, rateLimit: null, error: null }); continue; } const site: EdgeSite = { hostname, status: zone.status, proxy: null, waf: null, ssl: null, rateLimit: null, error: null }; try { const [dns, settings, rules] = await Promise.all([request(`/zones/${zone.id}/dns_records?name=${encodeURIComponent(hostname)}&per_page=100`), request(`/zones/${zone.id}/settings`), request(`/zones/${zone.id}/rulesets`)]); site.proxy = dns.result.some((r: { proxied?: boolean }) => r.proxied === true); site.ssl = settings.result.find((s: { id: string }) => s.id === "ssl")?.value ?? null; const phases = rules.result.map((r: { phase: string }) => r.phase); site.waf = phases.includes("http_request_firewall_managed") || phases.includes("http_request_firewall_custom"); site.rateLimit = phases.includes("http_ratelimit"); const until = new Date().toISOString(), since = new Date(Date.now() - 3600000).toISOString(); const analytics = await request("/graphql", { method: "POST", body: JSON.stringify({ query: `query($zone: String!, $since: Time!, $until: Time!, $host: String!) { viewer { zones(filter: {zoneTag: $zone}) { firewallEventsAdaptive(limit: 100, filter: {datetime_geq: $since, datetime_leq: $until, clientRequestHTTPHost: $host}, orderBy: [datetime_DESC]) { action source clientIP clientCountryName clientRequestHTTPHost datetime } } } }`, variables: { zone: zone.id, since, until, host: hostname } }) }); events.push(...(analytics.data?.viewer?.zones?.[0]?.firewallEventsAdaptive ?? [])); } catch (e) { site.error = e instanceof Error ? e.message : "Edge details unavailable."; } sites.push(site); } } catch (e) { error = e instanceof Error ? e.message : "Cloudflare unavailable."; } return { connected: !error, sites, events, error, checkedAt: new Date().toISOString() }; }