From 5dee36d30a3da95a9f0f1f3da2113beacd263c22 Mon Sep 17 00:00:00 2001 From: Jonathan Singer Date: Mon, 20 Jul 2026 12:02:06 -0400 Subject: [PATCH] Wire the viewer to run history, email auth, and report-send endpoints --- strix/viewer_src/src/data/serverSource.ts | 143 ++++++++++++++++++++-- strix/viewer_src/src/lib/cta.ts | 2 + 2 files changed, 132 insertions(+), 13 deletions(-) diff --git a/strix/viewer_src/src/data/serverSource.ts b/strix/viewer_src/src/data/serverSource.ts index 445d16dd..9b5b4351 100644 --- a/strix/viewer_src/src/data/serverSource.ts +++ b/strix/viewer_src/src/data/serverSource.ts @@ -56,30 +56,38 @@ async function getJson(path: string): Promise { return res.json(); } -export async function fetchRunSummary(): Promise<{ +/** Build a ``?run=`` suffix for run-scoped data endpoints. */ +function runQuery(runName?: string | null): string { + return runName ? `?run=${encodeURIComponent(runName)}` : ""; +} + +export async function fetchRunSummary(runName?: string | null): Promise<{ summary: ParsedRunSummary; raw: Record; finished: boolean; }> { - const raw = (await getJson("/api/run")) as Record; + const raw = (await getJson("/api/run" + runQuery(runName))) as Record; // parseRunJson tolerates extra keys and takes raw TEXT. const summary = parseRunJson(JSON.stringify(raw)); const finished = raw.finished === true; return { summary, raw, finished }; } -export async function fetchVulnerabilities(runId: string | null): Promise { - const arr = await getJson("/api/vulnerabilities"); +export async function fetchVulnerabilities( + runId: string | null, + runName?: string | null +): Promise { + const arr = await getJson("/api/vulnerabilities" + runQuery(runName)); return parseVulnerabilitiesJson(JSON.stringify(arr), runId); } -export async function fetchReportMarkdown(): Promise { - const obj = (await getJson("/api/report")) as { markdown?: string }; +export async function fetchReportMarkdown(runName?: string | null): Promise { + const obj = (await getJson("/api/report" + runQuery(runName))) as { markdown?: string }; return obj?.markdown ?? null; } -export async function fetchTranscript(): Promise { - const obj = (await getJson("/api/transcript")) as Partial; +export async function fetchTranscript(runName?: string | null): Promise { + const obj = (await getJson("/api/transcript" + runQuery(runName))) as Partial; return { agents: Array.isArray(obj?.agents) ? obj.agents : [], events: Array.isArray(obj?.events) ? obj.events : [], @@ -87,12 +95,121 @@ export async function fetchTranscript(): Promise { } /** One-shot fetch of every endpoint (used on mount and on final settle). */ -export async function fetchAll(): Promise { - const { summary, raw, finished } = await fetchRunSummary(); +export async function fetchAll(runName?: string | null): Promise { + const { summary, raw, finished } = await fetchRunSummary(runName); const [vulnerabilities, reportMarkdown, transcript] = await Promise.all([ - fetchVulnerabilities(summary.runId).catch(() => [] as Vulnerability[]), - fetchReportMarkdown().catch(() => null), - fetchTranscript().catch(() => ({ agents: [], events: [] }) as Transcript), + fetchVulnerabilities(summary.runId, runName).catch(() => [] as Vulnerability[]), + fetchReportMarkdown(runName).catch(() => null), + fetchTranscript(runName).catch(() => ({ agents: [], events: [] }) as Transcript), ]); return { summary, raw, finished, vulnerabilities, reportMarkdown, transcript }; } + +// --------------------------------------------------------------------------- +// Run history + email auth + report send +// +// These endpoints back the "Your runs" sidebar section. Auth and report-send +// responses carry a meaningful JSON body on non-2xx statuses (an ``error`` +// code), so they read the body regardless of status rather than throwing. +// --------------------------------------------------------------------------- + +export interface RunSeverityCounts { + critical: number; + high: number; + medium: number; + low: number; +} + +export interface RunListEntry { + name: string; + target: string | null; + scan_mode: string | null; + status: string | null; + start_time: string | null; + end_time: string | null; + finished: boolean; + severity_counts: RunSeverityCounts; +} + +export interface RunsPayload { + locked: boolean; + count: number; + runs: RunListEntry[]; +} + +export interface AuthStatus { + verified: boolean; + email: string | null; +} + +export type OtpStartResult = { ok: true } | { ok: false; error: string }; +export type OtpVerifyResult = + | { verified: true; email: string } + | { verified: false; error: string }; +export type SendReportResult = + | { ok: true; password: string; filename: string } + | { ok: false; error: string }; + +async function postJson( + path: string, + body: Record +): Promise<{ ok: boolean; status: number; data: Record }> { + const res = await fetch(path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + cache: "no-store", + }); + let data: Record = {}; + try { + const parsed = await res.json(); + if (parsed && typeof parsed === "object") data = parsed as Record; + } catch { + /* empty or non-JSON body */ + } + return { ok: res.ok, status: res.status, data }; +} + +export async function fetchRuns(): Promise { + const obj = (await getJson("/api/runs")) as Partial; + return { + locked: obj?.locked ?? true, + count: typeof obj?.count === "number" ? obj.count : 0, + runs: Array.isArray(obj?.runs) ? (obj.runs as RunListEntry[]) : [], + }; +} + +export async function fetchAuthStatus(): Promise { + const obj = (await getJson("/api/auth/status")) as Partial; + return { verified: obj?.verified === true, email: obj?.email ?? null }; +} + +export async function otpStart(email: string): Promise { + const { ok, data } = await postJson("/api/auth/otp/start", { email }); + if (ok && data.ok === true) return { ok: true }; + return { ok: false, error: String(data.error ?? "unavailable") }; +} + +export async function otpVerify(email: string, code: string): Promise { + const { ok, data } = await postJson("/api/auth/otp/verify", { email, code }); + if (ok && data.verified === true) { + return { verified: true, email: String(data.email ?? email) }; + } + return { verified: false, error: String(data.error ?? "invalid_code") }; +} + +export async function forgetAuth(): Promise { + await postJson("/api/auth/forget", {}); +} + +export async function sendReport(runName?: string | null): Promise { + const { ok, data } = await postJson("/api/report/send", runName ? { run: runName } : {}); + if (ok && data.ok === true) { + return { + ok: true, + password: String(data.password ?? ""), + filename: String(data.filename ?? "strix-report.pdf"), + }; + } + return { ok: false, error: String(data.error ?? "unavailable") }; +} diff --git a/strix/viewer_src/src/lib/cta.ts b/strix/viewer_src/src/lib/cta.ts index 1532791f..3c915c78 100644 --- a/strix/viewer_src/src/lib/cta.ts +++ b/strix/viewer_src/src/lib/cta.ts @@ -1,6 +1,8 @@ // All upsell / sign-up CTAs route anonymous local-viewer users to the public // cloud sign-up. Open in a new tab so the local results stay put. export const SIGNUP_URL = "https://app.strix.ai/api/auth/signup"; +export const DEMO_URL = "https://strix.ai/demo"; +export const PRICING_URL = "https://strix.ai/pricing"; // Best-effort, anonymous conversion tracking. The local server forwards this to // PostHog only if the user has telemetry enabled; it never blocks navigation.