From 2b7322f0c595daeba130cc9bb7e9f36be7f70650 Mon Sep 17 00:00:00 2001 From: Bruno Pavelja Date: Sat, 4 Apr 2026 23:45:29 +0200 Subject: [PATCH 1/6] Gitignore update --- .gitignore | 7 +++++++ packages/api/data/recon-web.db | Bin 32768 -> 0 bytes 2 files changed, 7 insertions(+) delete mode 100644 packages/api/data/recon-web.db diff --git a/.gitignore b/.gitignore index ab6b14b..a5e5ace 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist/ build/ *.tsbuildinfo +packages/api/data/* # Dependencies node_modules/ @@ -38,3 +39,9 @@ Thumbs.db # Internal @internal/ docs/superpowers/ + +# SQLite databases +*.db +*.db-journal +*.db-wal +data/ diff --git a/packages/api/data/recon-web.db b/packages/api/data/recon-web.db deleted file mode 100644 index a92bb0d27c1a4bc9f0eaf35212dff1013ef459dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI%&ui0Q7{KwQ>)N?lH&3~~m(m7?KiI+3z}mi(vD#_d;U-FmOVmJ@Vw2!59_GC2 z&5Qq&{t^2lMDSwzHgDN)Bo`@s18tK$ecnFr=XnXdwD+tX#J+el938l^STk0PLcw?_ zgkc!P^tF(_^ifJr#`pAL>XU0ei^iQVe=4_r8s()EW9el1N9FsiZumQ$XQ$oPf&G#1#=hsI`v{e*gGdi;zbKWgyO#Me2tEHW@D3d{dncQLx+rHlo1K>26&JoEwmTED`l)HR zWUpy&%$Ka1WxhH$cFLy4EnPA0m~}d4dSM?O&Xuid)jVFC_RSgjQ8I{C`}BG7LKk)9 zUz#~C$L#lX$4ERj>3Mo%?DAnalk0Z{zEDtsg(1n*GPMJ}j55^{RPv z Date: Sun, 5 Apr 2026 23:57:12 +0200 Subject: [PATCH 2/6] fix 5 TODO bugs, add self-hosted SSL grading, rewrite sitemap/TLS handlers, build Astro Starlight docs site --- README.md | 328 +- docs/DEVELOPMENT.md | 367 - docs/RELEASING.md | 265 - docs/astro.config.mjs | 69 + docs/package-lock.json | 6226 +++++++++++++++++ docs/package.json | 16 + docs/src/content.config.ts | 7 + docs/src/content/docs/checks/content.mdx | 115 + docs/src/content/docs/checks/dns.mdx | 107 + docs/src/content/docs/checks/meta.mdx | 137 + docs/src/content/docs/checks/network.mdx | 155 + docs/src/content/docs/checks/overview.mdx | 81 + docs/src/content/docs/checks/performance.mdx | 60 + docs/src/content/docs/checks/security.mdx | 230 + .../content/docs/deployment/docker-local.mdx | 120 + .../content/docs/deployment/docker-remote.mdx | 103 + .../content/docs/deployment/kubernetes.mdx | 168 + .../content/docs/deployment/standalone.mdx | 192 + .../docs/development/adding-handlers.mdx | 311 + .../content/docs/development/architecture.mdx | 141 + .../content/docs/development/contributing.mdx | 162 + .../docs/getting-started/configuration.mdx | 68 + .../docs/getting-started/first-scan.mdx | 69 + .../docs/getting-started/quick-start.mdx | 86 + docs/src/content/docs/guides/cli.mdx | 224 + .../src/content/docs/guides/notifications.mdx | 104 + docs/src/content/docs/guides/rest-api.mdx | 168 + .../content/docs/guides/scheduled-scans.mdx | 72 + docs/src/content/docs/guides/web-ui.mdx | 92 + docs/src/content/docs/index.mdx | 40 + docs/tsconfig.json | 3 + packages/core/src/handlers/sitemap.ts | 147 +- packages/core/src/handlers/ssl-labs.ts | 367 +- packages/core/src/handlers/tls.ts | 92 +- packages/core/src/handlers/txt-records.ts | 69 +- packages/core/src/presentation.ts | 4 +- packages/core/src/registry.ts | 4 +- packages/web/src/components/layout/Footer.tsx | 100 +- .../web/src/components/results/ResultGrid.tsx | 224 +- .../results/renderers/SitemapRenderer.tsx | 83 +- .../results/renderers/SslLabsRenderer.tsx | 76 +- .../results/renderers/TxtRecordsRenderer.tsx | 14 +- packages/web/src/pages/Results.tsx | 10 +- 43 files changed, 10286 insertions(+), 1190 deletions(-) delete mode 100644 docs/DEVELOPMENT.md delete mode 100644 docs/RELEASING.md create mode 100644 docs/astro.config.mjs create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/src/content.config.ts create mode 100644 docs/src/content/docs/checks/content.mdx create mode 100644 docs/src/content/docs/checks/dns.mdx create mode 100644 docs/src/content/docs/checks/meta.mdx create mode 100644 docs/src/content/docs/checks/network.mdx create mode 100644 docs/src/content/docs/checks/overview.mdx create mode 100644 docs/src/content/docs/checks/performance.mdx create mode 100644 docs/src/content/docs/checks/security.mdx create mode 100644 docs/src/content/docs/deployment/docker-local.mdx create mode 100644 docs/src/content/docs/deployment/docker-remote.mdx create mode 100644 docs/src/content/docs/deployment/kubernetes.mdx create mode 100644 docs/src/content/docs/deployment/standalone.mdx create mode 100644 docs/src/content/docs/development/adding-handlers.mdx create mode 100644 docs/src/content/docs/development/architecture.mdx create mode 100644 docs/src/content/docs/development/contributing.mdx create mode 100644 docs/src/content/docs/getting-started/configuration.mdx create mode 100644 docs/src/content/docs/getting-started/first-scan.mdx create mode 100644 docs/src/content/docs/getting-started/quick-start.mdx create mode 100644 docs/src/content/docs/guides/cli.mdx create mode 100644 docs/src/content/docs/guides/notifications.mdx create mode 100644 docs/src/content/docs/guides/rest-api.mdx create mode 100644 docs/src/content/docs/guides/scheduled-scans.mdx create mode 100644 docs/src/content/docs/guides/web-ui.mdx create mode 100644 docs/src/content/docs/index.mdx create mode 100644 docs/tsconfig.json diff --git a/README.md b/README.md index c5e9ea7..cc038c3 100644 --- a/README.md +++ b/README.md @@ -1,227 +1,229 @@ # recon-web -> **Early stage project** -under active development. APIs, configuration, and features may change without notice. Contributions and feedback are welcome. +Scan any website and get a full picture of its security, DNS, performance, and tech stack — in seconds. -Scan any website and get a detailed analysis of its DNS, security, performance, and tech stack -from the web UI, REST API, or command line. +Open-source, self-hosted, no account required. --- -## What does it do? +## Quick Start -recon-web runs **33 checks** against a target URL and presents the results in a clean, categorised dashboard. Checks include: +### One-liner (CLI only) -- **Security** -SSL certificates, HSTS, security headers, firewalls, block lists, threats -- **DNS** -A/AAAA/MX/NS/TXT records, DNSSEC, DNS-over-HTTPS -- **Network** -open ports, HTTP headers, cookies, redirects, traceroute -- **Content** -robots.txt, sitemap, social/meta tags, linked pages -- **Meta** -WHOIS, domain ranking, tech stack detection, Wayback archives - -Results are saved to history so you can compare scans over time. - ---- - -## Getting started +```bash +docker run --rm ghcr.io/brunoafk/recon-web/cli scan example.com +``` -### Option 1: Docker (recommended) +No setup, no cloning — just scan. -The fastest way to get running. Requires only Docker. +### Docker (full UI + API) ```bash -# Clone the repo git clone https://github.com/BrunoAFK/recon-web.git cd recon-web - -# Configure (optional -works without any API keys) cp .env.example .env - -# Start docker compose up ``` -Open **http://localhost:8080** in your browser. That's it. - -- Port 8080 -Web UI -- Port 3000 -REST API + Swagger docs at `/docs` +Open **http://localhost:8080** and enter any URL. Done. -### Option 2: Run from source +### No Docker? Run from source -Requires **Node.js 24+**. - -```bash -npm install - -# Terminal 1 -API server (http://localhost:3000) -npm run dev - -# Terminal 2 -Frontend (http://localhost:5173) -npm run dev:web -``` - -### Option 3: CLI only +Requires Node.js 24+. ```bash +git clone https://github.com/BrunoAFK/recon-web.git +cd recon-web npm install -npx tsc --build -# Full scan -node packages/cli/dist/index.js scan example.com - -# JSON output -node packages/cli/dist/index.js scan --json example.com - -# Single check -node packages/cli/dist/index.js dns example.com -node packages/cli/dist/index.js ssl github.com +npm run dev # API on http://localhost:3000 +npm run dev:web # UI on http://localhost:5173 (separate terminal) ``` --- -## Using the web UI - -1. Enter a URL on the home page and click **Scan** -2. Results stream in real-time as each check completes -3. Use **category filters** (Security, DNS, Network...) to focus on what matters -4. Use **status filters** (OK, Issues, Info, Skipped) to find problems quickly -5. Click any card's info icon for a human-readable explanation of what it checks -6. Visit **History** to browse past scans, compare two scans side-by-side, or download a PDF/HTML report +## What You Get + +Enter a URL and recon-web runs **39 checks** across 6 categories. Results stream in real-time — no waiting for everything to finish. + +### Security (12 checks) + +| Check | What it does | +|-------|-------------| +| **SSL Certificate** | Reads the TLS certificate: issuer, expiry date, trust chain | +| **SSL Grade** | Grades the TLS setup A+ to F (protocol, cipher, cert, HSTS) | +| **TLS Configuration** | Protocol version, cipher suite, certificate validity | +| **HSTS** | Checks Strict-Transport-Security header and preload status | +| **HTTP Security Headers** | Scores CSP, X-Frame-Options, X-Content-Type-Options, etc. | +| **Firewall** | Detects WAF providers (Cloudflare, AWS WAF, Akamai...) | +| **security.txt** | Checks for a vulnerability disclosure policy | +| **Threats** | Google Safe Browsing + malware databases | +| **Block Lists** | Checks 17 DNS block lists for reputation issues | +| **VirusTotal** | Scans against 70+ antivirus engines (needs API key) | +| **AbuseIPDB** | IP reputation and abuse history (needs API key) | +| **WordPress** | Detects WP installs, exposed files, plugin/theme versions | + +### DNS (5 checks) + +| Check | What it does | +|-------|-------------| +| **DNS Records** | A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR records | +| **DNS Provider** | Identifies nameserver provider and DoH support | +| **DNSSEC** | Validates DNSKEY, DS, and RRSIG records | +| **TXT Records** | Parses SPF, DKIM, domain verification entries | +| **Mail Config** | MX records, mail provider, SPF/DMARC analysis | + +### Network (8 checks) + +| Check | What it does | +|-------|-------------| +| **HTTP Status** | Status code and response time | +| **HTTP Headers** | Full response header dump | +| **Cookies** | Cookie names, flags (Secure, HttpOnly, SameSite) | +| **Redirects** | Follows the full redirect chain | +| **Open Ports** | Scans common ports (SSH, HTTP, HTTPS, MySQL, RDP...) | +| **IP Address** | Resolves the domain's IP | +| **Server Location** | GeoIP lookup — country, city, coordinates | +| **Traceroute** | Network hops from server to target | + +### Content (5 checks) + +| Check | What it does | +|-------|-------------| +| **robots.txt** | Parses allowed/disallowed paths and crawl directives | +| **Sitemap** | Finds and parses XML sitemaps (checks robots.txt first) | +| **Social Tags** | OpenGraph, Twitter Cards, meta description, preview image | +| **Linked Pages** | Counts and lists internal + external links | +| **SEO Audit** | Title, headings, images, canonical, structured data, score | + +### Meta (7 checks) + +| Check | What it does | +|-------|-------------| +| **WHOIS** | Domain registrar, creation date, expiry, nameservers | +| **Archive History** | Wayback Machine snapshots count and date range | +| **Domain Ranking** | Tranco top-1M popularity rank | +| **Legacy Ranking** | Cisco Umbrella ranking | +| **Features** | BuiltWith feature detection (needs API key) | +| **Tech Stack** | Detects frameworks, CMS, CDN, analytics from HTML | +| **Screenshot** | Visual capture of the page (needs Chromium) | + +### Performance (2 checks) + +| Check | What it does | +|-------|-------------| +| **Carbon Footprint** | Page weight, CO2 estimate, green hosting check | +| **PageSpeed** | Google Lighthouse scores (needs API key) | --- -## Using the API - -All endpoints are documented at `/docs` (Swagger UI). +## How to Use -| Method | Path | Description | -|--------|------|-------------| -| `GET` | `/health` | Health check | -| `GET` | `/api/handlers` | List all available checks | -| `GET` | `/api?url=` | Run all checks | -| `GET` | `/api/?url=` | Run a single check | -| `GET` | `/api/stream?url=` | Run all checks with SSE progress streaming | -| `GET` | `/api/history` | List past scans (`?limit=20&offset=0`) | -| `GET` | `/api/history/:id` | Get full scan results | -| `GET` | `/api/history/:id/report` | Download HTML report (`?format=pdf` for PDF) | -| `DELETE` | `/api/history/:id` | Delete a scan | +### Web UI -Example: -```bash -curl "http://localhost:3000/api?url=example.com" -curl "http://localhost:3000/api/dns?url=example.com" -``` +1. Enter a URL and click **Scan** +2. Results appear in real-time as each check completes +3. Filter by **category** (Security, DNS, Network, Content, Meta, Performance) +4. Filter by **status** (OK, Issues, Info, Skipped) to find problems fast +5. Sort A-Z / Z-A to find specific checks +6. Click the **info icon** on any card for an explanation of what it checks +7. Click the **code icon** to see the raw JSON data ---- +### History & Reports -## Using the CLI +- All scans are saved automatically +- Browse past scans on the **History** page +- **Compare** two scans side-by-side to see what changed +- **Download** a HTML or PDF report to share with your team -```bash -# Full scan with coloured output -node packages/cli/dist/index.js scan example.com +### REST API -# JSON output (pipe to jq, save to file, etc.) -node packages/cli/dist/index.js scan --json example.com +Interactive docs at **http://localhost:3000/docs** (Swagger UI). -# JUnit XML for CI/CD pipelines -node packages/cli/dist/index.js scan --format junit example.com +```bash +# Full scan +curl "http://localhost:3000/api?url=example.com" -# Fail if SSL is expired (CI gate) -node packages/cli/dist/index.js scan --fail-on ssl:expired example.com +# Single check +curl "http://localhost:3000/api/dns?url=example.com" +curl "http://localhost:3000/api/ssl?url=github.com" -# Compare with a previous scan -node packages/cli/dist/index.js scan --json example.com > baseline.json -node packages/cli/dist/index.js scan --diff baseline.json example.com +# Real-time streaming (SSE) +curl "http://localhost:3000/api/stream?url=example.com" -# Run only specific checks -node packages/cli/dist/index.js scan --only dns,ssl,headers example.com +# List all available checks +curl "http://localhost:3000/api/handlers" ``` ---- - -## Self-hosting - -### Docker Compose (local build) - -Builds images from source: +### CLI ```bash -cp .env.example .env # edit as needed -docker compose up -``` +# Full scan with coloured output +npx recon-web scan example.com -Services: -- **api** (port 3000) -Fastify + Chromium + SQLite -- **web** (port 8080) -nginx serving React app + reverse proxy to API +# JSON output +npx recon-web scan --json example.com -### Docker Compose (pre-built images) +# Single check +npx recon-web dns example.com -Pulls images from GitHub Container Registry -no build needed: +# JUnit XML for CI/CD +npx recon-web scan --format junit example.com -```bash -cp .env.example .env -docker compose -f docker-compose.remote.yml up -``` +# Fail build if SSL is expired +npx recon-web scan --fail-on ssl:expired example.com -Available images: -```bash -docker pull ghcr.io/brunoafk/recon-web/api:latest -docker pull ghcr.io/brunoafk/recon-web/web:latest -docker pull ghcr.io/brunoafk/recon-web/cli:latest +# Compare with a previous scan +npx recon-web scan --json example.com > baseline.json +npx recon-web scan --diff baseline.json example.com ``` -### Kubernetes (Helm) +Or via Docker: ```bash -helm install recon-web ./helm/recon-web \ - --set ingress.enabled=true \ - --set ingress.host=recon.example.com \ - --set persistence.enabled=true +docker run --rm ghcr.io/brunoafk/recon-web/cli scan example.com ``` -See `helm/recon-web/values.yaml` for full configuration. - -### Cloudflare Pages (light scan) - -Deploys a static frontend with ~16 HTTP-only checks running on the edge. No server required. +--- -```bash -npm run build:static -cd packages/static -npx wrangler pages deploy dist -``` +## Configuration -To connect a full API backend, set `API_ORIGIN` during build: -```bash -API_ORIGIN=https://your-api.example.com npm run build:static -``` +Copy `.env.example` to `.env`. Everything is optional — the app works out of the box without any API keys. ---- +### API Keys (optional) -## Configuration +All checks work without keys. Adding keys enables extra checks or removes rate limits: -Copy `.env.example` to `.env`. All settings are optional -the app works out of the box. +| Variable | What it unlocks | +|----------|----------------| +| `GOOGLE_CLOUD_API_KEY` | PageSpeed Insights + Google Safe Browsing | +| `VIRUSTOTAL_API_KEY` | VirusTotal scan (free: 500 req/day) | +| `ABUSEIPDB_API_KEY` | AbuseIPDB reputation (free: 1,000 req/day) | +| `CLOUDMERSIVE_API_KEY` | Malware scanning | +| `BUILT_WITH_API_KEY` | BuiltWith feature detection | +| `TRANCO_API_KEY` | Tranco domain ranking | ### Authentication -Disabled by default. To enable: - -```bash +```env AUTH_ENABLED=true -AUTH_TOKEN=your-secret-token-at-least-32-chars +AUTH_TOKEN=your-secret-token ``` -When enabled, all `/api/*` routes require `Authorization: Bearer `. The web UI shows a login page. +All `/api/*` routes will require `Authorization: Bearer `. The web UI shows a login page. -### Scheduled scans & notifications +### Scheduled Scans & Alerts -```bash +```env SCHEDULE_ENABLED=true -SCHEDULE_CRON=0 0 * * * # daily at midnight +SCHEDULE_CRON=0 0 * * * SCHEDULE_URLS=https://example.com,https://mysite.com -# Telegram alerts (optional) +# Telegram (optional) TELEGRAM_BOT_TOKEN=123456:ABC-DEF TELEGRAM_CHAT_ID=987654321 -# Email alerts (optional) +# Email (optional) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=you@gmail.com @@ -229,31 +231,31 @@ SMTP_PASS=app-password NOTIFY_EMAIL=alerts@example.com ``` -The scheduler compares each scan with the previous one and sends alerts when it detects changes (SSL expired, security headers removed, DNS changed, etc.). - -### API keys (optional) +The scheduler runs scans on a cron schedule, compares with previous results, and sends alerts when things change (SSL expired, headers removed, DNS changed, etc.). -All checks work without API keys. Adding keys enables extra checks or improves accuracy: +See [`.env.example`](.env.example) for all options with defaults. -| Variable | Used by | -|----------|---------| -| `GOOGLE_CLOUD_API_KEY` | PageSpeed Insights + Google Safe Browsing | -| `CLOUDMERSIVE_API_KEY` | Malware scanning | -| `BUILT_WITH_API_KEY` | Technology/feature detection | -| `TRANCO_API_KEY` | Domain ranking | +--- -### All environment variables +## Deployment -See [`.env.example`](.env.example) for the complete list with defaults and descriptions. +| Method | Guide | +|--------|-------| +| Docker Compose (build from source) | [docs/deploy-docker-local.md](docs/deploy-docker-local.md) | +| Docker Compose (pre-built images) | [docs/deploy-docker-remote.md](docs/deploy-docker-remote.md) | +| Kubernetes (Helm) | [docs/deploy-kubernetes.md](docs/deploy-kubernetes.md) | +| Standalone (Node.js, no Docker) | [docs/deploy-standalone.md](docs/deploy-standalone.md) | --- ## Development -If you want to contribute, fix bugs, or understand the codebase, see the [development guide](docs/DEVELOPMENT.md). +See the [development guide](docs/DEVELOPMENT.md) for architecture, running tests, and contributing. --- ## License -GPL-2.0-only -see [LICENSE](LICENSE). +[GPL-2.0-only](LICENSE) — free to use, modify, and distribute. + +Built by [Bruno Pavelja](https://pavelja.com). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md deleted file mode 100644 index 21d3faa..0000000 --- a/docs/DEVELOPMENT.md +++ /dev/null @@ -1,367 +0,0 @@ -# Development Guide - -Everything you need to know to work on recon-web. - ---- - -## Prerequisites - -- **Node.js 24+** (LTS) -- **npm** (comes with Node) -- **Docker** (optional, for container builds) - -```bash -nvm use # reads .nvmrc -npm install # installs all workspace packages -``` - ---- - -## Running locally - -```bash -# Terminal 1 -API server on http://localhost:3000 -npm run dev - -# Terminal 2 -Frontend on http://localhost:5173 (proxies to API) -npm run dev:web -``` - -The frontend dev server proxies `/api` requests to the API server automatically. - ---- - -## Project structure - -``` -recon-web/ -├── packages/ -│ ├── core/ # @recon-web/core -shared handler library -│ │ ├── src/ -│ │ │ ├── handlers/ # 33 analysis handlers (dns.ts, ssl.ts, ports.ts, ...) -│ │ │ ├── utils/ # URL parsing, network validation, retry, diff -│ │ │ ├── registry.ts # Handler registry with metadata -│ │ │ ├── runner.ts # Concurrent handler execution (p-limit + SSRF guard) -│ │ │ ├── types.ts # HandlerResult, HandlerOptions, ErrorCode, ErrorCategory -│ │ │ ├── presentation.ts # Display names and short descriptions for UI -│ │ │ └── index.ts # Public exports -│ │ └── package.json -│ │ -│ ├── api/ # @recon-web/api -Fastify REST server -│ │ ├── src/ -│ │ │ ├── index.ts # Server entry point -│ │ │ ├── server.ts # Fastify setup (CORS, rate-limit, Swagger, auth, scheduler) -│ │ │ ├── config.ts # Environment config + Chrome path detection -│ │ │ ├── routes.ts # All API endpoints + URL validation hook -│ │ │ ├── scan.ts # Scan orchestration, SSE streaming, in-flight dedup -│ │ │ ├── auth/ # Bearer token authentication plugin -│ │ │ ├── db/ # SQLite init, CRUD (scans + scan_results tables) -│ │ │ ├── scheduler/ # Cron-based scheduled scanning + change detection -│ │ │ └── report/ # HTML/PDF report generation with issue extraction -│ │ ├── Dockerfile -│ │ └── package.json -│ │ -│ ├── cli/ # @recon-web/cli -Commander CLI -│ │ ├── src/ -│ │ │ ├── index.ts # CLI entry + 33 auto-generated sub-commands -│ │ │ ├── runner.ts # run-all, run-single orchestration -│ │ │ ├── formatter.ts # Text, JSON, JUnit output + thresholds + diff -│ │ │ └── config.ts # Env vars, Chrome path -│ │ ├── Dockerfile -│ │ └── package.json -│ │ -│ ├── web/ # @recon-web/web -React SPA -│ │ ├── src/ -│ │ │ ├── App.tsx # Router + auth/theme providers -│ │ │ ├── pages/ # Home, Results, History, HistoryResults, Compare, Login, Settings -│ │ │ ├── components/ -│ │ │ │ ├── results/ -│ │ │ │ │ ├── ResultGrid.tsx # Main grid with category + status filters -│ │ │ │ │ ├── ResultCard.tsx # Individual handler result card -│ │ │ │ │ ├── classify-error.ts # Error classification (tool/site/info) -│ │ │ │ │ └── renderers/ # 36 specialised renderer components -│ │ │ │ └── layout/ # Header, footer, theme toggle -│ │ │ ├── hooks/use-scan.ts # useLiveScan, useHistoricalScan, useHandlers -│ │ │ ├── lib/api.ts # API client + SSE stream parser -│ │ │ └── index.css # Tailwind + custom styles -│ │ ├── Dockerfile # nginx reverse proxy -│ │ └── package.json -│ │ -│ └── static/ # @recon-web/static -Cloudflare Pages edge build -│ └── functions/ # CF Workers for ~16 HTTP-only handlers -│ -├── helm/recon-web/ # Kubernetes Helm chart -│ ├── Chart.yaml -│ ├── values.yaml -│ └── templates/ -│ -├── @internal/ # Internal docs, reference projects, assets (not published) -├── docs/ # Documentation -├── docker-compose.yml -├── docker-compose.remote.yml -├── .env.example -├── package.json # Root workspace config -└── tsconfig.json -``` - -### Package dependency graph - -``` -@recon-web/core ← shared library (handlers, registry, runner, types) - ↑ ↑ -@recon-web/api @recon-web/cli - ↑ -@recon-web/web (dev proxy only, no build dependency) -@recon-web/static (CF Pages edge deployment, uses core) -``` - ---- - -## Key concepts - -### Handlers - -A handler is a function that takes a URL and returns a result: - -```typescript -// packages/core/src/types.ts -type AnalysisHandler = (url: string, options?: HandlerOptions) => Promise>; - -interface HandlerResult { - data?: T; // success payload - error?: string; // error message - errorCode?: ErrorCode; // structured error code (MISSING_API_KEY, TIMEOUT, etc.) - errorCategory?: ErrorCategory; // tool | site | info - skipped?: string; // skip reason -} -``` - -Each handler is registered in `packages/core/src/registry.ts` with metadata (name, category, description, required capabilities). - -### Handler categories - -| Category | Examples | -|----------|----------| -| `security` | ssl, hsts, http-security, firewall, threats, block-lists | -| `dns` | dns, dns-server, dnssec, mail-config, txt-records | -| `network` | headers, status, cookies, redirects, ports, get-ip, trace-route | -| `content` | robots-txt, sitemap, social-tags, linked-pages | -| `meta` | whois, rank, tech-stack, archives, screenshot, features | -| `performance` | quality, carbon | - -### Error codes - -Handlers can return structured error codes to help the frontend classify errors without string matching: - -| ErrorCode | ErrorCategory | Meaning | -|-----------|--------------|---------| -| `MISSING_API_KEY` | `tool` | Handler requires an API key that isn't configured | -| `INVALID_URL` | `tool` | URL couldn't be parsed or normalised | -| `REQUIRES_CHROMIUM` | `tool` | Handler needs Chromium but it's not available | -| `TIMEOUT` | `site` | Handler timed out waiting for a response | -| `CONNECTION_REFUSED` | `site` | Target refused the connection | -| `DNS_FAILURE` | `site` | DNS resolution failed | -| `SSRF_BLOCKED` | `tool` | Target resolves to a private/internal IP | -| `NOT_FOUND` | `info` | Resource not found (normal for many sites) | -| `NO_DATA` | `info` | No data available (not an error) | - -### Runner and SSRF protection - -`packages/core/src/runner.ts` executes handlers with concurrency control (p-limit). Before running any handler, it: - -1. Validates the URL scheme (only `http://` and `https://` allowed) -2. Resolves the hostname and blocks private/internal IPs (127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1) - -### Scan flow - -``` -User enters URL - → API receives GET /api/stream?url=... - → preHandler validates URL (returns 400 if invalid) - → executeScan() starts - → SSRF guard checks hostname - → p-limit runs handlers concurrently (default: 8) - → SSE events stream to frontend (handler_started, handler_finished, ...) - → Results saved to SQLite - → Frontend renders cards as they arrive -``` - ---- - -## Testing - -```bash -npm test # all packages -npm test -w @recon-web/core # core only (~108 tests) -npm test -w @recon-web/api # api only -npm test -w @recon-web/cli # cli only (~17 tests) -npm test -w @recon-web/web # frontend component tests -``` - -Tests use **Vitest** with mocking (MSW for HTTP, `vi.mock` for Node modules). Frontend uses **Playwright** for E2E. - -### TypeScript checking - -```bash -npm run typecheck # all packages -npx tsc -p packages/core/tsconfig.json --noEmit # core only -``` - ---- - -## Adding a new handler - -1. Create `packages/core/src/handlers/my-handler.ts`: - -```typescript -import type { AnalysisHandler } from '../types.js'; -import { normalizeUrl } from '../utils/url.js'; - -export interface MyHandlerResult { - // your result shape -} - -export const myHandler: AnalysisHandler = async (url, options) => { - const targetUrl = normalizeUrl(url); - // ... your logic - return { data: { /* result */ } }; -}; -``` - -2. Register it in `packages/core/src/handlers/index.ts` (export) and `packages/core/src/registry.ts` (metadata) - -3. Add display info in `packages/core/src/presentation.ts` - -4. Optionally add a custom renderer in `packages/web/src/components/results/renderers/` - -5. Write tests in `packages/core/src/handlers/my-handler.test.ts` - ---- - -## Building - -```bash -npm run build # all packages (core → api → cli → web → static) -npm run build -w @recon-web/web # frontend only -npm run build:static # CF Pages build (web + static) -``` - -### Docker - -```bash -docker build -f packages/api/Dockerfile -t recon-web-api . -docker build -f packages/web/Dockerfile -t recon-web-web . -docker build -f packages/cli/Dockerfile -t recon-web-cli . -``` - -Build context is always the repo root. - ---- - -## Releasing - -See [RELEASING.md](RELEASING.md) for how to create versioned releases with `./scripts/version`. - ---- - -## Database - -SQLite with WAL mode. Two tables: - -- **scans** -`id, url, created_at, handler_count, status, duration_ms` -- **scan_results** -`id, scan_id, handler, result (JSON), duration_ms` - -Indices on `url`, `created_at DESC`, and `scan_id` for performance. - -Path configurable via `DB_PATH` env var (default: `./data/recon-web.db`). - ---- - -## CI/CD - -The project supports both **GitHub Actions** and **GitLab CI** with identical pipeline logic: - -``` -lint + typecheck ──┐ - ├──► build docker images (parallel) ──► trivy scan ──► push -tests ─────────────┘ -audit (deps) ──────┘ -``` - -### Pipeline stages - -| Stage | What it does | Blocks next? | -|-------|-------------|-------------| -| **quality** | Lint, typecheck, tests, dependency audit (Trivy fs scan) | Yes | -| **build** | Docker multi-stage build for api, web, cli (in parallel) | Yes | -| **scan** | Trivy container scan on each image (CRITICAL + HIGH) | Yes | -| **push** | Tag as `:sha` + `:latest` and push to registry | -| - -### How `:latest` stays safe - -Images are first tagged with the commit SHA (`:abc123`). Trivy scans that image. Only if the scan passes, the image is retagged as `:latest` and pushed. If Trivy fails, the pipeline stops -`:latest` on the registry remains pointing to the previous good build. - -### Trigger rules - -| Event | What runs | -|-------|-----------| -| Push to `main`/`master` | Full pipeline: quality → build → scan → push | -| Pull request / Merge request | Quality only: lint, typecheck, tests, audit | - -### Configuration files - -| File | Platform | -|------|----------| -| `.github/workflows/ci.yml` | GitHub Actions | -| `.gitlab-ci.yml` | GitLab CI | - -### Registry - -| Platform | Registry | Image paths | -|----------|----------|-------------| -| GitHub | `ghcr.io` | `ghcr.io/brunoafk/recon-web/api`, `/web`, `/cli` | -| GitLab | `registry.gitlab.com` | `registry.gitlab.com//recon-web/api`, `/web`, `/cli` | - -### Required CI/CD variables - -#### GitHub (Settings → Secrets and variables → Actions) - -`GITHUB_TOKEN` is automatically available -no manual setup needed for GHCR push. - -| Variable | Required | Description | -|----------|----------|-------------| -| `GITHUB_TOKEN` | Auto | Automatically provided, used for GHCR login and push | - -Optional: if you want Trivy results in the GitHub Security tab, ensure the repo has **Security → Code scanning** enabled (free for public repos). - -#### GitLab (Settings → CI/CD → Variables) - -`CI_REGISTRY_*` variables are automatically available -no manual setup needed for GitLab Container Registry. - -| Variable | Required | Description | -|----------|----------|-------------| -| `CI_REGISTRY` | Auto | GitLab Container Registry URL | -| `CI_REGISTRY_USER` | Auto | Registry username (CI job token) | -| `CI_REGISTRY_PASSWORD` | Auto | Registry password (CI job token) | -| `CI_REGISTRY_IMAGE` | Auto | Base image path for this project | - -#### Optional variables (both platforms) - -These can be set as CI secrets if you want handlers to use them during test or scan: - -| Variable | Description | -|----------|-------------| -| `GOOGLE_CLOUD_API_KEY` | PageSpeed + Safe Browsing checks | -| `CLOUDMERSIVE_API_KEY` | Malware scanning | -| `BUILT_WITH_API_KEY` | Feature detection | -| `TRANCO_API_KEY` | Domain ranking | - -### Trivy configuration - -Both pipelines use the same Trivy settings: - -- **Severity:** `CRITICAL,HIGH` -- **Exit code:** `1` (pipeline fails on findings) -- **Ignore unfixed:** `true` (skip vulnerabilities without patches) -- **GitHub:** Results uploaded as SARIF to Security tab -- **GitLab:** Results printed as table in job log diff --git a/docs/RELEASING.md b/docs/RELEASING.md deleted file mode 100644 index 545e8d4..0000000 --- a/docs/RELEASING.md +++ /dev/null @@ -1,265 +0,0 @@ -# Releasing - -How to create a new release of recon-web. - -## Quick start - -```bash -./scripts/version -v 1.0.0 -``` - -This will: -1. Update `version` in all `package.json` files -2. Create a release notes template and open your editor -3. Commit the version bump -4. Create git tag `v1.0.0` -5. Push and create a GitHub release -6. CI builds Docker images tagged `v1.0.0` + `latest` - -## Usage - -```bash -./scripts/version -v [options] -``` - -| Option | Short | Description | -|--------|-------|-------------| -| `--version` | `-v` | Version number (required, e.g. `1.0.0`) | -| `--description` | `-d` | Short description for auto-generated release notes | -| `--platform` | `-p` | `github` (default) or `gitlab` | -| `--file` | `-f` | Path to custom release notes markdown file | -| `--help` | `-h` | Show help | - -## Examples - -```bash -# Basic release -./scripts/version -v 1.0.0 - -# With description (used in auto-generated template) -./scripts/version -v 1.1.0 -d "Add status filters and error codes" - -# Release on GitLab instead of GitHub -./scripts/version -v 1.0.0 -p gitlab - -# Use a pre-written release notes file -./scripts/version -v 2.0.0 -f docs/migration-v2.md -``` - -## Release notes - -The script looks for release notes in this order: - -1. `--file` flag - uses the provided file directly -2. `.releases/.md` - if you prepared notes in advance -3. `.releases/new.md` - renames it to `.md` automatically -4. Creates a template and opens your editor (VS Code, then `$VISUAL`, then `$EDITOR`, then vim) - -To prepare release notes before running the script: - -```bash -# Option A: write notes for a specific version -vim .releases/1.2.0.md - -# Option B: write notes without knowing the version yet -vim .releases/new.md -# The script will rename it when you run ./scripts/version -v 1.2.0 -``` - -### Release notes format - -Release notes live in `.releases/.md`. Use this structure: - -```markdown -# v1.2.0 - -Short summary of what this release is about. - -## New Handlers - -- **Handler Name** — what it does (API key requirement) - -## UI Changes - -- **Feature** — description - -## Bug Fixes - -- **Component** — what was fixed - -## Docker images - -​```bash -docker pull ghcr.io/brunoafk/recon-web/api:v1.2.0 -docker pull ghcr.io/brunoafk/recon-web/web:v1.2.0 -docker pull ghcr.io/brunoafk/recon-web/cli:v1.2.0 -​``` -``` - -## CI Pipeline - -CI runs on every push and pull request. Docker images are only pushed to the registry when you manually create a release. - -### On push to main (or PR) - -``` -push to main - │ - ▼ -lint → test → audit → docker build → trivy scan - │ - ▼ - Images built and scanned - locally — NOT pushed to registry -``` - -Everything is validated but nothing is published. If any step fails, you see it immediately. - -### On release (you create it manually) - -``` -You create release on GitHub/GitLab UI - │ - ▼ -lint → test → audit → docker build → trivy scan → PUSH to registry - │ - ▼ - ghcr.io/brunoafk/recon-web/api:v1.2.0 - ghcr.io/brunoafk/recon-web/web:v1.2.0 - ghcr.io/brunoafk/recon-web/cli:v1.2.0 -``` - -Docker images are pushed **only** when you manually publish a release. - -### Safety guarantee - -| Stage | What it checks | Runs on | -|-------|---------------|---------| -| Lint | ESLint + TypeScript type checking | Always | -| Test | Vitest across all packages | Always | -| Audit | Trivy filesystem scan (CRITICAL + HIGH) | Always | -| Docker Build | All 3 images (api, web, cli) | Always | -| Trivy Scan | Docker image scan (CRITICAL) | Always | -| **Push** | Images pushed to GHCR | **Release only** | - -If **any** stage fails, images are **not** pushed. This prevents broken releases. - -### GitHub Actions - -Triggers in `.github/workflows/ci.yml`: -- `push` to main/master → quality + docker build/scan (no push) -- `pull_request` → quality + docker build/scan (no push) -- `release: [published]` → quality + docker build/scan + **push to GHCR** - -Uses `matrix` strategy to build all 3 images in parallel. - -### GitLab CI - -Triggers in `.gitlab-ci.yml`: -- Every push → quality + docker build/scan (no push) -- Tag matching `v\d+\.\d+\.\d+` → quality + docker build/scan + **push to registry** - -### How to create a release - -```bash -# 1. Write release notes (optional but recommended) -vim .releases/1.2.0.md - -# 2. Commit everything -git add . -git commit -m "release: v1.2.0" -git push origin main - -# 3. Create the release on GitHub UI -# → Go to Releases → Draft a new release -# → Tag: v1.2.0, Title: v1.2.0 -# → Paste or upload .releases/1.2.0.md content -# → Publish release - -# CI automatically: test → build → scan → push images -``` - -Or via CLI: - -```bash -gh release create v1.2.0 --title "v1.2.0" --notes-file .releases/1.2.0.md -``` - -## Manual release (local script) - -If you prefer to create releases locally (e.g., you need to edit notes interactively): - -```bash -./scripts/version -v 1.2.0 -``` - -This combines everything: version bump, editor, commit, tag, push, and release creation. Requires `gh` (GitHub) or `glab` (GitLab) CLI. - -## Pre-flight checks - -The local script validates before making any changes: - -- Working tree must be clean (no uncommitted changes) -- Tag must not already exist -- Repository must have at least one commit -- Release notes file must not be empty - -If any check fails, the script exits with a clear error message and no changes are made. - -## What happens after release - -When you publish a release, CI runs the full pipeline and pushes Docker images: - -```bash -docker pull ghcr.io/brunoafk/recon-web/api:v1.0.0 -docker pull ghcr.io/brunoafk/recon-web/web:v1.0.0 -docker pull ghcr.io/brunoafk/recon-web/cli:v1.0.0 -``` - -Each image is tagged with both the version tag and `:latest`. - -Pushes to `main` without a release run lint, tests, audit, and docker build+scan — but images are **not** pushed to the registry. - -## Re-releasing a version - -If you need to redo a release: - -```bash -# Delete the tag locally and remotely -git tag -d v1.0.0 -git push origin :refs/tags/v1.0.0 - -# Delete the GitHub/GitLab release manually, then re-run -./scripts/version -v 1.0.0 -``` - -## Version numbering - -The project follows [Semantic Versioning](https://semver.org/): - -| Bump | When | Example | -|------|------|---------| -| **Patch** (0.9.0 → 0.9.1) | Bug fixes, test fixes, CI changes | Fix failing tests | -| **Minor** (0.9.0 → 0.10.0) | New handlers, UI features, non-breaking changes | Add SEO handler | -| **Major** (0.9.0 → 1.0.0) | Breaking API changes, database migrations | API v2 | - -## Requirements - -| Tool | Required for | Install | -|------|-------------|---------| -| `git` | Always | - | -| `npm` | Version bump (local script) | Comes with Node.js | -| `gh` | Creating releases from CLI (optional — can use GitHub UI) | https://cli.github.com | -| `glab` | Creating releases from CLI (optional — can use GitLab UI) | https://gitlab.com/gitlab-org/cli | -| `code` / `vim` | Editing release notes locally | - | - -## Checklist - -Before releasing: - -- [ ] All tests pass locally (`npm test`) -- [ ] Typecheck passes (`npm run typecheck`) -- [ ] Build succeeds (`npm run build`) -- [ ] Release notes written in `.releases/.md` -- [ ] Version number follows semver -- [ ] No uncommitted changes diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 0000000..03242e6 --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,69 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; + +export default defineConfig({ + site: 'https://brunoafk.github.io', + base: '/recon-web', + integrations: [ + starlight({ + title: 'recon-web', + description: 'Open-source website reconnaissance and security analysis tool.', + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/BrunoAFK/recon-web' }, + ], + editLink: { + baseUrl: 'https://github.com/BrunoAFK/recon-web/edit/main/docs/', + }, + customCss: [], + sidebar: [ + { + label: 'Getting Started', + items: [ + { label: 'Quick Start', slug: 'getting-started/quick-start' }, + { label: 'Configuration', slug: 'getting-started/configuration' }, + { label: 'Your First Scan', slug: 'getting-started/first-scan' }, + ], + }, + { + label: 'Guides', + items: [ + { label: 'Web UI', slug: 'guides/web-ui' }, + { label: 'REST API', slug: 'guides/rest-api' }, + { label: 'CLI', slug: 'guides/cli' }, + { label: 'Scheduled Scans', slug: 'guides/scheduled-scans' }, + { label: 'Notifications', slug: 'guides/notifications' }, + ], + }, + { + label: 'Checks Reference', + items: [ + { label: 'Overview', slug: 'checks/overview' }, + { label: 'Security', slug: 'checks/security' }, + { label: 'DNS', slug: 'checks/dns' }, + { label: 'Network', slug: 'checks/network' }, + { label: 'Content', slug: 'checks/content' }, + { label: 'Meta', slug: 'checks/meta' }, + { label: 'Performance', slug: 'checks/performance' }, + ], + }, + { + label: 'Deployment', + items: [ + { label: 'Docker (Local Build)', slug: 'deployment/docker-local' }, + { label: 'Docker (Pre-built Images)', slug: 'deployment/docker-remote' }, + { label: 'Kubernetes (Helm)', slug: 'deployment/kubernetes' }, + { label: 'Standalone (Node.js)', slug: 'deployment/standalone' }, + ], + }, + { + label: 'Development', + items: [ + { label: 'Architecture', slug: 'development/architecture' }, + { label: 'Contributing', slug: 'development/contributing' }, + { label: 'Adding a Check', slug: 'development/adding-handlers' }, + ], + }, + ], + }), + ], +}); diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..1feadb1 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,6226 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@astrojs/starlight": "^0.38.2", + "astro": "^6.1.3", + "sharp": "^0.34.5" + } + }, + "node_modules/@astrojs/compiler": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-3.0.1.tgz", + "integrity": "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.8.0.tgz", + "integrity": "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.3" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.0.tgz", + "integrity": "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.8.0", + "@astrojs/prism": "4.0.1", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.3.tgz", + "integrity": "sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "7.1.0", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", + "integrity": "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.2.tgz", + "integrity": "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.38.2.tgz", + "integrity": "sha512-7AsrvG4EsXUmJT5uqiXJN4oZqKaY0wc/Ip7C6/zGnShHRVoTAA4jxeYIZ3wqbqA6zv4cnp9qk31vB2m2dUcmfg==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "^7.0.0", + "@astrojs/mdx": "^5.0.0", + "@astrojs/sitemap": "^3.7.1", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.41.6", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.1", + "hast-util-select": "^6.0.2", + "hast-util-to-string": "^3.0.0", + "hastscript": "^9.0.0", + "i18next": "^23.11.5", + "js-yaml": "^4.1.0", + "klona": "^2.0.6", + "magic-string": "^0.30.17", + "mdast-util-directive": "^3.0.0", + "mdast-util-to-markdown": "^2.1.0", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.3.0", + "rehype": "^13.0.1", + "rehype-format": "^5.0.0", + "remark-directive": "^3.0.0", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.2" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.2.0.tgz", + "integrity": "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.2.0.tgz", + "integrity": "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.2.0", + "fast-string-width": "^1.1.0", + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.41.7.tgz", + "integrity": "sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.41.7.tgz", + "integrity": "sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.7" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.41.7.tgz", + "integrity": "sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.7", + "shiki": "^3.2.2" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@expressive-code/plugin-shiki/node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.41.7.tgz", + "integrity": "sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.7" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", + "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", + "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.4.0.tgz", + "integrity": "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", + "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", + "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", + "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", + "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "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/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "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/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "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/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.3.tgz", + "integrity": "sha512-FUKbBYOdYYrRNZwDd9I5CVSfR6Nj9aZeNzcjcvh1FgHwR0uXawkYFR3HiGxmdmAB2m8fs0iIkDdsiUfwGeO8qA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@astrojs/compiler": "^3.0.1", + "@astrojs/internal-helpers": "0.8.0", + "@astrojs/markdown-remark": "7.1.0", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.6.3", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^7.3.1", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.41.7.tgz", + "integrity": "sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.41.7" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "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/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "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/defu": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.6.tgz", + "integrity": "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==", + "license": "MIT" + }, + "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/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "license": "MIT" + }, + "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/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.41.7.tgz", + "integrity": "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.7", + "@expressive-code/plugin-frames": "^0.41.7", + "@expressive-code/plugin-shiki": "^0.41.7", + "@expressive-code/plugin-text-markers": "^0.41.7" + } + }, + "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/fast-string-truncated-width": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz", + "integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz", + "integrity": "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^1.2.0" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz", + "integrity": "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^1.1.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.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", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.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/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "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/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "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-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.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-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "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-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.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-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.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-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.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-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "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/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.1.tgz", + "integrity": "sha512-yQS1vV2V7Q14MQrgD8jMNY5owPuGgVHVdSK8NqmKpOVajnjbaeMa6uLOzTALPtvJ7Vo4bw0BGsw7qfUT8z24Ig==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", + "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.4.0", + "@pagefind/darwin-x64": "1.4.0", + "@pagefind/freebsd-x64": "1.4.0", + "@pagefind/linux-arm64": "1.4.0", + "@pagefind/linux-x64": "1.4.0", + "@pagefind/windows-x64": "1.4.0" + } + }, + "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/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-expressive-code": { + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.41.7.tgz", + "integrity": "sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.41.7" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.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-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "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/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "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/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", + "integrity": "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "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/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "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/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^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-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.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-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.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-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.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/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.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": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "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/docs/package.json b/docs/package.json new file mode 100644 index 0000000..ef9de0b --- /dev/null +++ b/docs/package.json @@ -0,0 +1,16 @@ +{ + "name": "@recon-web/docs", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/starlight": "^0.38.2", + "astro": "^6.1.3", + "sharp": "^0.34.5" + } +} diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 0000000..6a7b7a0 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/docs/src/content/docs/checks/content.mdx b/docs/src/content/docs/checks/content.mdx new file mode 100644 index 0000000..29c8676 --- /dev/null +++ b/docs/src/content/docs/checks/content.mdx @@ -0,0 +1,115 @@ +--- +title: Content Checks +description: "5 content checks: robots.txt, sitemap, social tags, links, SEO audit" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 5 content checks that analyze how the site communicates with search engines, social platforms, and users through its on-page structure and metadata. + +--- + +## robots.txt + +**Handler:** `robots-txt` + +Fetches and parses the `/robots.txt` file, which tells search engine crawlers which pages to index and which to skip. Extracts `User-Agent` rules, `Disallow`/`Allow` directives, `Crawl-delay` settings, and sitemap references. + +**Why it matters:** A misconfigured robots.txt can accidentally hide an entire site from search engines or, conversely, expose paths that should remain private. Attackers also read robots.txt to discover admin panels and sensitive directories. + +**Good result:** File present, not blocking important pages, and not inadvertently revealing sensitive paths. A `Sitemap` directive points to the XML sitemap. + +**Bad result:** `Disallow: /` blocking all crawlers (sometimes set during development and never removed), or listing paths like `/admin`, `/wp-login`, `/api` that help attackers find entry points. Missing file means crawlers index everything, including staging content or internal API docs. + + + +--- + +## Sitemap + +**Handler:** `sitemap` + +Finds and parses the XML sitemap. First checks for a sitemap reference in robots.txt, then tries common paths (`/sitemap.xml`, `/sitemap_index.xml`). Counts the number of URLs and extracts the sitemap structure. + +**Why it matters:** Sitemaps help search engines discover all pages on your site and understand its hierarchy. Without one, search engines rely solely on link crawling, which is slower and less complete. + +**Good result:** Sitemap found, well-formed XML, and contains current URLs. Sitemap index present for large sites with multiple sitemap files. + +**Bad result:** No sitemap found (search engines may miss pages, especially on large or JavaScript-rendered sites). Outdated sitemap listing removed pages causes crawl waste and 404 errors in search results. + +--- + +## Social Tags + +**Handler:** `social-tags` + +Extracts social sharing metadata from the page HTML: + +| Tag type | Platform | Key properties | +|---|---|---| +| OpenGraph (`og:`) | Facebook, LinkedIn, Discord | `og:title`, `og:description`, `og:image`, `og:url` | +| Twitter Cards (`twitter:`) | X (Twitter) | `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` | +| Standard meta | All platforms (fallback) | ``, `meta description` | + +**Why it matters:** These tags control how the page appears when shared on social media. Missing tags mean platforms generate their own (often poor) preview, which reduces click-through rates. + +**Good result:** Complete OpenGraph tags including `og:image` (with a large, high-quality image), `og:title`, `og:description`, and matching Twitter Card tags. Meta description present and concise (under 160 characters). + +**Bad result:** Missing `og:image` (shared links show no preview image, dramatically lowering engagement), wrong `og:title` or `og:description` (misleading previews), or no meta description (search engines generate a random snippet). + +<Aside type="tip"> + Use the `summary_large_image` Twitter card type for 2-3x more engagement than `summary`. Test your tags with the [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) and [Twitter Card Validator](https://cards-dev.twitter.com/validator). +</Aside> + +--- + +## Linked Pages + +**Handler:** `linked-pages` + +Analyzes all links (`<a href="...">`) on the page. Counts internal links (same domain), external links (different domains), and identifies which external domains are linked to. + +**Why it matters:** Link structure affects both SEO and security. Too many external links dilute page authority and can look spammy. Links to compromised or malicious sites can get your own domain penalized by search engines. + +**Good result:** A healthy mix of internal links (good for navigation and SEO) with a reasonable number of external links to reputable domains. No broken links. + +**Bad result:** Very high external link ratio (may indicate spam or a compromised page with injected links), links to known malicious domains, or an excessive total link count that suggests automated content. + +<Aside type="note"> + This check analyzes the initial HTML response. Links injected by JavaScript after page load may not be captured. +</Aside> + +--- + +## SEO Audit + +**Handler:** `seo` + +Performs a comprehensive on-page SEO audit by fetching the HTML and analyzing key ranking factors. Returns a score from 0 to 100 based on the issues found. + +**Factors checked:** + +| Factor | What it checks | +|---|---| +| Title tag | Present, correct length (50-60 chars) | +| Meta description | Present, correct length (120-160 chars) | +| Headings | Exactly one H1, logical heading hierarchy (H1 > H2 > H3) | +| Images | Alt text coverage on all `<img>` elements | +| Canonical URL | `<link rel="canonical">` present and correct | +| Viewport meta | `<meta name="viewport">` present for mobile support | +| Structured data | JSON-LD schema markup (enables rich snippets) | +| Hreflang | Language/region tags for multilingual sites | +| Meta robots | No unintentional `noindex` or `nofollow` | +| Content quality | Word count, text-to-HTML ratio | + +**Why it matters:** On-page SEO directly affects search engine rankings. Missing titles, duplicate H1s, and images without alt text are among the most common issues that prevent pages from ranking well. + +**Good result:** Score of 80 or above. Title and meta description present with correct length, single H1, all images have alt text, canonical URL set, structured data present. + +**Bad result:** Score below 50. Typical issues include missing title tag, no H1 (or multiple H1s), images without alt text (accessibility and SEO issue), missing structured data (no rich snippets in search results), or thin content (fewer than 300 words). + +<Aside type="caution"> + A page with `noindex` in the meta robots tag or robots.txt will not appear in search results regardless of how well-optimized the content is. Make sure this directive is intentional. +</Aside> diff --git a/docs/src/content/docs/checks/dns.mdx b/docs/src/content/docs/checks/dns.mdx new file mode 100644 index 0000000..734a5c7 --- /dev/null +++ b/docs/src/content/docs/checks/dns.mdx @@ -0,0 +1,107 @@ +--- +title: DNS Checks +description: "5 DNS checks: records, DNSSEC, provider, TXT, mail configuration" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 5 DNS checks that examine how the domain's DNS infrastructure is configured, whether it is cryptographically secured, and how email routing is set up. + +--- + +## DNS Records + +**Handler:** `dns` + +Resolves all standard DNS record types for the domain: + +| Record type | Purpose | +|---|---| +| A | IPv4 address | +| AAAA | IPv6 address | +| MX | Mail server | +| NS | Authoritative nameservers | +| TXT | Verification, SPF, DKIM | +| CNAME | Alias to another domain | +| SOA | Start of Authority (zone metadata) | +| SRV | Service discovery | +| PTR | Reverse DNS | + +**Why it matters:** DNS records define how the domain's infrastructure works. Misconfigured MX records cause email delivery failures. Missing AAAA records mean no IPv6 support. TXT records reveal the email authentication setup. + +**Good result:** All expected record types resolve correctly. A and/or AAAA records point to the right servers. MX records are present if the domain handles email. NS records point to responsive nameservers. + +**Bad result:** Missing critical records. For example, missing MX records when the domain should receive email, or CNAME records that point to decommissioned hosts. + +--- + +## DNS Provider + +**Handler:** `dns-server` + +Identifies the authoritative DNS server for the domain and checks whether it supports DNS over HTTPS (DoH). DoH encrypts DNS queries, preventing ISPs and network observers from seeing which sites users visit. + +**Why it matters:** The DNS provider is a single point of failure for your entire domain. If your nameservers go down, your website, email, and every other service become unreachable. Knowing the provider also helps assess infrastructure resilience. + +**Good result:** Authoritative DNS resolves correctly with DoH support available. Using a reputable DNS provider (Cloudflare, AWS Route 53, Google Cloud DNS, etc.) with anycast routing. + +**Bad result:** DNS server does not support DoH (user queries are visible to network observers), or the provider is a single-location server with no redundancy. + +<Aside type="note"> + This check is informational. Not all DNS providers support DoH yet, and its absence is not a vulnerability — but it does mean DNS queries are transmitted in plaintext. +</Aside> + +--- + +## DNSSEC + +**Handler:** `dnssec` + +Checks DNSSEC (DNS Security Extensions) configuration by verifying the presence and validity of DNSKEY, DS, and RRSIG records. DNSSEC cryptographically signs DNS responses so clients can verify they have not been tampered with. + +**Why it matters:** Without DNSSEC, attackers can perform DNS cache poisoning — intercepting DNS responses and redirecting users to malicious servers. This is especially dangerous for banking, government, and healthcare sites. + +**Good result:** DNSSEC is fully configured. DNSKEY, DS, and RRSIG records are present with a valid signature chain. + +**Bad result:** DNSSEC is not configured (domain is vulnerable to DNS spoofing), or the chain is broken (misconfigured DS records can make the domain completely unreachable for validating resolvers). + +<Aside type="caution"> + A broken DNSSEC chain is worse than no DNSSEC at all. If DS records are published but the zone is not signed correctly, validating resolvers like Cloudflare and Google will refuse to resolve the domain entirely. +</Aside> + +--- + +## TXT Records + +**Handler:** `txt-records` + +Retrieves and parses all TXT records for the domain. These commonly contain: + +- **SPF policies** — which servers are allowed to send email for the domain +- **DKIM selectors** — public keys for email signature verification +- **DMARC policies** — instructions for handling emails that fail SPF/DKIM +- **Domain verification tokens** — Google, Microsoft, Facebook, and other service verifications + +**Why it matters:** TXT records are the foundation of email authentication. An SPF record without a strict `-all` qualifier allows anyone to send email pretending to be your domain. Verification tokens also reveal which third-party services the domain uses. + +**Good result:** SPF record present with a strict policy (`-all`). DKIM and DMARC records configured. No unnecessary verification tokens left from decommissioned services. + +**Bad result:** Missing SPF record (anyone can spoof email from this domain), overly permissive SPF with `+all` or `~all`, or no DMARC policy. + +--- + +## Mail Configuration + +**Handler:** `mail-config` + +Analyzes MX records to determine the email provider and configuration. Identifies whether the domain uses a hosted email service (Google Workspace, Microsoft 365, Zoho, etc.) or self-hosted mail servers. Also checks for backup MX records. + +**Why it matters:** Email is critical business infrastructure. Misconfigured MX records cause bounced emails. Missing backup MX records mean a single mail server failure stops all incoming email. + +**Good result:** Valid MX records pointing to a reputable email provider, with backup MX configured. SPF and DMARC policies align with the mail provider's servers. + +**Bad result:** No MX records (the domain cannot receive email, which may be intentional or a misconfiguration), MX records pointing to non-responsive servers, or SPF/DMARC policies that do not match the configured mail provider. + +<Aside type="tip"> + Knowing the mail provider is useful for phishing assessment. A bank or enterprise using a free email provider (Gmail, Outlook.com) instead of a business-grade service is a red flag. +</Aside> diff --git a/docs/src/content/docs/checks/meta.mdx b/docs/src/content/docs/checks/meta.mdx new file mode 100644 index 0000000..048b1a0 --- /dev/null +++ b/docs/src/content/docs/checks/meta.mdx @@ -0,0 +1,137 @@ +--- +title: Meta Checks +description: "7 meta checks: WHOIS, archives, ranking, tech stack, screenshot" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 7 meta checks that gather contextual information about the domain — its registration history, popularity, technology stack, and visual appearance. + +--- + +## WHOIS + +**Handler:** `whois` + +Performs a WHOIS lookup to retrieve domain registration details: registrar, creation date, expiration date, nameservers, and (if not privacy-protected) registrant contact information. + +**Why it matters:** WHOIS data reveals how long a domain has existed, when it expires, and who manages it. This is essential context for trust assessment and due diligence. + +**Good result:** Domain registered with a reputable registrar, well before its current use, with an expiration date far in the future. Nameservers match the expected DNS provider. + +**Bad result:** Domain expiring soon (risk of being snatched by squatters if renewal is missed), or recently registered (domains under 6 months old are statistically more likely to be used for phishing or spam). + +<Aside type="tip"> + Set up domain expiry alerts with your registrar. Losing a domain because of a missed renewal is one of the most preventable and damaging mistakes. +</Aside> + +--- + +## Archive History + +**Handler:** `archives` + +Queries the Wayback Machine (Internet Archive) for historical snapshots of the site. Returns the total number of times the site has been archived and the date range of captures. + +**Why it matters:** Archive history provides context about a site's longevity and evolution. A site with no archive history might be brand new, which is a potential red flag for scam or phishing sites. + +**Good result:** Multiple snapshots spanning a long time period, consistent with the domain's registration age. Indicates a legitimate, established website. + +**Bad result:** No snapshots (site is very new or has blocked the Wayback Machine), or a long gap in captures followed by a sudden restart (may indicate the domain changed hands). + +<Aside type="note"> + This check is informational. Archived versions can reveal previous content, owners, or security issues that were fixed. Legal disputes sometimes use Wayback Machine captures as evidence. +</Aside> + +--- + +## Domain Ranking + +**Handler:** `rank` + +Checks the domain's position in the Tranco top-1M list, a research-grade ranking based on Chrome User Experience Report data and Cloudflare DNS query volume. A lower rank number means higher popularity. + +**Why it matters:** Domain ranking provides an objective popularity signal. High-ranking domains are well-known and widely visited, which is useful context for risk assessment and competitive analysis. + +**Good result:** Domain appears in the ranking. A position in the top 100K indicates a well-known site. + +**Bad result:** Domain is not ranked. This simply means the site is outside the top 1 million — it is not necessarily a problem for smaller or niche sites. + +--- + +## Legacy Ranking + +**Handler:** `legacy-rank` + +Checks the domain's position in the Cisco Umbrella (formerly OpenDNS) popularity ranking. This provides an alternative popularity signal based on DNS query volume across enterprise networks. + +**Why it matters:** Umbrella ranking complements Tranco by reflecting enterprise DNS traffic patterns. Comparing both rankings reveals whether a site is more popular with businesses or consumers. + +**Good result:** Domain appears in the Umbrella ranking, indicating steady DNS query volume from enterprise networks. + +**Bad result:** Domain is not ranked. As with Tranco, this is not necessarily negative for smaller sites. + +<Aside type="note"> + Enterprise-focused domains may rank higher on Umbrella than on consumer-oriented rankings. Newly malicious domains rarely appear on the list, so absence is a weak (but not reliable) trust signal. +</Aside> + +--- + +## Features + +**Handler:** `features` + +Detects site features and technologies using the BuiltWith API. Identifies CMS platforms, frameworks, analytics tools, advertising networks, CDNs, payment processors, and other technology integrations. + +**Why it matters:** Understanding the technology stack behind a site is valuable for security auditing (known vulnerability patterns), competitive analysis, and technology discovery. + +**Good result:** Technologies detected and identified. Results typically include CMS, JavaScript frameworks, analytics tools, and hosting infrastructure. + +**Bad result:** No results (API key missing or site uses no detectable technologies). + +<Aside type="caution"> + Requires the `BUILT_WITH_API_KEY` environment variable. Without the key, this check is skipped. +</Aside> + +--- + +## Tech Stack + +**Handler:** `tech-stack` + +Detects technologies from HTTP headers, meta tags, HTML patterns, and JavaScript libraries without any API key. Identifies: + +- **Frameworks:** React, Vue, Angular, Next.js, Nuxt, Svelte +- **Server software:** nginx, Apache, IIS, Express, Caddy +- **CMS platforms:** WordPress, Drupal, Joomla, Ghost, Squarespace +- **CDNs:** Cloudflare, Fastly, Akamai, AWS CloudFront +- **Analytics:** Google Analytics, Plausible, Matomo, Hotjar +- **Other:** jQuery versions, font services, tag managers + +**Why it matters:** Knowing the tech stack narrows the attack surface for security testing. Outdated jQuery versions have known XSS vulnerabilities. Identifying the server-side framework reveals which common vulnerability patterns to check. + +**Good result:** Technologies detected and listed. All detected versions are current and actively maintained. + +**Bad result:** Outdated library versions with known vulnerabilities (e.g., jQuery < 3.5.0), or server headers revealing exact software versions that aid targeted attacks. + +<Aside type="note"> + No API key required. This check uses only publicly visible data from HTTP responses and HTML source. For deeper technology detection, see the [Features](#features) check which uses the BuiltWith API. +</Aside> + +--- + +## Screenshot + +**Handler:** `screenshot` + +Captures a full-viewport screenshot of the website using a headless Chromium browser. The screenshot shows how the site actually renders, including JavaScript-generated content. + +**Why it matters:** A screenshot provides a visual record of the site at the time of the scan. This is useful for detecting defacements, comparing changes across scans, identifying cloaking (showing different content to bots versus users), and archival documentation. + +**Good result:** Screenshot captured successfully, showing the site as expected. + +**Bad result:** Screenshot failed (Chromium not available), or the rendered page looks different from what was expected (potential compromise, broken layout, or cloaking). + +<Aside type="note"> + Requires a Chromium or Chrome binary. Set the `CHROME_PATH` environment variable if the binary is not in a standard location. When running in Docker, Chromium is included in the image. +</Aside> diff --git a/docs/src/content/docs/checks/network.mdx b/docs/src/content/docs/checks/network.mdx new file mode 100644 index 0000000..081b94f --- /dev/null +++ b/docs/src/content/docs/checks/network.mdx @@ -0,0 +1,155 @@ +--- +title: Network Checks +description: "8 network checks: HTTP status, headers, cookies, ports, traceroute, and more" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 8 network checks that examine how the server responds to requests, what it exposes, and how traffic flows across the internet to reach it. + +--- + +## HTTP Status + +**Handler:** `status` + +Checks the HTTP status code returned by the target URL and measures response time, including DNS lookup, TCP connect, and time to first byte (TTFB). + +**Why it matters:** The status code tells you whether the site is accessible. Response time directly affects user experience — pages that take more than 2 seconds to respond see significantly higher bounce rates. + +**Good result:** Status 200 (OK) with a fast response time (under 500ms TTFB). + +**Bad result:** 5xx errors (server failure), 4xx errors (broken URL), or response times consistently above 2 seconds. + +--- + +## HTTP Headers + +**Handler:** `headers` + +Fetches all HTTP response headers from the target URL. Headers reveal server software, caching configuration, content encoding, CORS policy, and custom application headers. + +**Why it matters:** Response headers are a window into the server's configuration. They can leak software versions that help attackers target specific vulnerabilities, or they may be missing caching directives that hurt performance. + +**Good result:** No unnecessary version information exposed. Proper caching headers (`Cache-Control`, `ETag`) set. Content encoding (gzip/brotli) enabled. + +**Bad result:** `Server` header revealing exact software versions (e.g., `Apache/2.4.41`), `X-Powered-By` leaking framework versions (e.g., `PHP/7.2`), or missing cache headers causing poor performance. + +<Aside type="tip"> + Review the headers for security-related entries as well, but see the [HTTP Security Headers](/recon-web/checks/security/#http-security-headers) check for a dedicated analysis of CSP, X-Frame-Options, and other security headers. +</Aside> + +--- + +## Cookies + +**Handler:** `cookies` + +Extracts all cookies set by the HTTP response and analyzes their security attributes: + +| Attribute | Purpose | +|---|---| +| `Secure` | Cookie only sent over HTTPS | +| `HttpOnly` | Cookie inaccessible to JavaScript | +| `SameSite` | Controls cross-site request behavior | +| `Expires` / `Max-Age` | Cookie lifetime | +| `Path` / `Domain` | Scope of the cookie | + +**Why it matters:** Cookies often carry session tokens and authentication data. Missing security flags expose users to session hijacking, cross-site request forgery (CSRF), and cross-site scripting (XSS) attacks. + +**Good result:** All session cookies have `Secure`, `HttpOnly`, and `SameSite=Strict` (or `Lax`) flags set. + +**Bad result:** Session cookies missing the `Secure` flag (sent over plain HTTP on public Wi-Fi), missing `HttpOnly` (XSS attacks can steal sessions via JavaScript), or missing `SameSite` (enables CSRF attacks and cross-site tracking). + +--- + +## Redirects + +**Handler:** `redirects` + +Follows the complete HTTP redirect chain from the initial URL to the final destination. Records each hop with its status code (301, 302, 307, 308) and target URL. + +**Why it matters:** Redirect chains affect both performance and SEO. Long chains slow page load and waste search engine crawl budget. Using the wrong redirect type (302 temporary instead of 301 permanent) prevents link equity from passing through. + +**Good result:** A clean, single-hop redirect from HTTP to HTTPS (e.g., `http://example.com` -> 301 -> `https://example.com`). No unnecessary intermediate hops. + +**Bad result:** Long redirect chains (3+ hops), redirect loops (site becomes completely inaccessible), or 302 redirects used where 301 is appropriate (hurts SEO rankings). + +--- + +## Open Ports + +**Handler:** `ports` + +Scans 33 common TCP ports on the target host, including: + +| Port | Service | Risk if exposed | +|---|---|---| +| 21 | FTP | Unencrypted file transfer, brute-force target | +| 22 | SSH | Brute-force target if password auth is enabled | +| 80 | HTTP | Expected for web servers | +| 443 | HTTPS | Expected for web servers | +| 3306 | MySQL | Direct database access from the internet | +| 5432 | PostgreSQL | Direct database access from the internet | +| 3389 | RDP | Remote desktop, common ransomware vector | +| 8080 | HTTP alt | Development/proxy servers, often misconfigured | + +**Why it matters:** Every open port is a potential entry point for attackers. Database and remote administration ports should never be directly accessible from the public internet. + +**Good result:** Only expected ports open (typically 80 and 443 for a web server). All management and database ports are closed or firewalled. + +**Bad result:** Database ports (3306, 5432) open to the internet, SSH with password authentication, or unexpected ports that may indicate a compromised server running unauthorized services. + +<Aside type="caution"> + Open database ports are a critical security risk. If MySQL (3306) or PostgreSQL (5432) is reachable from the internet, restrict access immediately using firewall rules or security groups. +</Aside> + +--- + +## IP Address + +**Handler:** `get-ip` + +Resolves the domain to its IP address(es). Shows the actual server IP, which may reveal the hosting provider, whether a CDN or proxy is in use, and if the site uses load balancing. + +**Why it matters:** Knowing the IP helps determine the hosting provider and infrastructure setup. IPs in Cloudflare or AWS ranges indicate CDN/proxy usage, meaning the real server IP is hidden. Multiple IPs suggest load balancing or geographic DNS routing. + +**Good result:** Domain resolves to one or more valid IP addresses. If a CDN is in use, the resolved IP belongs to the CDN provider (real server IP is protected). + +**Bad result:** Domain does not resolve (DNS failure), or the IP directly exposes the origin server when it should be behind a CDN. + +<Aside type="note"> + This check is informational. The resolved IP is used as input for other checks such as Server Location, AbuseIPDB, and Traceroute. +</Aside> + +--- + +## Server Location + +**Handler:** `server-location` + +Determines the physical location of the server by performing a GeoIP lookup on the resolved IP address. Returns city, region, country, timezone, latitude/longitude, ISP, and autonomous system number (ASN). Results are displayed on an interactive map. + +**Why it matters:** Server location affects latency (a site serving European users from a US data center adds 100-200ms per request) and may have compliance implications. GDPR, for example, has specific requirements about where data is processed and stored. + +**Good result:** Server located in a region close to the target audience. Hosting provider is reputable with good uptime and DDoS protection. + +**Bad result:** Server in an unexpected country (potential compliance issue), or ISP/ASN associated with known bulletproof hosting providers. + +--- + +## Traceroute + +**Handler:** `trace-route` + +Traces the network path from the recon-web server to the target host, recording each router hop along the way. Shows latency at each hop and identifies the networks traffic passes through. + +**Why it matters:** Traceroute helps diagnose slow connections by pinpointing which network hop introduces latency. It also reveals the geographic path of traffic, which may be relevant for data sovereignty requirements. + +**Good result:** Direct path with low latency at each hop. No packet loss at intermediate routers. Traffic stays within expected geographic regions. + +**Bad result:** High latency at a specific hop (network bottleneck), packet loss at intermediate hops (congestion or hardware issues), or traffic routing through unexpected countries. + +<Aside type="note"> + Traceroute results depend on where the recon-web instance is running. Results will differ if you run from a local machine versus a cloud server in another region. +</Aside> diff --git a/docs/src/content/docs/checks/overview.mdx b/docs/src/content/docs/checks/overview.mdx new file mode 100644 index 0000000..496ac32 --- /dev/null +++ b/docs/src/content/docs/checks/overview.mdx @@ -0,0 +1,81 @@ +--- +title: Checks Overview +description: All 39 checks that recon-web runs against a target URL +--- + +import { Aside } from '@astrojs/starlight/components'; + +Every time you scan a URL, recon-web runs **39 checks** across **6 categories**. Each check runs in parallel so the entire scan typically completes in under 30 seconds. + +Some checks depend on optional API keys. Without any keys configured, 34 checks run out of the box. See [Configuration](/recon-web/getting-started/configuration/) for details on adding API keys. + +<Aside type="tip"> + You can run a subset of checks from the CLI with the `--only` and `--skip` flags, or from the API by specifying which handlers to include. +</Aside> + +## All checks by category + +| # | Check | Category | Description | +|---|---|---|---| +| 1 | [SSL Certificate](/recon-web/checks/security/#ssl-certificate) | Security | Reads the TLS certificate — issuer, expiry, trust chain | +| 2 | [SSL Grade](/recon-web/checks/security/#ssl-grade) | Security | Letter grade (A+ to F) from Qualys SSL Labs | +| 3 | [TLS Configuration](/recon-web/checks/security/#tls-configuration) | Security | Protocol version, cipher suites, configuration quality | +| 4 | [HSTS](/recon-web/checks/security/#hsts) | Security | Strict-Transport-Security header and preload status | +| 5 | [HTTP Security Headers](/recon-web/checks/security/#http-security-headers) | Security | Scores CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy | +| 6 | [Firewall (WAF)](/recon-web/checks/security/#firewall-waf) | Security | Detects web application firewalls (Cloudflare, AWS WAF, Akamai, etc.) | +| 7 | [security.txt](/recon-web/checks/security/#securitytxt) | Security | Checks for a vulnerability disclosure policy file | +| 8 | [Threats](/recon-web/checks/security/#threats) | Security | Cross-references against Google Safe Browsing, URLHaus, PhishTank, Cloudmersive | +| 9 | [Block Lists](/recon-web/checks/security/#block-lists) | Security | Checks 17 DNS-based block lists | +| 10 | [VirusTotal](/recon-web/checks/security/#virustotal) | Security | Scans against 70+ antivirus engines | +| 11 | [AbuseIPDB](/recon-web/checks/security/#abuseipdb) | Security | IP reputation and abuse confidence scoring | +| 12 | [WordPress](/recon-web/checks/security/#wordpress) | Security | Detects WordPress, enumerates plugins/themes, finds misconfigurations | +| 13 | [DNS Records](/recon-web/checks/dns/#dns-records) | DNS | Resolves A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR | +| 14 | [DNS Provider](/recon-web/checks/dns/#dns-provider) | DNS | Identifies authoritative nameserver and DoH support | +| 15 | [DNSSEC](/recon-web/checks/dns/#dnssec) | DNS | Validates DNSKEY, DS, and RRSIG records | +| 16 | [TXT Records](/recon-web/checks/dns/#txt-records) | DNS | Parses SPF, DKIM, domain verification entries | +| 17 | [Mail Configuration](/recon-web/checks/dns/#mail-configuration) | DNS | MX records, mail provider identification, SPF/DMARC analysis | +| 18 | [HTTP Status](/recon-web/checks/network/#http-status) | Network | Status code and response time | +| 19 | [HTTP Headers](/recon-web/checks/network/#http-headers) | Network | Full response header dump | +| 20 | [Cookies](/recon-web/checks/network/#cookies) | Network | Cookie names and security flags | +| 21 | [Redirects](/recon-web/checks/network/#redirects) | Network | Full redirect chain with status codes | +| 22 | [Open Ports](/recon-web/checks/network/#open-ports) | Network | Scans 33 common TCP ports | +| 23 | [IP Address](/recon-web/checks/network/#ip-address) | Network | Resolves domain to IP | +| 24 | [Server Location](/recon-web/checks/network/#server-location) | Network | GeoIP lookup with map display | +| 25 | [Traceroute](/recon-web/checks/network/#traceroute) | Network | Network hops to target | +| 26 | [robots.txt](/recon-web/checks/content/#robotstxt) | Content | Parses crawler directives | +| 27 | [Sitemap](/recon-web/checks/content/#sitemap) | Content | Finds and parses XML sitemap | +| 28 | [Social Tags](/recon-web/checks/content/#social-tags) | Content | OpenGraph, Twitter Cards, meta description | +| 29 | [Linked Pages](/recon-web/checks/content/#linked-pages) | Content | Internal and external link analysis | +| 30 | [SEO Audit](/recon-web/checks/content/#seo-audit) | Content | On-page SEO scoring (0-100) | +| 31 | [WHOIS](/recon-web/checks/meta/#whois) | Meta | Domain registration details | +| 32 | [Archive History](/recon-web/checks/meta/#archive-history) | Meta | Wayback Machine snapshot count and date range | +| 33 | [Domain Ranking](/recon-web/checks/meta/#domain-ranking) | Meta | Tranco top-1M popularity ranking | +| 34 | [Legacy Ranking](/recon-web/checks/meta/#legacy-ranking) | Meta | Cisco Umbrella popularity ranking | +| 35 | [Features](/recon-web/checks/meta/#features) | Meta | BuiltWith feature and technology detection | +| 36 | [Tech Stack](/recon-web/checks/meta/#tech-stack) | Meta | Framework, CMS, CDN, analytics detection from HTML/headers | +| 37 | [Screenshot](/recon-web/checks/meta/#screenshot) | Meta | Visual capture of the rendered page | +| 38 | [Carbon Footprint](/recon-web/checks/performance/#carbon-footprint) | Performance | Page weight, CO2 estimate, green hosting check | +| 39 | [PageSpeed](/recon-web/checks/performance/#pagespeed) | Performance | Google Lighthouse performance, accessibility, best practices, SEO scores | + +## Category breakdown + +| Category | Checks | Focus area | +|---|---|---| +| [Security](/recon-web/checks/security/) | 12 | SSL/TLS, headers, WAF, threat intelligence, WordPress | +| [DNS](/recon-web/checks/dns/) | 5 | Records, DNSSEC, provider, email configuration | +| [Network](/recon-web/checks/network/) | 8 | Status, headers, cookies, ports, traceroute, geolocation | +| [Content](/recon-web/checks/content/) | 5 | robots.txt, sitemap, social tags, links, SEO | +| [Meta](/recon-web/checks/meta/) | 7 | WHOIS, archives, ranking, tech stack, screenshot | +| [Performance](/recon-web/checks/performance/) | 2 | Carbon footprint, Lighthouse audits | + +## Checks requiring API keys + +Five checks require optional API keys to function. Without the key, the check is skipped gracefully. + +| Check | Environment variable | Free tier | +|---|---|---| +| VirusTotal | `VIRUSTOTAL_API_KEY` | 500 requests/day | +| AbuseIPDB | `ABUSEIPDB_API_KEY` | 1,000 checks/day | +| PageSpeed | `GOOGLE_CLOUD_API_KEY` | Generous free quota | +| Features | `BUILT_WITH_API_KEY` | Limited free tier | +| Threats (partial) | `CLOUDMERSIVE_API_KEY` | Limited free tier | diff --git a/docs/src/content/docs/checks/performance.mdx b/docs/src/content/docs/checks/performance.mdx new file mode 100644 index 0000000..2923597 --- /dev/null +++ b/docs/src/content/docs/checks/performance.mdx @@ -0,0 +1,60 @@ +--- +title: Performance Checks +description: "2 performance checks: carbon footprint and PageSpeed" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 2 performance checks that measure the environmental impact and overall quality of the site using industry-standard methodologies. + +--- + +## Carbon Footprint + +**Handler:** `carbon` + +Estimates the website's carbon footprint per page view based on data transfer size. Uses the Sustainable Web Design methodology to calculate CO2 emissions from three sources: hosting infrastructure, network data transfer, and end-user device energy consumption. Also checks whether the hosting provider uses green (renewable) energy. + +**Why it matters:** The internet accounts for roughly 2-4% of global carbon emissions. Heavy websites are not only bad for the environment but also slow for users, especially on mobile networks. Optimizing for lower carbon footprint naturally improves performance. + +**Good result:** Small page weight (under 1 MB transferred), low CO2 estimate (under 0.5g per page view), and green hosting detected. + +**Bad result:** Heavy page (over 3 MB), high CO2 estimate, or non-green hosting. The average website produces about 0.5g of CO2 per page view — sites with unoptimized images and excessive JavaScript can be 5-10x worse. + +<Aside type="tip"> + Images are usually the biggest contributor to both page weight and carbon emissions. Converting to modern formats (WebP, AVIF) and properly sizing images is the single most impactful optimization. +</Aside> + +--- + +## PageSpeed + +**Handler:** `quality` + +Runs a Google PageSpeed Insights (Lighthouse) analysis, scoring the site across four categories on a 0-100 scale: + +| Category | What it measures | +|---|---| +| **Performance** | Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), Total Blocking Time (TBT), First Contentful Paint (FCP), Speed Index | +| **Accessibility** | Alt text, ARIA labels, color contrast, keyboard navigation, semantic HTML | +| **Best Practices** | HTTPS usage, deprecated APIs, console errors, image aspect ratios, mixed content | +| **SEO** | Crawlability, mobile-friendliness, meta tags, structured data, status codes | + +**Why it matters:** Google uses Core Web Vitals (LCP, CLS, and Interaction to Next Paint) as ranking signals. Performance below 50 directly impacts search rankings. Accessibility issues exclude users with disabilities and create legal risk (ADA, EAA). Best Practices failures indicate technical debt. + +**Good result:** All four scores at 90 or above (green). This indicates a fast, accessible, well-built site that follows current web standards. + +**Bad result:** Any score below 50 (red). Common issues include: + +- **Performance:** Unoptimized images, render-blocking JavaScript, excessive third-party scripts +- **Accessibility:** Missing alt text, insufficient color contrast, no ARIA labels on interactive elements +- **Best Practices:** Mixed HTTP/HTTPS content, use of deprecated browser APIs, missing doctype +- **SEO:** Missing meta description, no viewport meta tag, blocked by robots.txt + +<Aside type="caution"> + Requires the `GOOGLE_CLOUD_API_KEY` environment variable. Without the key, this check is skipped. The API has a generous free tier, but rate limits apply. +</Aside> + +<Aside type="note"> + PageSpeed results can vary between runs due to network conditions and server load. For reliable benchmarks, run the check multiple times and look at the trend rather than a single score. +</Aside> diff --git a/docs/src/content/docs/checks/security.mdx b/docs/src/content/docs/checks/security.mdx new file mode 100644 index 0000000..211dc6c --- /dev/null +++ b/docs/src/content/docs/checks/security.mdx @@ -0,0 +1,230 @@ +--- +title: Security Checks +description: "12 security checks: SSL, TLS, HSTS, headers, firewall, threats, and more" +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web runs 12 security-focused checks that cover certificate validation, protocol configuration, header hardening, threat intelligence, and CMS-specific vulnerabilities. + +--- + +## SSL Certificate + +**Handler:** `ssl` + +Fetches and analyzes the SSL/TLS certificate presented by the server. Extracts the issuer (who signed it), validity dates, subject alternative names (SANs), key algorithm, and the full chain of trust. + +**Why it matters:** An expired or misconfigured certificate triggers browser security warnings that immediately drive users away. Missing SANs mean HTTPS breaks on uncovered subdomains. + +**Good result:** A trusted certificate issued by a recognized CA, more than 30 days until expiry, and SANs covering all expected subdomains. + +**Bad result:** Expired certificate, self-signed certificate (browsers will warn), hostname mismatch, or expiry within the next 30 days. + +<Aside type="tip"> + Free Let's Encrypt certificates expire every 90 days. If you use them, make sure auto-renewal is working and monitor expiry dates. +</Aside> + +--- + +## SSL Grade + +**Handler:** `ssl-labs` + +Submits the domain to the Qualys SSL Labs API for a deep analysis of the server's SSL/TLS configuration. Returns a letter grade from A+ to F based on protocol support, key exchange strength, cipher strength, and certificate validity. If the domain resolves to multiple IPs, each endpoint is tested separately. + +**Why it matters:** The grade provides a single at-a-glance indicator of your TLS posture. Sites graded C or below are using weak cryptography that may be exploitable. + +**Good result:** Grade A or A+. An A+ requires HSTS with a long `max-age`. + +**Bad result:** Grade C or below. Grade B typically means the server still supports TLS 1.0/1.1. Grade F indicates a critical issue such as an expired certificate or vulnerability to known attacks (POODLE, Heartbleed). + +--- + +## TLS Configuration + +**Handler:** `tls` + +Analyzes TLS protocol configuration via Mozilla Observatory. Checks which TLS versions the server supports, what cipher suites it offers, and the overall configuration quality. + +**Why it matters:** Weak TLS configurations allow attackers to intercept or decrypt traffic. PCI DSS compliance requires TLS 1.2 or higher. + +**Good result:** TLS 1.3 with AEAD cipher suites (AES-GCM, ChaCha20-Poly1305) and Perfect Forward Secrecy (PFS). + +**Bad result:** TLS 1.0 or 1.1 still enabled, weak cipher suites (RC4, DES, 3DES), or missing Perfect Forward Secrecy. + +<Aside type="caution"> + TLS 1.0 and 1.1 were deprecated in 2021 (RFC 8996). If your server still supports them, update your configuration immediately. +</Aside> + +--- + +## HSTS + +**Handler:** `hsts` + +Checks for the `Strict-Transport-Security` response header and queries the HSTS preload list. HSTS instructs browsers to always connect over HTTPS, preventing SSL stripping attacks. + +**Why it matters:** Without HSTS, a user typing `example.com` into their browser briefly connects over plain HTTP before being redirected. An attacker on the same network can intercept that initial request. + +**Good result:** Header present with `max-age` of at least 31536000 (1 year), `includeSubDomains` directive, and listed on the HSTS preload list. + +**Bad result:** Header missing entirely (vulnerable to downgrade attacks), or `max-age` set too low (under 6 months weakens protection significantly). + +<Aside type="tip"> + HSTS preloading hardcodes HTTPS into browsers so even the very first visit is protected. Submit your domain at [hstspreload.org](https://hstspreload.org/). +</Aside> + +--- + +## HTTP Security Headers + +**Handler:** `http-security` + +Scores five critical HTTP security headers that defend against common web attacks: + +| Header | Protects against | +|---|---| +| `Content-Security-Policy` | Cross-site scripting (XSS) | +| `X-Frame-Options` | Clickjacking | +| `X-Content-Type-Options` | MIME type sniffing | +| `Referrer-Policy` | URL and session token leakage | +| `Permissions-Policy` | Unauthorized access to browser APIs (camera, mic, geolocation) | + +**Why it matters:** These headers are a defense-in-depth layer. Even if your application code is secure, missing headers leave users exposed to client-side attacks. + +**Good result:** All five headers present and correctly configured. + +**Bad result:** One or more missing. A missing CSP is the most critical gap — it is the primary defense against XSS, the most common web vulnerability. + +--- + +## Firewall (WAF) + +**Handler:** `firewall` + +Detects Web Application Firewalls by analyzing HTTP response headers, cookies, and server signatures. Identifies common WAF products including Cloudflare, AWS WAF, Akamai, Imperva, Sucuri, and others. + +**Why it matters:** WAFs filter malicious traffic and protect against SQL injection, XSS, and other OWASP Top 10 attacks before they reach your application. + +**Good result:** WAF detected. This means an additional protection layer sits between attackers and your application. + +**Bad result:** No WAF detected. This is not necessarily a vulnerability, but it does mean the application relies entirely on code-level security with no external filtering. + +<Aside type="note"> + WAF detection is informational. Some WAFs are configured to hide their presence, so a "not detected" result does not guarantee no WAF is in place. +</Aside> + +--- + +## security.txt + +**Handler:** `security-txt` + +Fetches and parses `/.well-known/security.txt`, the standard (RFC 9116) for websites to communicate vulnerability disclosure policies. Checks for required fields like `Contact` and `Expires`. + +**Why it matters:** Without a security.txt file, security researchers who discover vulnerabilities have no clear path to report them responsibly. This can lead to public disclosure without prior notice. + +**Good result:** File present with a valid `Contact` field (email or URL), an `Expires` date in the future, and optionally a PGP encryption key. + +**Bad result:** File missing or expired. Missing is not a critical vulnerability, but it is recommended by the IETF and increasingly expected for any production website. + +--- + +## Threats + +**Handler:** `threats` + +Cross-references the URL against four threat intelligence databases: + +| Database | What it checks | +|---|---| +| Google Safe Browsing | Malware, phishing, unwanted software | +| URLHaus | Known malware distribution URLs | +| PhishTank | Confirmed phishing URLs | +| Cloudmersive | Virus and threat scanning | + +**Why it matters:** A site flagged by Google Safe Browsing shows warnings in Chrome, Firefox, and Safari, which can devastate traffic overnight. Regularly checking your own domains catches compromises early. + +**Good result:** Not flagged by any database. + +**Bad result:** Flagged by one or more databases. Even a single flag warrants immediate investigation — it may indicate the site has been compromised or is hosting injected malware. + +<Aside type="note"> + The Cloudmersive check requires the `CLOUDMERSIVE_API_KEY` environment variable. The other three databases are checked without any key. +</Aside> + +--- + +## Block Lists + +**Handler:** `block-lists` + +Checks whether the domain appears on 17 major DNS-based block lists (DNSBLs) including AdGuard, CleanBrowsing, Cloudflare Family, OpenDNS, Norton ConnectSafe, Quad9, and Yandex Safe Browsing. + +**Why it matters:** Being listed on a block list means users who rely on that DNS provider cannot access your site at all. Corporate networks frequently use DNS filtering, so a listing can block access for entire organizations. + +**Good result:** Not listed on any of the 17 block lists. + +**Bad result:** Listed on one or more. False positives do happen (especially on shared hosting), so investigate the listing and file a removal request if unwarranted. + +--- + +## VirusTotal + +**Handler:** `virustotal` + +Submits the URL to VirusTotal, which scans it against 70+ antivirus engines and security vendors including Google Safe Browsing, Kaspersky, BitDefender, and Sophos. Returns the number of engines that flagged the URL as malicious or suspicious. + +**Why it matters:** VirusTotal aggregates the broadest set of security verdicts available. Even a few detections are a strong signal that something is wrong. + +**Good result:** Zero detections across all engines. + +**Bad result:** Any detections. Even 1-2 warrant investigation (false positives exist), but multiple flags are a very strong signal of compromise. + +<Aside type="caution"> + Requires the `VIRUSTOTAL_API_KEY` environment variable. Free tier allows 500 requests per day. Without the key, this check is skipped. +</Aside> + +--- + +## AbuseIPDB + +**Handler:** `abuse-ipdb` + +Resolves the domain to an IP address and checks it against AbuseIPDB, a community-driven database of reported malicious IPs. Returns an abuse confidence score (0-100%), total reports, ISP, and usage type. + +**Why it matters:** A high abuse confidence score means the IP hosting your site has been repeatedly reported for spam, brute-force attacks, or DDoS activity. This can affect email deliverability and reputation. + +**Good result:** Low confidence score (below 25%) with few or no recent reports. + +**Bad result:** High confidence score (above 25%). Note that shared hosting IPs may have elevated scores due to other tenants on the same IP — check the usage type to distinguish. + +<Aside type="caution"> + Requires the `ABUSEIPDB_API_KEY` environment variable. Free tier allows 1,000 checks per day. Without the key, this check is skipped. +</Aside> + +--- + +## WordPress + +**Handler:** `wordpress` + +Passively scans the website to detect whether it runs WordPress. If detected, enumerates plugins, themes, and their exposed version numbers from the page HTML. Checks for commonly exploited files and endpoints: + +| Target | Risk | +|---|---| +| `xmlrpc.php` | Brute-force login attacks, DDoS amplification via pingbacks | +| `wp-content/debug.log` | Leaks database queries, file paths, PHP errors | +| Config backups | Leaks database credentials | +| User enumeration endpoints | Reveals valid usernames for brute-force attacks | + +**Why it matters:** WordPress powers over 40% of the web, making it a prime target. Outdated plugins are the single most common WordPress attack vector. + +**Good result:** WordPress core, plugins, and themes are all up to date. No debug logs or config backups are publicly accessible. `xmlrpc.php` is blocked or disabled. + +**Bad result:** Outdated plugins with known CVEs, publicly accessible debug logs, or exposed `xmlrpc.php`. + +<Aside type="note"> + No API key required. All detection is performed using publicly visible data in the HTML response. +</Aside> diff --git a/docs/src/content/docs/deployment/docker-local.mdx b/docs/src/content/docs/deployment/docker-local.mdx new file mode 100644 index 0000000..7e85c5e --- /dev/null +++ b/docs/src/content/docs/deployment/docker-local.mdx @@ -0,0 +1,120 @@ +--- +title: Docker (Local Build) +description: Build and run recon-web from source with Docker Compose +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +This guide walks you through building and running recon-web from source using Docker Compose. This approach gives you the full stack -- API server, web UI, and CLI -- built from the code on your machine. + +## Prerequisites + +- Docker Engine 20+ +- Docker Compose v2+ (the `docker compose` plugin, not the legacy `docker-compose` binary) + +## Getting started + +<Steps> +1. Clone the repository: + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + ``` + +2. Create your environment file: + ```bash + cp .env.example .env + ``` + Edit `.env` to add API keys or enable authentication. See [Configuration](/recon-web/getting-started/configuration/) for all options. + +3. Start all services: + ```bash + docker compose up -d + ``` + Docker builds the images from source on first run. Subsequent starts reuse the cached images unless the source changes. + +4. Open the web UI at [http://localhost:8080](http://localhost:8080) and run your first scan. +</Steps> + +## Services + +| Service | Port | Description | +|---|---|---| +| `api` | `3000` | Fastify API server -- runs all handlers, stores scan history | +| `web` | `8080` | Nginx-served frontend -- proxies API requests to the `api` service | +| `cli` | _(none)_ | CLI container (on-demand, activated via Docker Compose profiles) | + +## Custom ports + +Override the default ports with environment variables: + +```bash +API_PORT=4000 WEB_PORT=9090 docker compose up -d +``` + +Or set them in your `.env` file: + +```bash +API_PORT=4000 +WEB_PORT=9090 +``` + +## Running CLI scans + +The CLI service uses a Docker Compose profile and only runs when explicitly invoked: + +```bash +docker compose run --rm cli scan https://example.com +``` + +```bash +docker compose run --rm cli ssl https://example.com +``` + +```bash +docker compose run --rm cli scan --json https://example.com > results.json +``` + +## Data persistence + +Scan history is stored in a SQLite database inside a Docker volume called `scan-data`. This volume persists across container restarts and rebuilds. + +### Backup + +```bash +docker compose cp api:/app/data/recon-web.db ./backup-recon-web.db +``` + +### Restore + +```bash +docker compose cp ./backup-recon-web.db api:/app/data/recon-web.db +docker compose restart api +``` + +## Rebuilding images + +If you pull new source code or make changes, rebuild the images: + +```bash +docker compose build +docker compose up -d +``` + +## Stopping + +Stop all services but keep volumes (scan history preserved): + +```bash +docker compose down +``` + +Stop all services and delete volumes (scan history deleted): + +```bash +docker compose down -v +``` + +<Aside type="caution"> + `docker compose down -v` permanently deletes the scan history database. Back it up first if you need the data. +</Aside> diff --git a/docs/src/content/docs/deployment/docker-remote.mdx b/docs/src/content/docs/deployment/docker-remote.mdx new file mode 100644 index 0000000..0edefc3 --- /dev/null +++ b/docs/src/content/docs/deployment/docker-remote.mdx @@ -0,0 +1,103 @@ +--- +title: Docker (Pre-built Images) +description: Run recon-web with pre-built images from GitHub Container Registry +--- + +import { Steps, Aside } from '@astrojs/starlight/components'; + +If you do not need to build from source, you can run recon-web using the pre-built Docker images published to GitHub Container Registry (GHCR). This is the fastest way to deploy without cloning the repository. + +## Available images + +| Image | Description | +|---|---| +| `ghcr.io/brunoafk/recon-web/api` | API server | +| `ghcr.io/brunoafk/recon-web/web` | Web UI (Nginx frontend) | +| `ghcr.io/brunoafk/recon-web/cli` | CLI tool | + +## Getting started + +<Steps> +1. Create a project directory and add an environment file: + ```bash + mkdir recon-web && cd recon-web + curl -O https://raw.githubusercontent.com/brunoafk/recon-web/main/.env.example + cp .env.example .env + ``` + +2. Download the remote Compose file: + ```bash + curl -O https://raw.githubusercontent.com/brunoafk/recon-web/main/docker-compose.remote.yml + ``` + +3. Start all services: + ```bash + docker compose -f docker-compose.remote.yml up -d + ``` + +4. Open the web UI at [http://localhost:8080](http://localhost:8080). +</Steps> + +## Version pinning + +By default, the `latest` tag is used. To pin to a specific release, set the `TAG` environment variable: + +```bash +TAG=1.0.0 docker compose -f docker-compose.remote.yml up -d +``` + +Or in your `.env` file: + +```bash +TAG=1.0.0 +``` + +<Aside type="tip"> + Pinning to a specific version is recommended for production deployments. The `latest` tag is updated on every release and may introduce breaking changes. +</Aside> + +## Custom registry + +If you mirror the images to a private registry, override the `REGISTRY` variable: + +```bash +REGISTRY=registry.example.com/recon-web docker compose -f docker-compose.remote.yml up -d +``` + +The Compose file constructs image references as `${REGISTRY}/api:${TAG}`, `${REGISTRY}/web:${TAG}`, etc. + +## Updating + +Pull the latest images and restart: + +```bash +docker compose -f docker-compose.remote.yml pull +docker compose -f docker-compose.remote.yml up -d +``` + +To update to a specific version: + +```bash +TAG=1.1.0 docker compose -f docker-compose.remote.yml pull +TAG=1.1.0 docker compose -f docker-compose.remote.yml up -d +``` + +## Running CLI scans + +```bash +docker compose -f docker-compose.remote.yml run --rm cli scan https://example.com +``` + +## Data persistence + +Scan data is stored in a `scan-data` Docker volume, identical to the [local build setup](/recon-web/deployment/docker-local/). The backup and restore process is the same. + +## Stopping + +```bash +# Keep data +docker compose -f docker-compose.remote.yml down + +# Delete data +docker compose -f docker-compose.remote.yml down -v +``` diff --git a/docs/src/content/docs/deployment/kubernetes.mdx b/docs/src/content/docs/deployment/kubernetes.mdx new file mode 100644 index 0000000..35afd6c --- /dev/null +++ b/docs/src/content/docs/deployment/kubernetes.mdx @@ -0,0 +1,168 @@ +--- +title: Kubernetes (Helm) +description: Deploy recon-web on Kubernetes with the included Helm chart +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +recon-web ships with a Helm chart at `helm/recon-web/` that deploys the API server, web frontend, and optional scheduled scanner as Kubernetes workloads. + +## Prerequisites + +- Kubernetes 1.25+ +- Helm 3.10+ +- `kubectl` configured to talk to your cluster + +## Quick install + +<Steps> +1. Clone the repository (or add the chart from a Helm registry if published): + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + ``` + +2. Install with default values: + ```bash + helm install recon-web ./helm/recon-web + ``` + +3. Port-forward to access the UI locally: + ```bash + kubectl port-forward svc/recon-web-web 8080:80 + ``` + +4. Open [http://localhost:8080](http://localhost:8080). +</Steps> + +## Custom values + +Create a `values-override.yaml` to configure the deployment for your environment: + +```yaml +# values-override.yaml + +ingress: + enabled: true + className: nginx + host: recon.example.com + tls: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + +auth: + enabled: true + token: "your-secret-token-at-least-32-chars-long" + +api: + env: + GOOGLE_CLOUD_API_KEY: "AIza..." + VIRUSTOTAL_API_KEY: "abcdef..." + MAX_CONCURRENCY: "4" + +scanner: + enabled: true + schedule: "0 */6 * * *" + urls: + - https://example.com + - https://another-site.com + +notifications: + telegram: + enabled: true + botToken: "123456789:ABCdefGHI..." + chatId: "123456789" + email: + enabled: true + smtpHost: smtp.gmail.com + smtpPort: "587" + smtpUser: alerts@example.com + smtpPass: "abcd efgh ijkl mnop" + notifyEmail: team@example.com + +persistence: + enabled: true + size: 5Gi + storageClass: gp3 +``` + +Install with the override file: + +```bash +helm install recon-web ./helm/recon-web -f values-override.yaml +``` + +## Resource defaults + +| Component | CPU request | CPU limit | Memory request | Memory limit | +|---|---|---|---|---| +| API | 100m | 1000m | 256Mi | 1Gi | +| Web | 50m | 200m | 64Mi | 128Mi | + +Override these under `api.resources` and `web.resources` in your values file. + +## Custom images + +To use images from a private registry: + +```yaml +api: + image: + repository: registry.example.com/recon-web/api + tag: "1.0.0" + pullPolicy: IfNotPresent + +web: + image: + repository: registry.example.com/recon-web/web + tag: "1.0.0" + pullPolicy: IfNotPresent +``` + +## Persistence + +The API server stores scan history in a SQLite database backed by a PersistentVolumeClaim. + +| Setting | Default | Description | +|---|---|---| +| `persistence.enabled` | `true` | Create a PVC for scan data | +| `persistence.size` | `1Gi` | Volume size | +| `persistence.storageClass` | _(cluster default)_ | Storage class to use | +| `persistence.accessMode` | `ReadWriteOnce` | PVC access mode | + +<Aside type="caution"> + With `ReadWriteOnce`, only one API pod can write to the volume at a time. Do not scale `api.replicas` above 1 unless you switch to a `ReadWriteMany` storage class or an external database. +</Aside> + +## Scheduled scanner + +The Helm chart can deploy a Kubernetes CronJob that runs the CLI image on a schedule: + +```yaml +scanner: + enabled: true + schedule: "0 0 * * *" + urls: + - https://example.com + image: + repository: ghcr.io/brunoafk/recon-web/cli + tag: latest +``` + +This is an alternative to the built-in scheduler (`SCHEDULE_ENABLED`). The Kubernetes CronJob approach gives you native retry policies, resource limits, and pod-level observability. + +## Upgrading + +```bash +helm upgrade recon-web ./helm/recon-web -f values-override.yaml +``` + +## Uninstalling + +```bash +helm uninstall recon-web +``` + +<Aside type="note"> + Uninstalling the Helm release does not delete PersistentVolumeClaims by default. To remove scan data, delete the PVC manually: `kubectl delete pvc -l app.kubernetes.io/name=recon-web`. +</Aside> diff --git a/docs/src/content/docs/deployment/standalone.mdx b/docs/src/content/docs/deployment/standalone.mdx new file mode 100644 index 0000000..d388dc0 --- /dev/null +++ b/docs/src/content/docs/deployment/standalone.mdx @@ -0,0 +1,192 @@ +--- +title: Standalone (Node.js) +description: Run recon-web directly with Node.js without Docker +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +This guide covers running recon-web directly on a machine with Node.js, without Docker. This is useful for development, for environments where Docker is not available, or when you want full control over the runtime. + +## Prerequisites + +- **Node.js 24+** (LTS recommended) +- **npm** (included with Node.js) +- **Chromium or Google Chrome** (optional, required for screenshots and Lighthouse checks) + +## Install and build + +<Steps> +1. Clone the repository: + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Create your environment file: + ```bash + cp .env.example .env + ``` + +4. Build all packages: + ```bash + npm run build + ``` + +5. Start the server: + ```bash + npm start + ``` + + The API server starts on port 3000 and serves the web UI from the built frontend assets. +</Steps> + +## Chromium setup + +Several handlers (screenshots, Lighthouse performance audits) need a Chromium binary. The server auto-detects common installation paths, but you can set it explicitly if needed. + +<Tabs> + <TabItem label="macOS"> + If you have Google Chrome installed, it is auto-detected at: + ``` + /Applications/Google Chrome.app/Contents/MacOS/Google Chrome + ``` + + Alternatively, install Chromium via Homebrew: + ```bash + brew install --cask chromium + ``` + + Then set the path in `.env`: + ```bash + CHROME_PATH=/Applications/Chromium.app/Contents/MacOS/Chromium + ``` + </TabItem> + + <TabItem label="Linux (Debian/Ubuntu)"> + Install Chromium from the system package manager: + ```bash + sudo apt-get update + sudo apt-get install -y chromium + ``` + + The server auto-detects `/usr/bin/chromium`. If your distribution installs it elsewhere: + ```bash + CHROME_PATH=/usr/bin/chromium-browser + ``` + </TabItem> + + <TabItem label="Linux (RHEL/Fedora)"> + ```bash + sudo dnf install chromium + ``` + + Set the path if needed: + ```bash + CHROME_PATH=/usr/bin/chromium-browser + ``` + </TabItem> +</Tabs> + +<Aside type="tip"> + If you do not need screenshot or Lighthouse checks, you can skip Chromium entirely. Handlers that require it will report as "skipped" and all other checks will run normally. +</Aside> + +## API-only mode + +By default the server serves the built frontend. To run in API-only mode (e.g. behind a separate frontend or for headless use), set: + +```bash +STATIC_DIR=none +``` + +In this mode the server only exposes the REST API and Swagger docs. No static files are served. + +## Running with PM2 + +For production use without Docker, [PM2](https://pm2.keymetrics.io/) provides process management, automatic restarts, log rotation, and cluster mode. + +<Steps> +1. Install PM2 globally: + ```bash + npm install -g pm2 + ``` + +2. Start recon-web: + ```bash + pm2 start npm --name recon-web -- start + ``` + +3. Save the process list so it survives reboots: + ```bash + pm2 save + pm2 startup + ``` + +4. Monitor: + ```bash + pm2 logs recon-web + pm2 monit + ``` +</Steps> + +## Nginx reverse proxy + +In production you will typically place recon-web behind a reverse proxy for TLS termination, custom domain, and caching. Here is a minimal Nginx configuration: + +```nginx +server { + listen 80; + server_name recon.example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name recon.example.com; + + ssl_certificate /etc/letsencrypt/live/recon.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/recon.example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SSE streaming support + location /api/stream { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + } +} +``` + +<Aside type="caution"> + The `/api/stream` location block disables proxy buffering so that SSE events are forwarded to the client immediately. Without this, results will not stream in real time. +</Aside> + +## Development mode + +For development with hot reloading, use the dev scripts instead of building first: + +```bash +# Terminal 1 — API server with watch mode +npm run dev + +# Terminal 2 — Web UI dev server +npm run dev:web +``` + +The web UI dev server typically runs on port 8080 and proxies API requests to port 3000. diff --git a/docs/src/content/docs/development/adding-handlers.mdx b/docs/src/content/docs/development/adding-handlers.mdx new file mode 100644 index 0000000..0a33b9f --- /dev/null +++ b/docs/src/content/docs/development/adding-handlers.mdx @@ -0,0 +1,311 @@ +--- +title: Adding a Check +description: How to create a new analysis handler for recon-web +--- + +import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; + +Adding a new check to recon-web involves creating a handler in **core**, registering it, building a renderer in **web**, and writing tests. Follow the steps below. + +<Steps> + +1. ## Create the handler + + Create a new file at `packages/core/src/handlers/my-check.ts`. + + Every handler must satisfy the `AnalysisHandler<T>` signature and return a `HandlerResult<T>`. Import helpers from the shared utilities. + + ```ts title="packages/core/src/handlers/my-check.ts" + import type { AnalysisHandler, HandlerResult } from '../types.js'; + import { extractHostname } from '../utils.js'; + + export interface MyCheckResult { + headerPresent: boolean; + headerValue: string | null; + } + + export const myCheckHandler: AnalysisHandler<MyCheckResult> = async (url, options) => { + const hostname = extractHostname(url); + + try { + const response = await fetch(url, { + signal: options?.signal, + }); + + const value = response.headers.get('x-custom-header'); + + return { + data: { + headerPresent: value !== null, + headerValue: value, + }, + }; + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + errorCode: 'TIMEOUT', + errorCategory: 'site', + }; + } + }; + ``` + + <Aside type="caution"> + Never throw from a handler. Always return an error result so the runner can classify and report the failure cleanly. + </Aside> + + ### Error codes reference + + Use the appropriate `ErrorCode` when returning an error: + + | ErrorCode | When to use | + |---|---| + | `TIMEOUT` | The request exceeded the allowed time | + | `DNS_FAILURE` | The hostname could not be resolved | + | `CONNECTION_REFUSED` | TCP connection was rejected | + | `MISSING_API_KEY` | A required third-party API key is not configured | + | `NO_DATA` | The check ran successfully but produced no meaningful result | + | `INVALID_URL` | The input URL is malformed | + | `SSRF_BLOCKED` | The URL resolved to a private/internal IP | + | `REQUIRES_CHROMIUM` | The check needs a headless browser that is not available | + | `NOT_FOUND` | The target resource was not found (404) | + + ### Error categories + + | ErrorCategory | Meaning | + |---|---| + | `tool` | A problem with the recon-web framework or infrastructure | + | `site` | A problem with the target website | + | `info` | The check was intentionally skipped (e.g., not applicable) | + +2. ## Export from the handlers barrel + + Add the export to `packages/core/src/handlers/index.ts`: + + ```ts title="packages/core/src/handlers/index.ts" + // ... existing exports + export { myCheckHandler } from './my-check.js'; + export type { MyCheckResult } from './my-check.js'; + ``` + +3. ## Register in the registry + + Open `packages/core/src/registry.ts` and add an entry: + + ```ts title="packages/core/src/registry.ts" + import { myCheckHandler } from './handlers/my-check.js'; + + registry.register({ + name: 'my-check', + description: 'Check for the presence of X-Custom-Header', + category: 'http', + requires: [], + handler: myCheckHandler, + }); + ``` + + The `requires` array declares runtime dependencies. Common values: + + - `'chromium'` — needs a headless browser + - `'apiKey:shodan'` — needs a third-party API key + + Leave the array empty if the handler only needs outbound HTTP. + +4. ## Add presentation metadata + + Open `packages/core/src/presentation.ts` and add a display name and short description: + + ```ts title="packages/core/src/presentation.ts" + // ... existing entries + 'my-check': { + displayName: 'Custom Header Check', + shortDescription: 'Looks for X-Custom-Header in the HTTP response', + }, + ``` + +5. ## Create a web renderer + + Create `packages/web/src/components/results/renderers/MyCheckRenderer.tsx`: + + ```tsx title="packages/web/src/components/results/renderers/MyCheckRenderer.tsx" + import type { MyCheckResult } from '@recon-web/core'; + import { ChecklistItem } from '../primitives/ChecklistItem'; + import { KeyValueTable } from '../primitives/KeyValueTable'; + import { SectionLabel } from '../primitives/SectionLabel'; + + interface Props { + data: MyCheckResult; + } + + export function MyCheckRenderer({ data }: Props) { + return ( + <div> + <SectionLabel>Custom Header</SectionLabel> + <ChecklistItem + label="X-Custom-Header present" + passed={data.headerPresent} + /> + {data.headerValue && ( + <KeyValueTable + rows={[{ key: 'Value', value: data.headerValue }]} + /> + )} + </div> + ); + } + ``` + + Then register it in `packages/web/src/components/results/renderers/index.ts`: + + ```ts title="packages/web/src/components/results/renderers/index.ts" + import { MyCheckRenderer } from './MyCheckRenderer'; + + // ... inside the renderers map + 'my-check': MyCheckRenderer, + ``` + + ### Available renderer primitives + + | Component | Purpose | + |---|---| + | `ChecklistItem` | A pass/fail item with a label and boolean status | + | `KeyValueTable` | A two-column table of key-value pairs | + | `SectionLabel` | A heading for grouping related results | + | `Chip` | A small label/badge (e.g., severity, status) | + | `CodeBlock` | A syntax-highlighted block for raw output | + +6. ## Write tests + + Create `packages/core/src/handlers/my-check.test.ts`. Use **Vitest** and **MSW** to mock HTTP responses: + + ```ts title="packages/core/src/handlers/my-check.test.ts" + import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; + import { setupServer } from 'msw/node'; + import { http, HttpResponse } from 'msw'; + import { myCheckHandler } from './my-check.js'; + + const server = setupServer(); + + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('myCheckHandler', () => { + it('detects the header when present', async () => { + server.use( + http.get('https://example.com/', () => + HttpResponse.text('OK', { + headers: { 'x-custom-header': 'some-value' }, + }), + ), + ); + + const result = await myCheckHandler('https://example.com/'); + + expect(result.data?.headerPresent).toBe(true); + expect(result.data?.headerValue).toBe('some-value'); + }); + + it('reports absence when the header is missing', async () => { + server.use( + http.get('https://example.com/', () => HttpResponse.text('OK')), + ); + + const result = await myCheckHandler('https://example.com/'); + + expect(result.data?.headerPresent).toBe(false); + expect(result.data?.headerValue).toBeNull(); + }); + + it('returns an error result on network failure', async () => { + server.use( + http.get('https://example.com/', () => HttpResponse.error()), + ); + + const result = await myCheckHandler('https://example.com/'); + + expect(result.error).toBeDefined(); + expect(result.errorCategory).toBe('site'); + }); + }); + ``` + +7. ## Build and verify + + ```bash + # Rebuild core with the new handler + npx tsc -b packages/core + + # Run core tests + npm test -w @recon-web/core + + # Confirm the full build still works + npx tsc --build + ``` + +</Steps> + +## Complete example: HTTP header checker + +Below is a self-contained handler that checks whether a site sends common security headers. This is a realistic example you can use as a starting template. + +```ts title="packages/core/src/handlers/security-headers.ts" +import type { AnalysisHandler, HandlerResult } from '../types.js'; +import { normalizeUrl } from '../utils.js'; + +const SECURITY_HEADERS = [ + 'strict-transport-security', + 'content-security-policy', + 'x-content-type-options', + 'x-frame-options', + 'referrer-policy', + 'permissions-policy', +] as const; + +export interface SecurityHeadersResult { + headers: Array<{ + name: string; + present: boolean; + value: string | null; + }>; + score: number; // 0-100 +} + +export const securityHeadersHandler: AnalysisHandler<SecurityHeadersResult> = async ( + url, + options, +) => { + const normalized = normalizeUrl(url); + + try { + const response = await fetch(normalized, { + method: 'HEAD', + redirect: 'follow', + signal: options?.signal, + }); + + const headers = SECURITY_HEADERS.map((name) => { + const value = response.headers.get(name); + return { name, present: value !== null, value }; + }); + + const presentCount = headers.filter((h) => h.present).length; + const score = Math.round((presentCount / SECURITY_HEADERS.length) * 100); + + return { data: { headers, score } }; + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return { error: 'Request timed out', errorCode: 'TIMEOUT', errorCategory: 'site' }; + } + return { + error: error instanceof Error ? error.message : String(error), + errorCode: 'CONNECTION_REFUSED', + errorCategory: 'site', + }; + } +}; +``` + +<Aside type="tip"> +After registering this handler, both the CLI (`recon-web security-headers https://example.com`) and the API (`POST /api/scan` with the handler selected) will pick it up automatically thanks to the registry pattern. +</Aside> diff --git a/docs/src/content/docs/development/architecture.mdx b/docs/src/content/docs/development/architecture.mdx new file mode 100644 index 0000000..d6c93be --- /dev/null +++ b/docs/src/content/docs/development/architecture.mdx @@ -0,0 +1,141 @@ +--- +title: Architecture +description: "How recon-web is built: monorepo structure, packages, data flow" +--- + +import { Aside, Steps, Card, CardGrid, TabItem, Tabs } from '@astrojs/starlight/components'; + +## Monorepo structure + +recon-web is organized as a monorepo using **npm workspaces** and **TypeScript project references**. Each package lives under `packages/` and declares its own `tsconfig.json` that references sibling packages as needed. + +``` +recon-web/ +├── packages/ +│ ├── core/ # Handlers, registry, runner, types, utilities +│ ├── api/ # Fastify REST server + SQLite database +│ ├── cli/ # Commander CLI +│ ├── web/ # React SPA +│ └── static/ # Cloudflare Pages edge deployment +├── package.json # Workspace root +└── tsconfig.json # Root project references +``` + +## Packages + +<CardGrid> + <Card title="@recon-web/core"> + **39 handler functions**, the handler registry, the concurrent runner, shared types, and utility functions. Every other package depends on core. + </Card> + <Card title="@recon-web/api"> + **Fastify 5** REST server with a **SQLite** database (better-sqlite3), SSE streaming for real-time scan progress, scheduled scans, report generation, and authentication. + </Card> + <Card title="@recon-web/cli"> + **Commander**-based CLI. Commands are auto-generated from the handler registry so every registered handler is instantly available on the command line. Supports JSON and JUnit output formats. + </Card> + <Card title="@recon-web/web"> + **React 18** single-page application styled with **Tailwind CSS 4**. Uses React Router for navigation, React Query for data fetching, and a real-time scan UI powered by SSE. + </Card> + <Card title="@recon-web/static"> + **Cloudflare Pages** edge deployment that runs a subset of approximately 16 HTTP-only handlers without needing a full server. + </Card> +</CardGrid> + +## Handler architecture + +Every analysis check in recon-web is a **handler** — an async function with a consistent signature: + +```ts +type AnalysisHandler<T> = (url: string, options?: HandlerOptions) => Promise<HandlerResult<T>>; +``` + +Handlers are collected in a **registry** that stores metadata alongside each function: + +```ts +registry.register({ + name: 'ssl-certificate', + description: 'Inspect the SSL/TLS certificate chain', + category: 'security', + requires: [], // e.g. ['chromium'] or ['apiKey:shodan'] + handler: sslCertificateHandler, +}); +``` + +The registry pattern means the CLI can auto-generate commands, the API can list available checks, and the web UI can render per-handler controls — all driven from a single source of truth. + +## Runner + +The **runner** executes handlers concurrently using `p-limit` to cap parallelism. Before any handler runs, the runner performs: + +1. **URL validation** — rejects malformed input. +2. **SSRF protection** — blocks private/internal IP ranges. +3. **Pre-flight checks** — DNS resolution and an initial HTTP probe. + +Errors returned from handlers are classified using two enums: + +| Concept | Examples | +|---|---| +| `ErrorCode` | `TIMEOUT`, `DNS_FAILURE`, `CONNECTION_REFUSED`, `MISSING_API_KEY`, `NO_DATA`, `INVALID_URL`, `SSRF_BLOCKED`, `REQUIRES_CHROMIUM`, `NOT_FOUND` | +| `ErrorCategory` | `tool` (framework issue), `site` (target issue), `info` (intentionally skipped) | + +## Data flow + +``` +User + │ + ▼ +Web UI / API / CLI + │ + ▼ +URL validation + │ + ▼ +Pre-flight checks (DNS, HTTP) + │ + ▼ +Create scan record (SQLite) + │ + ▼ +Run handlers concurrently (p-limit) + │ + ▼ +Stream results via SSE + │ + ▼ +Save results to DB + │ + ▼ +Return final results +``` + +1. The user submits a URL through one of three entry points: the **web UI**, the **REST API**, or the **CLI**. +2. The URL is validated and pre-flight checks confirm the target is reachable. +3. A **scan record** is created in SQLite. +4. Handlers run concurrently. As each completes, its result is **streamed** to the client over SSE (API/web) or printed incrementally (CLI). +5. All results are persisted to the `scan_results` table. +6. The complete scan result set is returned to the caller. + +## Database + +recon-web uses **SQLite** via `better-sqlite3` for zero-configuration persistence. The schema is minimal: + +| Table | Purpose | +|---|---| +| `scans` | One row per scan — URL, status, timestamps, metadata | +| `scan_results` | One row per handler result — linked to a scan, stores the handler name and its JSON output | + +<Aside type="tip"> +SQLite is embedded in the API process, so there is no external database to manage. The database file lives at the path configured via the `DATABASE_PATH` environment variable. +</Aside> + +## Key technologies + +| Layer | Technology | +|---|---| +| Server | Fastify 5 | +| Database | SQLite (better-sqlite3) | +| Frontend | React 18, Tailwind CSS 4, React Router, React Query | +| CLI | Commander | +| Testing | Vitest, Playwright, MSW | +| Containerisation | Docker, Helm | +| Edge | Cloudflare Pages / Workers | diff --git a/docs/src/content/docs/development/contributing.mdx b/docs/src/content/docs/development/contributing.mdx new file mode 100644 index 0000000..1b9d354 --- /dev/null +++ b/docs/src/content/docs/development/contributing.mdx @@ -0,0 +1,162 @@ +--- +title: Contributing +description: Set up development environment and contribute to recon-web +--- + +import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; + +## Prerequisites + +| Tool | Minimum version | +|---|---| +| Node.js | 24+ | +| npm | 10+ | +| Git | latest | + +## Setup + +<Steps> + +1. **Clone the repository** + + ```bash + git clone https://github.com/user/recon-web.git + cd recon-web + ``` + +2. **Install dependencies** + + ```bash + npm install + ``` + + npm workspaces will hoist shared dependencies and link the five packages together automatically. + +</Steps> + +## Development servers + +<Tabs> + <TabItem label="API"> + ```bash + npm run dev + ``` + Starts the Fastify API server on **http://localhost:3000** with file-watch restarts. + </TabItem> + <TabItem label="Web UI"> + ```bash + npm run dev:web + ``` + Starts the Vite dev server on **http://localhost:5173** with hot module replacement. The dev server proxies API requests to port 3000, so run both commands in parallel for a full-stack environment. + </TabItem> +</Tabs> + +## Building + +Core must be compiled before packages that depend on it. The recommended build order: + +```bash +# Build core first (other packages reference its types) +npx tsc -b packages/core + +# Build everything +npx tsc --build + +# Build the web frontend (production bundle) +npm run build -w @recon-web/web +``` + +<Aside type="caution"> +If you change types in `@recon-web/core`, always rebuild core before running the API or CLI — otherwise you will see stale type declarations. +</Aside> + +## Testing + +<Tabs> + <TabItem label="Core"> + ```bash + npm test -w @recon-web/core + ``` + Runs approximately **109 tests** covering all handlers, the runner, and utility functions. Tests use **Vitest** with **MSW** for HTTP mocking. + </TabItem> + <TabItem label="CLI"> + ```bash + npm test -w @recon-web/cli + ``` + Runs approximately **17 tests** verifying command generation and output formatting. + </TabItem> + <TabItem label="API"> + ```bash + npm test -w @recon-web/api + ``` + Tests the Fastify routes, database layer, and SSE streaming. + + <Aside type="tip"> + If you recently changed your Node.js version you may need to rebuild the native SQLite binding first: + + ```bash + npm rebuild better-sqlite3 + ``` + </Aside> + </TabItem> +</Tabs> + +### Running all tests + +```bash +npm test --workspaces +``` + +## Linting and type checking + +```bash +# ESLint across all packages +npm run lint + +# TypeScript type checking (no emit) +npm run typecheck +``` + +## Docker builds + +You can build a production Docker image for the API server: + +```bash +docker build -f packages/api/Dockerfile -t recon-web-api:test . +``` + +<Aside> +The Docker build context is the repository root so that workspace dependencies are available during the build. +</Aside> + +## Pull request guidelines + +<Steps> + +1. **Branch from `main`** + + ```bash + git checkout -b feat/my-change main + ``` + +2. **Make your changes** — follow existing code style, add or update tests. + +3. **Verify locally** + + ```bash + npm run lint + npm run typecheck + npm test --workspaces + ``` + +4. **Commit with a descriptive message** + + Use conventional-style prefixes where appropriate (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`). + + ```bash + git commit -m "feat(core): add robots-txt handler" + ``` + +5. **Push and open a pull request** against `main`. The CI pipeline will run lint, tests, audit, a Docker build, and Trivy scanning automatically. + +</Steps> diff --git a/docs/src/content/docs/getting-started/configuration.mdx b/docs/src/content/docs/getting-started/configuration.mdx new file mode 100644 index 0000000..3e1a2c6 --- /dev/null +++ b/docs/src/content/docs/getting-started/configuration.mdx @@ -0,0 +1,68 @@ +--- +title: Configuration +description: Environment variables, API keys, and authentication +--- + +import { Aside } from '@astrojs/starlight/components'; + +All configuration in recon-web is managed through environment variables. The easiest way to set them is with a `.env` file in the project root. + +```bash +cp .env.example .env +``` + +## Core settings + +These control general server behavior and resource limits. + +| Variable | Default | Description | +|---|---|---| +| `PORT` | `3000` | Port the API server listens on. | +| `HOST` | `0.0.0.0` | Bind address for the API server. | +| `API_TIMEOUT_LIMIT` | `30000` | Maximum time in milliseconds for a single check to complete before it is aborted. | +| `API_CORS_ORIGIN` | `*` | Allowed CORS origins. Set to your domain in production. | +| `MAX_CONCURRENCY` | `8` | Number of checks that run in parallel during a scan. Lower this on resource-constrained machines. | +| `DB_PATH` | _(built-in default)_ | Path to the SQLite database file used for scan history. | +| `CHROME_PATH` | _(auto-detected)_ | Path to a Chromium or Chrome binary. Required for screenshot and some rendering-based checks. | + +<Aside type="tip"> + If you are running inside Docker, `CHROME_PATH` is already set in the image. You only need to configure it when running from source on a machine where Chromium is installed in a non-standard location. +</Aside> + +## API keys + +API keys are **optional**. Every built-in check works without any keys configured. Adding keys enables extra checks or removes rate limits imposed by third-party services. + +| Variable | Service | Notes | +|---|---|---| +| `GOOGLE_CLOUD_API_KEY` | Google PageSpeed Insights and Safe Browsing | Enables Lighthouse performance audits and malware/phishing reputation checks. | +| `VIRUSTOTAL_API_KEY` | VirusTotal | Free tier allows 500 requests per day. Used for domain and URL reputation scanning. | +| `ABUSEIPDB_API_KEY` | AbuseIPDB | Free tier allows 1,000 checks per day. Used for IP abuse confidence scoring. | +| `CLOUDMERSIVE_API_KEY` | Cloudmersive | Provides additional threat intelligence and URL safety checks. | +| `BUILT_WITH_API_KEY` | BuiltWith | Reveals the full technology stack behind a target site. | +| `TRANCO_API_KEY` | Tranco List | Checks the target's ranking in the Tranco top-sites list. | + +<Aside type="note"> + You do not need all keys to get useful results. Start with no keys, review your scan output, and add keys for the services whose extra data you find valuable. +</Aside> + +## Authentication + +By default, recon-web is open — anyone who can reach the server can run scans. To restrict access, enable bearer-token authentication. + +| Variable | Default | Description | +|---|---|---| +| `AUTH_ENABLED` | `false` | Set to `true` to require a bearer token on every API request. | +| `AUTH_TOKEN` | _(none)_ | The secret token clients must send in the `Authorization` header. | + +When authentication is enabled, all requests to the API must include the header: + +``` +Authorization: Bearer <your-token> +``` + +The web UI will prompt for the token on first load and store it in the browser's local storage. CLI users can pass it via the `--token` flag or the `RECON_WEB_TOKEN` environment variable. + +<Aside type="caution"> + Always enable authentication if your instance is exposed to the internet. Without it, anyone can trigger scans from your server. +</Aside> diff --git a/docs/src/content/docs/getting-started/first-scan.mdx b/docs/src/content/docs/getting-started/first-scan.mdx new file mode 100644 index 0000000..5f5da18 --- /dev/null +++ b/docs/src/content/docs/getting-started/first-scan.mdx @@ -0,0 +1,69 @@ +--- +title: Your First Scan +description: Walk through your first website scan step by step +--- + +import { Steps } from '@astrojs/starlight/components'; + +This guide walks you through running a scan and understanding the results. + +<Steps> + +1. **Enter a URL** + + Open the recon-web home page and type a full URL (e.g., `https://example.com`) into the input field. Press **Scan** to start. The URL must include the protocol — `https://` or `http://`. + +2. **Watch results stream in real time** + + recon-web uses **Server-Sent Events (SSE)** to push each check's result to the browser the moment it finishes. You will see a progress bar advance as checks complete, and individual result cards appear on the page one by one. There is no need to wait for the entire scan — you can start reading results immediately. + +3. **Understand the results** + + Every check belongs to one of six categories: + + | Category | What it covers | + |---|---| + | **Security** | TLS/SSL configuration, HTTP security headers, cookie flags, CORS policy, content security policy, known vulnerabilities. | + | **DNS** | DNS records (A, AAAA, MX, TXT, CNAME, NS), DNSSEC status, CAA records, mail configuration. | + | **Network** | Open ports, traceroute, IP geolocation, ASN information, firewall detection. | + | **Content** | Technology stack, CMS detection, linked pages, canonical tags, robots.txt, sitemap analysis. | + | **Meta** | WHOIS data, domain age, Tranco ranking, social media tags, favicon, language detection. | + | **Performance** | Lighthouse scores, page load timing, resource sizes, redirect chains, compression checks. | + + Each result also has a **status indicator**: + + | Color | Status | Meaning | + |---|---|---| + | Green | **OK** | The check passed with no issues. | + | Red | **Issues** | A potential problem or misconfiguration was detected. | + | Grey | **Info** | Informational data — nothing actionable, but useful context. | + | Dimmed | **Skipped** | The check could not run (e.g., missing API key or the target did not respond). | + +4. **Filter and sort** + + Use the controls above the results to narrow down what you see: + + - **Category pills** — click a category name to show only checks in that category. + - **Status pills** — click a status (OK, Issues, Info, Skipped) to filter by outcome. + - **Search** — type a keyword to filter results by name or content. + - **Sort A-Z** — toggle alphabetical sorting to quickly find a specific check by name. + + Filters combine, so you can for example view only **Security** checks with an **Issues** status. + +5. **Explore details** + + Each result card has two action icons: + + - **Info icon** — opens a plain-language explanation of what the check does, why it matters, and what you can do if it flagged an issue. + - **Code icon** — shows the raw JSON response returned by the check, useful for automation or debugging. + +6. **Review history and compare scans** + + Every scan is saved automatically. Open the **History** page to see all past scans. From there you can: + + - **Compare two scans** — select any two scans of the same (or different) targets and view a side-by-side diff highlighting what changed between them. + - **Download reports** — export a scan as a JSON or PDF report for sharing or archival. + +</Steps> + +You now have everything you need to start scanning. For deeper customization, see the [Configuration](/getting-started/configuration/) reference. diff --git a/docs/src/content/docs/getting-started/quick-start.mdx b/docs/src/content/docs/getting-started/quick-start.mdx new file mode 100644 index 0000000..39d450f --- /dev/null +++ b/docs/src/content/docs/getting-started/quick-start.mdx @@ -0,0 +1,86 @@ +--- +title: Quick Start +description: Get recon-web running in under a minute +--- + +import { Steps, Tabs, TabItem } from '@astrojs/starlight/components'; + +There are several ways to get recon-web up and running. Pick the method that suits your workflow best. + +<Tabs> + <TabItem label="Docker (One-liner)"> + The fastest way to run a scan — no setup required. Just pull and run: + + ```bash + docker run --rm ghcr.io/brunoafk/recon-web/cli scan example.com + ``` + + This pulls the latest CLI image, scans the target, prints the results to stdout, and cleans up the container automatically. + </TabItem> + + <TabItem label="Docker Compose"> + For the full experience — web UI, persistent history, and all services — use Docker Compose. + + <Steps> + 1. Clone the repository: + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + ``` + + 2. Create your environment file: + ```bash + cp .env.example .env + ``` + + 3. Start all services: + ```bash + docker compose up + ``` + + 4. Open the web UI at [http://localhost:8080](http://localhost:8080) and run your first scan. + </Steps> + </TabItem> + + <TabItem label="From Source"> + If you want to develop or customize recon-web, run it directly with Node.js. + + **Prerequisites:** Node.js 24+ and npm. + + <Steps> + 1. Clone and install dependencies: + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + npm install + ``` + + 2. Copy the example environment file: + ```bash + cp .env.example .env + ``` + + 3. Start the API server and web UI in separate terminals: + ```bash + # Terminal 1 — API server + npm run dev + + # Terminal 2 — Web UI + npm run dev:web + ``` + + 4. Open [http://localhost:8080](http://localhost:8080) in your browser. + </Steps> + </TabItem> +</Tabs> + +## What you'll see + +Once everything is running, the scan flow works like this: + +1. You enter a target URL in the web UI (or pass it as a CLI argument). +2. recon-web kicks off dozens of security and reconnaissance checks in parallel — DNS lookups, TLS analysis, header inspection, port scanning, performance audits, and more. +3. Results stream back to the UI in real time via server-sent events. Each check appears as it completes, so you don't have to wait for the full scan to finish. +4. When all checks are done, the full report is saved to your scan history for later review, comparison, and export. + +Head over to [Configuration](/getting-started/configuration/) to add API keys and tune settings, or jump straight to [Your First Scan](/getting-started/first-scan/) for a detailed walkthrough of the results. diff --git a/docs/src/content/docs/guides/cli.mdx b/docs/src/content/docs/guides/cli.mdx new file mode 100644 index 0000000..e5cf623 --- /dev/null +++ b/docs/src/content/docs/guides/cli.mdx @@ -0,0 +1,224 @@ +--- +title: CLI +description: Using the recon-web command-line interface +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +The recon-web CLI lets you run scans, inspect individual handlers, and integrate results into CI/CD pipelines -- all from the terminal. + +## Installation + +<Tabs> + <TabItem label="From source"> + Clone the monorepo and install dependencies: + + ```bash + git clone https://github.com/brunoafk/recon-web.git + cd recon-web + npm install + ``` + + The CLI entry point is in `packages/cli`. You can run it directly with: + + ```bash + node packages/cli/dist/index.js scan example.com + ``` + + Or link it globally during development: + + ```bash + npm run build + npm link --workspace=packages/cli + recon-web scan example.com + ``` + </TabItem> + + <TabItem label="Docker"> + No installation needed. Pull and run the pre-built image: + + ```bash + docker run --rm ghcr.io/brunoafk/recon-web/cli scan example.com + ``` + </TabItem> +</Tabs> + +## Commands + +### `scan` -- full scan (default) + +Run all analysis handlers against a URL: + +```bash +recon-web scan https://example.com +``` + +The `scan` command is the default, so you can also write: + +```bash +recon-web https://example.com +``` + +### Individual handler commands + +Every registered handler is available as a sub-command. These are auto-generated from the handler registry, so the list grows as new handlers are added. Examples: + +```bash +recon-web ssl https://example.com +recon-web dns https://example.com +recon-web headers https://example.com +recon-web ports https://example.com +recon-web whois https://example.com +recon-web tech-stack https://example.com +``` + +Run `recon-web --help` to see the full list of available handlers. + +## Output formats + +### Colored text (default) + +The default output uses coloured text with category badges, status indicators, and compact summaries: + +```bash +recon-web scan https://example.com +``` + +Add `--verbose` (`-v`) for expanded output showing all fields: + +```bash +recon-web scan -v https://example.com +``` + +### JSON + +Use `--json` or `--format json` to get machine-readable output: + +```bash +recon-web scan --json https://example.com > results.json +``` + +The JSON output contains `{ url, results }` where `results` is a map of handler names to their result objects. + +### JUnit XML + +Use `--format junit` to produce JUnit XML, suitable for CI test reporters: + +```bash +recon-web scan --format junit https://example.com > results.xml +``` + +Each handler becomes a `<testcase>`. Handlers that errored become `<failure>` elements and skipped handlers become `<skipped>` elements. + +## CI/CD integration + +### Fail-on rules + +The `--fail-on` flag causes the CLI to exit with code 1 if specified conditions are met. This lets you gate deployments on security criteria. + +```bash +recon-web scan --fail-on ssl:expired https://example.com +``` + +Rules follow the format `handler:condition[:value]`. Supported conditions: + +| Rule | Meaning | +|---|---| +| `ssl:expired` | Fail if the SSL certificate is expired | +| `security-score:below:80` | Fail if the handler's `score` field is below the threshold | +| `headers:missing:content-security-policy` | Fail if a specific field is missing from the result | + +Multiple rules can be passed: + +```bash +recon-web scan \ + --fail-on ssl:expired \ + --fail-on security-score:below:80 \ + https://example.com +``` + +### JUnit for test reporters + +Most CI systems (GitHub Actions, GitLab CI, Jenkins) can ingest JUnit XML to display test results in the UI: + +```yaml +# GitHub Actions example +- name: Security scan + run: recon-web scan --format junit https://staging.example.com > scan-results.xml + +- name: Publish results + uses: mikepenz/action-junit-report@v4 + with: + report_paths: scan-results.xml +``` + +## Diff comparison + +Compare the current scan against a saved baseline to detect changes: + +```bash +# Save a baseline +recon-web scan --json https://example.com > baseline.json + +# Later, compare against it +recon-web scan --diff baseline.json https://example.com +``` + +The diff output highlights handlers that were added, removed, or changed between the two scans. + +## The `--only` flag + +Run a subset of handlers by passing a comma-separated list: + +```bash +recon-web scan --only ssl,dns,headers https://example.com +``` + +This is useful for faster, focused scans when you only care about specific checks. + +## Docker usage + +The CLI Docker image works exactly like the local CLI. Pass arguments after the image name: + +```bash +# Full scan +docker run --rm ghcr.io/brunoafk/recon-web/cli scan https://example.com + +# JSON output +docker run --rm ghcr.io/brunoafk/recon-web/cli scan --json https://example.com + +# Single handler +docker run --rm ghcr.io/brunoafk/recon-web/cli ssl https://example.com + +# With API keys +docker run --rm \ + -e GOOGLE_CLOUD_API_KEY=your-key \ + ghcr.io/brunoafk/recon-web/cli scan https://example.com + +# Fail-on in CI +docker run --rm ghcr.io/brunoafk/recon-web/cli \ + scan --fail-on ssl:expired --format junit https://example.com +``` + +<Aside type="tip"> + The Docker image includes Chromium, so handlers that require a browser (screenshots, Lighthouse) work out of the box. When running from source you may need to set `CHROME_PATH`. +</Aside> + +## Configuration + +The CLI reads configuration from two sources: + +1. **`.recon-web.json`** -- looked up first in the current directory, then in your home directory. Supports `apiKeys` and `chromePath`. +2. **Environment variables** -- override the config file. The same `GOOGLE_CLOUD_API_KEY`, `VIRUSTOTAL_API_KEY`, etc. variables used by the API server work with the CLI. + +Example `.recon-web.json`: + +```json +{ + "apiKeys": { + "GOOGLE_CLOUD_API_KEY": "AIza...", + "VIRUSTOTAL_API_KEY": "abcdef..." + }, + "chromePath": "/usr/bin/chromium" +} +``` diff --git a/docs/src/content/docs/guides/notifications.mdx b/docs/src/content/docs/guides/notifications.mdx new file mode 100644 index 0000000..72f187e --- /dev/null +++ b/docs/src/content/docs/guides/notifications.mdx @@ -0,0 +1,104 @@ +--- +title: Notifications +description: Set up Telegram and email alerts for scan changes +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +recon-web can notify you when a [scheduled scan](/recon-web/guides/scheduled-scans/) detects changes from the previous scan. Two channels are supported: Telegram and email. You can enable one or both. + +## Notification method + +| Variable | Default | Description | +|---|---|---| +| `NOTIFY_METHOD` | `both` | Delivery channel: `telegram`, `email`, or `both` | + +When set to `both`, recon-web sends to every channel that has valid credentials configured. A channel with missing or incomplete credentials is silently skipped. + +## Telegram setup + +<Steps> +1. **Create a bot** -- open a chat with [@BotFather](https://t.me/BotFather) on Telegram and send `/newbot`. Follow the prompts to choose a name. BotFather will give you a bot token. + +2. **Get your chat ID** -- send any message to your new bot, then open this URL in your browser (replace `<TOKEN>` with your bot token): + ``` + https://api.telegram.org/bot<TOKEN>/getUpdates + ``` + Look for the `"chat":{"id": 123456789}` value in the response. That number is your chat ID. + +3. **Configure the environment** -- add both values to your `.env` file: + ```bash + TELEGRAM_BOT_TOKEN=123456789:ABCdefGHI-jklMNOpqrSTUvwxYZ + TELEGRAM_CHAT_ID=123456789 + ``` +</Steps> + +<Aside type="tip"> + For group notifications, add the bot to a group chat and use the group's chat ID (which will be a negative number like `-1001234567890`). +</Aside> + +## Email setup (SMTP) + +Configure an SMTP server to send email alerts: + +| Variable | Example | Description | +|---|---|---| +| `SMTP_HOST` | `smtp.gmail.com` | SMTP server hostname | +| `SMTP_PORT` | `587` | SMTP port (587 for STARTTLS, 465 for SSL) | +| `SMTP_USER` | `you@gmail.com` | SMTP username / email address | +| `SMTP_PASS` | `abcd efgh ijkl mnop` | SMTP password or app password | +| `NOTIFY_EMAIL` | `alerts@example.com` | Recipient email address for notifications | + +### Gmail-specific instructions + +<Steps> +1. Enable 2-Step Verification on your Google account at [myaccount.google.com/security](https://myaccount.google.com/security). + +2. Generate an **App Password** at [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords). Select "Mail" as the app and "Other" as the device. Google will give you a 16-character password. + +3. Use the app password (not your regular Google password) as `SMTP_PASS`: + ```bash + SMTP_HOST=smtp.gmail.com + SMTP_PORT=587 + SMTP_USER=you@gmail.com + SMTP_PASS=abcd efgh ijkl mnop + NOTIFY_EMAIL=alerts@example.com + ``` +</Steps> + +<Aside type="caution"> + Do not use your regular Gmail password. Google blocks sign-ins from "less secure apps" by default. App passwords are the supported method. +</Aside> + +## What triggers notifications + +Notifications are sent when a **scheduled scan** detects that one or more handler results changed compared to the previous scan of the same URL. Examples of changes that trigger a notification: + +- SSL certificate renewed or expired +- DNS records added, removed, or modified +- Security headers added or removed +- New open ports detected +- Tech stack version changes +- WHOIS registrar or expiry date changes + +Manual scans (through the web UI, API, or CLI) are saved to history but do not trigger notifications. + +## Notification format + +Each notification message lists the changes with their severity: + +``` +recon-web: Changes detected + +[!!!] ssl / validTo + was: 2026-03-15T00:00:00Z + now: 2026-09-15T00:00:00Z + +[!] dns / MX + was: ["10 mail.example.com"] + now: ["10 mail.example.com","20 backup.example.com"] + +[i] tech-stack / technologies + was: [{"name":"Next.js","confidence":100}] + now: [{"name":"Next.js","confidence":100},{"name":"Tailwind CSS","confidence":90}] +``` diff --git a/docs/src/content/docs/guides/rest-api.mdx b/docs/src/content/docs/guides/rest-api.mdx new file mode 100644 index 0000000..27922ac --- /dev/null +++ b/docs/src/content/docs/guides/rest-api.mdx @@ -0,0 +1,168 @@ +--- +title: REST API +description: Using the recon-web REST API +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +The recon-web API is a REST service built on Fastify. It powers the web UI but can be used independently for automation, CI/CD pipelines, or custom integrations. + +## Base URL + +``` +http://localhost:3000 +``` + +The port is configurable via the `PORT` environment variable (default `3000`). + +## Interactive docs + +Swagger UI is available at [`/docs`](http://localhost:3000/docs). It lists every endpoint, shows request/response schemas, and lets you try requests directly from the browser. + +## Endpoints + +| Method | Path | Description | +|---|---|---| +| `GET` | `/health` | Health check -- returns handler count, uptime, and component status | +| `GET` | `/api/handlers` | List all available analysis handlers with metadata | +| `GET` | `/api?url=` | Run **all** handlers against a URL (synchronous, returns full results) | +| `GET` | `/api/stream?url=` | Run all handlers with **SSE streaming** (results arrive as they complete) | +| `GET` | `/api/<handler>?url=` | Run a **single** handler (e.g. `/api/ssl?url=example.com`) | +| `GET` | `/api/history` | List past scans (supports `?limit=` and `?offset=` for pagination) | +| `GET` | `/api/history/:id` | Get a single scan with all handler results | +| `GET` | `/api/history/:id/report` | Generate an HTML or PDF report (`?format=html` or `?format=pdf`) | +| `DELETE` | `/api/history/:id` | Delete a scan and its results | + +## SSE streaming + +The `/api/stream?url=` endpoint uses **Server-Sent Events** to push progress updates as each handler completes. This is what the web UI uses internally. + +### Event types + +| Event | Payload | When | +|---|---|---| +| `scan_started` | `{ type, url, scanId, totalHandlers }` | Scan begins | +| `handler_started` | `{ type, handler }` | A handler starts executing | +| `handler_finished` | `{ type, handler, result }` | A handler completes (includes data, error, or skipped) | +| `scan_completed` | `{ type, scanId, totalHandlers, durationMs }` | All handlers finished | + +### Example: consuming SSE with curl + +```bash +curl -N "http://localhost:3000/api/stream?url=https://example.com" +``` + +Each line follows the SSE format: + +``` +event: scan_started +data: {"type":"scan_started","url":"https://example.com","scanId":"abc123","totalHandlers":39} + +event: handler_finished +data: {"type":"handler_finished","handler":"ssl","result":{"data":{...}}} + +event: scan_completed +data: {"type":"scan_completed","scanId":"abc123","totalHandlers":39,"durationMs":12450} +``` + +### Example: consuming SSE with JavaScript + +```js +const source = new EventSource( + "http://localhost:3000/api/stream?url=https://example.com" +); + +source.addEventListener("handler_finished", (e) => { + const { handler, result } = JSON.parse(e.data); + console.log(`${handler}:`, result); +}); + +source.addEventListener("scan_completed", () => { + source.close(); +}); +``` + +## Authentication + +When `AUTH_ENABLED=true`, every request to `/api/*` endpoints (except `/api/handlers`) must include a bearer token: + +``` +Authorization: Bearer <your-token> +``` + +Set the token with the `AUTH_TOKEN` environment variable. See the [Configuration](/recon-web/getting-started/configuration/) reference for details. + +<Aside type="caution"> + Always enable authentication if your instance is exposed to the internet. Without it, anyone can trigger scans from your server. +</Aside> + +## curl examples + +### Health check + +```bash +curl http://localhost:3000/health +``` + +### List handlers + +```bash +curl http://localhost:3000/api/handlers +``` + +### Full scan (synchronous) + +```bash +curl "http://localhost:3000/api?url=https://example.com" +``` + +### Single handler + +```bash +curl "http://localhost:3000/api/ssl?url=https://example.com" +``` + +### Authenticated request + +```bash +curl -H "Authorization: Bearer my-secret-token" \ + "http://localhost:3000/api?url=https://example.com" +``` + +### List scan history + +```bash +curl "http://localhost:3000/api/history?limit=10&offset=0" +``` + +### Get a scan by ID + +```bash +curl http://localhost:3000/api/history/abc123 +``` + +### Download HTML report + +```bash +curl -o report.html http://localhost:3000/api/history/abc123/report +``` + +### Download PDF report + +```bash +curl -o report.pdf "http://localhost:3000/api/history/abc123/report?format=pdf" +``` + +### Delete a scan + +```bash +curl -X DELETE http://localhost:3000/api/history/abc123 +``` + +## Rate limiting + +The API enforces a default rate limit of **100 requests per 10-minute window** per IP address. If you exceed this limit you will receive a `429 Too Many Requests` response with a `Retry-After` header. + +<Aside type="tip"> + If you are running recon-web behind a reverse proxy, make sure the proxy forwards the client's real IP address (e.g. via `X-Forwarded-For`) so rate limiting applies per-client rather than per-proxy. +</Aside> diff --git a/docs/src/content/docs/guides/scheduled-scans.mdx b/docs/src/content/docs/guides/scheduled-scans.mdx new file mode 100644 index 0000000..51f6ad9 --- /dev/null +++ b/docs/src/content/docs/guides/scheduled-scans.mdx @@ -0,0 +1,72 @@ +--- +title: Scheduled Scans +description: Automate recurring scans with change detection +--- + +import { Aside } from '@astrojs/starlight/components'; + +recon-web can run scans on a cron schedule and automatically detect changes between consecutive scans of the same URL. When paired with [notifications](/recon-web/guides/notifications/), this gives you a lightweight monitoring system for your websites. + +## Enabling scheduled scans + +Add the following to your `.env` file: + +```bash +SCHEDULE_ENABLED=true +SCHEDULE_URLS=https://example.com,https://another-site.com +``` + +That is all you need. The scheduler starts automatically when the API server boots. + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `SCHEDULE_ENABLED` | `false` | Set to `true` to activate the scheduler | +| `SCHEDULE_CRON` | `0 0 * * *` | Cron expression controlling scan frequency | +| `SCHEDULE_URLS` | _(empty)_ | Comma-separated list of URLs to scan on each tick | + +### Cron syntax + +The `SCHEDULE_CRON` variable accepts standard five-field cron expressions: + +``` +┌───────────── minute (0-59) +│ ┌───────────── hour (0-23) +│ │ ┌───────────── day of month (1-31) +│ │ │ ┌───────────── month (1-12) +│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday) +│ │ │ │ │ +* * * * * +``` + +Common examples: + +| Expression | Schedule | +|---|---| +| `0 0 * * *` | Daily at midnight (default) | +| `0 */6 * * *` | Every 6 hours | +| `0 9 * * 1` | Every Monday at 09:00 | +| `*/30 * * * *` | Every 30 minutes | + +## Change detection + +Each time a scheduled scan completes, recon-web compares its results against the most recent previous scan of the same URL. If any handler's data changed -- for example a DNS record was added, an SSL certificate was renewed, or a security header was removed -- the changes are recorded. + +Changes are classified by severity: + +| Severity | Icon | Examples | +|---|---|---| +| **Critical** | `[!!!]` | SSL certificate expired, security header removed | +| **Warning** | `[!]` | DNS record changed, new open port detected | +| **Info** | `[i]` | Whois registrar updated, tech stack version bumped | + +When notifications are configured, changes are dispatched immediately after detection. See the [Notifications](/recon-web/guides/notifications/) guide for setup instructions. + +<Aside type="note"> + Change detection only triggers on scheduled scans. Manual scans through the web UI or API are saved to history but do not trigger notifications. +</Aside> + +## Kubernetes CronJob alternative + +If you are deploying on Kubernetes, you can use a native CronJob instead of the built-in scheduler. This gives you Kubernetes-native retry policies, resource limits, and observability. See the [Kubernetes deployment guide](/recon-web/deployment/kubernetes/) for a ready-made CronJob configuration in the Helm chart's `scanner` section. diff --git a/docs/src/content/docs/guides/web-ui.mdx b/docs/src/content/docs/guides/web-ui.mdx new file mode 100644 index 0000000..2e07389 --- /dev/null +++ b/docs/src/content/docs/guides/web-ui.mdx @@ -0,0 +1,92 @@ +--- +title: Web UI +description: Using the recon-web web interface +--- + +import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +The recon-web web UI is a single-page application that lets you scan any website, explore results in real time, and manage scan history from the browser. By default it is served at [http://localhost:8080](http://localhost:8080). + +## Dashboard overview + +The home page is centred around a single URL input. Type a full URL (including `https://`) and press **Scan** to kick off an analysis. Once the scan starts, the dashboard switches to a results view where check cards appear one by one as they complete. + +## Starting a scan + +<Steps> +1. Enter the target URL in the input field (e.g. `https://example.com`). +2. Press **Scan** or hit Enter. +3. The UI connects to the API via server-sent events and streams results as each handler finishes. +</Steps> + +<Aside type="tip"> + You can share a scan URL directly. The target is encoded in the query string, so bookmarking or sharing the link lets others see the same scan. +</Aside> + +## Real-time streaming + +recon-web uses **Server-Sent Events (SSE)** to push results to the browser the moment each check finishes. A progress bar at the top tracks overall completion so you can tell at a glance how far along the scan is. There is no need to wait for the full scan to finish before reading results. + +## Filtering results + +### Category filters + +Every check belongs to one of six categories. Click a category pill to show only results in that group: + +| Category | What it covers | +|---|---| +| **Security** | TLS/SSL, HTTP security headers, cookies, CORS, CSP, known vulnerabilities | +| **DNS** | A, AAAA, MX, TXT, CNAME, NS records, DNSSEC, CAA, mail config | +| **Network** | Open ports, traceroute, IP geolocation, ASN, firewall detection | +| **Content** | Tech stack, CMS, linked pages, canonical tags, robots.txt, sitemap | +| **Meta** | WHOIS, domain age, Tranco ranking, social tags, favicon, language | +| **Performance** | Lighthouse scores, page load timing, resource sizes, redirects, compression | + +### Status filters + +Each result card has a coloured status indicator. Click a status pill to filter by outcome: + +| Status | Meaning | +|---|---| +| **OK** (green) | Check passed with no issues | +| **Issues** (red) | A potential problem or misconfiguration was detected | +| **Info** (grey) | Informational data -- useful context, nothing actionable | +| **Skipped** (dimmed) | Check could not run (missing API key, target unresponsive, etc.) | + +### Search + +The search box above the results lets you filter by keyword. It matches against check names and result content, so typing `ssl` narrows the list to SSL-related results instantly. + +### Sorting + +Click the **A-Z** toggle to sort results alphabetically by check name. This is helpful when you know the exact check you are looking for. + +<Aside type="note"> + Filters combine. For example, selecting the **Security** category and the **Issues** status shows only security checks that flagged a problem. +</Aside> + +## Viewing details + +Each result card has two action icons in its header: + +- **Info icon** -- opens a plain-language explanation of what the check does, why it matters, and what to do if it flagged an issue. +- **Code icon** -- shows the raw JSON response returned by the handler. This is the same payload you would get from the REST API, useful for automation or debugging. + +## Scan history + +Every completed scan is saved automatically. Open the **History** page from the sidebar to browse past scans. Each entry shows the target URL, the date and time, and a summary of the results. + +### Comparing scans side-by-side + +Select any two scans from the history list and click **Compare**. The comparison view shows a side-by-side diff highlighting what changed between the two scans -- for example a new open port, an expiring SSL certificate, or a DNS record change. You can compare scans of the same target over time or scans of different targets. + +## Downloading reports + +From the history detail view, click **Download** to export the scan: + +- **HTML** -- a self-contained report you can open in any browser or email to a colleague. +- **PDF** -- a print-ready document (requires Chromium on the server for rendering). + +## Dark and light mode + +The UI respects your system colour-scheme preference and includes a toggle in the top bar to switch between dark and light mode manually. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 0000000..a1717b7 --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,40 @@ +--- +title: recon-web Documentation +description: Open-source website reconnaissance and security analysis tool. +template: splash +hero: + title: recon-web + tagline: Scan any website. Get the full picture — security, DNS, performance, tech stack — in seconds. + actions: + - text: Get Started + link: /recon-web/getting-started/quick-start/ + icon: right-arrow + variant: primary + - text: View on GitHub + link: https://github.com/BrunoAFK/recon-web + icon: external + variant: minimal +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; + +## What is recon-web? + +recon-web runs **39 checks** against any website and presents the results in a clean dashboard. It covers security, DNS, network, content, metadata, and performance — all from a single scan. + +Use it from the **web UI**, **REST API**, or **command line**. + +<CardGrid> + <Card title="Quick Start" icon="rocket"> + Up and running in under a minute with Docker or Node.js. [Get started →](/recon-web/getting-started/quick-start/) + </Card> + <Card title="39 Checks" icon="magnifier"> + SSL, DNS, open ports, tech stack, SEO, and more. [See all checks →](/recon-web/checks/overview/) + </Card> + <Card title="REST API" icon="setting"> + Swagger-documented API with SSE streaming. [API guide →](/recon-web/guides/rest-api/) + </Card> + <Card title="Self-hosted" icon="server"> + Docker, Kubernetes, or bare Node.js. Your data stays with you. [Deploy →](/recon-web/deployment/docker-local/) + </Card> +</CardGrid> diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..bcbf8b5 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} diff --git a/packages/core/src/handlers/sitemap.ts b/packages/core/src/handlers/sitemap.ts index 3dba887..28dc67f 100644 --- a/packages/core/src/handlers/sitemap.ts +++ b/packages/core/src/handlers/sitemap.ts @@ -1,58 +1,133 @@ import axios from 'axios'; import * as xml2js from 'xml2js'; -import type { AnalysisHandler, HandlerResult } from '../types.js'; +import type { AnalysisHandler } from '../types.js'; import { normalizeUrl } from '../utils/url.js'; export interface SitemapResult { - urls?: string[]; + sitemapUrl: string; + source: 'direct' | 'robots.txt'; + type: 'urlset' | 'sitemapindex' | 'unknown'; + urls: string[]; message?: string; [key: string]: unknown; } +/** + * Extract all sitemap URLs from robots.txt content. + */ +function extractSitemapUrlsFromRobots(robotsTxt: string): string[] { + const urls: string[] = []; + for (const line of robotsTxt.split('\n')) { + const match = line.match(/^\s*sitemap\s*:\s*(.+)/i); + if (match) urls.push(match[1].trim()); + } + return urls; +} + +/** + * Recursively find all <loc> values in parsed XML, case-insensitive. + */ +function extractLocs(obj: unknown): string[] { + if (obj == null) return []; + if (typeof obj === 'string') return []; + if (Array.isArray(obj)) return obj.flatMap(extractLocs); + + const record = obj as Record<string, unknown>; + const locs: string[] = []; + + for (const [key, value] of Object.entries(record)) { + if (key.toLowerCase() === 'loc') { + if (Array.isArray(value)) { + for (const v of value) { + if (typeof v === 'string') locs.push(v); + } + } else if (typeof value === 'string') { + locs.push(value); + } + } else if (typeof value === 'object' && value !== null) { + locs.push(...extractLocs(value)); + } + } + return locs; +} + +/** + * Detect sitemap type from parsed XML root keys (case-insensitive). + */ +function detectType(parsed: Record<string, unknown>): 'urlset' | 'sitemapindex' | 'unknown' { + const rootKey = Object.keys(parsed).find( + (k) => k !== '$' && k !== '_', + ); + if (!rootKey) return 'unknown'; + const lower = rootKey.toLowerCase(); + if (lower === 'urlset') return 'urlset'; + if (lower === 'sitemapindex') return 'sitemapindex'; + return 'unknown'; +} + export const sitemapHandler: AnalysisHandler<SitemapResult> = async (url, options) => { const targetUrl = normalizeUrl(url); - let sitemapUrl = `${targetUrl}/sitemap.xml`; - const hardTimeOut = options?.timeout ?? 5000; + const timeout = options?.timeout ?? 10_000; + const axiosOpts = { timeout, maxRedirects: 5 }; + // Strategy: try robots.txt first (it often has the canonical sitemap URL), + // then fall back to common sitemap paths. + const candidates: { url: string; source: 'robots.txt' | 'direct' }[] = []; + + // 1. Try robots.txt for sitemap directives try { - let sitemapRes; + const robotsRes = await axios.get(`${targetUrl}/robots.txt`, axiosOpts); + if (typeof robotsRes.data === 'string') { + const robotsSitemaps = extractSitemapUrlsFromRobots(robotsRes.data); + for (const s of robotsSitemaps) { + candidates.push({ url: s, source: 'robots.txt' }); + } + } + } catch { + // robots.txt not available — that's fine + } + + // 2. Add common paths as fallback (only if not already in candidates) + const commonPaths = [`${targetUrl}/sitemap.xml`, `${targetUrl}/sitemap_index.xml`]; + for (const p of commonPaths) { + if (!candidates.some((c) => c.url === p)) { + candidates.push({ url: p, source: 'direct' }); + } + } + + // 3. Try each candidate until one works + for (const candidate of candidates) { try { - sitemapRes = await axios.get(sitemapUrl, { timeout: hardTimeOut }); - } catch (error) { - const axiosError = error as any; - if (axiosError.response && axiosError.response.status === 404) { - // Try to fetch sitemap URL from robots.txt - const robotsRes = await axios.get(`${targetUrl}/robots.txt`, { timeout: hardTimeOut }); - const robotsTxt: string[] = robotsRes.data.split('\n'); - let foundSitemapUrl = ''; - - for (const line of robotsTxt) { - if (line.toLowerCase().startsWith('sitemap:')) { - foundSitemapUrl = line.split(' ')[1].trim(); - break; - } - } + const res = await axios.get(candidate.url, axiosOpts); + if (res.status !== 200 || typeof res.data !== 'string') continue; - if (!foundSitemapUrl) { - return { data: { urls: [], message: 'No sitemap was found for this site.' } }; - } + // Must look like XML + const trimmed = res.data.trim(); + if (!trimmed.startsWith('<?xml') && !trimmed.startsWith('<')) continue; - sitemapUrl = foundSitemapUrl; - sitemapRes = await axios.get(sitemapUrl, { timeout: hardTimeOut }); - } else { - throw error; + const parser = new xml2js.Parser({ strict: false, normalizeTags: true }); + let parsed: Record<string, unknown>; + try { + parsed = await parser.parseStringPromise(res.data); + } catch { + continue; // Malformed XML — try next candidate } - } - const parser = new xml2js.Parser(); - const sitemap = await parser.parseStringPromise(sitemapRes.data); + const type = detectType(parsed); + const urls = extractLocs(parsed); - return { data: sitemap as SitemapResult }; - } catch (error) { - const err = error as any; - if (err.code === 'ECONNABORTED') { - return { error: `Request timed-out after ${hardTimeOut}ms`, errorCode: 'TIMEOUT', errorCategory: 'site' }; + return { + data: { + sitemapUrl: candidate.url, + source: candidate.source, + type, + urls, + }, + }; + } catch { + continue; // Network error — try next candidate } - return { error: (error as Error).message }; } + + return { data: { sitemapUrl: '', source: 'direct', type: 'unknown', urls: [], message: 'No sitemap was found for this site.' } }; }; diff --git a/packages/core/src/handlers/ssl-labs.ts b/packages/core/src/handlers/ssl-labs.ts index 9a98031..27fa1dd 100644 --- a/packages/core/src/handlers/ssl-labs.ts +++ b/packages/core/src/handlers/ssl-labs.ts @@ -1,3 +1,5 @@ +import tls from 'node:tls'; +import https from 'node:https'; import type { AnalysisHandler } from '../types.js'; import { extractHostname } from '../utils/url.js'; @@ -20,141 +22,310 @@ export interface SslLabsResult { hasWarnings: boolean; endpoints: SslLabsEndpoint[]; testTime: string | null; + details?: SslGradeDetails; } -interface SslLabsApiResponse { - host?: string; - port?: number; - protocol?: string; - status?: string; - startTime?: number; - testTime?: number; - endpoints?: Array<{ - ipAddress?: string; - grade?: string; - gradeTrustIgnored?: string; - hasWarnings?: boolean; - isExceptional?: boolean; - delegation?: number; - }>; - [key: string]: unknown; +interface SslGradeDetails { + protocolVersion: string | null; + cipherSuite: string | null; + keyExchange: string | null; + certValid: boolean; + certExpiresDays: number | null; + certIssuer: string | null; + certSubject: string | null; + hasHsts: boolean; + hstsMaxAge: number | null; + checks: GradeCheck[]; } -/** Retryable status codes from SSL Labs (overloaded / rate-limited). */ -function isRetryable(status: number): boolean { - return status === 429 || status === 529 || status === 503; +interface GradeCheck { + id: string; + label: string; + passed: boolean; + severity: 'critical' | 'warning' | 'info'; + detail?: string; } -async function fetchWithRetry( - url: string, - signal?: AbortSignal, - maxRetries = 3, -): Promise<Response> { - for (let attempt = 0; attempt <= maxRetries; attempt++) { - if (signal?.aborted) throw new Error('Aborted'); +// Strong cipher suites (TLS 1.2+ AEAD ciphers) +const STRONG_CIPHERS = new Set([ + 'TLS_AES_128_GCM_SHA256', + 'TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-CHACHA20-POLY1305', + 'ECDHE-RSA-CHACHA20-POLY1305', +]); - const res = await fetch(url, { signal }); +// Weak protocols +const WEAK_PROTOCOLS = new Set(['SSLv3', 'TLSv1', 'TLSv1.1']); - if (!isRetryable(res.status) || attempt === maxRetries) { - return res; - } +/** + * Perform a TLS connection and gather all data needed for grading. + */ +async function gatherTlsData( + host: string, + timeout: number, +): Promise<{ + protocol: string | null; + cipher: { name: string; version: string } | null; + cert: tls.PeerCertificate | null; + authorized: boolean; + authError: string | null; +}> { + return new Promise((resolve) => { + const socket = tls.connect( + { host, port: 443, rejectUnauthorized: false, servername: host }, + () => { + const protocol = socket.getProtocol(); + const cipher = socket.getCipher(); + const cert = socket.getPeerCertificate(); + const authorized = socket.authorized; + const authError = socket.authorizationError ?? null; + socket.destroy(); + resolve({ + protocol, + cipher: cipher ? { name: cipher.name, version: cipher.version } : null, + cert: cert && Object.keys(cert).length > 0 ? cert : null, + authorized, + authError: authError ? String(authError) : null, + }); + }, + ); - // Wait 3-6s between retries - const delay = 3_000 + attempt * 1_500; - await new Promise((r) => setTimeout(r, delay)); - } + socket.setTimeout(timeout, () => { + socket.destroy(); + resolve({ protocol: null, cipher: null, cert: null, authorized: false, authError: 'TIMEOUT' }); + }); - // Unreachable but satisfies TS - throw new Error('SSL Labs: max retries exceeded'); + socket.on('error', (err) => { + socket.destroy(); + resolve({ protocol: null, cipher: null, cert: null, authorized: false, authError: err.message }); + }); + }); } /** - * Poll SSL Labs API until the assessment is complete. - * Uses fromCache=on to avoid triggering new scans (rate-limit friendly). - * Falls back to startNew=on if no cached result exists. + * Check HSTS header by making an HTTPS request. */ -async function assess( - host: string, - timeout: number, - signal?: AbortSignal, -): Promise<SslLabsApiResponse> { - const base = 'https://api.ssllabs.com/api/v3/analyze'; - - // First try cached result - const cacheUrl = `${base}?host=${encodeURIComponent(host)}&fromCache=on&maxAge=24`; - const cacheRes = await fetchWithRetry(cacheUrl, signal); - - if (cacheRes.ok) { - const data = (await cacheRes.json()) as SslLabsApiResponse; - if (data.status === 'READY') return data; - if (data.status === 'ERROR') throw new Error('SSL Labs assessment error'); - } else if (!isRetryable(cacheRes.status)) { - throw new Error(`SSL Labs API returned ${cacheRes.status}`); +async function checkHsts(host: string, timeout: number): Promise<{ has: boolean; maxAge: number | null }> { + return new Promise((resolve) => { + const req = https.get( + `https://${host}`, + { timeout, rejectUnauthorized: false, headers: { 'User-Agent': 'recon-web/1.0' } }, + (res) => { + const hsts = res.headers['strict-transport-security']; + res.resume(); + if (!hsts) { + resolve({ has: false, maxAge: null }); + return; + } + const match = hsts.match(/max-age=(\d+)/i); + resolve({ has: true, maxAge: match ? parseInt(match[1], 10) : null }); + }, + ); + req.on('error', () => resolve({ has: false, maxAge: null })); + req.on('timeout', () => { req.destroy(); resolve({ has: false, maxAge: null }); }); + }); +} + +/** + * Compute an SSL grade (A+, A, B, C, D, F) based on collected TLS data. + * + * Grading criteria (inspired by SSL Labs methodology): + * - Protocol version (TLS 1.3 best, TLS 1.2 good, TLS 1.1/1.0 bad, SSL terrible) + * - Certificate validity and trust chain + * - Certificate expiration (>30 days good, <30 warning, expired fail) + * - Cipher strength (AEAD good, CBC ok, RC4/DES/NULL terrible) + * - HSTS presence and max-age + * - Key exchange (ECDHE/DHE good, RSA-only less good) + */ +function computeGrade( + tlsData: Awaited<ReturnType<typeof gatherTlsData>>, + hsts: { has: boolean; maxAge: number | null }, +): { grade: string; hasWarnings: boolean; checks: GradeCheck[] } { + const checks: GradeCheck[] = []; + let score = 100; + let hasWarnings = false; + let hasCriticalFail = false; + + // 1. TLS connection successful? + if (!tlsData.protocol) { + return { + grade: 'F', + hasWarnings: true, + checks: [{ id: 'no-tls', label: 'TLS connection failed', passed: false, severity: 'critical', detail: tlsData.authError ?? 'Connection failed' }], + }; } - // No cache — start new assessment (publish=off to be respectful) - const startUrl = `${base}?host=${encodeURIComponent(host)}&publish=off&all=done`; - const startRes = await fetchWithRetry(startUrl, signal); + // 2. Certificate trusted? + if (tlsData.authorized) { + checks.push({ id: 'cert-trust', label: 'Certificate is trusted', passed: true, severity: 'critical' }); + } else { + checks.push({ id: 'cert-trust', label: 'Certificate is trusted', passed: false, severity: 'critical', detail: tlsData.authError ?? 'Not trusted' }); + score -= 40; + hasCriticalFail = true; + } - if (!startRes.ok) { - if (isRetryable(startRes.status)) { - throw new Error('SSL Labs is currently overloaded. Try again later.'); + // 3. Certificate expiration + if (tlsData.cert) { + const validTo = new Date(tlsData.cert.valid_to); + const now = new Date(); + const daysLeft = Math.floor((validTo.getTime() - now.getTime()) / (86400 * 1000)); + + if (daysLeft < 0) { + checks.push({ id: 'cert-expiry', label: 'Certificate not expired', passed: false, severity: 'critical', detail: `Expired ${Math.abs(daysLeft)} days ago` }); + score -= 40; + hasCriticalFail = true; + } else if (daysLeft < 30) { + checks.push({ id: 'cert-expiry', label: 'Certificate not expiring soon', passed: false, severity: 'warning', detail: `Expires in ${daysLeft} days` }); + score -= 10; + hasWarnings = true; + } else { + checks.push({ id: 'cert-expiry', label: 'Certificate not expiring soon', passed: true, severity: 'info', detail: `Expires in ${daysLeft} days` }); } - throw new Error(`SSL Labs API returned ${startRes.status}`); } - // Poll until ready (max ~timeout ms) - const deadline = Date.now() + Math.min(timeout, 90_000); - const pollUrl = `${base}?host=${encodeURIComponent(host)}&all=done`; + // 4. Protocol version + const protocol = tlsData.protocol!; + if (protocol === 'TLSv1.3') { + checks.push({ id: 'protocol', label: 'Modern protocol (TLS 1.3)', passed: true, severity: 'info' }); + } else if (protocol === 'TLSv1.2') { + checks.push({ id: 'protocol', label: 'Acceptable protocol (TLS 1.2)', passed: true, severity: 'info' }); + score -= 5; + } else if (WEAK_PROTOCOLS.has(protocol)) { + checks.push({ id: 'protocol', label: 'Protocol is secure', passed: false, severity: 'critical', detail: `Uses deprecated ${protocol}` }); + score -= 30; + hasCriticalFail = true; + } + + // 5. Cipher suite + if (tlsData.cipher) { + const cipherName = tlsData.cipher.name; + if (STRONG_CIPHERS.has(cipherName)) { + checks.push({ id: 'cipher', label: 'Strong cipher suite', passed: true, severity: 'info', detail: cipherName }); + } else if (cipherName.includes('CBC')) { + checks.push({ id: 'cipher', label: 'Strong cipher suite', passed: false, severity: 'warning', detail: `${cipherName} (CBC mode, prefer AEAD)` }); + score -= 10; + hasWarnings = true; + } else if (cipherName.includes('RC4') || cipherName.includes('DES') || cipherName.includes('NULL')) { + checks.push({ id: 'cipher', label: 'Strong cipher suite', passed: false, severity: 'critical', detail: `${cipherName} (insecure)` }); + score -= 30; + hasCriticalFail = true; + } else { + checks.push({ id: 'cipher', label: 'Cipher suite', passed: true, severity: 'info', detail: cipherName }); + } - while (Date.now() < deadline) { - if (signal?.aborted) throw new Error('Aborted'); - await new Promise((r) => setTimeout(r, 5_000)); + // Key exchange + if (cipherName.startsWith('TLS_') || cipherName.includes('ECDHE') || cipherName.includes('DHE')) { + checks.push({ id: 'key-exchange', label: 'Forward secrecy (ECDHE/DHE)', passed: true, severity: 'info' }); + } else { + checks.push({ id: 'key-exchange', label: 'Forward secrecy (ECDHE/DHE)', passed: false, severity: 'warning', detail: 'No forward secrecy' }); + score -= 10; + hasWarnings = true; + } + } - const pollRes = await fetchWithRetry(pollUrl, signal, 1); - if (!pollRes.ok) continue; + // 6. HSTS + if (hsts.has) { + if (hsts.maxAge && hsts.maxAge >= 31536000) { + checks.push({ id: 'hsts', label: 'HSTS enabled (1+ year)', passed: true, severity: 'info' }); + } else { + checks.push({ id: 'hsts', label: 'HSTS max-age >= 1 year', passed: false, severity: 'warning', detail: `max-age=${hsts.maxAge}` }); + score -= 5; + hasWarnings = true; + } + } else { + checks.push({ id: 'hsts', label: 'HSTS enabled', passed: false, severity: 'warning', detail: 'No Strict-Transport-Security header' }); + score -= 10; + hasWarnings = true; + } - const data = (await pollRes.json()) as SslLabsApiResponse; - if (data.status === 'READY') return data; - if (data.status === 'ERROR') throw new Error('SSL Labs assessment failed'); + // Compute letter grade from score + let grade: string; + if (hasCriticalFail) { + grade = score <= 20 ? 'F' : score <= 50 ? 'D' : 'C'; + } else if (score >= 95) { + grade = hasWarnings ? 'A' : 'A+'; + } else if (score >= 85) { + grade = 'A'; + } else if (score >= 70) { + grade = 'B'; + } else if (score >= 55) { + grade = 'C'; + } else if (score >= 40) { + grade = 'D'; + } else { + grade = 'F'; } - throw new Error('SSL Labs assessment timed out — the server may be slow to respond'); + return { grade, hasWarnings, checks }; } export const sslLabsHandler: AnalysisHandler<SslLabsResult> = async (url, options) => { try { const host = extractHostname(url); - const timeout = options?.timeout ?? 30_000; + const timeout = options?.timeout ?? 15_000; + + // Gather TLS data and HSTS in parallel + const [tlsData, hsts] = await Promise.all([ + gatherTlsData(host, timeout), + checkHsts(host, timeout), + ]); - const result = await assess(host, timeout); + if (!tlsData.protocol && !tlsData.cert) { + return { + error: tlsData.authError ?? `Could not establish TLS connection to ${host}`, + errorCode: 'CONNECTION_REFUSED', + errorCategory: 'site', + }; + } - const endpoints: SslLabsEndpoint[] = (result.endpoints ?? []).map((ep) => ({ - ipAddress: ep.ipAddress ?? '', - grade: ep.grade ?? '?', - gradeTrustIgnored: ep.gradeTrustIgnored ?? '?', - hasWarnings: ep.hasWarnings ?? false, - isExceptional: ep.isExceptional ?? false, - delegation: ep.delegation ?? 0, - })); + const { grade, hasWarnings, checks } = computeGrade(tlsData, hsts); - const bestGrade = endpoints.length > 0 - ? endpoints.reduce((best, ep) => (ep.grade < best ? ep.grade : best), endpoints[0].grade) + const cert = tlsData.cert; + const daysLeft = cert + ? Math.floor((new Date(cert.valid_to).getTime() - Date.now()) / (86400 * 1000)) : null; return { data: { - host: result.host ?? host, - port: result.port ?? 443, - protocol: result.protocol ?? 'https', - status: result.status ?? 'UNKNOWN', - grade: bestGrade, - gradeTrustIgnored: endpoints[0]?.gradeTrustIgnored ?? null, - hasWarnings: endpoints.some((ep) => ep.hasWarnings), - endpoints, - testTime: result.testTime - ? new Date(result.testTime).toISOString() - : null, + host, + port: 443, + protocol: 'https', + status: 'READY', + grade, + gradeTrustIgnored: grade, + hasWarnings, + endpoints: [ + { + ipAddress: host, + grade, + gradeTrustIgnored: grade, + hasWarnings, + isExceptional: grade === 'A+', + delegation: 0, + }, + ], + testTime: new Date().toISOString(), + details: { + protocolVersion: tlsData.protocol, + cipherSuite: tlsData.cipher?.name ?? null, + keyExchange: tlsData.cipher?.name.includes('ECDHE') + ? 'ECDHE' + : tlsData.cipher?.name.includes('DHE') + ? 'DHE' + : 'RSA', + certValid: tlsData.authorized, + certExpiresDays: daysLeft, + certIssuer: String(cert?.issuer?.O ?? cert?.issuer?.CN ?? '') || null, + certSubject: String(cert?.subject?.CN ?? '') || null, + hasHsts: hsts.has, + hstsMaxAge: hsts.maxAge, + checks, + }, }, }; } catch (error) { diff --git a/packages/core/src/handlers/tls.ts b/packages/core/src/handlers/tls.ts index 279af45..00c55b5 100644 --- a/packages/core/src/handlers/tls.ts +++ b/packages/core/src/handlers/tls.ts @@ -1,41 +1,83 @@ -import axios from 'axios'; -import type { AnalysisHandler, HandlerResult } from '../types.js'; +import tls from 'node:tls'; +import type { AnalysisHandler } from '../types.js'; import { extractHostname } from '../utils/url.js'; -const MOZILLA_TLS_OBSERVATORY_API = 'https://tls-observatory.services.mozilla.com/api/v1'; - export interface TlsResult { - id?: number; - timestamp?: string; target?: string; - replay?: number; has_tls?: boolean; - cert_id?: number; - trust_id?: number; is_valid?: boolean; completion_perc?: number; - connection_info?: Record<string, unknown>; + connection_info?: { + protocol?: string; + cipher?: string; + cert_valid?: boolean; + }; analysis?: Record<string, unknown>[]; [key: string]: unknown; } export const tlsHandler: AnalysisHandler<TlsResult> = async (url, options) => { - try { - const domain = extractHostname(url); - const scanResponse = await axios.post( - `${MOZILLA_TLS_OBSERVATORY_API}/scan?target=${domain}`, - ); - const scanId = scanResponse.data.scan_id; + const domain = extractHostname(url); + const timeout = options?.timeout ?? 10_000; + + return new Promise((resolve) => { + const socket = tls.connect( + { host: domain, port: 443, rejectUnauthorized: false, servername: domain }, + () => { + try { + const cipher = socket.getCipher(); + const protocol = socket.getProtocol(); + const cert = socket.getPeerCertificate(); + const authorized = socket.authorized; - if (typeof scanId !== 'number') { - return { error: 'Failed to get scan_id from TLS Observatory', errorCode: 'NO_DATA', errorCategory: 'info' }; - } + const analysis: Record<string, unknown>[] = []; - const resultResponse = await axios.get( - `${MOZILLA_TLS_OBSERVATORY_API}/results?id=${scanId}`, + if (cert && cert.subject) { + analysis.push({ + id: 'certificate', + subject: cert.subject, + issuer: cert.issuer, + valid_from: cert.valid_from, + valid_to: cert.valid_to, + serialNumber: cert.serialNumber, + fingerprint256: cert.fingerprint256, + }); + } + + socket.destroy(); + resolve({ + data: { + target: domain, + has_tls: true, + is_valid: authorized, + completion_perc: 100, + connection_info: { + protocol: protocol ?? undefined, + cipher: cipher?.name ?? undefined, + cert_valid: authorized, + }, + analysis, + }, + }); + } catch { + socket.destroy(); + resolve({ error: 'Failed to read TLS connection details', errorCode: 'NO_DATA', errorCategory: 'site' }); + } + }, ); - return { data: resultResponse.data as TlsResult }; - } catch (error) { - return { error: (error as Error).message }; - } + + socket.setTimeout(timeout, () => { + socket.destroy(); + resolve({ error: `TLS connection timed out after ${timeout}ms`, errorCode: 'TIMEOUT', errorCategory: 'site' }); + }); + + socket.on('error', (err) => { + socket.destroy(); + if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) { + resolve({ error: `Could not connect to ${domain}:443`, errorCode: 'CONNECTION_REFUSED', errorCategory: 'site' }); + } else { + resolve({ error: err.message }); + } + }); + }); }; diff --git a/packages/core/src/handlers/txt-records.ts b/packages/core/src/handlers/txt-records.ts index 0007eeb..ae2f671 100644 --- a/packages/core/src/handlers/txt-records.ts +++ b/packages/core/src/handlers/txt-records.ts @@ -1,4 +1,5 @@ import dns from 'dns/promises'; +import psl from 'psl'; import type { AnalysisHandler, HandlerResult } from '../types.js'; import { extractHostname } from '../utils/url.js'; @@ -6,29 +7,57 @@ export interface TxtRecordsResult { [key: string]: string; } +function parseTxtRecords(txtRecords: string[][]): TxtRecordsResult { + const result: TxtRecordsResult = {}; + for (const recordArray of txtRecords) { + const full = recordArray.join(''); + const eqIdx = full.indexOf('='); + if (eqIdx === -1) { + // No key=value format — use the whole string as key + result[full] = ''; + } else { + let key = full.substring(0, eqIdx); + const value = full.substring(eqIdx + 1); + // Handle duplicate keys by appending a suffix + if (key in result) { + let i = 2; + while (`${key} (${i})` in result) i++; + key = `${key} (${i})`; + } + result[key] = value; + } + } + return result; +} + +async function tryResolveTxt(hostname: string): Promise<string[][] | null> { + try { + return await dns.resolveTxt(hostname); + } catch { + return null; + } +} + export const txtRecordsHandler: AnalysisHandler<TxtRecordsResult> = async (url, options) => { try { const hostname = extractHostname(url); - const txtRecords = await dns.resolveTxt(hostname); - - // Parsing and formatting TXT records into a single object - const readableTxtRecords: TxtRecordsResult = txtRecords.reduce( - (acc: TxtRecordsResult, recordArray: string[]) => { - const recordObject = recordArray.reduce( - (recordAcc: TxtRecordsResult, recordString: string) => { - const splitRecord = recordString.split('='); - const key = splitRecord[0]; - const value = splitRecord.slice(1).join('='); - return { ...recordAcc, [key]: value }; - }, - {}, - ); - return { ...acc, ...recordObject }; - }, - {}, - ); - - return { data: readableTxtRecords }; + + // Try the exact hostname first + let txtRecords = await tryResolveTxt(hostname); + + // If no TXT records and hostname is a subdomain, try the root domain + if (!txtRecords) { + const parsed = psl.parse(hostname); + if ('listed' in parsed && parsed.domain && parsed.domain !== hostname) { + txtRecords = await tryResolveTxt(parsed.domain); + } + } + + if (!txtRecords || txtRecords.length === 0) { + return { data: {} as TxtRecordsResult }; + } + + return { data: parseTxtRecords(txtRecords) }; } catch (error) { const err = error as NodeJS.ErrnoException; if (err.code === 'ERR_INVALID_URL') { diff --git a/packages/core/src/presentation.ts b/packages/core/src/presentation.ts index 7049fdf..5db42a0 100644 --- a/packages/core/src/presentation.ts +++ b/packages/core/src/presentation.ts @@ -152,8 +152,8 @@ export const handlerPresentation: Record<string, HandlerPresentation> = { shortDescription: 'Scan the URL against 70+ antivirus and security engines.', }, 'ssl-labs': { - displayName: 'SSL Labs Grade', - shortDescription: 'Get a Qualys SSL Labs grade for the server\'s TLS configuration.', + displayName: 'SSL Grade', + shortDescription: 'Grade the server\'s TLS setup: protocol, cipher, certificate, and HSTS.', }, 'server-location': { displayName: 'Server Location', diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index d40175c..ae08448 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -356,9 +356,9 @@ export const registry: Record<string, HandlerRegistryEntry> = { handler: sslLabsHandler, metadata: { name: 'ssl-labs', - description: 'Run a Qualys SSL Labs assessment to get an overall SSL/TLS grade (A+, A, B, C, F) and detailed endpoint analysis.', + description: 'Grade the server\'s SSL/TLS configuration (A+ to F) by checking protocol version, cipher strength, certificate validity, and HSTS.', category: 'security', - requires: ['http'], + requires: ['tls'], }, }, virustotal: { diff --git a/packages/web/src/components/layout/Footer.tsx b/packages/web/src/components/layout/Footer.tsx index 4b0229d..67d094a 100644 --- a/packages/web/src/components/layout/Footer.tsx +++ b/packages/web/src/components/layout/Footer.tsx @@ -1,77 +1,47 @@ import { Link } from "react-router-dom"; -import { Github, BookOpen, Globe, Terminal } from "lucide-react"; +import { Github, Globe } from "lucide-react"; export default function Footer() { return ( <footer className="border-t border-border/50 mt-16"> - <div className="max-w-7xl mx-auto px-6 py-10"> - <div className="grid grid-cols-2 sm:grid-cols-4 gap-8 text-sm"> + <div className="max-w-7xl mx-auto px-6 py-6"> + <div className="flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted"> {/* Brand */} - <div className="col-span-2 sm:col-span-1"> - <Link to="/" className="inline-flex items-center gap-2 text-foreground mb-3"> - <Globe className="h-4 w-4 text-accent" /> - <span className="font-bold" style={{ fontFamily: "var(--font-display)" }}> - recon-web - </span> - </Link> - <p className="text-muted text-xs leading-relaxed"> - Open-source website reconnaissance and security analysis tool. - </p> - </div> - - {/* Product */} - <div> - <h4 className="font-semibold text-foreground text-xs uppercase tracking-wider mb-3">Product</h4> - <ul className="space-y-2 text-muted"> - <li> - <Link to="/" className="hover:text-foreground transition-colors">Home</Link> - </li> - <li> - <Link to="/history" className="hover:text-foreground transition-colors">History</Link> - </li> - <li> - <a href="/docs" className="hover:text-foreground transition-colors inline-flex items-center gap-1.5"> - <BookOpen size={12} /> - API Docs - </a> - </li> - </ul> - </div> + <Link to="/" className="inline-flex items-center gap-2 text-foreground hover:text-accent transition-colors"> + <Globe className="h-4 w-4 text-accent" /> + <span className="font-bold" style={{ fontFamily: "var(--font-display)" }}> + recon-web + </span> + </Link> - {/* Resources */} - <div> - <h4 className="font-semibold text-foreground text-xs uppercase tracking-wider mb-3">Resources</h4> - <ul className="space-y-2 text-muted"> - <li> - <Link to="/settings" className="hover:text-foreground transition-colors">Settings</Link> - </li> - <li> - <a - href="https://github.com/BrunoAFK/recon-web" - target="_blank" - rel="noopener noreferrer" - className="hover:text-foreground transition-colors inline-flex items-center gap-1.5" - > - <Github size={12} /> - GitHub - </a> - </li> - </ul> + {/* Links */} + <div className="flex items-center gap-5 text-xs"> + <a href="/docs" className="hover:text-foreground transition-colors"> + Docs + </a> + <a + href="https://github.com/BrunoAFK/recon-web" + target="_blank" + rel="noopener noreferrer" + className="hover:text-foreground transition-colors inline-flex items-center gap-1.5" + > + <Github size={12} /> + GitHub + </a> </div> - {/* CLI */} - <div> - <h4 className="font-semibold text-foreground text-xs uppercase tracking-wider mb-3">CLI</h4> - <div className="rounded-md bg-background/50 border border-border/30 px-2.5 py-2 font-mono text-[11px] text-muted break-all"> - <Terminal className="h-3 w-3 inline mr-1 text-accent" /> - docker run --rm ghcr.io/brunoafk/recon-web/cli scan - </div> - </div> - </div> - - <div className="mt-8 pt-4 border-t border-border/30 flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-muted/50"> - <span>© {new Date().getFullYear()} recon-web</span> - <span>GPL-2.0 License</span> + {/* Copyright */} + <span className="text-xs text-muted/50"> + © {new Date().getFullYear()}{" "} + <a + href="https://pavelja.com" + target="_blank" + rel="noopener noreferrer" + className="hover:text-foreground transition-colors" + > + Bruno Pavelja + </a> + </span> </div> </div> </footer> diff --git a/packages/web/src/components/results/ResultGrid.tsx b/packages/web/src/components/results/ResultGrid.tsx index 5cd9664..642753a 100644 --- a/packages/web/src/components/results/ResultGrid.tsx +++ b/packages/web/src/components/results/ResultGrid.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { MinusCircle, Search, @@ -49,6 +49,96 @@ function stringifySearchableResult(result: HandlerResultData | undefined): strin } } +/** + * DOM-aware masonry grid. Renders all items in a hidden single-column + * measurement container, reads their heights, then distributes them + * across columns using a shortest-column-first algorithm. + * Falls back to round-robin before measurements are available. + */ +function MasonryGrid<T extends { key: string }>({ + items, + columnCount, + renderItem, +}: { + items: T[]; + columnCount: number; + renderItem: (item: T, index: number) => React.ReactNode; +}) { + const cols = Math.max(1, columnCount); + const measureRef = useRef<HTMLDivElement>(null); + const [distribution, setDistribution] = useState<number[][]>(() => + buildRoundRobin(items.length, cols), + ); + + const itemKeys = items.map((i) => i.key).join(','); + + // After items render in the measurement container, read heights and distribute + useLayoutEffect(() => { + if (cols <= 1) { + setDistribution([items.map((_, i) => i)]); + return; + } + + const el = measureRef.current; + if (!el) { + setDistribution(buildRoundRobin(items.length, cols)); + return; + } + + const children = Array.from(el.children) as HTMLElement[]; + if (children.length !== items.length) { + setDistribution(buildRoundRobin(items.length, cols)); + return; + } + + const GAP = 16; + const colHeights = new Array(cols).fill(0); + const newDist: number[][] = Array.from({ length: cols }, () => []); + + for (let i = 0; i < items.length; i++) { + const shortest = colHeights.indexOf(Math.min(...colHeights)); + newDist[shortest].push(i); + colHeights[shortest] += children[i].offsetHeight + GAP; + } + + setDistribution(newDist); + }, [items.length, cols, itemKeys]); + + return ( + <> + {/* Hidden measurement container — single column, all items rendered */} + <div + ref={measureRef} + aria-hidden + className="fixed left-[-9999px] top-0" + style={{ width: `calc((100% - ${(cols - 1) * 16}px) / ${cols})`, visibility: 'hidden' }} + > + {items.map((item, i) => ( + <div key={item.key}>{renderItem(item, i)}</div> + ))} + </div> + + {/* Visible columns */} + <div className="flex gap-4"> + {distribution.map((indices, colIdx) => ( + <div key={colIdx} className="flex-1 min-w-0 space-y-4"> + {indices.map((i) => { + const item = items[i]; + return item ? <React.Fragment key={item.key}>{renderItem(item, i)}</React.Fragment> : null; + })} + </div> + ))} + </div> + </> + ); +} + +function buildRoundRobin(count: number, cols: number): number[][] { + const dist: number[][] = Array.from({ length: cols }, () => []); + for (let i = 0; i < count; i++) dist[i % cols].push(i); + return dist; +} + const CATEGORY_ORDER: HandlerCategory[] = [ "security", "dns", @@ -96,6 +186,18 @@ export default function ResultGrid({ const [statusOrder, setStatusOrder] = useState<StatusOrder>(getDefaultStatusOrder); const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set()); + // Track how many CSS columns are active based on viewport breakpoints + const [columnCount, setColumnCount] = useState(1); + useEffect(() => { + function update() { + const w = window.innerWidth; + setColumnCount(w >= 1280 ? 3 : w >= 768 ? 2 : 1); + } + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, []); + const { normalHandlers, skippedHandlers } = useMemo(() => { const normal: HandlerMetadata[] = []; const skipped: HandlerMetadata[] = []; @@ -347,7 +449,7 @@ export default function ResultGrid({ <div className="relative flex-1 min-w-[180px] max-w-xs"> <Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted" /> <input - type="search" + type="text" value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search" @@ -372,26 +474,24 @@ export default function ResultGrid({ </section> {groupBy === "none" ? ( - <div className="columns-1 gap-4 md:columns-2 xl:columns-3"> - {filteredNormalHandlers.map((handler) => { - const idx = animIndex++; - return ( - <div key={handler.name} className="mb-4 break-inside-avoid"> - <ResultCard - name={handler.name} - displayName={handler.displayName} - category={handler.category} - description={handler.description} - shortDescription={handler.shortDescription} - result={results[handler.name]} - isLoading={isLoading && !results[handler.name]} - animDelay={idx * 30} - url={url} - /> - </div> - ); - })} - </div> + <MasonryGrid + items={filteredNormalHandlers.map((h) => ({ ...h, key: h.name }))} + columnCount={columnCount} + renderItem={(handler, idx) => ( + <ResultCard + key={handler.name} + name={handler.name} + displayName={handler.displayName} + category={handler.category} + description={handler.description} + shortDescription={handler.shortDescription} + result={results[handler.name]} + isLoading={isLoading && !results[handler.name]} + animDelay={idx * 30} + url={url} + /> + )} + /> ) : ( <GroupedCards handlers={filteredNormalHandlers} @@ -411,6 +511,7 @@ export default function ResultGrid({ return next; }); }} + columnCount={columnCount} /> )} @@ -426,27 +527,25 @@ export default function ResultGrid({ </h2> <span className="text-sm text-muted/40 tabular-nums">{filteredSkippedHandlers.length}</span> </div> - <div className="columns-1 gap-4 md:columns-2 xl:columns-3"> - {filteredSkippedHandlers.map((handler) => { - const idx = animIndex++; - return ( - <div key={handler.name} className="mb-4 break-inside-avoid"> - <ResultCard - name={handler.name} - displayName={handler.displayName} - category={handler.category} - description={handler.description} - shortDescription={handler.shortDescription} - result={results[handler.name]} - isLoading={false} - animDelay={idx * 30} - variant="skipped" - url={url} - /> - </div> - ); - })} - </div> + <MasonryGrid + items={filteredSkippedHandlers.map((h) => ({ ...h, key: h.name }))} + columnCount={columnCount} + renderItem={(handler, idx) => ( + <ResultCard + key={handler.name} + name={handler.name} + displayName={handler.displayName} + category={handler.category} + description={handler.description} + shortDescription={handler.shortDescription} + result={results[handler.name]} + isLoading={false} + animDelay={idx * 30} + variant="skipped" + url={url} + /> + )} + /> </section> )} </div> @@ -569,6 +668,7 @@ function GroupedCards({ animIndexRef, collapsedGroups, onToggleGroup, + columnCount, }: { handlers: HandlerMetadata[]; results: Record<string, HandlerResultData>; @@ -578,6 +678,7 @@ function GroupedCards({ statusOrder: StatusOrder; getResultStatus: (r: HandlerResultData | undefined) => StatusFilter; animIndexRef: { current: number }; + columnCount: number; collapsedGroups: Set<string>; onToggleGroup: (key: string) => void; }) { @@ -630,26 +731,27 @@ function GroupedCards({ <span className="text-sm text-muted tabular-nums">{items.length}</span> </button> {!isCollapsed && ( - <div className="columns-1 gap-4 md:columns-2 xl:columns-3"> - {items.map((handler) => { - const idx = animIndexRef.current++; + <MasonryGrid + items={items.map((h) => ({ ...h, key: h.name }))} + columnCount={columnCount} + renderItem={(handler, idx) => { + animIndexRef.current++; return ( - <div key={handler.name} className="mb-4 break-inside-avoid"> - <ResultCard - name={handler.name} - displayName={handler.displayName} - category={handler.category} - description={handler.description} - shortDescription={handler.shortDescription} - result={results[handler.name]} - isLoading={isLoading && !results[handler.name]} - animDelay={idx * 30} - url={url} - /> - </div> + <ResultCard + key={handler.name} + name={handler.name} + displayName={handler.displayName} + category={handler.category} + description={handler.description} + shortDescription={handler.shortDescription} + result={results[handler.name]} + isLoading={isLoading && !results[handler.name]} + animDelay={idx * 30} + url={url} + /> ); - })} - </div> + }} + /> )} </section> ); diff --git a/packages/web/src/components/results/renderers/SitemapRenderer.tsx b/packages/web/src/components/results/renderers/SitemapRenderer.tsx index 9fd29b1..6af27be 100644 --- a/packages/web/src/components/results/renderers/SitemapRenderer.tsx +++ b/packages/web/src/components/results/renderers/SitemapRenderer.tsx @@ -1,74 +1,55 @@ import { KeyValueRow } from "./primitives"; import type { RendererProps } from "./types"; +interface SitemapData { + sitemapUrl?: string; + source?: string; + type?: string; + urls?: string[]; + message?: string; + // Legacy format (xml2js raw output) + urlset?: Record<string, unknown>; + sitemapindex?: Record<string, unknown>; +} + +/** + * Extract URL strings from the handler result, handling both the new + * structured format ({ urls: string[] }) and legacy xml2js raw output. + */ function extractUrls(data: unknown): string[] { - if (Array.isArray(data)) { - return data.filter((item): item is string => typeof item === "string"); - } - if (data && typeof data === "object") { - const d = data as Record<string, unknown>; - // Handle { urls: string[] } - if (Array.isArray(d.urls)) return d.urls.filter((u): u is string => typeof u === "string"); - // Handle xml2js parsed sitemap: { urlset: { url: [{ loc: [string] }] } } - const urlset = d.urlset as Record<string, unknown> | undefined; - if (urlset && typeof urlset === "object") { - const urlEntries = (urlset as Record<string, unknown>).url; - if (Array.isArray(urlEntries)) { - return urlEntries - .map((entry: unknown) => { - if (typeof entry === "string") return entry; - if (entry && typeof entry === "object") { - const loc = (entry as Record<string, unknown>).loc; - if (Array.isArray(loc)) return String(loc[0]); - if (typeof loc === "string") return loc; - } - return null; - }) - .filter((u): u is string => u !== null); - } - } - // Handle { sitemapindex: { sitemap: [{ loc: [string] }] } } - const sitemapindex = d.sitemapindex as Record<string, unknown> | undefined; - if (sitemapindex && typeof sitemapindex === "object") { - const sitemaps = (sitemapindex as Record<string, unknown>).sitemap; - if (Array.isArray(sitemaps)) { - return sitemaps - .map((entry: unknown) => { - if (typeof entry === "string") return entry; - if (entry && typeof entry === "object") { - const loc = (entry as Record<string, unknown>).loc; - if (Array.isArray(loc)) return String(loc[0]); - if (typeof loc === "string") return loc; - } - return null; - }) - .filter((u): u is string => u !== null); - } - } + if (!data || typeof data !== "object") return []; + const d = data as SitemapData; + + // New format: handler already extracted urls + if (Array.isArray(d.urls)) { + return d.urls.filter((u): u is string => typeof u === "string"); } + return []; } export function SitemapRenderer({ data }: RendererProps) { - const message = - data && typeof data === "object" && !Array.isArray(data) && typeof (data as { message?: unknown }).message === "string" - ? (data as { message: string }).message - : undefined; + const d = data as SitemapData | undefined; + const message = d?.message; const urls = extractUrls(data); if (urls.length === 0) { return <span className="text-sm text-muted">{message ?? "No sitemap URLs found"}</span>; } + const isIndex = d?.type === "sitemapindex"; + return ( <div> - <KeyValueRow label="Total URLs" value={String(urls.length)} /> + {d?.sitemapUrl && <KeyValueRow label="Sitemap URL" value={d.sitemapUrl} />} + {d?.source && <KeyValueRow label="Found via" value={d.source} />} + <KeyValueRow label={isIndex ? "Sub-sitemaps" : "Total URLs"} value={String(urls.length)} /> <ul className="mt-1.5 space-y-0.5"> - {urls.slice(0, 5).map((url, i) => ( + {urls.slice(0, 8).map((url, i) => ( <li key={i} className="text-sm text-muted truncate">{url}</li> ))} - {urls.length > 5 && ( - <li className="text-sm text-muted">+{urls.length - 5} more</li> + {urls.length > 8 && ( + <li className="text-sm text-muted">+{urls.length - 8} more</li> )} </ul> </div> diff --git a/packages/web/src/components/results/renderers/SslLabsRenderer.tsx b/packages/web/src/components/results/renderers/SslLabsRenderer.tsx index 2aa0e63..04f1088 100644 --- a/packages/web/src/components/results/renderers/SslLabsRenderer.tsx +++ b/packages/web/src/components/results/renderers/SslLabsRenderer.tsx @@ -1,12 +1,24 @@ -import { KeyValueTable, SectionLabel, Chip } from "./primitives"; +import { ChecklistItem, KeyValueTable, SectionLabel, Chip } from "./primitives"; import type { RendererProps } from "./types"; -interface SslLabsEndpoint { - ipAddress: string; - grade: string; - gradeTrustIgnored: string; - hasWarnings: boolean; - isExceptional: boolean; +interface GradeCheck { + id: string; + label: string; + passed: boolean; + severity: "critical" | "warning" | "info"; + detail?: string; +} + +interface SslLabsDetails { + protocolVersion?: string | null; + cipherSuite?: string | null; + certValid?: boolean; + certExpiresDays?: number | null; + certIssuer?: string | null; + certSubject?: string | null; + hasHsts?: boolean; + hstsMaxAge?: number | null; + checks?: GradeCheck[]; } interface SslLabsData { @@ -14,8 +26,13 @@ interface SslLabsData { grade?: string | null; gradeTrustIgnored?: string | null; hasWarnings?: boolean; - endpoints?: SslLabsEndpoint[]; + endpoints?: Array<{ + ipAddress: string; + grade: string; + hasWarnings: boolean; + }>; testTime?: string | null; + details?: SslLabsDetails; } function gradeColor(grade: string): "success" | "warning" | "danger" | "default" { @@ -28,7 +45,7 @@ function gradeColor(grade: string): "success" | "warning" | "danger" | "default" export function SslLabsRenderer({ data }: RendererProps) { const d = data as SslLabsData | undefined; - if (!d) return <span className="text-sm text-muted">No SSL Labs data</span>; + if (!d) return <span className="text-sm text-muted">No SSL grade data</span>; const grade = d.grade; @@ -52,7 +69,7 @@ export function SslLabsRenderer({ data }: RendererProps) { {grade} </span> <div> - <p className="text-sm font-medium text-foreground">SSL Labs Grade</p> + <p className="text-sm font-medium text-foreground">SSL Grade</p> {d.hasWarnings && ( <p className="text-xs text-warning">Has warnings</p> )} @@ -60,21 +77,32 @@ export function SslLabsRenderer({ data }: RendererProps) { </div> )} - {/* Endpoint details */} - {d.endpoints && d.endpoints.length > 0 && ( + {/* Detailed checks */} + {d.details?.checks && d.details.checks.length > 0 && ( + <div className="space-y-1.5"> + {d.details.checks.map((check) => ( + <div key={check.id}> + <ChecklistItem label={check.label} passed={check.passed} /> + {check.detail && !check.passed && ( + <p className="text-xs text-muted ml-6">{check.detail}</p> + )} + </div> + ))} + </div> + )} + + {/* Connection info */} + {d.details && ( <div> - <SectionLabel>Endpoints</SectionLabel> - <div className="space-y-2 mt-1"> - {d.endpoints.map((ep) => ( - <div - key={ep.ipAddress} - className="flex items-center justify-between gap-3 py-1.5 border-b border-border/15 last:border-0" - > - <span className="text-sm text-foreground font-mono break-all">{ep.ipAddress}</span> - <Chip label={ep.grade} variant={gradeColor(ep.grade)} /> - </div> - ))} - </div> + <SectionLabel>Connection</SectionLabel> + <KeyValueTable + items={[ + ...(d.details.protocolVersion ? [{ label: "Protocol", value: d.details.protocolVersion }] : []), + ...(d.details.cipherSuite ? [{ label: "Cipher", value: d.details.cipherSuite }] : []), + ...(d.details.certIssuer ? [{ label: "Issuer", value: d.details.certIssuer }] : []), + ...(d.details.certExpiresDays != null ? [{ label: "Cert expires", value: `${d.details.certExpiresDays} days` }] : []), + ]} + /> </div> )} diff --git a/packages/web/src/components/results/renderers/TxtRecordsRenderer.tsx b/packages/web/src/components/results/renderers/TxtRecordsRenderer.tsx index d320401..6dc375b 100644 --- a/packages/web/src/components/results/renderers/TxtRecordsRenderer.tsx +++ b/packages/web/src/components/results/renderers/TxtRecordsRenderer.tsx @@ -9,14 +9,12 @@ export function TxtRecordsRenderer({ data }: RendererProps) { let records: string[] = []; if (Array.isArray(data)) { - // Could be string[][] or string[] — handle both records = data.flatMap((item) => { if (typeof item === "string") return [item]; if (Array.isArray(item)) return item.filter((s): s is string => typeof s === "string"); return [String(item)]; }); } else if (typeof data === "object") { - // Could be { records: string[][] } or similar wrapper const d = data as Record<string, unknown>; const inner = d.records ?? d.txtRecords ?? d.txt; if (Array.isArray(inner)) { @@ -25,6 +23,18 @@ export function TxtRecordsRenderer({ data }: RendererProps) { if (Array.isArray(item)) return item.filter((s): s is string => typeof s === "string"); return [String(item)]; }); + } else { + // Key-value format: { "v": "spf1...", "google-site-verification": "abc" } + // Convert to "key=value" strings, skipping known wrapper keys + const skipKeys = new Set(["records", "txtRecords", "txt"]); + for (const [key, value] of Object.entries(d)) { + if (skipKeys.has(key)) continue; + if (value === "") { + records.push(key); + } else { + records.push(`${key}=${String(value)}`); + } + } } } diff --git a/packages/web/src/pages/Results.tsx b/packages/web/src/pages/Results.tsx index e10c16c..bb9b8d6 100644 --- a/packages/web/src/pages/Results.tsx +++ b/packages/web/src/pages/Results.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Link, useParams, useNavigate } from "react-router-dom"; +import { Link, useParams } from "react-router-dom"; import { ArrowLeft, AlertTriangle, ExternalLink, Loader2, Globe, Eye } from "lucide-react"; import { useHandlers, useLiveScan } from "@/hooks/use-scan"; import ResultGrid from "@/components/results/ResultGrid"; @@ -13,7 +13,6 @@ export default function Results() { const decodedUrl = url ? decodeURIComponent(url) : ""; const scan = useLiveScan(decodedUrl); const handlers = useHandlers(); - const navigate = useNavigate(); // Whether the user dismissed the loading screen to see partial results const [showPartial, setShowPartial] = useState(false); @@ -37,13 +36,6 @@ export default function Results() { } }, [scan.scanId]); - // Navigate to history page once scan completes to show final results - useEffect(() => { - if (scan.status === "completed" && scan.scanId) { - navigate(`/history/${scan.scanId}`, { replace: true }); - } - }, [scan.status, scan.scanId, navigate]); - // Stale scan timeout — if no scanId arrives within 15s, something is wrong const [staleTimeout, setStaleTimeout] = useState(false); useEffect(() => { From 16fe727afed6ebb2d6bb30b37df7ab42c331227e Mon Sep 17 00:00:00 2001 From: Bruno Pavelja <bruno@pavelja.me> Date: Mon, 6 Apr 2026 00:05:43 +0200 Subject: [PATCH 3/6] add GitHub Actions workflow for docs deployment to GitHub Pages --- .github/workflows/docs.yml | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..99ea5cd --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,51 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: docs/package-lock.json + + - name: Install dependencies + working-directory: docs + run: npm ci + + - name: Build docs + working-directory: docs + run: npx astro build + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 From be55b0eec9f696b72159130a8c095566891e783f Mon Sep 17 00:00:00 2001 From: Bruno Pavelja <bruno@pavelja.me> Date: Mon, 6 Apr 2026 00:13:09 +0200 Subject: [PATCH 4/6] add Cloudflare Pages deployment for static lite scan --- .github/workflows/static.yml | 36 ++++++++++++++++++++++++++++++++++++ .gitlab-ci.yml | 23 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..f9c3172 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,36 @@ +name: Deploy Static (Cloudflare Pages) + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build core + run: npx tsc -b packages/core + + - name: Build static site + run: npm run build -w @recon-web/static + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy packages/static/dist --project-name=recon-web diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 838728b..4701f46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ stages: - build - scan - push + - deploy variables: NODE_VERSION: "24" @@ -164,3 +165,25 @@ push-cli: - docker tag ${IMAGE_PREFIX}/cli:${CI_COMMIT_SHA} ${IMAGE_PREFIX}/cli:latest - docker push ${IMAGE_PREFIX}/cli:${CI_COMMIT_TAG} - docker push ${IMAGE_PREFIX}/cli:latest + +# ── Stage 7: Deploy static site to Cloudflare Pages ───────── + +deploy-static: + stage: deploy + image: node:${NODE_VERSION} + rules: + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ + - when: manual + allow_failure: true + cache: + key: npm-${CI_COMMIT_REF_SLUG} + paths: + - node_modules/ + script: + - npm ci + - npx tsc -b packages/core + - npm run build -w @recon-web/static + - npx wrangler pages deploy packages/static/dist --project-name=recon-web + environment: + name: cloudflare-pages + url: https://recon-web.pages.dev From 81fb6736a099b228aa798916dddfcacadbc005c8 Mon Sep 17 00:00:00 2001 From: Bruno Pavelja <bruno@pavelja.me> Date: Mon, 6 Apr 2026 00:32:07 +0200 Subject: [PATCH 5/6] Gitlab ci update --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4701f46..535a9f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,7 @@ audit: services: - docker:27-dind before_script: - - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} build-api: extends: .docker-build @@ -134,7 +134,7 @@ scan-cli: rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/ before_script: - - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} push-api: extends: .docker-push From a772a3d769bc96261381d377ef1e7a0830c604e6 Mon Sep 17 00:00:00 2001 From: Bruno Pavelja <bruno@pavelja.me> Date: Mon, 6 Apr 2026 00:47:43 +0200 Subject: [PATCH 6/6] isolate SQLite DB per test file to prevent SQLITE_BUSY in parallel runs --- .github/ISSUE_TEMPLATE/bug_report.yml | 60 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 40 +++++++++++++++ .github/pull_request_template.md | 32 ++++++++++++ packages/api/src/auth/auth.test.ts | 18 +++++-- packages/api/src/server.test.ts | 8 +++ 6 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..d85d97d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: Bug Report +description: Something isn't working as expected +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: What happened? + placeholder: Describe the bug clearly — what did you expect vs what actually happened. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Go to ... + 2. Enter ... + 3. Click ... + 4. See error + validations: + required: true + + - type: dropdown + id: component + attributes: + label: Component + options: + - Web UI + - REST API + - CLI + - Specific handler/check + - Docker / deployment + - Other + validations: + required: true + + - type: input + id: handler + attributes: + label: Handler name (if applicable) + placeholder: e.g. ssl, dns, sitemap + + - type: textarea + id: environment + attributes: + label: Environment + placeholder: | + - OS: macOS 15 / Ubuntu 24.04 / Windows 11 + - Node.js: 24.x + - Browser: Chrome 130 + - Deployment: Docker / source / Helm + render: text + + - type: textarea + id: screenshots + attributes: + label: Screenshots or logs + placeholder: Paste screenshots, error messages, or relevant log output. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c3680fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Documentation + url: https://brunoafk.github.io/recon-web + about: Check the docs before opening an issue diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a49d1ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,40 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + placeholder: What problem does this solve? Why do you need it? + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + placeholder: How should it work? Be as specific as you can. + validations: + required: true + + - type: dropdown + id: component + attributes: + label: Component + options: + - Web UI + - REST API + - CLI + - New handler/check + - Docker / deployment + - Documentation + - Other + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + placeholder: Any other approaches you've thought about? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..377c94e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +## Summary + +<!-- What does this PR do? 1-3 bullet points. --> + +- + +## Type + +- [ ] Bug fix +- [ ] New feature +- [ ] Improvement / refactor +- [ ] Documentation +- [ ] CI / infrastructure + +## Changes + +<!-- Which packages/files are affected? --> + +- + +## Testing + +<!-- How did you verify this works? --> + +- [ ] Existing tests pass (`npm test`) +- [ ] New tests added +- [ ] Manual testing done +- [ ] Build passes (`npx tsc --build`) + +## Screenshots + +<!-- If UI changes, paste before/after screenshots. Delete this section if not applicable. --> diff --git a/packages/api/src/auth/auth.test.ts b/packages/api/src/auth/auth.test.ts index 93bf9ef..c59edc2 100644 --- a/packages/api/src/auth/auth.test.ts +++ b/packages/api/src/auth/auth.test.ts @@ -1,11 +1,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { randomUUID } from 'node:crypto'; +import { unlinkSync } from 'node:fs'; import type { FastifyInstance } from 'fastify'; import { buildServer } from '../server.js'; let app: FastifyInstance; let savedEnv: Record<string, string | undefined>; +let testDbPath: string; -const AUTH_VARS = ['AUTH_ENABLED', 'AUTH_TOKEN']; +const AUTH_VARS = ['AUTH_ENABLED', 'AUTH_TOKEN', 'DB_PATH']; beforeEach(() => { // Save current env @@ -13,9 +16,19 @@ beforeEach(() => { for (const key of AUTH_VARS) { savedEnv[key] = process.env[key]; } + // Use unique DB per test to avoid SQLITE_BUSY + testDbPath = `/tmp/recon-web-auth-test-${randomUUID()}.db`; + process.env.DB_PATH = testDbPath; }); afterEach(async () => { + if (app) { + await app.close(); + } + // Clean up test DB + try { unlinkSync(testDbPath); } catch {} + try { unlinkSync(testDbPath + '-wal'); } catch {} + try { unlinkSync(testDbPath + '-shm'); } catch {} // Restore env for (const key of AUTH_VARS) { if (savedEnv[key] === undefined) { @@ -24,9 +37,6 @@ afterEach(async () => { process.env[key] = savedEnv[key]; } } - if (app) { - await app.close(); - } }); describe('Auth plugin', () => { diff --git a/packages/api/src/server.test.ts b/packages/api/src/server.test.ts index 7db0cd0..cfc376f 100644 --- a/packages/api/src/server.test.ts +++ b/packages/api/src/server.test.ts @@ -1,16 +1,24 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { randomUUID } from 'node:crypto'; +import { unlinkSync } from 'node:fs'; import { buildServer } from './server.js'; import type { FastifyInstance } from 'fastify'; let server: FastifyInstance; +const testDbPath = `/tmp/recon-web-server-test-${randomUUID()}.db`; beforeAll(async () => { + process.env.DB_PATH = testDbPath; server = await buildServer(); await server.ready(); }); afterAll(async () => { await server.close(); + try { unlinkSync(testDbPath); } catch {} + try { unlinkSync(testDbPath + '-wal'); } catch {} + try { unlinkSync(testDbPath + '-shm'); } catch {} + delete process.env.DB_PATH; }); describe('API', () => {