diff --git a/.gitignore b/.gitignore index 895fe898..2e4e4e61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,25 @@ +# Node / local-viewer SPA source (the built bundle in +# strix/viewer/static/ is committed and shipped; do not ignore it) +node_modules/ +strix/viewer/frontend/node_modules/ +strix/viewer/frontend/.vite/ + # Python __pycache__/ *.py[cod] *$py.class *.so .Python -build/ +# Anchored to the repo root: these are Python build-artifact dir names, but +# unanchored they also match nested source dirs (e.g. the viewer's src/lib). +/build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ parts/ sdist/ var/ @@ -46,7 +54,7 @@ pip-delete-this-directory.txt .env.production.local # MongoDB -data/ +/data/ mongod.log *.mongodb *.mongorc.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f14e5e36..20cb009a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,6 +99,20 @@ We welcome feature ideas! Please: - Consider implementation approach - Be open to discussion +## 🖥️ Local viewer SPA + +`strix view` serves a prebuilt web UI whose source lives in +`strix/viewer/frontend/` (a Vite + React project) and whose built output is +committed to `strix/viewer/static/` and shipped in the package. End users never +run a JS build. If you change anything under `strix/viewer/frontend/`, rebuild +and commit the output: + +```bash +make viewer # or: cd strix/viewer/frontend && npm ci && npm run build +``` + +Commit both the source change and the regenerated `strix/viewer/static/`. + ## 🤝 Community - **Discord**: [Join our community](https://discord.gg/strix-ai) diff --git a/Makefile b/Makefile index faaade56..656c0346 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install dev-install format lint type-check security check-all clean pre-commit setup-dev dev +.PHONY: help install dev-install format lint type-check security check-all clean pre-commit setup-dev dev viewer help: @echo "Available commands:" @@ -15,6 +15,7 @@ help: @echo "" @echo "Development:" @echo " pre-commit - Run pre-commit hooks on all files" + @echo " viewer - Rebuild the local-viewer SPA (commit the output)" @echo " clean - Clean up cache files and artifacts" install: @@ -66,5 +67,10 @@ clean: find . -name "*.pyc" -delete 2>/dev/null || true @echo "✅ Cleanup complete!" +viewer: + @echo "🖥️ Building the local-viewer SPA..." + cd strix/viewer/frontend && npm ci && npm run build + @echo "✅ Viewer built to strix/viewer/static/ (commit the changes)." + dev: format lint type-check @echo "✅ Development cycle complete!" diff --git a/README.md b/README.md index f436c754..2122d04b 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,31 @@ Advanced multi-agent orchestration for comprehensive automated penetration testi --- +## 🖥️ Local Web Viewer + +Every scan writes its results to disk as it runs. Bring them up in a local dashboard with a single command: + +```bash +# Open the most recent run +strix view + +# ...or open a specific run by name +strix view my-run-name +``` + +`strix view` starts a lightweight local server (bound to `127.0.0.1` on a random port) and opens your browser to a private, tokened link. Nothing leaves your machine: the dashboard reads the run's files straight off disk, with no cloud account or upload required. The UI ships prebuilt with Strix, so there is no extra install and no JS build step. + +### What's in the dashboard + +- **Overview**: run status, target, and a severity breakdown of everything found so far. +- **Vulnerabilities**: each validated finding with its severity, details, and reproduction steps. +- **Agent graph**: a live map of the multi-agent team, showing which agent is doing what. +- **Steering**: send instructions to a live scan from the browser to redirect the agents mid-run. +- **History**: browse past runs on this machine and jump between them. +- **Reports**: generate a shareable report and email it to yourself or your team. + +--- + ## Usage Examples ### Basic Usage diff --git a/pyproject.toml b/pyproject.toml index 384773f4..3bc079a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,9 @@ dependencies = [ "requests>=2.32.0", "cvss>=3.2", "caido-sdk-client>=0.2.0", + "reportlab>=4.0", + "pypdf>=5.0", + "cryptography>=42", ] [project.optional-dependencies] @@ -74,6 +77,10 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["strix"] +# The prebuilt viewer bundle under strix/viewer/static/ ships automatically +# (hatchling includes non-.py files under the package). The Vite SOURCE lives +# under the package dir too (strix/viewer/frontend/) but must never ship in the wheel. +exclude = ["strix/viewer/frontend", "strix/viewer/frontend/**"] # ============================================================================ # Type Checking Configuration @@ -111,6 +118,8 @@ module = [ "docker.*", "caido_sdk_client.*", "pydantic_settings.*", + "reportlab.*", + "pypdf.*", ] ignore_missing_imports = true disable_error_code = ["import-untyped"] @@ -201,6 +210,15 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] +# Test doubles use fixture tokens/passwords and match a callee signature whose +# args they intentionally ignore. +"tests/test_viewer_auth.py" = ["S105", "S106", "ARG001"] +"tests/test_report_pdf.py" = ["S105", "S106"] +# Stdlib HTTP handler overrides (do_GET/do_POST) and lazy imports that avoid a +# circular dependency with strix.telemetry / strix.viewer.report_pdf. +"strix/viewer/server.py" = ["N802", "PLC0415"] +# Lazy telemetry import to avoid importing PostHog before the viewer starts. +"strix/viewer/cli.py" = ["PLC0415"] # Lazy imports inside functions to avoid circular dependency with # strix.telemetry / strix.report.dedupe / cvss. "strix/tools/notes/tools.py" = ["PLC0415", "TC002"] diff --git a/strix.spec b/strix.spec index 78aa35d4..7c94cca6 100644 --- a/strix.spec +++ b/strix.spec @@ -25,6 +25,13 @@ for tcss_file in strix_root.rglob('*.tcss'): rel_path = tcss_file.relative_to(project_root) datas.append((str(tcss_file), str(rel_path.parent))) +# Prebuilt local-viewer SPA (served by `strix view`). +viewer_static = strix_root / 'viewer' / 'static' +for asset in viewer_static.rglob('*'): + if asset.is_file(): + rel_path = asset.relative_to(project_root) + datas.append((str(asset), str(rel_path.parent))) + datas += collect_data_files('textual') datas += collect_data_files('tiktoken') @@ -151,6 +158,21 @@ hiddenimports = [ 'strix.report.dedupe', 'strix.report.state', 'strix.report.writer', + 'strix.viewer', + 'strix.viewer.auth', + 'strix.viewer.cli', + 'strix.viewer.report_pdf', + 'strix.viewer.server', + 'strix.viewer.transcript', + + # PDF report generation + encryption + 'reportlab', + 'reportlab.pdfgen', + 'reportlab.pdfbase', + 'reportlab.lib', + 'reportlab.platypus', + 'pypdf', + 'cryptography', 'strix.runtime', 'strix.runtime.backends', 'strix.runtime.caido_bootstrap', @@ -178,6 +200,16 @@ hiddenimports += collect_submodules('textual') hiddenimports += collect_submodules('rich') hiddenimports += collect_submodules('pydantic') hiddenimports += collect_submodules('pygments') +# reportlab loads renderers/fonts dynamically, so pull its whole tree in. +hiddenimports += collect_submodules('reportlab') + +# reportlab ships bundled fonts (.pfb/.afm) it needs at runtime. +datas += collect_data_files('reportlab') + +# reportlab imports PIL (pillow) lazily for image handling, so it must be +# bundled explicitly and kept out of the excludes list below. +hiddenimports += collect_submodules('PIL') +datas += collect_data_files('PIL') excludes = [ # Sandbox-only packages @@ -225,7 +257,6 @@ excludes = [ 'numpy', 'pandas', 'scipy', - 'PIL', 'cv2', ] diff --git a/strix/config/settings.py b/strix/config/settings.py index 84339218..8e480cb1 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -72,6 +72,15 @@ class IntegrationSettings(BaseSettings): perplexity_api_key: str | None = Field(default=None, alias="PERPLEXITY_API_KEY") +class ViewerSettings(BaseSettings): + model_config = _BASE_CONFIG + + # Base URL of the Strix relay the local viewer proxies to for email + # verification and encrypted report delivery. The browser never talks to + # the relay directly; the local server is the only caller. + app_url: str = Field(default="https://app.strix.ai", alias="STRIX_APP_URL") + + class Settings(BaseSettings): model_config = _BASE_CONFIG @@ -79,3 +88,4 @@ class Settings(BaseSettings): runtime: RuntimeSettings = Field(default_factory=RuntimeSettings) telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings) integrations: IntegrationSettings = Field(default_factory=IntegrationSettings) + viewer: ViewerSettings = Field(default_factory=ViewerSettings) diff --git a/strix/core/paths.py b/strix/core/paths.py index c8e21b73..2cd7ba0b 100644 --- a/strix/core/paths.py +++ b/strix/core/paths.py @@ -21,3 +21,20 @@ def runtime_state_dir(run_dir: Path) -> Path: def run_record_path(run_dir: Path) -> Path: return run_dir / RUN_RECORD_FILENAME + + +def runs_base_dir(*, cwd: Path | None = None) -> Path: + base = cwd or Path.cwd() + return base / RUNS_DIR_NAME + + +def latest_run_dir(*, cwd: Path | None = None) -> Path | None: + base = runs_base_dir(cwd=cwd) + if not base.is_dir(): + return None + candidates = [child for child in base.iterdir() if run_record_path(child).is_file()] + if not candidates: + return None + # run.json is rewritten on status/end changes, so its mtime tracks activity + # more reliably than the directory mtime (a live run sorts to the top). + return max(candidates, key=lambda child: run_record_path(child).stat().st_mtime) diff --git a/strix/interface/assets/tui_styles.tcss b/strix/interface/assets/tui_styles.tcss index d2984c8b..7e964dfa 100644 --- a/strix/interface/assets/tui_styles.tcss +++ b/strix/interface/assets/tui_styles.tcss @@ -67,6 +67,16 @@ Toast.-information .toast--title { display: none; } +#viewer_cta { + height: auto; + background: transparent; + border: round #333333; + color: #60a5fa; + padding: 0 1; + margin-bottom: 1; + text-align: center; +} + #agents_tree { height: 1fr; background: transparent; diff --git a/strix/interface/main.py b/strix/interface/main.py index 2403200d..e307d61c 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -7,6 +7,7 @@ import argparse import asyncio import shutil import sys +import time from datetime import UTC, datetime from pathlib import Path @@ -720,7 +721,9 @@ def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser args.scan_mode = persisted_scan_mode -def display_completion_message(args: argparse.Namespace, results_path: Path) -> None: +def display_completion_message( + args: argparse.Namespace, results_path: Path, web_url: str | None = None +) -> None: console = Console() report_state = get_global_report_state() @@ -759,6 +762,29 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) -> results_text.append(str(results_path), style="#60a5fa") panel_parts.extend(["\n", results_text]) + if web_url: + web_text = Text() + web_text.append("\n") + web_text.append("View in web", style="dim") + web_text.append(" ") + # OSC-8 hyperlink: clickable in modern terminals, falls back to the URL. + web_text.append(web_url, style=f"#60a5fa link {web_url}") + panel_parts.extend(["\n", web_text]) + + reopen_text = Text() + reopen_text.append("\n") + reopen_text.append("Reopen", style="dim") + reopen_text.append(" ") + reopen_text.append(f"strix view {args.run_name}", style="#22c55e") + panel_parts.extend(["\n", reopen_text]) + else: + view_text = Text() + view_text.append("\n") + view_text.append("View", style="dim") + view_text.append(" ") + view_text.append(f"strix view {args.run_name}", style="#22c55e") + panel_parts.extend(["\n", view_text]) + if not scan_completed: resume_text = Text() resume_text.append("\n") @@ -846,6 +872,14 @@ def main() -> None: if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # `strix view []` is a viewer-only subcommand, dispatched before the + # scan argument parser (which requires a target) and before any scan setup. + if len(sys.argv) > 1 and sys.argv[1] == "view": + from strix.viewer.cli import run_view + + run_view(sys.argv[2:]) + return + args = parse_arguments() if args.config: @@ -940,7 +974,40 @@ def main() -> None: scarf.end(report_state, exit_reason=exit_reason) results_path = run_dir_for(args.run_name) - display_completion_message(args, results_path) + + # For an interactive run, host the local viewer so the completion panel can + # show a clickable "View in web" link. Skipped in non-interactive/CI runs + # (no TTY to serve and it would block the process). + viewer_httpd = None + web_url = None + if not args.non_interactive and sys.stdout.isatty(): + from strix.viewer.server import authorized_url, bundle_is_built, serve + + if bundle_is_built(): + try: + viewer_httpd, base_url, token = serve(results_path, open_browser=False) + # The completion panel's "View in web" link must authorize the + # browser, so hand it the tokened URL rather than the bare host. + web_url = authorized_url(base_url, token) + posthog.viewer_opened(source="post_scan", live=False) + except Exception: + logger.debug("could not start local viewer", exc_info=True) + viewer_httpd, web_url = None, None + + display_completion_message(args, results_path, web_url=web_url) + + if viewer_httpd is not None: + console = Console() + console.print("[dim]Hosting the local viewer. Press Ctrl-C to stop.[/]") + console.print() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + console.print("\n[dim]Viewer stopped.[/]") + finally: + viewer_httpd.shutdown() + viewer_httpd.server_close() if args.non_interactive: report_state = get_global_report_state() diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index 73a7ec0d..5f350898 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -6,6 +6,7 @@ import logging import signal import sys import threading +import webbrowser from collections.abc import Callable from importlib.metadata import PackageNotFoundError from importlib.metadata import version as pkg_version @@ -768,6 +769,7 @@ class StrixTUIApp(App): # type: ignore[misc] Binding("ctrl+q", "request_quit", "Quit", priority=True), Binding("ctrl+c", "request_quit", "Quit", priority=True), Binding("escape", "stop_selected_agent", "Stop Agent", priority=True), + Binding("ctrl+o", "open_viewer", "Open Viewer", priority=True), ] def __init__(self, args: argparse.Namespace): @@ -794,6 +796,8 @@ class StrixTUIApp(App): # type: ignore[misc] self._displayed_events: list[str] = [] self._scan_thread: threading.Thread | None = None + self._viewer_httpd: Any = None + self._viewer_url: str | None = None self._scan_loop: asyncio.AbstractEventLoop | None = None self._scan_stop_event = threading.Event() self._scan_completed = threading.Event() @@ -903,7 +907,12 @@ class StrixTUIApp(App): # type: ignore[misc] vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel") - sidebar = Vertical(agents_tree, vulnerabilities_panel, stats_scroll, id="sidebar") + viewer_cta = Static(self._viewer_cta_markup(), id="viewer_cta") + viewer_cta.ALLOW_SELECT = False + + sidebar = Vertical( + viewer_cta, agents_tree, vulnerabilities_panel, stats_scroll, id="sidebar" + ) content_container.mount(chat_area_container) content_container.mount(sidebar) @@ -1805,6 +1814,7 @@ class StrixTUIApp(App): # type: ignore[misc] async def action_custom_quit(self) -> None: self._fire_sandbox_cleanup() + self._shutdown_viewer() if self._scan_thread and self._scan_thread.is_alive(): self._scan_stop_event.set() @@ -1813,6 +1823,70 @@ class StrixTUIApp(App): # type: ignore[misc] self.exit() + def _viewer_cta_markup(self, url: str | None = None) -> str: + if url: + return f"[@click=app.open_viewer][#22c55e]● Viewer running[/][/]\n[dim]{url}[/]" + return "[@click=app.open_viewer]▶ Watch live in browser[/]" + + def _set_viewer_cta(self, markup: str) -> None: + with contextlib.suppress(Exception): + self.query_one("#viewer_cta", Static).update(markup) + + def action_open_viewer(self) -> None: + if self._viewer_url: + with contextlib.suppress(Exception): + webbrowser.open(self._viewer_url) + return + try: + from strix.viewer.server import authorized_url, bundle_is_built, serve + + if not bundle_is_built(): + self._set_viewer_cta("[#eab308]Viewer UI not built[/]") + return + run_dir = self.report_state.get_run_dir() + + def _viewer_steer(agent_id: str, message: str) -> bool: + # Reuse the exact TUI delivery path, but target the agent the + # web graph selected (not the TUI's current selection). + return send_user_message_to_agent( + coordinator=self.coordinator, + loop=self._scan_loop, + live_view=self.live_view, + target_agent_id=agent_id, + message=message, + ) + + httpd, url, token = serve(run_dir, open_browser=True, steer_handler=_viewer_steer) + except Exception: + logger.debug("failed to start local viewer", exc_info=True) + self._set_viewer_cta("[red]Viewer failed to start[/]") + return + self._viewer_httpd = httpd + # Store the tokened URL so reopening the CTA re-authorizes the browser + # (this viewer carries a steer handler, so the session is required). + self._viewer_url = authorized_url(url, token) + self._set_viewer_cta(self._viewer_cta_markup(self._viewer_url)) + + with contextlib.suppress(Exception): + from strix.telemetry import posthog + + live = self.report_state.run_record.get("status") not in { + "completed", + "stopped", + "failed", + "interrupted", + } + posthog.viewer_opened(source="tui", live=live) + + def _shutdown_viewer(self) -> None: + httpd = self._viewer_httpd + if httpd is None: + return + self._viewer_httpd = None + with contextlib.suppress(Exception): + httpd.shutdown() + httpd.server_close() + def _fire_sandbox_cleanup(self) -> None: self.coordinator.mark_shutting_down() loop = self._scan_loop diff --git a/strix/interface/tui/history.py b/strix/interface/tui/history.py index 1598b1a3..999ca08c 100644 --- a/strix/interface/tui/history.py +++ b/strix/interface/tui/history.py @@ -24,14 +24,26 @@ def load_session_history(run_dir: Path, agent_ids: Any) -> list[tuple[str, dict[ if not agents_db.exists() or not session_ids: return [] session_id_set = set(session_ids) + # Open read-only: the scan process may be actively writing this WAL database + # from another process (the local viewer tails it live), and a reader must + # never lock or mutate it. mode=ro (not immutable=1) still reads the latest + # committed WAL state; WAL permits concurrent readers alongside the writer. + conn: sqlite3.Connection | None = None try: - with sqlite3.connect(agents_db) as conn: - rows = conn.execute( - "select id, session_id, message_data, created_at from agent_messages order by id" - ).fetchall() + conn = sqlite3.connect( + f"file:{agents_db}?mode=ro", + uri=True, + check_same_thread=False, + ) + rows = conn.execute( + "select id, session_id, message_data, created_at from agent_messages order by id" + ).fetchall() except sqlite3.Error: logger.exception("Failed to hydrate TUI history from %s", agents_db) return [] + finally: + if conn is not None: + conn.close() items: list[tuple[str, dict[str, Any], str]] = [] for row_id, agent_id, message_data, created_at in rows: diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index e2da6282..ef3c8d4f 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -142,6 +142,44 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None: ) +def viewer_opened(source: str, live: bool) -> None: + _send( + "viewer_opened", + { + **base_props(), + "source": source, + "live": live, + }, + ) + + +def viewer_cta_clicked(cta: str, surface: str | None = None) -> None: + props = { + **base_props(), + "cta": cta[:64], + } + if surface: + props["surface"] = surface[:64] + _send("viewer_cta_clicked", props) + + +_VIEWER_EMAIL_STEPS = frozenset( + {"email_submitted", "email_verified", "report_sent", "work_email_required"} +) + + +def viewer_email_event(step: str, purpose: str | None = None) -> None: + if step not in _VIEWER_EMAIL_STEPS: + return + _send( + f"viewer_{step}", + { + **base_props(), + **({"purpose": purpose} if purpose else {}), + }, + ) + + def error(error_type: str) -> None: props = {**base_props(), "error_type": error_type} _send("error", props) diff --git a/strix/viewer/__init__.py b/strix/viewer/__init__.py new file mode 100644 index 00000000..5a0842a7 --- /dev/null +++ b/strix/viewer/__init__.py @@ -0,0 +1,12 @@ +"""Local web viewer for Strix runs. + +Serves a prebuilt single-page app that renders a run (live or finished) read +directly from the run's on-disk files. No cloud dependency, no file picker. +""" + +from __future__ import annotations + +from strix.viewer.server import serve + + +__all__ = ["serve"] diff --git a/strix/viewer/auth.py b/strix/viewer/auth.py new file mode 100644 index 00000000..1c7c5fef --- /dev/null +++ b/strix/viewer/auth.py @@ -0,0 +1,251 @@ +"""Viewer email verification state and the relay client. + +The local viewer proxies email verification and encrypted-report delivery to +the Strix relay (``STRIX_APP_URL``). The browser never talks to the relay +directly, and the report password generated locally is never sent to it. + +State lives in ``~/.strix/viewer-auth.json`` (0600). ``is_verified`` is a local +flag that unlocks browsing the run history list; the relay still enforces token +expiry when a report is actually sent. +""" + +from __future__ import annotations + +import base64 +import contextlib +import json +import logging +import urllib.error +import urllib.request +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from strix.config.loader import load_settings + + +logger = logging.getLogger(__name__) + +AUTH_PATH = Path.home() / ".strix" / "viewer-auth.json" + +_OTP_TIMEOUT = 15 +_SEND_TIMEOUT = 30 + + +class RelayError(Exception): + """A relay call failed. ``code`` is a stable, machine-readable reason.""" + + def __init__(self, code: str, message: str | None = None) -> None: + self.code = code + super().__init__(message or code) + + +# --- local state ------------------------------------------------------------ + + +def read_auth() -> dict[str, Any] | None: + """Return the stored ``{email, token, verified_at}`` record, or None.""" + try: + data = json.loads(AUTH_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + if not isinstance(data, dict): + return None + email = data.get("email") + token = data.get("token") + if not isinstance(email, str) or not email or not isinstance(token, str) or not token: + return None + return data + + +def parse_expiry(raw: object) -> datetime | None: + """Parse a relay ``expires_at`` value into an aware UTC datetime. + + Accepts both ISO 8601 strings and epoch seconds (as a number or numeric + string) so a valid relay expiry is not misread as missing. Returns None only + when it is genuinely absent or unparseable; both the local gate (see + ``is_verified``) and OTP verification (see ``otp_verify``) fail closed on such + values, matching the relay, which rejects a token with no valid expiry. + """ + if isinstance(raw, bool): + return None + if isinstance(raw, int | float): + return _from_epoch(raw) + if not isinstance(raw, str) or not raw: + return None + try: + return _from_epoch(float(raw)) + except ValueError: + pass + try: + parsed = datetime.fromisoformat(raw.replace("Z", "+00:00")) + except ValueError: + return None + return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC) + + +def _expiry(record: dict[str, Any]) -> datetime | None: + """The stored ``verified_at`` parsed to a datetime, or None if unusable.""" + return parse_expiry(record.get("verified_at")) + + +def _from_epoch(seconds: float) -> datetime | None: + """Epoch seconds → aware UTC datetime, or None if out of range.""" + try: + return datetime.fromtimestamp(seconds, tz=UTC) + except (OverflowError, OSError, ValueError): + return None + + +def is_verified() -> bool: + """True when a usable email + token record with a valid future expiry exists. + + The expiry returned by OTP verification is enforced here so history stops + unlocking once the token lapses. It fails closed: a record whose expiry is + absent, blank, or unparseable requires re-verification rather than unlocking + forever, keeping the local gate in step with the relay (which rejects an + expired token on report send). + """ + record = read_auth() + if record is None: + return False + expiry = _expiry(record) + return expiry is not None and expiry > datetime.now(UTC) + + +def write_auth(email: str, token: str, verified_at: str) -> None: + """Atomically persist the auth record with 0600 permissions.""" + AUTH_PATH.parent.mkdir(parents=True, exist_ok=True) + payload = json.dumps({"email": email, "token": token, "verified_at": verified_at}) + tmp = AUTH_PATH.with_suffix(".json.tmp") + tmp.write_text(payload, encoding="utf-8") + with contextlib.suppress(OSError): + tmp.chmod(0o600) + tmp.replace(AUTH_PATH) + with contextlib.suppress(OSError): + AUTH_PATH.chmod(0o600) + + +def forget() -> None: + """Delete the stored auth record. No-op if it is absent.""" + with contextlib.suppress(OSError): + AUTH_PATH.unlink() + + +# --- relay client ----------------------------------------------------------- + + +def _app_url() -> str: + return load_settings().viewer.app_url.rstrip("/") + + +def _post_json(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int, dict[str, Any]]: + """POST JSON to the relay. Returns (status, parsed body). + + Raises RelayError("unavailable") for network/transport failures. HTTP + error responses (4xx/5xx) are returned as (status, body) for the caller to + map, not raised. + """ + url = f"{_app_url()}{path}" + body = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( # noqa: S310 - fixed https relay URL + url, + data=body, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 + return response.status, _parse_body(response.read()) + except urllib.error.HTTPError as exc: + return exc.code, _parse_body(exc.read()) + except (urllib.error.URLError, TimeoutError, OSError) as exc: + logger.warning("relay request to %s failed: %s", path, exc) + raise RelayError("unavailable") from exc + + +def _parse_body(raw: bytes) -> dict[str, Any]: + try: + data = json.loads(raw or b"{}") + except json.JSONDecodeError: + return {} + return data if isinstance(data, dict) else {} + + +def otp_start(email: str) -> None: + """Ask the relay to email a verification code. Raises RelayError on failure.""" + status, data = _post_json("/api/oss/otp/start", {"email": email}, timeout=_OTP_TIMEOUT) + if status == 200: + return + if status == 429: + raise RelayError("rate_limited") + if status == 400: + # The relay uses 400 both for a malformed address and, separately, to + # reject a free/personal email domain (it wants a work email). + if data.get("error") == "work_email_required": + raise RelayError("work_email_required") + raise RelayError("invalid_email") + raise RelayError("unavailable") + + +def otp_verify(email: str, code: str) -> dict[str, Any]: + """Verify a code. Returns ``{token, email, expires_at}`` or raises RelayError.""" + status, data = _post_json( + "/api/oss/otp/verify", + {"email": email, "code": code}, + timeout=_OTP_TIMEOUT, + ) + if status == 200 and isinstance(data.get("token"), str): + # A token with no usable expiry cannot unlock history locally (the gate + # fails closed), so treat such a response as a failed verification rather + # than reporting success and then leaving the user stuck unverified. + if parse_expiry(data.get("expires_at")) is None: + raise RelayError("unavailable") + return data + if status == 403: + raise RelayError("invalid_code") + raise RelayError("unavailable") + + +def report_send( + token: str, + pdf_bytes: bytes, + filename: str, + run_name: str, + target: str, +) -> None: + """Forward the encrypted PDF to the relay for delivery. + + The report password is NEVER part of this payload; only the encrypted PDF + bytes travel to the relay. + """ + payload = { + "token": token, + "pdf_base64": base64.b64encode(pdf_bytes).decode("ascii"), + "filename": filename, + "run_name": run_name, + "target": target, + } + status, _ = _post_json("/api/oss/report/send", payload, timeout=_SEND_TIMEOUT) + if status == 200: + return + if status == 401: + raise RelayError("reverify") + if status == 413: + raise RelayError("too_large") + if status == 403: + raise RelayError("forbidden") + raise RelayError("unavailable") + + +__all__ = [ + "AUTH_PATH", + "RelayError", + "forget", + "is_verified", + "otp_start", + "otp_verify", + "read_auth", + "report_send", + "write_auth", +] diff --git a/strix/viewer/cli.py b/strix/viewer/cli.py new file mode 100644 index 00000000..4a3f4d22 --- /dev/null +++ b/strix/viewer/cli.py @@ -0,0 +1,142 @@ +"""`strix view []` command: serve a run's viewer UI locally.""" + +from __future__ import annotations + +import argparse +import logging +import time +from typing import TYPE_CHECKING + +from rich.console import Console + +from strix.core.paths import ( + RUNS_DIR_NAME, + latest_run_dir, + run_dir_for, + run_record_path, + runs_base_dir, +) +from strix.viewer.server import authorized_url, bundle_is_built, serve +from strix.viewer.transcript import read_run_summary + + +if TYPE_CHECKING: + from pathlib import Path + from typing import NoReturn + + +logger = logging.getLogger(__name__) + + +def run_view(argv: list[str]) -> None: + parser = argparse.ArgumentParser( + prog="strix view", + description="Open a local web view of a Strix run (live or finished).", + ) + parser.add_argument( + "run", + nargs="?", + default=None, + help=f"Run name under ./{RUNS_DIR_NAME} (defaults to the most recent run).", + ) + parser.add_argument( + "--port", + type=int, + default=0, + help="Port to serve on (default: an available ephemeral port).", + ) + parser.add_argument("--host", default="127.0.0.1", help=argparse.SUPPRESS) + parser.add_argument( + "--no-open", + action="store_true", + help="Do not open the browser automatically.", + ) + args = parser.parse_args(argv) + + console = Console() + + if not bundle_is_built(): + console.print( + "[bold red]Viewer UI is not built.[/]\n" + "Build it with: [cyan]cd strix/viewer/frontend && npm ci && npm run build[/]" + ) + raise SystemExit(1) + + run_dir = _resolve_run_dir(args.run, console) + + httpd, url, token = serve( + run_dir, + host=args.host, + port=args.port, + open_browser=not args.no_open, + ) + # The tokened URL is what authorizes the browser (steering, report sending, + # history). Print it rather than the bare URL so the operator -- and only + # the operator -- can open or share an authorized link. + open_url = authorized_url(url, token) + + run_name = run_dir.name + summary = read_run_summary(run_dir) + live = not summary.get("finished", False) + + from strix.telemetry import posthog + + posthog.viewer_opened(source="cli", live=live) + + state_label = "[#eab308]live[/]" if live else "[#22c55e]finished[/]" + console.print() + console.print(f"Serving [bold white]{run_name}[/] ({state_label}) at:") + # Print the URL alone on its own line with soft_wrap so Rich never inserts a + # wrap into the (long, tokened) link -- that keeps it selectable/copyable. + console.print(f" [#60a5fa]{open_url}[/]", soft_wrap=True) + console.print("[dim]This link authorizes the browser; anyone you share it with can steer[/]") + console.print("[dim]a live scan and browse history. Press Ctrl-C to stop the viewer.[/]") + console.print() + + try: + while True: + time.sleep(1.0) + except KeyboardInterrupt: + console.print("\n[dim]Viewer stopped.[/]") + finally: + httpd.shutdown() + httpd.server_close() + + +def _resolve_run_dir(run: str | None, console: Console) -> Path: + if run: + run_dir = run_dir_for(run) + if not run_record_path(run_dir).is_file(): + _fail_no_run(console, requested=run) + return run_dir + + latest = latest_run_dir() + if latest is None: + _fail_no_run(console, requested=None) + return latest + + +def _fail_no_run(console: Console, *, requested: str | None) -> NoReturn: + base = runs_base_dir() + available = ( + sorted( + (child.name for child in base.iterdir() if run_record_path(child).is_file()), + reverse=True, + ) + if base.is_dir() + else [] + ) + + if requested: + console.print(f"[bold red]No run named '{requested}' under ./{RUNS_DIR_NAME}.[/]") + else: + console.print(f"[bold red]No runs found under ./{RUNS_DIR_NAME}.[/]") + + if available: + console.print("Available runs:") + for name in available[:20]: + console.print(f" [cyan]{name}[/]") + raise SystemExit(1) + + +__all__ = ["run_view"] diff --git a/strix/viewer/frontend/index.html b/strix/viewer/frontend/index.html new file mode 100644 index 00000000..dba7938a --- /dev/null +++ b/strix/viewer/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Strix Results + + +
+ + + diff --git a/strix/viewer/frontend/package-lock.json b/strix/viewer/frontend/package-lock.json new file mode 100644 index 00000000..1c74cd0c --- /dev/null +++ b/strix/viewer/frontend/package-lock.json @@ -0,0 +1,4272 @@ +{ + "name": "strix-viewer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "strix-viewer", + "version": "0.0.0", + "dependencies": { + "@dagrejs/dagre": "^2.0.4", + "@xyflow/react": "^12.10.1", + "clsx": "^2.1.1", + "diff": "^8.0.3", + "highlight.js": "^11.11.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.0", + "@types/diff": "^7.0.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dagrejs/dagre": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-2.0.4.tgz", + "integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "3.0.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-3.0.4.tgz", + "integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", + "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.79", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", + "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/strix/viewer/frontend/package.json b/strix/viewer/frontend/package.json new file mode 100644 index 00000000..37eaff55 --- /dev/null +++ b/strix/viewer/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "strix-viewer", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@dagrejs/dagre": "^2.0.4", + "@xyflow/react": "^12.10.1", + "clsx": "^2.1.1", + "diff": "^8.0.3", + "highlight.js": "^11.11.1", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.0", + "@types/diff": "^7.0.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } +} diff --git a/strix/viewer/frontend/public/logo.png b/strix/viewer/frontend/public/logo.png new file mode 100644 index 00000000..699eb837 Binary files /dev/null and b/strix/viewer/frontend/public/logo.png differ diff --git a/strix/viewer/frontend/src/App.tsx b/strix/viewer/frontend/src/App.tsx new file mode 100644 index 00000000..8afc025c --- /dev/null +++ b/strix/viewer/frontend/src/App.tsx @@ -0,0 +1,810 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ArrowLeft, + AlertCircle, + Waypoints, + Mail, + ChevronDown, + Wrench, + FileCheck2, + CalendarClock, + Radar, + GitPullRequest, + Rocket, + ArrowUpRight, + History, +} from "lucide-react"; +import type { Vulnerability, VulnerabilitySeverity } from "@/types/issues"; +import { SEVERITY_COLORS } from "@/types/issues"; +import { getSeverityDot } from "@/lib/vulnerability-utils"; +import VulnerabilityDetail from "@/components/vulnerability/VulnerabilityDetail"; +import { ContentSection } from "@/components/vulnerability/ContentSection"; +import { IssueSeveritySummary } from "@/components/IssueSeveritySummary"; +import AgentGraph from "@/components/live/AgentGraph"; +import { buildGraphAgents } from "@/components/live/AgentTranscript"; +import AgentDetailModal from "@/components/live/AgentDetailModal"; +import { ScanPromptComposer } from "@/components/live/ScanPromptComposer"; +import { severityCounts, type ParsedRunSummary } from "@/lib/local-run-parser"; +import { + fetchAll, + fetchAuthStatus, + fetchCapabilities, + fetchRunSummary, + fetchRuns, + fetchTranscript, + fetchVulnerabilities, + forgetAuth, + type AuthStatus, + type LoadedRun, + type RunsPayload, +} from "@/data/serverSource"; +import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta"; +import { runTitle } from "@/lib/target-utils"; +import Sidebar from "@/components/Sidebar"; +import PastRunsView from "@/components/PastRunsView"; +import EmailReportView from "@/components/EmailReportView"; +import { RunDetails } from "@/components/RunDetails"; +import { TrustToast } from "@/components/TrustToast"; +import FeatureDetail from "@/components/FeatureDetail"; +import { ProTile, ProInlineCta, type ProItem } from "@/components/ProCta"; +import { FEATURES } from "@/lib/pro-features"; + +export type View = "overview" | "issues" | "agents" | "history" | "feature" | "email"; + +const TRUST_BANNER = + "Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix."; + +const SEVERITY_ORDER: VulnerabilitySeverity[] = ["critical", "high", "medium", "low"]; +const POLL_MS = 500; + +// Curated inline CTAs. Continuous-coverage row on Overview (the restyled upsell +// tiles), plus the recommendations pairing. +const RECOMMENDATION_CTAS: ProItem[] = [ + { title: "One-click autofix + open a fix PR", desc: "Fix it for you and open a PR, retested.", slug: "autofix", icon: Wrench }, + { title: "Export SOC 2 / ISO 27001 report", desc: "Share an auditor-ready report with your team.", slug: "compliance", icon: FileCheck2 }, +]; +const COVERAGE_CTAS: ProItem[] = [ + { title: "Scheduled pentesting", desc: "Continuous coverage for your whole org.", slug: "scheduled", icon: CalendarClock }, + { title: "Attack surface monitoring", desc: "Continuous coverage for your whole org.", slug: "asm", icon: Radar }, + { title: "PR reviews", desc: "Pentest every pull request your team opens.", slug: "pr_reviews", icon: GitPullRequest }, +]; + +export default function App() { + const [activeRun, setActiveRun] = useState(null); + const [run, setRun] = useState(null); + const [error, setError] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [view, setView] = useState("overview"); + const [activeFeature, setActiveFeature] = useState(null); + const [auth, setAuth] = useState(null); + const [runs, setRuns] = useState(null); + const [emailPurpose, setEmailPurpose] = useState<"report" | "verify">("report"); + const [emailSkipDisclosure, setEmailSkipDisclosure] = useState(false); + // Whether this viewer can steer a live scan (true only inside the in-TUI + // launcher that shares the running scan's coordinator + event loop). + const [canSteer, setCanSteer] = useState(false); + + const refreshAuth = useCallback(async () => { + try { + setAuth(await fetchAuthStatus()); + } catch { + /* auth status is best-effort; the launched run stays viewable */ + } + }, []); + + const refreshRuns = useCallback(async () => { + try { + setRuns(await fetchRuns()); + } catch { + /* history list is best-effort */ + } + }, []); + + useEffect(() => { + void refreshAuth(); + void refreshRuns(); + // Capabilities never change over a session, so fetch once on mount. + fetchCapabilities() + .then((caps) => setCanSteer(caps.can_steer)) + .catch(() => { + /* absence of steering is the safe default */ + }); + }, [refreshAuth, refreshRuns]); + + // Live polling, scoped to the active run. Re-runs when the active run changes + // so switching to a past run (?run=) reloads its data; a finished run + // does a single full fetch and stops. + const finishedRef = useRef(false); + useEffect(() => { + let cancelled = false; + let timer: ReturnType | undefined; + finishedRef.current = false; + + const schedule = () => { + timer = setTimeout(tick, POLL_MS); + }; + + const tick = async () => { + if (cancelled) return; + try { + const { summary, raw, finished } = await fetchRunSummary(activeRun); + if (cancelled) return; + if (finished && !finishedRef.current) { + finishedRef.current = true; + const full = await fetchAll(activeRun); + if (!cancelled) setRun(full); + return; // stop polling + } + const [transcript, vulnerabilities] = await Promise.all([ + fetchTranscript(activeRun).catch(() => ({ agents: [], events: [] })), + fetchVulnerabilities(summary.runId, activeRun).catch(() => [] as Vulnerability[]), + ]); + if (cancelled) return; + setRun((prev) => ({ + summary, + raw, + finished, + transcript, + vulnerabilities, + reportMarkdown: prev?.reportMarkdown ?? null, + })); + schedule(); + } catch (e) { + if (cancelled) return; + setError(e instanceof Error ? e.message : "Could not load run data."); + schedule(); + } + }; + + (async () => { + try { + const full = await fetchAll(activeRun); + if (cancelled) return; + setRun(full); + if (full.finished) { + finishedRef.current = true; + } else { + schedule(); + } + } catch (e) { + if (cancelled) return; + setError(e instanceof Error ? e.message : "Could not load run data."); + schedule(); + } + })(); + + return () => { + cancelled = true; + if (timer) clearTimeout(timer); + }; + }, [activeRun]); + + const counts = useMemo( + () => (run ? severityCounts(run.vulnerabilities) : null), + [run] + ); + const selected = run?.vulnerabilities.find((v) => v.id === selectedId) ?? null; + const agentCount = run?.transcript.agents.length ?? 0; + const verified = auth?.verified === true; + + // Per-run guard for the default view: land on Agents while a scan is live, + // Overview once it finishes. Applied at most once per run and never once the + // user has navigated manually (userSetView flips the guard). + const initialViewAppliedRef = useRef(false); + + // Reset the guard whenever the active run changes so the newly selected run + // gets its own default. + useEffect(() => { + initialViewAppliedRef.current = false; + }, [activeRun]); + + useEffect(() => { + if (initialViewAppliedRef.current || !run) return; + if (run.finished) { + initialViewAppliedRef.current = true; + setView("overview"); + } else if (agentCount > 0) { + // Live and agents have appeared: default to the agent graph. If it is + // live but no agents exist yet, wait (do not apply, do not set the flag). + initialViewAppliedRef.current = true; + setView("agents"); + } + }, [run, agentCount]); + + // User-initiated navigation: mark the default guard applied so the per-run + // default effect never yanks the user off the view they chose. + const userSetView = useCallback((v: View) => { + initialViewAppliedRef.current = true; + setView(v); + }, []); + + const selectRun = useCallback((name: string) => { + setActiveRun(name); + setSelectedId(null); + setRun(null); + setError(null); + // Reset the guard so the per-run default applies to the newly selected run. + initialViewAppliedRef.current = false; + }, []); + + const goEmail = useCallback((skipDisclosure: boolean, surface: string) => { + trackCta("email_report", surface); + setEmailPurpose("report"); + setEmailSkipDisclosure(skipDisclosure); + userSetView("email"); + }, [userSetView]); + + // Sidebar entry keeps the disclosure (first place those users see it); + const openEmail = useCallback(() => goEmail(false, "sidebar"), [goEmail]); + // the Overview CTA already states the tradeoff, so it starts the flow directly. + const openEmailFromOverview = useCallback(() => goEmail(true, "overview"), [goEmail]); + + const openHistory = useCallback(() => { + void refreshRuns(); + userSetView("history"); + }, [refreshRuns, userSetView]); + + const onPastRunsVerified = useCallback(async () => { + await refreshAuth(); + await refreshRuns(); + }, [refreshAuth, refreshRuns]); + + const selectFeature = useCallback((slug: string) => { + trackCta(slug, "sidebar_nav"); + setActiveFeature(slug); + userSetView("feature"); + }, [userSetView]); + + const onForget = useCallback(async () => { + await forgetAuth(); + await refreshAuth(); + await refreshRuns(); + }, [refreshAuth, refreshRuns]); + + return ( +
+ { + if (v === "history") openHistory(); + else userSetView(v); + }} + activeFeature={activeFeature} + onSelectFeature={selectFeature} + issuesCount={run?.vulnerabilities.length ?? 0} + agentCount={agentCount} + runCount={runs?.count ?? 0} + finished={run?.finished ?? false} + verified={verified} + email={auth?.email ?? null} + onOpenEmail={openEmail} + onOpenHistory={openHistory} + onForget={() => void onForget()} + /> + +
+ {/* Top bar */} + + +
+ {error && !run && view !== "history" && view !== "email" && view !== "feature" && ( +
+
+ )} + + {view === "email" ? ( + { + void refreshAuth(); + void refreshRuns(); + }} + onExit={(dest) => setView(dest === "history" ? "history" : "overview")} + /> + ) : view === "feature" && activeFeature && FEATURES[activeFeature] ? ( + + ) : view === "history" ? ( +
+
+
+ void onPastRunsVerified()} + /> +
+ ) : !run && !error ? ( +
+
+

Loading run data…

+
+ ) : run && counts ? ( + <> + + + {/* Tab strip: shown on small screens where the sidebar is hidden. */} +
+ userSetView("overview")}> + Overview + + userSetView("issues")}> + Issues{run.vulnerabilities.length > 0 ? ` (${run.vulnerabilities.length})` : ""} + + {agentCount > 0 && ( + userSetView("agents")}> + Agents ({agentCount}) + + )} +
+ + {view === "overview" ? ( + + ) : view === "agents" && agentCount > 0 ? ( + + ) : selected ? ( +
+ + +
+ ) : ( + setSelectedId(id)} + /> + )} + + ) : null} +
+
+ +
+ ); +} + +function RunSwitcher({ + runs, + activeRun, + launchedName, + onSelect, +}: { + runs: RunsPayload; + activeRun: string | null; + launchedName: string; + onSelect: (name: string) => void; +}) { + const [open, setOpen] = useState(false); + const activeEntry = runs.runs.find((r) => r.name === activeRun); + const current = activeEntry ? runTitle(activeEntry.target, activeEntry.name) : launchedName; + return ( +
+ + {open && ( +
+ {runs.runs.map((r) => { + const active = r.name === activeRun; + return ( + + ); + })} +
+ )} +
+ ); +} + +function LiveIndicator({ finished }: { finished: boolean }) { + if (finished) { + return ( + + + Complete + + ); + } + return ( + + + + + + Live + + ); +} + +function formatDuration(seconds: number | null): string | null { + if (seconds == null) return null; + if (seconds < 60) return `${seconds}s`; + const m = Math.floor(seconds / 60); + if (m < 60) return `${m}m`; + const h = Math.floor(m / 60); + return `${h}h ${m % 60}m`; +} + +function SummaryHeader({ summary }: { summary: ParsedRunSummary }) { + const duration = formatDuration(summary.durationSeconds); + return ( +
+

+ {runTitle(summary.targets[0] ?? null, summary.runName ?? summary.runId ?? "Scan results")} +

+
+ {summary.targets.length > 0 && ( + {summary.targets.join(", ")} + )} + {summary.scanMode && } + {duration && } + {summary.status && } +
+
+ ); +} + +function Meta({ label }: { label: string }) { + return ( + <> + · + {label} + + ); +} + +function FindingsList({ + vulnerabilities, + finished, + onSelect, +}: { + vulnerabilities: Vulnerability[]; + finished: boolean; + onSelect: (id: string) => void; +}) { + const sorted = [...vulnerabilities].sort( + (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity) + ); + if (sorted.length === 0) { + return ( +
+
+ {finished ? "No findings in this run." : "No findings yet. The scan is still running…"} +
+ {finished && ( +
+

Stay ahead of new exposures

+

+ Attack surface monitoring catches new exposures for your org over time. +

+ +
+ )} +
+ ); + } + return ( +
+ {sorted.map((v) => ( + + ))} +
+ ); +} + +/** Strip a single leading markdown heading (report sections embed their own). */ +function stripLeadingHeading(md: string): string { + return md.replace(/^\s*#{1,6}[ \t]+.*(?:\r?\n)+/, "").trimStart(); +} + +function dedupeHeadings(md: string): string { + const out: string[] = []; + let lastHeading: string | null = null; + for (const line of md.split("\n")) { + const m = line.match(/^#{1,6}\s+(.*)$/); + if (m) { + const norm = m[1].trim().toLowerCase(); + if (norm === lastHeading) continue; + lastHeading = norm; + } else if (line.trim() !== "") { + lastHeading = null; + } + out.push(line); + } + return out.join("\n"); +} + +/** Primary local CTA: email an encrypted PDF. Verify-email affordance, no lock. */ +function EmailReportCta({ onOpenEmail }: { onOpenEmail: () => void }) { + return ( + + ); +} + +function OverviewTab({ + summary, + counts, + total, + reportMarkdown, + raw, + finished, + onOpenEmail, +}: { + summary: ParsedRunSummary; + counts: Record; + total: number; + reportMarkdown: string | null; + raw: Record; + finished: boolean; + onOpenEmail: () => void; +}) { + const sections = ( + [ + ["Executive Summary", summary.executiveSummary], + ["Technical Analysis", summary.technicalAnalysis], + ["Methodology", summary.methodology], + ["Recommendations", summary.recommendations], + ] as const + ) + .filter(([, content]) => !!content) + .map(([title, content]) => ({ title, content: stripLeadingHeading(content as string) })); + + return ( +
+ + + {total > 0 && ( +
+ +
+ )} + + {/* Primary CTA: the one primary on Overview. Hidden until the run is + finished, since a live scan would only email a partial report. */} + {finished && } + + {sections.length > 0 ? ( +
+ {sections.map((s) => ( + + ))} +
+ ) : reportMarkdown ? ( +
+ +
+ ) : ( + total === 0 && ( +

No summary available for this run yet.

+ ) + )} + + {/* Near Recommendations: act on the fixes. */} +
+ {RECOMMENDATION_CTAS.map((item) => ( + + ))} +
+ + {/* Continuous coverage for your org (restyled upsell tiles). */} +
+

Continuous coverage for your org

+
+ {COVERAGE_CTAS.map((item) => ( + + ))} +
+
+
+ ); +} + +function TabButton({ + active, + onClick, + children, +}: { + active: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) { + const { agents, events } = run.transcript; + const graphAgents = useMemo(() => buildGraphAgents(agents, events), [agents, events]); + // Clicking a graph node opens the agent's transcript in a modal (matching the + // cloud app); no node selected means no modal. + const [selectedId, setSelectedId] = useState(null); + const selectedAgent = selectedId ? (agents.find((a) => a.id === selectedId) ?? null) : null; + + // Live steering is only possible in-process (canSteer) while the scan runs. + const steerable = canSteer && !run.finished; + + return ( +
+
+
+
+

+ Click an agent to open its full transcript. +

+
+ setSelectedId(id)} + eventsLoaded + eventsEmpty={graphAgents.size === 0} + scanCompleted={run.finished} + /> +
+
+ + {/* Live steering: only in-process while the scan runs. Otherwise omitted. */} + {steerable && } + + {/* Re-run always routes to Strix Cloud. */} +
+

Run this scan with more depth

+

Re-run this scan on managed infra in the cloud.

+
+ +
+
+ + {selectedAgent && ( + setSelectedId(null)} + /> + )} +
+ ); +} diff --git a/strix/viewer/frontend/src/components/AddRepositoryDialog.tsx b/strix/viewer/frontend/src/components/AddRepositoryDialog.tsx new file mode 100644 index 00000000..ee7a2853 --- /dev/null +++ b/strix/viewer/frontend/src/components/AddRepositoryDialog.tsx @@ -0,0 +1,20 @@ +// Extracted ProviderIcon from strix-app's AddRepositoryDialog. The dialog itself +// (and its next/link dependency) is dropped; the IssueSidebar only needs this SVG +// switch to badge a finding's source-control provider. Web-app targets resolve to +// provider === null and never reach here (they render a globe icon instead). +import { Github, Gitlab } from "lucide-react"; + +function BitbucketIcon({ className }: { className?: string }) { + return ( + + ); +} + +export function ProviderIcon({ provider, className }: { provider: string; className?: string }) { + const cls = className ?? "w-4 h-4"; + if (provider === "gitlab") return ; + if (provider === "bitbucket") return ; + return ; +} diff --git a/strix/viewer/frontend/src/components/EmailReportView.tsx b/strix/viewer/frontend/src/components/EmailReportView.tsx new file mode 100644 index 00000000..7f84cddf --- /dev/null +++ b/strix/viewer/frontend/src/components/EmailReportView.tsx @@ -0,0 +1,375 @@ +import { useEffect, useRef, useState } from "react"; +import { Mail, ShieldCheck, Lock, Copy, Check, Loader2, AlertCircle, ArrowLeft } from "lucide-react"; +import { + otpStart, + otpVerify, + sendReport, + type AuthStatus, +} from "@/data/serverSource"; +import { track } from "@/lib/cta"; + +/** + * The email-report / email-verification flow rendered as its own page (not a + * modal, so it never floats over another surface). Report mode ends in the + * one-time password panel; verify mode just confirms the email and returns to + * the caller. The page unmounts when you navigate away, so state resets each + * time it is opened. + */ + +type Step = "disclosure" | "email" | "code" | "sending" | "password"; + +interface EmailReportViewProps { + activeRun: string | null; + auth: AuthStatus | null; + purpose: "report" | "verify"; + /** + * Skip the report disclosure and start the flow directly (used by the + * Overview CTA, which already states the tradeoff). Unverified users land on + * the email step; already-verified users send immediately. + */ + skipDisclosure?: boolean; + /** Refresh auth + runs after a successful verify (lifts state to App). */ + onAuthChanged: () => void; + /** Leave this page (report "Done" -> overview; verify success -> history). */ + onExit: (dest: "overview" | "history") => void; +} + +const OTP_START_ERRORS: Record = { + work_email_required: "Please use your work email, not a personal one.", + rate_limited: "Too many requests. Wait a minute and try again.", + invalid_email: "That email does not look right. Check it and try again.", + unavailable: "The email service is unavailable right now. Try again shortly.", +}; + +const SEND_ERRORS: Record = { + forbidden: "This email was unsubscribed from Strix, so we cannot send to it.", + too_large: "This report is too large to email. Try a smaller run.", + unavailable: "The email service is unavailable right now. Try again shortly.", +}; + +// A small set of common personal providers for instant client-side feedback. +// The relay is authoritative (it checks the full free-email-domains list). +const COMMON_FREE_DOMAINS = new Set([ + "gmail.com", "googlemail.com", "yahoo.com", "ymail.com", "outlook.com", + "hotmail.com", "live.com", "icloud.com", "me.com", "aol.com", "proton.me", + "protonmail.com", "gmx.com", "mail.com", +]); + +export default function EmailReportView({ + activeRun, + auth, + purpose, + skipDisclosure = false, + onAuthChanged, + onExit, +}: EmailReportViewProps) { + const verified = auth?.verified === true; + const verifyOnly = purpose === "verify"; + // Verify mode (and the Overview CTA, which skips the disclosure) start on the + // email step; a verified user who skips the disclosure sends immediately. + const [step, setStep] = useState(() => { + if (verifyOnly) return "email"; + if (skipDisclosure) return verified ? "sending" : "email"; + return "disclosure"; + }); + const [email, setEmail] = useState(auth?.email ?? ""); + const [code, setCode] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); + const [password, setPassword] = useState(""); + const [filename, setFilename] = useState(""); + const [copied, setCopied] = useState(false); + const [sentTo, setSentTo] = useState(""); + const autoSentRef = useRef(false); + + const doSend = async () => { + setStep("sending"); + setError(null); + const result = await sendReport(activeRun); + if (result.ok) { + track("report_sent"); + setPassword(result.password); + setFilename(result.filename); + setStep("password"); + return; + } + if (result.error === "reverify" || result.error === "unverified") { + setNotice("Your verification expired. Enter your email to verify again."); + setStep("email"); + return; + } + setError(SEND_ERRORS[result.error] ?? "Could not send the report. Try again."); + setStep("disclosure"); + }; + + const startFlow = () => { + setError(null); + setNotice(null); + if (verified) void doSend(); + else setStep("email"); + }; + + // A verified user who skipped the disclosure (Overview CTA) sends on arrival. + useEffect(() => { + if (!verifyOnly && skipDisclosure && verified && !autoSentRef.current) { + autoSentRef.current = true; + void doSend(); + } + // Run once on mount; the page remounts fresh each time it is opened. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const submitEmail = async () => { + const value = email.trim(); + if (!value) { + setError("Enter your email to continue."); + return; + } + const domain = value.slice(value.lastIndexOf("@") + 1).toLowerCase(); + if (COMMON_FREE_DOMAINS.has(domain)) { + track("work_email_required"); + setError(OTP_START_ERRORS.work_email_required); + return; + } + setBusy(true); + setError(null); + const result = await otpStart(value); + setBusy(false); + if (result.ok) { + track("email_submitted", { purpose }); + setNotice(`We sent a 6-digit code to ${value}.`); + setStep("code"); + } else { + if (result.error === "work_email_required") track("work_email_required"); + setError(OTP_START_ERRORS[result.error] ?? "Could not send a code. Try again."); + } + }; + + const submitCode = async () => { + const value = code.trim(); + if (value.length < 4) { + setError("Enter the 6-digit code from your email."); + return; + } + setBusy(true); + setError(null); + const result = await otpVerify(email.trim(), value); + setBusy(false); + if (!result.verified) { + setError("That code did not match. Check it and try again."); + return; + } + track("email_verified", { purpose }); + setSentTo(result.email); + onAuthChanged(); + if (verifyOnly) onExit("history"); + else void doSend(); + }; + + const copyPassword = async () => { + try { + await navigator.clipboard.writeText(password); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + /* clipboard may be unavailable; the password is visible to copy manually */ + } + }; + + const confirmationEmail = sentTo || auth?.email || email.trim(); + + return ( +
+ + +
+
+ +
+

+ {verifyOnly + ? "We send a one-time code to confirm it is you." + : "Verified by a one-time code sent to your email"} +

+ + {error && ( +
+
+ )} + {notice && !error && step !== "password" && ( +

{notice}

+ )} + + {step === "disclosure" && ( +
+
+
+
+
+
+
+ + {verified && auth?.email && ( +

Sending to {auth.email}

+ )} +
+ )} + + {step === "email" && ( +
{ + e.preventDefault(); + void submitEmail(); + }} + > + + +
+ )} + + {step === "code" && ( +
{ + e.preventDefault(); + void submitCode(); + }} + > + + + +
+ )} + + {step === "sending" && ( +
+
+ )} + + {step === "password" && ( +
+
+
+
+ Your one-time password +
+ {password} + +
+

+ Save this now. Strix never stores it, so we cannot show it again. File:{" "} + {filename} +

+
+ +
+ )} +
+
+ ); +} diff --git a/strix/viewer/frontend/src/components/EmailVerifyInline.tsx b/strix/viewer/frontend/src/components/EmailVerifyInline.tsx new file mode 100644 index 00000000..9318f854 --- /dev/null +++ b/strix/viewer/frontend/src/components/EmailVerifyInline.tsx @@ -0,0 +1,163 @@ +import { useState } from "react"; +import { Loader2, AlertCircle } from "lucide-react"; +import { otpStart, otpVerify } from "@/data/serverSource"; +import { track } from "@/lib/cta"; + +/** + * Compact inline email -> 6-digit-code verify flow. Unlike EmailReportView this + * has no page chrome, no report send, and no password panel: it just confirms + * the email so the past-runs list can unlock in place. On success it calls + * `onVerified` (the parent refreshes auth + runs). + */ + +const OTP_START_ERRORS: Record = { + work_email_required: "Please use your work email, not a personal one.", + rate_limited: "Too many requests. Wait a minute and try again.", + invalid_email: "That email does not look right. Check it and try again.", + unavailable: "The email service is unavailable right now. Try again shortly.", +}; + +// A small set of common personal providers for instant client-side feedback. +// The relay is authoritative (it checks the full free-email-domains list). +const COMMON_FREE_DOMAINS = new Set([ + "gmail.com", "googlemail.com", "yahoo.com", "ymail.com", "outlook.com", + "hotmail.com", "live.com", "icloud.com", "me.com", "aol.com", "proton.me", + "protonmail.com", "gmx.com", "mail.com", +]); + +export default function EmailVerifyInline({ onVerified }: { onVerified: () => void }) { + const [step, setStep] = useState<"email" | "code">("email"); + const [email, setEmail] = useState(""); + const [code, setCode] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); + + const submitEmail = async () => { + const value = email.trim(); + if (!value) { + setError("Enter your email to continue."); + return; + } + const domain = value.slice(value.lastIndexOf("@") + 1).toLowerCase(); + if (COMMON_FREE_DOMAINS.has(domain)) { + track("work_email_required"); + setError(OTP_START_ERRORS.work_email_required); + return; + } + setBusy(true); + setError(null); + const result = await otpStart(value); + setBusy(false); + if (result.ok) { + track("email_submitted", { purpose: "verify" }); + setNotice(`We sent a 6-digit code to ${value}.`); + setStep("code"); + } else { + if (result.error === "work_email_required") track("work_email_required"); + setError(OTP_START_ERRORS[result.error] ?? "Could not send a code. Try again."); + } + }; + + const submitCode = async () => { + const value = code.trim(); + if (value.length < 4) { + setError("Enter the 6-digit code from your email."); + return; + } + setBusy(true); + setError(null); + const result = await otpVerify(email.trim(), value); + setBusy(false); + if (!result.verified) { + setError("That code did not match. Check it and try again."); + return; + } + track("email_verified", { purpose: "verify" }); + onVerified(); + }; + + return ( +
+ {error && ( +
+
+ )} + {notice && !error &&

{notice}

} + + {step === "email" ? ( +
{ + e.preventDefault(); + void submitEmail(); + }} + > + + +
+ ) : ( +
{ + e.preventDefault(); + void submitCode(); + }} + > + + + +
+ )} +
+ ); +} diff --git a/strix/viewer/frontend/src/components/FeatureDetail.tsx b/strix/viewer/frontend/src/components/FeatureDetail.tsx new file mode 100644 index 00000000..d5eb4326 --- /dev/null +++ b/strix/viewer/frontend/src/components/FeatureDetail.tsx @@ -0,0 +1,96 @@ +import { + CalendarClock, + WandSparkles, + Puzzle, + Users, + ArrowUpRight, +} from "lucide-react"; +import { SIGNUP_URL, PRICING_URL, ctaUrl, trackCta } from "@/lib/cta"; +import type { ProFeature } from "@/lib/pro-features"; +import { ProTag } from "@/components/ProCta"; + +/** + * In-app upsell page for a single platform feature. Modeled on the cloud app's + * Networks upsell: a centered bordered card with an icon medallion, tier pill, + * headline, one-line description, a shared "Included in Strix Pro" bullet list, + * then a primary sign-up CTA and a secondary link to all plans. + */ + +const INCLUDED = [ + { + icon: CalendarClock, + text: "Continuous coverage: scheduled pentests and attack surface monitoring", + }, + { icon: WandSparkles, text: "One-click autofix that opens a retested pull request" }, + { icon: Puzzle, text: "Two-way sync to Jira, Linear, and Slack" }, + { icon: Users, text: "Your whole team, with roles and shared history" }, +]; + +export default function FeatureDetail({ feature }: { feature: ProFeature }) { + const Icon = feature.icon; + return ( + + ); +} diff --git a/strix/viewer/frontend/src/components/IssueSeveritySummary.tsx b/strix/viewer/frontend/src/components/IssueSeveritySummary.tsx new file mode 100644 index 00000000..1be2d7e5 --- /dev/null +++ b/strix/viewer/frontend/src/components/IssueSeveritySummary.tsx @@ -0,0 +1,77 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +export interface IssueSeveritySummaryFindings { + total: number; + critical: number; + high: number; + medium: number; + low: number; +} + +interface IssueSeveritySummaryProps { + findings: IssueSeveritySummaryFindings; + className?: string; + /** Noun for the total count (e.g. "issues", "CVEs"). Defaults to "issues". */ + unit?: string; + /** Optional content rendered at the end of the count row (e.g. a KEV badge). */ + trailing?: React.ReactNode; +} + +const SEVERITIES = [ + { key: "critical", label: "critical", dotClass: "bg-red-500", textClass: "text-red-500" }, + { key: "high", label: "high", dotClass: "bg-orange-500", textClass: "text-orange-500" }, + { key: "medium", label: "medium", dotClass: "bg-yellow-500", textClass: "text-yellow-500" }, + { key: "low", label: "low", dotClass: "bg-blue-500", textClass: "text-blue-500" }, +] as const; + +export function IssueSeveritySummary({ + findings, + className, + unit = "issues", + trailing, +}: IssueSeveritySummaryProps) { + if (findings.total <= 0) return null; + + return ( +
+
+
+ {findings.total} + {unit} +
+
+ {SEVERITIES.map(({ key, label, dotClass, textClass }) => { + const count = findings[key]; + if (count <= 0) return null; + + return ( +
+ + ); + })} +
+ {trailing ?
{trailing}
: null} +
+ +
+ {SEVERITIES.map(({ key, dotClass }) => { + const count = findings[key]; + if (count <= 0) return null; + + return ( +
+ ); + })} +
+
+ ); +} diff --git a/strix/viewer/frontend/src/components/PastRunsView.tsx b/strix/viewer/frontend/src/components/PastRunsView.tsx new file mode 100644 index 00000000..ac043097 --- /dev/null +++ b/strix/viewer/frontend/src/components/PastRunsView.tsx @@ -0,0 +1,176 @@ +import { useState } from "react"; +import { History, ChevronRight, Terminal } from "lucide-react"; +import type { RunListEntry, RunsPayload, RunSeverityCounts } from "@/data/serverSource"; +import { runTitle } from "@/lib/target-utils"; +import { trackCta } from "@/lib/cta"; +import EmailVerifyInline from "@/components/EmailVerifyInline"; + +/** + * "Past runs" panel. Unverified users see a tease with the run count and a + * verify affordance (the launched run stays fully visible; the CLI + * `strix view ` still works). Verified users get the full history and can + * switch the active run, which threads ?run= through the data fetches. + */ + +const SEV = [ + { key: "critical", dot: "bg-red-500", text: "text-red-500" }, + { key: "high", dot: "bg-orange-500", text: "text-orange-500" }, + { key: "medium", dot: "bg-yellow-500", text: "text-yellow-500" }, + { key: "low", dot: "bg-blue-500", text: "text-blue-500" }, +] as const; + +function SeverityChips({ counts }: { counts: RunSeverityCounts }) { + const shown = SEV.filter((s) => counts[s.key] > 0); + if (shown.length === 0) { + return No findings; + } + return ( +
+ {shown.map((s) => ( +
+
+ ))} +
+ ); +} + +function formatDate(iso: string | null): string | null { + if (!iso) return null; + const normalized = iso.trim().replace(" UTC", "Z").replace(" ", "T"); + const d = new Date(normalized); + if (Number.isNaN(d.getTime())) return null; + return d.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); +} + +/** + * Relative time ("just now" / "5m ago" / "3h ago" / "2d ago"), falling back to + * the absolute date for anything older than a week (mirrors the pro app). + */ +function formatTimeAgo(iso: string | null): string | null { + if (!iso) return null; + const normalized = iso.trim().replace(" UTC", "Z").replace(" ", "T"); + const d = new Date(normalized); + if (Number.isNaN(d.getTime())) return null; + const diffMs = Date.now() - d.getTime(); + const mins = Math.floor(diffMs / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d ago`; + return formatDate(iso); +} + +interface PastRunsViewProps { + runs: RunsPayload | null; + activeRun: string | null; + onSelectRun: (name: string) => void; + onVerified: () => void; +} + +export default function PastRunsView({ + runs, + activeRun, + onSelectRun, + onVerified, +}: PastRunsViewProps) { + const count = runs?.count ?? 0; + const [showVerify, setShowVerify] = useState(false); + + if (!runs || runs.locked) { + return ( +
+
+
+

Browse every run on this machine

+

+ You have {count} past {count === 1 ? "run" : "runs"} on this machine. +

+ {showVerify ? ( + <> +

+ Verify your email with a one-time code to unlock the full history. +

+ + + ) : ( + + )} +

+

+
+ ); + } + + if (runs.runs.length === 0) { + return ( +
+ No past runs found on this machine yet. +
+ ); + } + + return ( +
+ {runs.runs.map((run: RunListEntry) => { + const active = run.name === activeRun; + const date = formatTimeAgo(run.start_time) ?? formatTimeAgo(run.end_time); + const title = runTitle(run.target, run.name); + return ( + + ); + })} +
+ ); +} diff --git a/strix/viewer/frontend/src/components/ProCta.tsx b/strix/viewer/frontend/src/components/ProCta.tsx new file mode 100644 index 00000000..686b4d40 --- /dev/null +++ b/strix/viewer/frontend/src/components/ProCta.tsx @@ -0,0 +1,184 @@ +import React, { useState } from "react"; +import { ArrowUpRight } from "lucide-react"; +import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta"; +import type { ProFeature } from "@/lib/pro-features"; + +/** + * Shared Pro CTA primitives. Every Pro item is a direct link-out to the cloud + * sign-up in a new tab with a hover tooltip one-liner (no modal, no lock icon). + * Built once here and reused by the sidebar Platform section, the top upsell + * row, and the inline CTAs in the tabs. + */ + +/** Small tier pill ("Pro" or "Enterprise"). Deliberately not a padlock. */ +export function ProTag({ label = "Pro", className = "" }: { label?: string; className?: string }) { + return ( + + {label} + + ); +} + +/** + * Lightweight hover tooltip. Wraps a trigger and reveals `text` above it on + * hover/focus. Plain Tailwind + local state (no radix vendored). + */ +export function Tooltip({ + text, + children, + className = "", +}: { + text: string; + children: React.ReactNode; + className?: string; +}) { + const [open, setOpen] = useState(false); + return ( + setOpen(true)} + onMouseLeave={() => setOpen(false)} + onFocus={() => setOpen(true)} + onBlur={() => setOpen(false)} + > + {children} + {open && ( + + {text} + + )} + + ); +} + +export interface ProItem { + title: string; + desc: string; + slug: string; + icon: React.ElementType; +} + +/** + * Card-style Pro feature tile: icon + name + one-liner + Pro tag + arrow. + * Used in the top upsell row and inline CTA grids. + */ +export function ProTile({ item, surface }: { item: ProItem; surface?: string }) { + const Icon = item.icon; + return ( + trackCta(item.slug, surface)} + title={item.desc} + className="group block cursor-pointer rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-4 text-left transition-colors hover:border-[#444]" + > +
+
+

{item.title}

+

{item.desc}

+
+ ); +} + +/** + * Sidebar-row Pro item: a two-line row (icon + label + short one-liner + * underneath) with a small right-aligned tier tag. Opens the in-app + * FeatureDetail view via onClick (no link-out) so it sits uniformly beside the + * run/local rows in the themed nav list. + */ +export function ProNavItem({ + feature, + active, + onClick, + collapsed = false, +}: { + feature: ProFeature; + active?: boolean; + onClick: () => void; + collapsed?: boolean; +}) { + const Icon = feature.icon; + if (collapsed) { + return ( + + ); + } + return ( + + ); +} + +/** + * Inline Pro CTA button (compact). Used in the finding detail and per-surface + * rows where a full card is too heavy. + */ +export function ProInlineCta({ + label, + desc, + slug, + icon: Icon, + surface, +}: { + label: string; + desc: string; + slug: string; + icon: React.ElementType; + surface?: string; +}) { + return ( + + trackCta(slug, surface)} + className="group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white" + > + + + ); +} diff --git a/strix/viewer/frontend/src/components/RunDetails.tsx b/strix/viewer/frontend/src/components/RunDetails.tsx new file mode 100644 index 00000000..a387950b --- /dev/null +++ b/strix/viewer/frontend/src/components/RunDetails.tsx @@ -0,0 +1,200 @@ +import { useState } from "react"; +import { ChevronDown, ChevronUp, Info } from "lucide-react"; +import { formatNumber } from "@/lib/display-number"; + +/** + * "Run details" card for the Overview tab: the launch configuration the run was + * started with (targets, instruction, scope, mode) and its LLM usage + cost. + * Everything is read defensively from the raw run.json record, which may be + * partial while a scan is still live. + */ + +type Rec = Record; + +function rec(v: unknown): Rec { + return v && typeof v === "object" && !Array.isArray(v) ? (v as Rec) : {}; +} +function arr(v: unknown): unknown[] { + return Array.isArray(v) ? v : []; +} +function str(v: unknown): string | null { + return typeof v === "string" && v.trim() ? v : null; +} +function num(v: unknown): number | null { + return typeof v === "number" && Number.isFinite(v) ? v : null; +} +function humanize(s: string): string { + return s.replace(/_/g, " "); +} +function cap(s: string | null): string | null { + return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; +} +function fmtDuration(seconds: number | null): string { + if (seconds == null || seconds < 0) return "n/a"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h) return `${h}h ${m}m ${s}s`; + if (m) return `${m}m ${s}s`; + return `${s}s`; +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +export function RunDetails({ + raw, + durationSeconds, +}: { + raw: Rec; + durationSeconds: number | null; +}) { + const [open, setOpen] = useState(true); + + // Configuration (launch inputs) + const targets = arr(raw.targets_info).map((t) => { + const o = rec(t); + const display = str(o.original) ?? str(rec(o.details).target_url) ?? "unknown target"; + const type = str(o.type); + return { display, type: type ? humanize(type) : null }; + }); + const instruction = str(raw.instruction); + const scanMode = cap(str(raw.scan_mode)); + const scopeMode = str(raw.scope_mode); + const diff = rec(raw.diff_scope); + const diffActive = diff.active === true; + const diffMode = str(diff.mode); + const diffBase = str(raw.diff_base); + const nonInteractive = raw.non_interactive === true; + const localSources = arr(raw.local_sources).map((x) => String(x)).filter(Boolean); + const status = cap(str(raw.status)); + + let scope = scopeMode ?? "auto"; + if (diffActive) { + scope += ` (diff${diffMode ? `: ${diffMode}` : ""}${diffBase ? ` vs ${diffBase}` : ""})`; + } + + // Usage & cost + const usage = rec(raw.llm_usage); + const hasUsage = Object.keys(usage).length > 0; + const agents = arr(usage.agents).map(rec); + const models = Array.from( + new Set(agents.map((a) => str(a.model)).filter((m): m is string => !!m)) + ); + const requests = num(usage.requests); + const inputTokens = num(usage.input_tokens); + const cached = num(rec(arr(usage.input_tokens_details)[0]).cached_tokens); + const outputTokens = num(usage.output_tokens); + const reasoning = num(rec(arr(usage.output_tokens_details)[0]).reasoning_tokens); + const totalTokens = num(usage.total_tokens); + const cost = num(usage.cost); + + const sub = (n: number, word: string) => ( + ({formatNumber(n)} {word}) + ); + + return ( +
+ + + {open && ( +
+
+

+ Configuration +

+
+ {targets.length > 0 && ( + +
+ {targets.map((t, i) => ( +
+ {t.display} + {t.type && ( + + {t.type} + + )} +
+ ))} +
+
+ )} + + {instruction ? ( + {instruction} + ) : ( + None + )} + + {scanMode && {scanMode}} + {scope} + {nonInteractive ? "Non-interactive" : "Interactive"} + {localSources.length > 0 && ( + +
+ {localSources.map((s, i) => ( +
{s}
+ ))} +
+
+ )} + {status && {status}} +
+
+ +
+

+ Usage & cost +

+ {hasUsage ? ( +
+ {models.length ? models.join(", ") : "n/a"} + {fmtDuration(durationSeconds)} + {requests != null && {formatNumber(requests)}} + {inputTokens != null && ( + + {formatNumber(inputTokens)} + {cached != null && sub(cached, "cached")} + + )} + {outputTokens != null && ( + + {formatNumber(outputTokens)} + {reasoning != null && sub(reasoning, "reasoning")} + + )} + {totalTokens != null && {formatNumber(totalTokens)}} + {cost != null && ${cost.toFixed(2)}} + {agents.length > 0 && {formatNumber(agents.length)}} +
+ ) : ( +

Not available yet.

+ )} +
+
+ )} +
+ ); +} + +export default RunDetails; diff --git a/strix/viewer/frontend/src/components/Sidebar.tsx b/strix/viewer/frontend/src/components/Sidebar.tsx new file mode 100644 index 00000000..6ff870c3 --- /dev/null +++ b/strix/viewer/frontend/src/components/Sidebar.tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState } from "react"; +import { + FileText, + Bug, + Waypoints, + History, + Mail, + ArrowUpRight, + LogOut, + ShieldCheck, + PanelLeftClose, + PanelLeft, +} from "lucide-react"; +import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta"; +import { ProNavItem } from "@/components/ProCta"; +import { FEATURES, PLATFORM_ORDER } from "@/lib/pro-features"; +import type { View } from "@/App"; + +/** + * Persistent left rail. A single, ungrouped, ordered list of uniform two-line + * rows (icon + label + short one-liner): the current run's views, the local + * run-history + email-report actions, then the platform features. No section + * headers. Tier is shown only by the inline Pro/Enterprise tag on platform + * rows. Matches App.tsx's dark palette. + * + * Can collapse to a narrow icon-only rail; the collapsed state persists in + * localStorage and each icon row keeps a `title` tooltip so the labels stay + * discoverable. + */ + +const COLLAPSE_KEY = "strix_viewer_sidebar_collapsed"; + +interface SidebarProps { + view: View; + onSelectView: (view: View) => void; + activeFeature: string | null; + onSelectFeature: (slug: string) => void; + issuesCount: number; + agentCount: number; + runCount: number; + finished: boolean; + verified: boolean; + email: string | null; + onOpenEmail: () => void; + onOpenHistory: () => void; + onForget: () => void; +} + +export default function Sidebar({ + view, + onSelectView, + activeFeature, + onSelectFeature, + issuesCount, + agentCount, + runCount, + finished, + verified, + email, + onOpenEmail, + onOpenHistory, + onForget, +}: SidebarProps) { + const [collapsed, setCollapsed] = useState(false); + + useEffect(() => { + try { + setCollapsed(localStorage.getItem(COLLAPSE_KEY) === "1"); + } catch { + /* localStorage may be unavailable; default to expanded */ + } + }, []); + + const toggleCollapsed = () => { + setCollapsed((prev) => { + const next = !prev; + try { + localStorage.setItem(COLLAPSE_KEY, next ? "1" : "0"); + } catch { + /* best-effort persistence */ + } + return next; + }); + }; + + return ( + + ); +} + +function NavItem({ + icon: Icon, + label, + desc, + count, + active, + onClick, + collapsed = false, +}: { + icon: React.ElementType; + label: string; + desc: string; + count?: number; + active?: boolean; + onClick: () => void; + collapsed?: boolean; +}) { + if (collapsed) { + return ( + + ); + } + return ( + + ); +} diff --git a/strix/viewer/frontend/src/components/TrustToast.tsx b/strix/viewer/frontend/src/components/TrustToast.tsx new file mode 100644 index 00000000..deec179b --- /dev/null +++ b/strix/viewer/frontend/src/components/TrustToast.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { ShieldCheck, X } from "lucide-react"; + +const DISMISS_KEY = "strix_viewer_trust_dismissed"; + +/** + * One-time privacy notice, shown as a toast pinned over the sidebar. Dismissing + * it persists to localStorage so it never returns on reload or view changes. + */ +export function TrustToast({ message }: { message: string }) { + const [dismissed, setDismissed] = useState(() => { + try { + return localStorage.getItem(DISMISS_KEY) === "1"; + } catch { + return false; + } + }); + + if (dismissed) return null; + + const dismiss = () => { + try { + localStorage.setItem(DISMISS_KEY, "1"); + } catch { + /* non-fatal: worst case the toast shows again next session */ + } + setDismissed(true); + }; + + return ( +
+
+
+
+ ); +} + +export default TrustToast; diff --git a/strix/viewer/frontend/src/components/live/AgentDetailModal.tsx b/strix/viewer/frontend/src/components/live/AgentDetailModal.tsx new file mode 100644 index 00000000..529e918a --- /dev/null +++ b/strix/viewer/frontend/src/components/live/AgentDetailModal.tsx @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useRef } from "react"; +import { X } from "lucide-react"; +import { AgentTranscript } from "./AgentTranscript"; +import { ScanPromptComposer } from "./ScanPromptComposer"; +import type { TranscriptAgent, TranscriptEvent } from "@/data/serverSource"; + +/** Status -> the small leading dot color, matching the graph node styling. */ +const STATUS_DOT: Record = { + completed: "bg-emerald-400", + running: "bg-blue-400", + waiting: "bg-yellow-400", + stopped: "bg-[#888]", + crashed: "bg-red-400", + failed: "bg-red-400", +}; + +/** Consider the user "at the bottom" within this many px. */ +const NEAR_BOTTOM_PX = 80; + +/** + * Overlay modal showing a single agent's full transcript. Matches the cloud + * app: a fixed-size panel with a pinned header (status dot + agent name), the + * transcript scrolling beneath it, and a footer. Auto-scrolls to follow new + * activity while the user is near the bottom (so a live run trails). Closes on + * backdrop click, the X button, or Escape. + */ +export function AgentDetailModal({ + agent, + events, + steerable, + onClose, +}: { + agent: TranscriptAgent; + events: TranscriptEvent[]; + steerable: boolean; + onClose: () => void; +}) { + const scrollRef = useRef(null); + const nearBottom = useRef(false); + + const handleScroll = useCallback(() => { + const el = scrollRef.current; + if (!el) return; + nearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_PX; + }, []); + + // Follow new activity when the user is near the bottom (live trailing). + useEffect(() => { + const el = scrollRef.current; + if (!el || !nearBottom.current) return; + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); + }); + }, [events]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", onKey); + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = prevOverflow; + }; + }, [onClose]); + + return ( +
+
e.stopPropagation()} + > +
+
+ + {agent.name} + {agent.id} +
+ +
+ +
+ +
+ + {steerable && ( +
+ +
+ )} +
+
+ ); +} + +export default AgentDetailModal; diff --git a/strix/viewer/frontend/src/components/live/AgentGraph.tsx b/strix/viewer/frontend/src/components/live/AgentGraph.tsx new file mode 100644 index 00000000..38502843 --- /dev/null +++ b/strix/viewer/frontend/src/components/live/AgentGraph.tsx @@ -0,0 +1,254 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { + ReactFlow, + Background, + Controls, + MiniMap, + useNodesState, + useEdgesState, + useReactFlow, + type Node, + type Edge, +} from "@xyflow/react"; +import dagre from "@dagrejs/dagre"; +import AgentNodeComponent from "./AgentNode"; +import GraphSkeleton from "./GraphSkeleton"; +import type { AgentNode } from "@/types/events"; + +import "@xyflow/react/dist/style.css"; + +const NODE_WIDTH = 260; +const NODE_HEIGHT = 80; + +const nodeTypes = { agentNode: AgentNodeComponent }; + +function getLayoutedElements( + agents: Map, + selectedAgentId: string | null +) { + const g = new dagre.graphlib.Graph(); + g.setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: "TB", nodesep: 60, ranksep: 80 }); + + const nodes: Node[] = []; + const edges: Edge[] = []; + + for (const [id, agent] of agents) { + g.setNode(id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + nodes.push({ + id, + type: "agentNode", + position: { x: 0, y: 0 }, + data: { ...agent, isSelected: id === selectedAgentId }, + }); + + if (agent.parentId && agents.has(agent.parentId)) { + const edgeId = `${agent.parentId}->${id}`; + g.setEdge(agent.parentId, id); + edges.push({ + id: edgeId, + source: agent.parentId, + target: id, + style: { stroke: "#2a2a2a", strokeWidth: 1.5 }, + }); + } + } + + dagre.layout(g); + + for (const node of nodes) { + const pos = g.node(node.id); + if (pos) { + node.position = { + x: pos.x - NODE_WIDTH / 2, + y: pos.y - NODE_HEIGHT / 2, + }; + } + } + + return { nodes, edges }; +} + +const ZOOM_DURATION = 300; + + +/** Centers viewport on the root node (no parentId) at a fixed zoom — only once on first load */ +function CenterOnRoot({ nodes }: { nodes: Node[] }) { + const { setCenter } = useReactFlow(); + const hasCentered = useRef(false); + useEffect(() => { + if (nodes.length > 0 && !hasCentered.current) { + const root = nodes.find((n) => !(n.data as Record).parentId); + const target = root ?? nodes[0]; + hasCentered.current = true; + const cx = target.position.x + NODE_WIDTH / 2; + const cy = target.position.y + NODE_HEIGHT / 2; + setTimeout(() => setCenter(cx, cy, { zoom: 0.85, duration: 400 }), 60); + } + }, [nodes, setCenter]); + return null; +} + +function SmoothControls() { + const { zoomIn, zoomOut, fitView } = useReactFlow(); + return ( + +
+ + + +
+
+ ); +} + +interface AgentGraphProps { + agents: Map; + selectedAgentId: string | null; + onSelectAgent: (id: string | null) => void; + eventsLoaded?: boolean; + eventsEmpty?: boolean; + scanCompleted?: boolean; +} + +export default function AgentGraph({ + agents, + selectedAgentId, + onSelectAgent, + eventsLoaded, + eventsEmpty, + scanCompleted, +}: AgentGraphProps) { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + if (agents.size === 0) return; + const { nodes: ln, edges: le } = getLayoutedElements(agents, selectedAgentId); + setNodes(ln); + setEdges(le); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [agents.size, setNodes, setEdges]); + + // Sync agent data (status, name, etc.) into existing nodes without re-layout + useEffect(() => { + if (agents.size === 0) return; + setNodes((nds) => + nds.map((n) => { + const agent = agents.get(n.id); + if (!agent) return n; + return { ...n, data: { ...agent, isSelected: n.id === selectedAgentId } }; + }) + ); + }, [agents, selectedAgentId, setNodes]); + + const nodeClickedRef = useRef(false); + + const onNodeClick = useCallback( + (_: React.MouseEvent, node: Node) => { + nodeClickedRef.current = true; + onSelectAgent(node.id); + }, + [onSelectAgent] + ); + + const onPaneClick = useCallback(() => { + if (nodeClickedRef.current) { + nodeClickedRef.current = false; + return; + } + onSelectAgent(null); + }, [onSelectAgent]); + + // Convex responded, zero events — show empty state (not skeleton) + if (agents.size === 0 && eventsLoaded && eventsEmpty) { + return ( +
+
+ {scanCompleted ? ( + + + + ) : ( +
+ )} +
+

+ {scanCompleted + ? "Agent trace data is not available for this pentest" + : "Waiting for agent data\u2026"} +

+
+ ); + } + + const showGraph = agents.size > 0; + + return ( +
+ {/* Skeleton overlay — fades out when graph is ready */} +
+ +
+ + {/* Graph — fades in */} +
+ + + + + { + const status = (n.data as Record)?.status as string; + if (status === "running") return "#3b82f6"; + if (status === "completed") return "#10b981"; + if (status === "failed" || status === "error") return "#ef4444"; + return "#555"; + }} + maskColor="rgba(0,0,0,0.8)" + style={{ width: 80, height: 50 }} + className="!bg-[#0a0a0a] !border-[#222]" + /> + +
+
+ ); +} diff --git a/strix/viewer/frontend/src/components/live/AgentNode.tsx b/strix/viewer/frontend/src/components/live/AgentNode.tsx new file mode 100644 index 00000000..c7bc3ae1 --- /dev/null +++ b/strix/viewer/frontend/src/components/live/AgentNode.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { memo } from "react"; +import { Handle, Position, type NodeProps } from "@xyflow/react"; +import type { AgentNode as AgentNodeData } from "@/types/events"; + +const STATUS_STYLES: Record = { + running: "bg-blue-500", + completed: "bg-emerald-500", + failed: "bg-red-500", + error: "bg-red-500", +}; + +function AgentNodeComponent({ data, selected }: NodeProps) { + const agent = data as unknown as AgentNodeData & { isSelected: boolean }; + + return ( +
+ + +
+ + + + + + {agent.name} + +
+ + 0 ? "!bg-[#444]" : "!bg-transparent"}`} /> +
+ ); +} + +export default memo(AgentNodeComponent); diff --git a/strix/viewer/frontend/src/components/live/AgentTranscript.tsx b/strix/viewer/frontend/src/components/live/AgentTranscript.tsx new file mode 100644 index 00000000..da25ceab --- /dev/null +++ b/strix/viewer/frontend/src/components/live/AgentTranscript.tsx @@ -0,0 +1,302 @@ +import { Component, useMemo, type ReactNode } from "react"; +import { Brain, Bot } from "lucide-react"; +import { getToolRenderer, getToolIcon } from "./tool-renderers"; +import ChatBubble from "./tool-renderers/ChatBubble"; +import type { ToolRendererProps, AgentNode as GraphAgentNode } from "@/types/events"; +import type { TranscriptAgent, TranscriptEvent } from "@/data/serverSource"; + +/* ---------- Error boundary so one bad event never blanks the transcript ---------- */ +class RendererErrorBoundary extends Component< + { toolName: string; children: ReactNode }, + { hasError: boolean } +> { + constructor(props: { toolName: string; children: ReactNode }) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError() { + return { hasError: true }; + } + render() { + if (this.state.hasError) { + return ( + + {this.props.toolName.replace(/_/g, " ")} + + ); + } + return this.props.children; + } +} + +function SafeToolRenderer(props: ToolRendererProps) { + const Renderer = getToolRenderer(props.toolName); + return ( + + + + ); +} + +/* ---------- Value coercion ---------- + * args/result arrive as either a JSON object or a Python-repr string + * ("{'thought': '...'}"). Try JSON, then a naive python->json pass, then wrap + * the raw string so the fallback renderer can display it. Never throws. */ +function coerce(value: unknown): unknown { + if (value == null || typeof value !== "string") return value; + const t = value.trim(); + if (!t) return value; + try { + return JSON.parse(t); + } catch { + /* not JSON */ + } + try { + const jsonish = t + .replace(/\bNone\b/g, "null") + .replace(/\bTrue\b/g, "true") + .replace(/\bFalse\b/g, "false") + .replace(/'/g, '"'); + return JSON.parse(jsonish); + } catch { + return { __raw: value }; + } +} + +function asRecord(value: unknown): Record { + const c = coerce(value); + if (c && typeof c === "object" && !Array.isArray(c)) return c as Record; + if (c == null) return {}; + return { __raw: typeof c === "string" ? c : JSON.stringify(c) }; +} + +/** Numeric suffix of an event id ("tool_37" -> 37) for stable ordering. */ +function eventSeq(id: string): number { + const m = /(\d+)$/.exec(id); + return m ? parseInt(m[1], 10) : 0; +} + +/** A chat event whose author is the human/user (vs an assistant "thinking"). */ +function isUserChat(event: TranscriptEvent): boolean { + const role = event.data?.role; + return event.type === "chat" && (role === "user" || role === "human"); +} + +/** + * Inter-agent message deliveries land in the recipient's session as user-role + * items prefixed with a header (see the engine's message formatter). They are + * already represented via the sending agent's tool renderer, so we never render + * them as chat bubbles here. + */ +function isInterAgentDelivery(event: TranscriptEvent): boolean { + return isUserChat(event) && String(event.data?.content ?? "").startsWith("[Message from "); +} + +/** + * The reconstructed SDK session records every incoming user-role item for an + * agent: its initial input (the root's assembled brief, or a subagent's spawn / + * inherited-context prompt), inter-agent deliveries, AND genuine human steering + * messages sent live from the viewer or TUI. We only want the last group. Given + * an agent's events in order, hide the first user message (its initial input) + * and every inter-agent delivery; keep the rest, which are the human's live + * instructions, rendered as "User" bubbles. + */ +function hiddenUserEventIds(agentEventsInOrder: TranscriptEvent[]): Set { + const hidden = new Set(); + let sawInitialInput = false; + for (const e of agentEventsInOrder) { + if (!isUserChat(e)) continue; + if (isInterAgentDelivery(e)) { + hidden.add(e.id); + continue; + } + if (!sawInitialInput) { + sawInitialInput = true; + hidden.add(e.id); + } + } + return hidden; +} + +const STATUS_STYLE: Record = { + completed: "text-emerald-400 border-emerald-500/30 bg-emerald-500/10", + running: "text-blue-400 border-blue-500/30 bg-blue-500/10", + waiting: "text-yellow-400 border-yellow-500/30 bg-yellow-500/10", + stopped: "text-[#aaa] border-[#333] bg-[#1a1a1a]", + crashed: "text-red-400 border-red-500/30 bg-red-500/10", + failed: "text-red-400 border-red-500/30 bg-red-500/10", +}; + +/** Map our engine agent statuses onto the graph node's status union. */ +function graphStatus(status: string): GraphAgentNode["status"] { + if (status === "completed") return "completed"; + if (status === "running") return "running"; + if (status === "failed" || status === "crashed") return "failed"; + // waiting / stopped / unknown → keep the raw string; AgentNode/MiniMap fall + // back to a neutral gray for anything they don't explicitly style. + return status as GraphAgentNode["status"]; +} + +/** + * Adapt transcript agents + events into the Map that the live + * AgentGraph renders: children from parent_id, tool/message counts by scanning + * events, and a task pulled from the spawning create_agent call where present. + */ +export function buildGraphAgents( + agents: TranscriptAgent[], + events: TranscriptEvent[] +): Map { + const childrenOf = new Map(); + for (const a of agents) { + if (a.parent_id) { + const arr = childrenOf.get(a.parent_id) ?? []; + arr.push(a.id); + childrenOf.set(a.parent_id, arr); + } + } + + const toolCount = new Map(); + const messageCount = new Map(); + // A create_agent call names the child but not its id, so map spawned tasks by + // agent NAME (best-effort — used only for the graph node subtitle). + const taskByName = new Map(); + for (const e of events) { + if (e.type === "tool") { + toolCount.set(e.agent_id, (toolCount.get(e.agent_id) ?? 0) + 1); + if (e.data?.tool_name === "create_agent") { + const args = asRecord(e.data.args); + const name = (args.name as string) ?? (args.agent_name as string) ?? ""; + const task = (args.task as string) ?? ""; + if (name && task) taskByName.set(name, task); + } + } else if (!isUserChat(e)) { + // Count only assistant messages for the graph node subtitle. + messageCount.set(e.agent_id, (messageCount.get(e.agent_id) ?? 0) + 1); + } + } + + const map = new Map(); + for (const a of agents) { + map.set(a.id, { + id: a.id, + name: a.name, + task: taskByName.get(a.name) ?? "", + status: graphStatus(a.status), + parentId: a.parent_id, + children: childrenOf.get(a.id) ?? [], + createdAt: a.created_at, + toolCount: toolCount.get(a.id) ?? 0, + messageCount: messageCount.get(a.id) ?? 0, + }); + } + return map; +} + +/* ---------- Per-agent transcript ---------- */ +export function AgentTranscript({ + agent, + events, + showHeader = true, +}: { + agent: TranscriptAgent; + events: TranscriptEvent[]; + showHeader?: boolean; +}) { + const mine = useMemo(() => { + const ordered = events + .filter((e) => e.agent_id === agent.id) + .sort((a, b) => eventSeq(a.id) - eventSeq(b.id)); + const hidden = hiddenUserEventIds(ordered); + return ordered.filter((e) => !hidden.has(e.id)); + }, [events, agent.id]); + + const toolCount = mine.filter((e) => e.type === "tool").length; + const msgCount = mine.length - toolCount; + + return ( +
+ {showHeader && ( + <> +
+ {agent.name} + + {agent.status} + + {agent.id} +
+

+ {msgCount} message{msgCount === 1 ? "" : "s"} · {toolCount} tool call + {toolCount === 1 ? "" : "s"} +

+ + )} + + {mine.length === 0 ? ( +

No recorded activity for this agent.

+ ) : ( +
+ {mine.map((event, i) => { + const isLast = i === mine.length - 1; + const isTool = event.type === "tool"; + const toolName = isTool ? String(event.data?.tool_name ?? "tool") : ""; + const role = !isTool ? String(event.data?.role ?? "assistant") : ""; + + let Icon; + let iconColor: string; + if (isTool) { + const meta = getToolIcon(toolName); + Icon = meta.icon; + iconColor = meta.color; + } else { + const isUser = role === "user" || role === "human"; + Icon = isUser ? Bot : Brain; + iconColor = isUser ? "text-blue-400" : "text-purple-400"; + } + + const status = isTool ? String(event.data?.status ?? "completed") : "completed"; + + return ( +
+
+
+ +
+ {!isLast &&
} +
+
+ {isTool ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/strix/viewer/frontend/src/components/live/GraphSkeleton.tsx b/strix/viewer/frontend/src/components/live/GraphSkeleton.tsx new file mode 100644 index 00000000..853b1af5 --- /dev/null +++ b/strix/viewer/frontend/src/components/live/GraphSkeleton.tsx @@ -0,0 +1,70 @@ +"use client"; + +function SkeletonNode({ w = 24 }: { w?: number }) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); +} + +function VLine() { + return
; +} + +function HBranch({ count }: { count: number }) { + return ( +
+
+
+ ); +} + +export default function GraphSkeleton() { + return ( +
+
+ + + +
+ {[18, 22, 16].map((w, i) => ( +
+ + +
+ ))} +
+
+
+ + +
+ {[14, 20].map((w, i) => ( +
+ + +
+ ))} +
+
+
+ + + + +
+
+
+
+
+ ); +} diff --git a/strix/viewer/frontend/src/components/live/ScanPromptComposer.tsx b/strix/viewer/frontend/src/components/live/ScanPromptComposer.tsx new file mode 100644 index 00000000..7d91c0fc --- /dev/null +++ b/strix/viewer/frontend/src/components/live/ScanPromptComposer.tsx @@ -0,0 +1,315 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { ArrowUp, ChevronDown, ChevronUp, Loader2, Sparkles } from "lucide-react"; +import { steerAgent, type TranscriptAgent } from "@/data/serverSource"; +import { track } from "@/lib/cta"; +import { cn } from "@/lib/utils"; + +const ROOT_TARGET_VALUE = "__root__"; + +interface ScanPromptComposerProps { + /** All agents in the run; used to resolve the root and running children. */ + agents: TranscriptAgent[]; + /** + * Single-agent (modal) mode: pins the composer to one agent and shows a + * static "Target: " pill instead of the dropdown. Omit for the + * multi-agent graph variant. + */ + fixedAgentId?: string; + className?: string; +} + +/** + * Faithful port of the pro app's ScanPromptComposer for the local viewer. + * Collapsed by default into a "Guide the agent" pill; expands into a card with + * an auto-resizing textarea and a target control. The viewer's steering is + * immediate (no Enterprise lock, no bridge-connecting state), so this is only + * rendered by callers when steering is available. Sends via steerAgent, which + * requires a concrete agent id, so "Root agent" resolves to the root agent's id. + */ +export function ScanPromptComposer({ + agents, + fixedAgentId, + className, +}: ScanPromptComposerProps) { + const textareaRef = useRef(null); + const [expanded, setExpanded] = useState(false); + const [focused, setFocused] = useState(false); + const [value, setValue] = useState(""); + const [sending, setSending] = useState(false); + const [feedback, setFeedback] = useState(null); + + const isModal = fixedAgentId != null; + + // Root = the agent with no parent; fall back to the first agent. + const rootAgent = useMemo( + () => agents.find((a) => !a.parent_id) ?? agents[0] ?? null, + [agents] + ); + + // Multi-agent dropdown options: running child agents plus Root (added in JSX). + const targetOptions = useMemo( + () => agents.filter((a) => a.parent_id && a.status === "running"), + [agents] + ); + + // Selected target for the multi-agent variant. ROOT sentinel by default. + const [selectedTarget, setSelectedTarget] = useState(ROOT_TARGET_VALUE); + const [menuOpen, setMenuOpen] = useState(false); + + // If the selected child target disappears (finished), fall back to Root. + useEffect(() => { + if ( + selectedTarget !== ROOT_TARGET_VALUE && + !targetOptions.some((a) => a.id === selectedTarget) + ) { + setSelectedTarget(ROOT_TARGET_VALUE); + } + }, [selectedTarget, targetOptions]); + + // Resolve the concrete agent id + display name for the current target. + const { targetId, targetName } = useMemo(() => { + if (isModal) { + const agent = agents.find((a) => a.id === fixedAgentId) ?? null; + return { + targetId: fixedAgentId ?? null, + targetName: agent?.name ?? "this agent", + }; + } + if (selectedTarget === ROOT_TARGET_VALUE) { + return { + targetId: rootAgent?.id ?? null, + targetName: "Root agent", + }; + } + const agent = agents.find((a) => a.id === selectedTarget) ?? null; + return { + targetId: agent?.id ?? rootAgent?.id ?? null, + targetName: agent?.name ?? "Root agent", + }; + }, [agents, fixedAgentId, isModal, rootAgent, selectedTarget]); + + const empty = value.trim().length === 0; + + // Grow the textarea with its content, capped by max-h via CSS. + useLayoutEffect(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }, [value]); + + const handleExpand = useCallback(() => { + setExpanded(true); + requestAnimationFrame(() => textareaRef.current?.focus()); + }, []); + + const handleCollapse = useCallback(() => { + setExpanded(false); + setFocused(false); + setMenuOpen(false); + }, []); + + const handleSend = useCallback(async () => { + if (sending) return; + const message = value.trim(); + if (!message || !targetId) return; + + setSending(true); + setFeedback(null); + const name = targetName; + const res = await steerAgent(targetId, message); + setSending(false); + if (res.ok) { + setValue(""); + setFeedback(`Sent to ${name}`); + track("agent_steered"); + } else if (res.error === "not_delivered") { + setFeedback("Could not reach that agent (it may have finished)."); + } else { + setFeedback("Could not send that message. Try again."); + } + }, [sending, value, targetId, targetName]); + + if (!expanded) { + return ( + + ); + } + + return ( +
+
+
+
+ +

Live prompt

+
+

Connected

+
+
+ {isModal ? ( +
+ Target: {targetName} +
+ ) : ( +
+ Target: +
+ + {menuOpen && ( +
+ { + setSelectedTarget(ROOT_TARGET_VALUE); + setMenuOpen(false); + }} + /> + {targetOptions.map((option) => ( + { + setSelectedTarget(option.id); + setMenuOpen(false); + }} + /> + ))} +
+ )} +
+
+ )} + +
+
+ +
+