diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5ddd32..3220b45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,20 @@ jobs: - name: Install dependencies run: npm ci + # Lint — Biome's correctness + suspicious rules only. Style rules are + # mostly disabled to avoid a big churn day-one; the goal here is to + # catch real bugs (unused imports, shadowed variables, assign-in-cond) + # rather than enforce a specific house style. + - name: Lint + run: npm run lint + + # Unit tests — Vitest covers the pure-logic modules (risk classifier, + # EU AI Act evaluator + scoring, AI compliance risk generation) with + # focused per-function assertions. Adds meaningful coverage beyond + # "did not throw" while still running in milliseconds. + - name: Unit tests + run: npm test + # Run the scanner against this repo itself. Catches scanner-side # regressions end-to-end: scan rules, policy rendering, framework # evaluation, and report generation all exercised on a real tree. @@ -40,4 +54,4 @@ jobs: # outage where an older stored manifest took /api/repos and the # homepage down with an uncaught TypeError. - name: Dashboard render smoke - run: npx tsx scripts/smoke-dashboard.ts + run: npm run smoke:dashboard diff --git a/README.md b/README.md index 0ac36bd..652b9fa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ On every push or PR, the scanner produces: | `security.txt` | `.well-known/` | RFC 9116 security contact file | | `risk-assessment.md` | `.grc/` | Likelihood x impact matrix with framework mappings | | `nist-csf-report.md` | `.grc/` | 18 NIST CSF controls with SOC 2 + ISO 27001 cross-mapping | -| `security-headers-report.md` | `.grc/` | Header status + copy-paste fix | +| `security-headers-report.md` | `.grc/` | Header status + starter-snippet fixes (CSP typically needs manual review) | | `access-controls-report.md` | `.grc/` | Branch protection and auth findings | Reports (`.grc/`) are gitignored and regenerated each scan. Policies (`docs/policies/`, `.well-known/`) are committed to your PR branch so they ship with your code. @@ -208,7 +208,7 @@ Badge states: - `fail NN%` — critical vulnerabilities, detected secrets, or very low compliance - `not scanned` — the dashboard has no manifest for that repo/branch yet -GitHub's GitHub App badge UI is a separate static logo upload. See [docs/github-app-badge.md](docs/github-app-badge.md) for the distinction and setup steps. +GitHub's GitHub App badge UI is a separate static logo upload. See [docs/badges.md](docs/badges.md) for the distinction and setup steps. ## Scanner @@ -234,6 +234,16 @@ Reports are written to `/path/to/repo/.grc/`. Policies are written to `/path/to/ - **TLS** - HTTPS enforcement, certificate expiry (live URL check) - **AI Systems** - detects AI SDKs (OpenAI, Anthropic, Cohere, Gemini, HuggingFace, Mistral, Groq, LangChain, LlamaIndex, Vercel AI SDK), training libs (TensorFlow, PyTorch), vector DBs (Pinecone, Weaviate, ChromaDB, Qdrant), and outbound API calls. Supports Node (`package.json`), Python (`requirements.txt`, `pyproject.toml`), and monorepos. +### Language coverage + +The scanner's Node/JavaScript path is the most mature — forms, endpoints, dependencies, secrets, tracking, and AI SDK detection all fully work on `.ts` / `.tsx` / `.js` / `.jsx` / `.mjs` / `.cjs` trees. + +**Python** support is partial: `requirements.txt` and `pyproject.toml` are scanned for AI packages and third-party services, but form/endpoint/secret detection only has basic regex coverage. Flask, Django, and FastAPI idioms aren't specifically recognised yet. + +**Go, Ruby, Java, Rust, PHP** — not meaningfully supported. Files are walked for secret regexes and outbound AI API URL patterns; nothing else. A repo in any of these languages will scan without erroring but the findings list will be sparse compared to a Node repo. + +If you're running the scanner against a non-Node repo, expect partial signal and treat missing findings as absence of evidence, not evidence of absence. + ## AI Enhancements (Optional) Add to `.grc/config.yml`: diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e0e8776 --- /dev/null +++ b/biome.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json", + "files": { + "includes": [ + "scanner/**/*.ts", + "dashboard/**/*.ts", + "scripts/**/*.ts", + "!node_modules/**", + "!dist/**", + "!.grc/**", + "!.well-known/**", + "!.wrangler/**" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "noUnusedPrivateClassMembers": "error", + "useExhaustiveDependencies": "off", + "noInvalidUseBeforeDeclaration": "error" + }, + "suspicious": { + "noDoubleEquals": "error", + "noDuplicateCase": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noConstEnum": "error", + "noAssignInExpressions": "error", + "noExplicitAny": "off", + "noConsole": "off", + "useAwait": "off" + }, + "style": { + "noNonNullAssertion": "off", + "useTemplate": "off", + "useConst": "error", + "useSingleVarDeclarator": "off", + "noParameterAssign": "off" + }, + "complexity": { + "noForEach": "off", + "noUselessConstructor": "off", + "useOptionalChain": "off" + }, + "performance": { + "noDelete": "off" + } + } + }, + "formatter": { + "enabled": false + } +} diff --git a/dashboard/worker.ts b/dashboard/worker.ts index 7ba9f27..229943e 100644 --- a/dashboard/worker.ts +++ b/dashboard/worker.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { parse } from "yaml"; -import type { Manifest, AISystem } from "../scanner/types.js"; +import type { Manifest, } from "../scanner/types.js"; import { evaluateFramework } from "../scanner/generators/framework-report.js"; import { evaluateEUAIAct, calcAIComplianceScore } from "../scanner/frameworks/eu-ai-act.js"; import { renderDashboard, renderRepoDetail, renderNistView, renderBranchComparison, renderTrendChart, renderAIComplianceView, renderInventoryView } from "./views/render.js"; diff --git a/docs/architecture.md b/docs/architecture.md index 69666e7..c6eb7be 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,92 +1,81 @@ # Architecture -## System Design: Hub and Spoke +## Hub and spoke ``` -┌─────────────────────────────────────────┐ -│ Central GRC Dashboard │ -│ (aggregates everything) │ -└──────────┬──────────┬──────────┬────────┘ - │ │ │ - Repo A Repo B Repo C - (GH Action) (GH Action) (GH Action) - scans on scans on scans on - PR/deploy PR/deploy PR/deploy + ┌──────────────────────────┐ + │ Cloudflare Worker │ + │ (dashboard + KV) │ + └──────────┬───────────────┘ + │ POST /api/report (OIDC-authed) + │ + ┌─────────────┼─────────────┐ + │ │ │ + Repo A Repo B Repo C + GitHub Action GitHub Action GitHub Action + writes .grc/ writes .grc/ writes .grc/ + commits policy commits policy commits policy ``` -Each repo: -- Runs the same reusable GitHub Action -- Generates a local compliance badge and manifest file committed to the repo -- POSTs the manifest to the central dashboard API +Every consuming repo runs the same composite action (`action.yml`). Each run: -The central dashboard: -- Receives and stores manifests from all repos -- Provides the org-wide view, trends, and reporting +1. Scans the repo tree (Node source + `package.json` + `requirements.txt` + `pyproject.toml`) and the live URL if configured. +2. Writes a YAML manifest to `.grc/manifest.yml` and generated policy markdown to `docs/policies/` + `/.well-known/security.txt`. +3. On PRs, commits generated policies back to the PR branch (attributed to `grc-bot`). +4. Mints a short-lived GitHub OIDC JWT and POSTs the manifest to the dashboard's `/api/report`. -## Single Source of Truth Principle +The dashboard (Hono on Cloudflare Workers) verifies the JWT against GitHub's JWKS, stores the manifest in KV keyed by `manifest::`, and renders HTML views with HTMX. + +## Single source of truth + +The scan is the authority. ``` -❌ Traditional (drift-prone): +❌ Drift-prone: Lawyer writes policy → hope code matches → audit finds gaps -✅ Our approach (compliance-as-code): - Code is scanned → scan produces facts → facts generate policy - → facts feed dashboard +✅ Compliance-as-code: + Code scanned → scan produces facts → facts generate policy + → facts feed dashboard + → facts feed framework scoring ``` -The scan is the authority. The privacy policy, ToS, and dashboard status are all derivatives of what the scan found. If the scan says "this repo collects email via a form and sends it through Resend," then: -- The privacy policy gets a section about email collection and Resend as a processor -- The dashboard shows "data collection: email (processor: Resend)" as a tracked item - -## GitHub Action Flow - -The reusable GitHub Action runs in its own container and can scan ANY repo regardless of language: - -**Outputs:** -1. `manifest.yml` — structured compliance data (committed to repo) -2. POST to dashboard API — feeds the central dashboard -3. Compliance badge SVG — visual status indicator -4. Generated policies — only for repos serving public-facing sites - -## Tech Stack - -- **GitHub Action**: Reusable workflow (`.github/workflows/grc-scan.yml`) -- **Scanner**: Node.js script using AST parsing + regex -- **Dashboard API**: Express endpoint (or separate service) -- **Dashboard UI**: HTMX (matches joeeftekhari.com stack) -- **Storage**: Postgres on Digital Ocean droplet (or JSON files to start) - -## Build Tiers - -### Tier 1 (Start Here) -- Scanner detects: data collection, security headers, dependencies, secrets, TLS, security.txt -- Outputs: manifest.yml, generated policies (privacy policy, ToS, security.txt, vulnerability disclosure) -- Dashboard: checklist view per repo - -### Tier 2 (After Tier 1 Works) -- Add: framework mapping (NIST CSF controls → scan results) -- Add: branch comparison (main vs feature branches) -- Add: trend tracking over time -- Dashboard: framework compliance percentages - -### Tier 3 (Portfolio Showstopper) -- Add: audit evidence export (PDF/ZIP per framework) -- Add: AI-powered gap analysis ("you're missing X for SOC 2") -- Add: remediation suggestions with auto-fix PRs -- Dashboard: auditor-ready report generation - -## Where AI Fits In - -| Task | Deterministic Scan | AI Layer | -|---|---|---| -| "Is there a form?" | Regex for `:` for current state, `history:` for trend data. +- **Auth:** GitHub OIDC on `POST /api/report`. No shared secrets; see `dashboard/auth.ts`. + +## Where AI fits in + +The AI enhancement layer (Phase 4) is optional — the scanner works fully without it. When enabled and given an API key, an LLM refines PII classification, rewrites risk narratives in plain English, and generates gap analyses with prioritized recommendations. + +The EU AI Act detection + risk classification (Phase 8) is a separate thing: purely deterministic scanning of consuming repos for AI SDK imports, framework imports, and outbound AI API URLs. No LLM involvement in that path. + +## What each folder is for + +| Folder | Purpose | +|---|---| +| `scanner/rules/` | Per-concept scan rules producing structured findings | +| `scanner/templates/` | Handlebars policy templates | +| `scanner/generators/` | Markdown report generators | +| `scanner/frameworks/` | Framework definitions (NIST CSF, EU AI Act) and cross-maps | +| `scanner/ai/` | Optional LLM enhancement layer | +| `dashboard/` | Cloudflare Worker + render functions | +| `dashboard/views/render.ts` | All HTML rendering — server-rendered, HTMX for tab swaps | +| `scripts/` | Standalone tsx utilities (smoke tests, one-off maintenance) | +| `docs/` | Reference documentation (this file, checklist, GRC fundamentals, badges) | + +## Not covered here + +- **What the scanner detects** — see the "What It Scans" section in the [README](../README.md). +- **How to set up a fork** — see [README § Setup](../README.md#setup). +- **The manifest schema** — `scanner/types.ts` is the authoritative source. The TypeScript types are the schema. +- **Roadmap** — [implementation-checklist.md](implementation-checklist.md). +- **How to extend the scanner** — [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/github-app-badge.md b/docs/badges.md similarity index 100% rename from docs/github-app-badge.md rename to docs/badges.md diff --git a/docs/compliance-scope.md b/docs/compliance-scope.md deleted file mode 100644 index 13a80ad..0000000 --- a/docs/compliance-scope.md +++ /dev/null @@ -1,100 +0,0 @@ -# Compliance Scope — What the Dashboard Tracks - -## The Three Pillars of GRC - -### Governance (policies, rules, oversight) - -| Artifact | Purpose | How We Generate/Detect It | -|---|---|---| -| Privacy Policy | Legal requirement for data-collecting sites | Auto-generated from data collection scan (GDPR Art. 13, CCPA §1798.100) | -| Terms of Service | Defines legal relationship with users | Template + site-specific terms from manifest | -| Acceptable Use Policy | What users can/can't do on your platform | Template-driven | -| Data Retention Policy | How long you keep data, when you delete it | Scan DB schemas, storage configs | -| Incident Response Plan | What you do when something goes wrong | Check for file existence (e.g., docs/irp.md) | -| Change Management Policy | How changes get reviewed before deploy | Check for PR requirements, branch protection | -| Access Control Policy | Who has access to what | Scan GitHub org permissions, SSH keys | -| security.txt | Standardized security contact info | Check for /.well-known/security.txt | -| Vulnerability Disclosure | How to report security issues | Check for disclosure page/policy | - -### Risk (what could go wrong, how bad, what you're doing about it) - -| Check | Purpose | How We Detect It | -|---|---|---| -| Risk Register | Living document of identified risks | Auto-generated from scan findings | -| Vulnerability Management | Known CVEs in dependencies | `npm audit`, `trivy`, `dependabot` | -| Threat Modeling | Attack surface analysis | AI-assisted — analyze routes, inputs, auth | -| Business Continuity | Recovery capability | Check for backup configs, redundancy | -| Third-Party Risk | Vendor/dependency trustworthiness | Scan for third-party services, check SOC 2 status | -| Certificate Monitoring | TLS cert expiry | Check cert expiry dates | -| Secrets in Code | Leaked credentials | Pattern-based scanning | - -### Compliance (proving you meet specific frameworks) - -| Framework | Who Cares | What It Requires | -|---|---|---| -| **NIST CSF** | US government, enterprise clients | 5 functions: Identify, Protect, Detect, Respond, Recover | -| **SOC 2** | SaaS companies, B2B | Trust principles: Security, Availability, Processing Integrity, Confidentiality, Privacy | -| **ISO 27001** | International, enterprise | ~93 controls across 4 domains | -| **GDPR** | Anyone with EU users | Data protection, consent, breach notification | -| **CCPA/CPRA** | Anyone with California users | Consumer data rights | -| **PCI DSS** | Anyone handling payment data | Card data security (not relevant unless handling payments) | -| **HIPAA** | Healthcare data | Not relevant unless handling health info | - -**Key insight:** These frameworks overlap massively. A security header check satisfies controls in NIST CSF, SOC 2, AND ISO 27001 simultaneously. The dashboard maps each scan result to every framework it satisfies. - -## Dashboard Views - -### Per-Repo View -``` -Repository: example.com -Branch: main -Last Scan: 2026-04-08 - -GOVERNANCE ARTIFACTS -├── Privacy Policy ✅ Auto-generated, current -├── Terms of Service ✅ Auto-generated, current -├── security.txt ✅ Present -├── Vulnerability Disclosure ❌ Missing -├── Incident Response Plan ✅ Present at /docs/irp.md -└── Change Management ✅ Branch protection enabled - -RISK POSTURE -├── Dependencies ⚠️ 2 high vulns (express 4.x, lodash) -├── Secrets in Code ✅ None detected -├── TLS Certificate ✅ Valid, expires 2026-09-15 -├── Security Headers ⚠️ 3/6 present -├── Third-Party Processors ✅ 1 identified (Resend — DPA on file) -└── Backup Configuration ❌ No backup strategy detected - -FRAMEWORK MAPPING -├── NIST CSF 72% ██████████░░░░ -│ ├── Identify 90% -│ ├── Protect 65% -│ ├── Detect 40% -│ ├── Respond 80% -│ └── Recover 30% -├── SOC 2 (Type I) 68% █████████░░░░░ -└── GDPR 85% ████████████░░ -``` - -### Branch Comparison (Killer Feature) -If someone adds a form on a feature branch, the scan catches it and flags the privacy policy as stale BEFORE it hits production. The PR can't merge until compliance is green. This is shift-left compliance. - -### Auditor Evidence Export (Tier 3) -``` -📦 Audit Evidence Package — SOC 2 Type I - Generated: 2026-04-08 - Scope: your organization - - CC6.1 — Logical Access Controls - ├── Evidence: GitHub branch protection rules (API export) - ├── Evidence: SSH key inventory from org settings - └── Status: SATISFIED - - CC6.6 — Security Measures Against Threats - ├── Evidence: Dependency scan results (0 critical, 2 high) - ├── Evidence: Security header configuration - └── Status: PARTIAL — remediation plan attached -``` - -This is what Drata and Vanta charge $15-30k/year for. diff --git a/docs/implementation-checklist.md b/docs/implementation-checklist.md index 231e1ba..6515ded 100644 --- a/docs/implementation-checklist.md +++ b/docs/implementation-checklist.md @@ -454,10 +454,10 @@ Recommended build order: - [ ] Vulnerability management deep-dive: CVSS scoring, dev vs prod dep distinction, exploitability vs severity, accepted-risk tracking ### Dashboard -- [ ] Authentication on API endpoints (API key validation on POST) +- [x] Authentication on API endpoints — POST /api/report verifies a GitHub OIDC JWT and asserts the token's `repository` claim matches the manifest (PR #28). Consumer config is one permission line. - [ ] Accepted-risk workflow (declare risks as acknowledged in `.grc/config.yml`, dashboard respects and shows as "accepted") - [ ] Multi-tenant / multi-org support -- [ ] Tests — none currently exist +- [x] Tests — Vitest unit tests cover the pure-logic modules (risk classifier, EU AI Act evaluator + scoring, AI compliance risk generation) plus render-level smoke tests in `scripts/smoke-dashboard.ts`. A full coverage push is still open. - Note: auditor evidence export moved to Phase 9 Sub-phase B ## Phase 11: Blog Content @@ -479,20 +479,20 @@ These cut across all phases and should be addressed opportunistically. ### Unfixed Technical Debt - [ ] joeeftekhari.com has 1 critical + 3 high CVEs we haven't addressed. We don't use our own tool's output. - [ ] joeeftekhari.com doesn't yet serve policies at public URLs — files exist in `docs/policies/` but no Express routes to serve them. Once routes exist, user will configure `policy_urls` and Check Production will verify them. -- [ ] No unit tests — smoke tests exist (scripts/smoke-dashboard.ts covers render pipeline) but no assertions beyond "did not throw / no bare 'undefined' in output". A real unit test suite (Vitest or similar) is still open. -- [x] CI on every PR — `.github/workflows/ci.yml` runs the scanner against this repo and smoke-tests every dashboard render function with both new-shape and old-shape manifest fixtures. Catches regressions like the 2026-04-18 outage. -- [ ] Lint / type-check still missing — `tsc --noEmit` fights with `verbatimModuleSyntax` + the no-@types/node config the runtime relies on (`tsx` handles this at runtime but `tsc` doesn't). Adding a lint pass is tracked separately. -- [ ] Documentation in `docs/` has overlapping content across files -- [ ] Deploy workflow uses `sed` for placeholder injection in wrangler.toml — works but is fragile +- [x] Unit tests — Vitest covers risk classifier, EU AI Act evaluator + scoring, and AI-compliance risk generation (51 tests). Smoke tests in `scripts/smoke-dashboard.ts` cover render pipelines with both new-shape and old-shape manifests. Broader coverage still welcome but the floor is up. +- [x] CI on every PR — `.github/workflows/ci.yml` runs lint + unit tests + scanner smoke + dashboard render smoke. Catches regressions like the 2026-04-18 outage. +- [x] Lint — Biome configured with correctness + suspicious rules (no style churn). Catches unused imports, shadowed vars, assign-in-conditions, etc. Type-check via `tsc --noEmit` still skipped because of tsconfig/runtime mismatch; tracked. +- [x] Docs — `docs/` cleaned up: `compliance-scope.md` deleted (outdated + duplicated README content), `manifest-spec.md` trimmed to point at `scanner/types.ts` as authoritative, `github-app-badge.md` → `badges.md`, `architecture.md` rewritten to reflect the current Hono/Workers/KV stack (was still referencing Express + Postgres). +- [ ] Deploy workflow uses `sed` for the KV id placeholder in `wrangler.toml` — works but fragile; `ORG_NAME` + `GRC_AUDIENCE` already moved off `sed` onto `wrangler deploy --var` in PR #30. Last `sed` removed would require either an API call to set the KV binding or a script that rewrites the TOML via a proper parser. - [ ] Monorepo support is poor (scans root only, no per-package awareness) -- [ ] Python support is placeholder-only; Go/Ruby/Java/Rust essentially unsupported -- [ ] CSP auto-generator only catches HTML-embedded CDN imports (Google Analytics) — misses CDN script tags (unpkg, jsdelivr, cdnjs), so output needs manual review +- [x] Python support tier documented explicitly in README — ("Language coverage" section calls out that Python has partial coverage and Go/Ruby/Java/Rust are essentially unsupported). Fixing the under-coverage is still open but the claim no longer overpromises. +- [ ] CSP auto-generator only catches HTML-embedded CDN imports (Google Analytics) — misses CDN script tags (unpkg, jsdelivr, cdnjs), so output needs manual review. README security-headers report now includes an explicit "CSP caveat" paragraph. ### Unvalidated Claims - [x] ~~AI layer has never been run with a real API key~~ — validated with OpenAI gpt-4o-mini, all 4 enhancements work (PR #19) -- [ ] Open-source setup instructions have never been fork-tested from scratch -- [ ] "Copy-paste ready" middleware claim overstated (CSP usually requires manual edits) -- [ ] "Works on any Node/Python/Go repo" claim overstated (Node works, others are placeholder) +- [x] ~~Open-source setup instructions have never been fork-tested from scratch~~ — dry-ran from a fresh clone (PR #30); found and fixed an ORG_NAME deploy bug in the process. +- [x] ~~"Copy-paste ready" middleware claim overstated~~ — relabelled to "Express Middleware (starter)" in the generated report with an explicit CSP caveat paragraph about dynamic CDNs. +- [x] ~~"Works on any Node/Python/Go repo" claim overstated~~ — README now includes a "Language coverage" section saying exactly what works per language (Node mature, Python partial, Go/Ruby/Java/Rust basic regex only). ## Certification Pairing (recommended for entry-level GRC) - CompTIA Security+ diff --git a/docs/manifest-spec.md b/docs/manifest-spec.md index 5e7d23e..e8bdd59 100644 --- a/docs/manifest-spec.md +++ b/docs/manifest-spec.md @@ -1,90 +1,47 @@ -# Compliance Manifest Specification +# Manifest Specification -The manifest is the structured output of the GRC scan. It is the single source of truth that drives both generated policies and the dashboard. +The manifest is the structured output of every scan and the single source of truth for both generated policies and the dashboard. It's a YAML file written to `.grc/manifest.yml` and POSTed to the dashboard on every successful scan. -## Format +## Authoritative schema -YAML file committed to each repo at `.grc/manifest.yml`. Auto-generated — never hand-edited. +[`scanner/types.ts`](../scanner/types.ts) is the authoritative schema. The `Manifest` interface in that file defines every field; any doc that duplicates it will drift. -## Schema +The key top-level fields as of this writing: -```yaml -# .grc/manifest.yml -repo: your-org/your-site -scan_date: 2026-04-08T12:00:00Z -branch: main -commit: abc123 +- `repo`, `scanDate`, `branch`, `commit` — identification +- `dataCollection[]` — forms, endpoints, cookies, tracking +- `thirdPartyServices[]` — detected third-party data processors with optional DPA URLs +- `securityHeaders`, `https`, `dependencies` — live-check results (nullable; only populated when a site URL is configured) +- `secretsScan`, `accessControls`, `artifacts` — static-scan results always populated +- `aiSystems[]` — detected AI SDKs, frameworks, vector DBs, or outbound AI API calls, each with a risk tier (Phase 8) +- `policyUrls?` — user-declared URLs for each generated policy on the live site (opt-in) -data_collection: - - type: email # what kind of data - source: contact-form # how it's collected - location: src/routes/contact.ts # where in the code - processor: resend # third-party that handles it - retention: transient # transient | persistent | unknown +## Format conventions -third_party_services: - - name: Resend - purpose: email delivery - data_shared: [email, name, message_body] - dpa_url: https://resend.com/legal/dpa # Data Processing Agreement +- **camelCase keys** in both YAML and JSON (matching the TypeScript interface exactly — no snake_case translation). +- **Optional fields use `undefined` semantics**: absent key means "not scanned" or "not applicable", not "missing". +- **Live-check fields are nullable**: `securityHeaders`, `https`, `dependencies` are `null` when no site URL is configured or when the live fetch failed. -security_headers: - csp: missing | present | partial - hsts: missing | present - x_frame_options: missing | present - x_content_type_options: missing | present - referrer_policy: missing | present - permissions_policy: missing | present +## Flow -https: - enforced: true | false - cert_expiry: 2026-09-15 # date or null - -dependencies: - critical_vulnerabilities: 0 - high_vulnerabilities: 2 - medium_vulnerabilities: 5 - outdated_packages: 12 - last_audit: 2026-04-08 - -secrets_scan: - detected: false - findings: [] # list of file:line if detected (redacted) - -artifacts: - privacy_policy: generated | manual | missing - terms_of_service: generated | manual | missing - security_txt: present | missing - vulnerability_disclosure: present | missing - incident_response_plan: present | missing - -access_controls: - branch_protection: true | false - required_reviews: 0 # number of required PR reviews - signed_commits: true | false - -backup: - strategy: none | manual | automated - last_verified: null # date or null ``` - -## How the Manifest Drives Policies - -The manifest is consumed by Handlebars (or similar) templates: - -``` -manifest.yml + templates/privacy-policy.hbs → public/privacy-policy.html -manifest.yml + templates/terms-of-service.hbs → public/terms.html +scanner/index.ts scan pipeline: + 1. Parallel scan rules populate findings + 2. Policy templates render against the manifest + 3. Policy files written to output_dir; artifacts rescanned + 4. Framework evaluation (NIST CSF, EU AI Act) + 5. Manifest written to .grc/manifest.yml — last + 6. Action POSTs the file to the dashboard ``` -Template logic example: "IF the manifest lists email collection, include the email section with the processor name filled in." No data collection → that section doesn't appear. +The scanner writes the manifest *last* so it reflects the real filesystem state after policy generation — not a pre-generation snapshot. This matters for the `artifacts` field specifically: it's set from the actual files on disk after rendering, so it correctly reports `"generated"` rather than `"missing"`. -## How the Manifest Feeds the Dashboard +## Idempotency -The GitHub Action POSTs the manifest to the dashboard API: +Two consecutive scans against an unchanged repo must produce byte-identical manifests (and byte-identical generated policies). The scanner enforces this by: -``` -manifest.yml → POST https://grc-dashboard.joeeftekhari.com/api/report -``` +- Excluding `scanDate` from template bodies (git history records timing). +- Pinning `security.txt` `Expires` to Jan 1 of the next year rather than a rolling expiry. +- Never including the commit hash inside policy bodies. -The dashboard stores historical manifests to enable trend tracking and branch comparison. +If you're extending the scanner and a field introduces non-determinism, add a test fixture to `scripts/smoke-dashboard.ts` that would catch the regression, or add a real Vitest unit test. diff --git a/package-lock.json b/package-lock.json index 2697106..df32a76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "ISC", "dependencies": { "@types/node": "^25.5.2", - "express": "^5.2.1", "handlebars": "^4.7.9", "hono": "^4.12.12", "tsx": "^4.21.0", @@ -19,7 +18,171 @@ "yaml": "^2.8.3" }, "devDependencies": { - "@types/express": "^5.0.6" + "@biomejs/biome": "2.4.12", + "vitest": "^4.1.4" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.12.tgz", + "integrity": "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.12", + "@biomejs/cli-darwin-x64": "2.4.12", + "@biomejs/cli-linux-arm64": "2.4.12", + "@biomejs/cli-linux-arm64-musl": "2.4.12", + "@biomejs/cli-linux-x64": "2.4.12", + "@biomejs/cli-linux-x64-musl": "2.4.12", + "@biomejs/cli-win32-arm64": "2.4.12", + "@biomejs/cli-win32-x64": "2.4.12" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.12.tgz", + "integrity": "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.12.tgz", + "integrity": "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.12.tgz", + "integrity": "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.12.tgz", + "integrity": "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.12.tgz", + "integrity": "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.12.tgz", + "integrity": "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.12.tgz", + "integrity": "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.12.tgz", + "integrity": "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -47,9 +210,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260405.1.tgz", - "integrity": "sha512-EbmdBcmeIGogKG4V1odSWQe7z4rHssUD4iaXv0cXA22/MFrzH3iQT0R+FJFyhucGtih/9B9E+6j0QbSQD8xT3w==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260415.1.tgz", + "integrity": "sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg==", "cpu": [ "x64" ], @@ -63,9 +226,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260405.1.tgz", - "integrity": "sha512-r44r418bOQtoP+Odu+L/BQM9q5cRSXRd1N167PgZQIo4MlqzTwHO4L0wwXhxbcV/PF46rrQre/uTFS8R0R+xSQ==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260415.1.tgz", + "integrity": "sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ==", "cpu": [ "arm64" ], @@ -79,9 +242,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260405.1.tgz", - "integrity": "sha512-Aaq3RWnaTCzMBo77wC8fjOx+SFdO/rlcXa6HAf+PJs51LyMISFOBCJKqSlS6Irphen0WHHxFKPHUO9bjfj8g2g==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260415.1.tgz", + "integrity": "sha512-tU+9pwsqCy8afOVlGtiWrWQc/fedQK4SRm4KPIAt+zOiQWDxWASm6YGBUJis5c648WN80yz47qnmdDi8DQNOcA==", "cpu": [ "x64" ], @@ -95,9 +258,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260405.1.tgz", - "integrity": "sha512-Lbp9Z2wiMzy3Sji3YwMHK5WDlejsH3jF4swAFEv7+jIf3NowZHga3GzwTypNRmcwnfz/XrqQ7Hc0Ul9OoU/lCw==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260415.1.tgz", + "integrity": "sha512-bR9uITnV19r5NQ14xnypi2xHXu2iQvfYV8cVgx0JouFUmWwTEEAwFVojDdssGq93VHX9hr/pi2IRUZeegbYBog==", "cpu": [ "arm64" ], @@ -111,9 +274,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260405.1.tgz", - "integrity": "sha512-FhE0kt93kj5JnSPVqi4BAXpQQENyKnuSOoJLd35mkMMGhtPrwv5EsReJdck0S8hUocCBlb+U0RmP8ta6k41HjQ==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260415.1.tgz", + "integrity": "sha512-4NuMLlerI0Ijua3Ir8HXQ+qyNvCUDEG5gDco5Om+sAiK6rnWiz+aGoSlbB8W16yW9QAgzCstbmXLiVknUBflfQ==", "cpu": [ "x64" ], @@ -138,12 +301,37 @@ "node": ">=12" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "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, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { "tslib": "^2.4.0" } @@ -1054,6 +1242,35 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", @@ -1080,345 +1297,525 @@ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@speed-highlight/core": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", - "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", - "license": "CC0-1.0" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=14.0.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "license": "MIT", "engines": { "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "license": "CC0-1.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", - "engines": { - "node": ">=6.6.0" + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=6.0" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "msw": { + "optional": true + }, + "vite": { "optional": true } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "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/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "license": "MIT" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-object-atoms": { + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": ">= 0.4" + "node": ">=8" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -1460,101 +1857,42 @@ "@esbuild/win32-x64": "0.27.7" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "@types/estree": "^1.0.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.6" + "node": ">=12.0.0" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/fsevents": { @@ -1571,56 +1909,10 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -1629,18 +1921,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -1662,30 +1942,6 @@ "uglify-js": "^3.1.4" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hono": { "version": "4.12.14", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", @@ -1695,137 +1951,296 @@ "node": ">=16.9.0" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 0.8" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/miniflare": { - "version": "4.20260405.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260405.0.tgz", - "integrity": "sha512-tpr4XdWMq7zFdsHH+CS0XS47nQzlRZH0rMJ1vobOZbkrs3cIj7qbD40ON616hDnzHxwqwB2qKHzmmuj6oRisSQ==", + "version": "4.20260415.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260415.0.tgz", + "integrity": "sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng==", "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", - "undici": "7.24.4", - "workerd": "1.20260405.1", + "undici": "7.24.8", + "workerd": "1.20260415.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, @@ -1845,19 +2260,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">= 0.6" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/neo-async": { @@ -1866,57 +2285,22 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "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==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", @@ -1924,56 +2308,54 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">=0.6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.10" + "node": "^10 || ^12 || >=14" } }, "node_modules/resolve-pkg-maps": { @@ -1985,28 +2367,40 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">= 18" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -2019,57 +2413,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -2114,77 +2457,12 @@ "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, "node_modules/source-map": { "version": "0.6.1", @@ -2195,15 +2473,30 @@ "node": ">=0.10.0" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -2216,13 +2509,48 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=14.0.0" } }, "node_modules/tslib": { @@ -2237,6 +2565,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -2251,24 +2580,10 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2292,18 +2607,18 @@ } }, "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", "license": "MIT", "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/unenv": { @@ -2316,22 +2631,190 @@ "pathe": "^2.0.3" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "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", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">= 0.8" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, "node_modules/wordwrap": { @@ -2341,12 +2824,11 @@ "license": "MIT" }, "node_modules/workerd": { - "version": "1.20260405.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260405.1.tgz", - "integrity": "sha512-bSaRWCv9iO8/FWpgZRjHLGZLolX5s1AErRSYaTECMMHOZKuCbl2+ehnSyc+ZZ/70y+9owADmN6HoYEWvBlJdYw==", + "version": "1.20260415.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260415.1.tgz", + "integrity": "sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -2354,27 +2836,27 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260405.1", - "@cloudflare/workerd-darwin-arm64": "1.20260405.1", - "@cloudflare/workerd-linux-64": "1.20260405.1", - "@cloudflare/workerd-linux-arm64": "1.20260405.1", - "@cloudflare/workerd-windows-64": "1.20260405.1" + "@cloudflare/workerd-darwin-64": "1.20260415.1", + "@cloudflare/workerd-darwin-arm64": "1.20260415.1", + "@cloudflare/workerd-linux-64": "1.20260415.1", + "@cloudflare/workerd-linux-arm64": "1.20260415.1", + "@cloudflare/workerd-windows-64": "1.20260415.1" } }, "node_modules/wrangler": { - "version": "4.81.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.81.0.tgz", - "integrity": "sha512-9fLPDuDcb8Nu6iXrl5E3HGYt3TVhQr/UvqtTvWr9Nl1X7PlQrmWMwQCfSioqN8VHYyQCyESV5jQsoKg8Sx+sEA==", + "version": "4.83.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.83.0.tgz", + "integrity": "sha512-gw5g3LCiuAqVWxaoKY6+quE0HzAUEFb/FV3oAlNkE1ttd4XP3FiV91XDkkzUCcdqxS4WjhQvPhIDBNdhEi8P0A==", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", - "miniflare": "4.20260405.0", + "miniflare": "4.20260415.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260405.1" + "workerd": "1.20260415.1" }, "bin": { "wrangler": "bin/wrangler.js", @@ -2387,7 +2869,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260405.1" + "@cloudflare/workers-types": "^4.20260415.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2852,18 +3334,6 @@ "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/wrangler/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -2890,6 +3360,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -2922,19 +3393,6 @@ "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } - }, - "node_modules/youch/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" - } } } } diff --git a/package.json b/package.json index d701fcc..c89e073 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,12 @@ }, "scripts": { "scan": "tsx scanner/index.ts", - "dashboard": "tsx dashboard/server.ts", - "test": "echo \"Error: no test specified\" && exit 1" + "dashboard": "wrangler dev --local", + "test": "vitest run", + "test:watch": "vitest", + "smoke:dashboard": "tsx scripts/smoke-dashboard.ts", + "lint": "biome lint scanner dashboard scripts", + "lint:fix": "biome lint scanner dashboard scripts --write --unsafe" }, "repository": { "type": "git", @@ -25,7 +29,6 @@ "homepage": "https://github.com/shipstuff/GRC-Observability-Dashboard#readme", "dependencies": { "@types/node": "^25.5.2", - "express": "^5.2.1", "handlebars": "^4.7.9", "hono": "^4.12.12", "tsx": "^4.21.0", @@ -34,6 +37,7 @@ "yaml": "^2.8.3" }, "devDependencies": { - "@types/express": "^5.0.6" + "@biomejs/biome": "2.4.12", + "vitest": "^4.1.4" } } diff --git a/scanner/frameworks/eu-ai-act.test.ts b/scanner/frameworks/eu-ai-act.test.ts new file mode 100644 index 0000000..9c0ac18 --- /dev/null +++ b/scanner/frameworks/eu-ai-act.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, it } from "vitest"; +import type { AISystem, Manifest } from "../types.js"; +import { + evaluateEUAIAct, + calcAIComplianceScore, + getAIPhaseScores, +} from "./eu-ai-act.js"; + +function baseManifest(overrides: Partial = {}): Manifest { + return { + repo: "test/repo", + scanDate: "2026-04-17T00:00:00Z", + branch: "main", + commit: "abc1234", + dataCollection: [], + thirdPartyServices: [], + securityHeaders: null, + https: null, + dependencies: null, + secretsScan: { detected: false, findings: [] }, + artifacts: { + privacyPolicy: "generated", + termsOfService: "generated", + securityTxt: "present", + vulnerabilityDisclosure: "present", + incidentResponsePlan: "present", + }, + accessControls: { branchProtection: true, requiredReviews: 1, signedCommits: false }, + aiSystems: [], + ...overrides, + }; +} + +function aiSystem(overrides: Partial = {}): AISystem { + return { + provider: "OpenAI", + sdk: "openai", + location: "package.json", + category: "inference", + dataFlows: [], + riskTier: "limited", + riskTierSource: "heuristic", + euMarket: false, + ...overrides, + }; +} + +describe("evaluateEUAIAct", () => { + it("returns 13 articles regardless of manifest shape", () => { + expect(evaluateEUAIAct(baseManifest()).length).toBe(13); + expect(evaluateEUAIAct(baseManifest({ aiSystems: [aiSystem()] })).length).toBe(13); + }); + + it("handles pre-Phase-8 manifests that predate the aiSystems field (regression guard for 2026-04-18 outage)", () => { + const pre = baseManifest(); + // Simulate a pre-Phase-8 manifest: the aiSystems field was added then. + delete (pre as Partial).aiSystems; + const results = evaluateEUAIAct(pre as Manifest); + expect(results.length).toBe(13); + // Article 5 still evaluates (always applicable) — cannot throw. + expect(results.find(r => r.articleId === "ART-5")?.status).toBe("pass"); + }); + + describe("Article 5 — always applicable", () => { + it("passes when no AI systems are present", () => { + const art5 = evaluateEUAIAct(baseManifest()).find(r => r.articleId === "ART-5")!; + expect(art5.status).toBe("pass"); + }); + + it("passes when only non-prohibited systems are present", () => { + const art5 = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "limited" })] }), + ).find(r => r.articleId === "ART-5")!; + expect(art5.status).toBe("pass"); + }); + + it("fails when any system is classified as prohibited", () => { + const art5 = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "prohibited", location: "src/social-score/rank.ts" })] }), + ).find(r => r.articleId === "ART-5")!; + expect(art5.status).toBe("fail"); + expect(art5.evidence).toContain("OpenAI"); + }); + }); + + describe("Article 50 — transparency, applies at limited+", () => { + it("is not-applicable when no AI systems are present", () => { + const art50 = evaluateEUAIAct(baseManifest()).find(r => r.articleId === "ART-50")!; + expect(art50.status).toBe("not-applicable"); + }); + + it("passes when an AI usage policy artifact is present", () => { + const art50 = evaluateEUAIAct( + baseManifest({ + aiSystems: [aiSystem({ riskTier: "limited" })], + artifacts: { ...baseManifest().artifacts, aiUsagePolicy: "present" }, + }), + ).find(r => r.articleId === "ART-50")!; + expect(art50.status).toBe("pass"); + }); + + it("is partial when AI systems are present but no usage policy artifact", () => { + const art50 = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "limited" })] }), + ).find(r => r.articleId === "ART-50")!; + expect(art50.status).toBe("partial"); + }); + }); + + describe("Articles 11 / 12 / 13 / 14 / 15 — high-risk only", () => { + it.each(["ART-11", "ART-12", "ART-13", "ART-14", "ART-15"])( + "%s is not-applicable when no high-risk system present", + (id) => { + const r = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "limited" })] }), + ).find(x => x.articleId === id)!; + expect(r.status).toBe("not-applicable"); + }, + ); + + it("ART-11 credits a generated model card artifact as partial", () => { + const r = evaluateEUAIAct( + baseManifest({ + aiSystems: [aiSystem({ riskTier: "high" })], + artifacts: { ...baseManifest().artifacts, modelCards: "present" }, + }), + ).find(x => x.articleId === "ART-11")!; + expect(r.status).toBe("partial"); + }); + + it("ART-11 fails when a high-risk system is present and no model card artifact", () => { + const r = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "high" })] }), + ).find(x => x.articleId === "ART-11")!; + expect(r.status).toBe("fail"); + }); + }); + + describe("Article 27 — FRIA, high-risk + eu_market gated", () => { + it("is not-applicable when a high-risk system exists but is not on EU market", () => { + const r = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "high", euMarket: false })] }), + ).find(x => x.articleId === "ART-27")!; + expect(r.status).toBe("not-applicable"); + }); + + it("fails when EU-market high-risk exists and no FRIA artifact", () => { + const r = evaluateEUAIAct( + baseManifest({ aiSystems: [aiSystem({ riskTier: "high", euMarket: true })] }), + ).find(x => x.articleId === "ART-27")!; + expect(r.status).toBe("fail"); + }); + + it("is partial when EU-market high-risk exists and FRIA artifact is present", () => { + const r = evaluateEUAIAct( + baseManifest({ + aiSystems: [aiSystem({ riskTier: "high", euMarket: true })], + artifacts: { ...baseManifest().artifacts, fria: "present" }, + }), + ).find(x => x.articleId === "ART-27")!; + expect(r.status).toBe("partial"); + }); + }); +}); + +describe("calcAIComplianceScore", () => { + it("returns 100 when every applicable article passes", () => { + const results = [ + { articleId: "ART-5", article: 5, title: "x", phase: "Map" as const, description: "", status: "pass" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + { articleId: "ART-50", article: 50, title: "x", phase: "Manage" as const, description: "", status: "pass" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + ]; + expect(calcAIComplianceScore(results)).toBe(100); + }); + + it("counts partial as 0.5", () => { + const results = [ + { articleId: "A", article: 1, title: "x", phase: "Map" as const, description: "", status: "pass" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + { articleId: "B", article: 2, title: "x", phase: "Map" as const, description: "", status: "partial" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + ]; + // 1 pass + 1 partial = 1.5 / 2 applicable = 75% + expect(calcAIComplianceScore(results)).toBe(75); + }); + + it("excludes not-applicable from both numerator and denominator", () => { + const results = [ + { articleId: "A", article: 1, title: "x", phase: "Map" as const, description: "", status: "pass" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + { articleId: "B", article: 2, title: "x", phase: "Map" as const, description: "", status: "not-applicable" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + ]; + expect(calcAIComplianceScore(results)).toBe(100); + }); + + it("returns 100 when no articles are applicable (vacuous truth)", () => { + const results = [ + { articleId: "A", article: 1, title: "x", phase: "Map" as const, description: "", status: "not-applicable" as const, evidence: "", nistAiRmf: [], iso42001: [] }, + ]; + expect(calcAIComplianceScore(results)).toBe(100); + }); +}); + +describe("getAIPhaseScores", () => { + it("returns one entry per phase", () => { + const phases = getAIPhaseScores(evaluateEUAIAct(baseManifest())); + expect(phases.map(p => p.name)).toEqual(["Govern", "Map", "Measure", "Manage"]); + }); + + it("reports applicable=0 and percentage=100 for phases that are entirely not-applicable", () => { + const phases = getAIPhaseScores(evaluateEUAIAct(baseManifest())); + for (const p of phases) { + // With no AI systems, every article is not-applicable or always-applicable-pass. + expect(p.percentage).toBeGreaterThanOrEqual(0); + expect(p.percentage).toBeLessThanOrEqual(100); + } + }); +}); diff --git a/scanner/generators/risk-assessment.test.ts b/scanner/generators/risk-assessment.test.ts new file mode 100644 index 0000000..8f686d3 --- /dev/null +++ b/scanner/generators/risk-assessment.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from "vitest"; +import type { Manifest } from "../types.js"; +import type { SiteConfig } from "../config.js"; +import { assessRisks } from "./risk-assessment.js"; + +function manifestWithAI(aiOverrides: Partial = {}): Manifest { + return { + repo: "test/repo", + scanDate: "2026-04-17T00:00:00Z", + branch: "main", + commit: "abc1234", + dataCollection: [], + thirdPartyServices: [], + securityHeaders: null, + https: null, + dependencies: null, + secretsScan: { detected: false, findings: [] }, + artifacts: { + privacyPolicy: "generated", + termsOfService: "generated", + securityTxt: "present", + vulnerabilityDisclosure: "present", + incidentResponsePlan: "present", + }, + accessControls: { branchProtection: true, requiredReviews: 1, signedCommits: false }, + aiSystems: [ + { + provider: "OpenAI", + sdk: "openai", + location: "package.json", + category: "inference", + dataFlows: [], + riskTier: "limited", + riskTierSource: "heuristic", + euMarket: false, + ...aiOverrides, + }, + ], + }; +} + +function baseConfig(): SiteConfig { + return { + siteName: "Test Site", + siteUrl: "https://example.com", + ownerName: "Test", + contactEmail: "test@example.com", + securityContact: "test@example.com", + logRetentionDays: 90, + jurisdiction: ["gdpr"], + preferredLanguages: ["en"], + outputDir: "docs/policies", + policyUrls: {}, + ai: { enabled: false, provider: "anthropic" }, + aiSystemOverrides: [], + }; +} + +describe("assessRisks — AI compliance risks", () => { + it("emits one ai-compliance Risk per failing/partial EU AI Act article", () => { + const risks = assessRisks(manifestWithAI(), baseConfig(), []); + const aiRisks = risks.filter(r => r.category === "ai-compliance"); + // At minimum, Article 4 (partial) and Article 50 (partial since no aiUsagePolicy artifact) + expect(aiRisks.length).toBeGreaterThan(0); + expect(aiRisks.every(r => r.category === "ai-compliance")).toBe(true); + }); + + it("rates a prohibited Article 5 failure as critical severity", () => { + const risks = assessRisks( + manifestWithAI({ riskTier: "prohibited", location: "src/social-score/rank.ts" }), + baseConfig(), + [], + ); + const art5 = risks.find(r => r.title.includes("ART-5")); + expect(art5).toBeDefined(); + expect(art5?.severity).toBe("critical"); + expect(art5?.likelihood).toBe("high"); + expect(art5?.impact).toBe("high"); + }); + + it("rates failed high-risk articles as high severity", () => { + const risks = assessRisks( + manifestWithAI({ riskTier: "high" }), + baseConfig(), + [], + ); + // ART-11 (technical documentation) fails without model cards and a high-risk system is present + const art11 = risks.find(r => r.title.includes("ART-11")); + expect(art11).toBeDefined(); + expect(art11?.likelihood).toBe("high"); + expect(art11?.impact).toBe("high"); + }); + + it("rates partial articles on minimal-tier systems as low severity", () => { + // A manifest with no AI systems at all: Article 4/5/etc. are either + // not-applicable or pass. Compare to one with a limited-tier system + // where Article 4 becomes partial. + const risks = assessRisks(manifestWithAI({ riskTier: "limited" }), baseConfig(), []); + const art4 = risks.find(r => r.title.includes("ART-4")); + expect(art4).toBeDefined(); + expect(art4?.severity).toBe("low"); + expect(art4?.likelihood).toBe("low"); + }); + + it("does not emit ai-compliance risks for not-applicable articles", () => { + // No AI systems → most articles are N/A. Only Article 5 (always-applicable) + // and never-N/A articles should produce risks at most. + const noAIManifest = manifestWithAI(); + noAIManifest.aiSystems = []; + const risks = assessRisks(noAIManifest, baseConfig(), []); + const aiRisks = risks.filter(r => r.category === "ai-compliance"); + // Zero: Article 5 passes (no prohibited system), no others apply. + expect(aiRisks.length).toBe(0); + }); + + it("populates framework cross-references for each AI risk", () => { + const risks = assessRisks(manifestWithAI({ riskTier: "high", euMarket: true }), baseConfig(), []); + const aiRisks = risks.filter(r => r.category === "ai-compliance"); + expect(aiRisks.length).toBeGreaterThan(0); + for (const r of aiRisks) { + expect(r.framework.some(f => f.startsWith("EU AI Act"))).toBe(true); + } + }); +}); diff --git a/scanner/generators/security-headers.ts b/scanner/generators/security-headers.ts index c2bb53a..051654a 100644 --- a/scanner/generators/security-headers.ts +++ b/scanner/generators/security-headers.ts @@ -16,7 +16,7 @@ export function generateHeaderRecommendations( const h = manifest.securityHeaders; if (!h) return []; - const siteUrl = config.siteUrl.replace(/\/$/, ""); + const _siteUrl = config.siteUrl.replace(/\/$/, ""); const recommendations: HeaderRecommendation[] = []; // HSTS @@ -143,10 +143,11 @@ export function generateHeaderReport(recommendations: HeaderRecommendation[]): s lines.push(""); } - lines.push("## Express Middleware (copy-paste ready)\n"); + lines.push("## Express Middleware (starter)\n"); lines.push("```typescript"); lines.push(generateExpressMiddleware(missing)); lines.push("```\n"); + lines.push("**CSP caveat.** The `Content-Security-Policy` value above is generated from resources visible in the site's root HTML. Third-party CDNs loaded dynamically (unpkg, jsdelivr, cdnjs), inline scripts injected by frameworks, and analytics beacons from SPAs often aren't captured and will need to be added by hand before the policy becomes enforceable. Start by deploying with `Content-Security-Policy-Report-Only` and tighten from there.\n"); lines.push("## Nginx Config (if using reverse proxy)\n"); lines.push("```nginx"); diff --git a/scanner/rules/access-controls.ts b/scanner/rules/access-controls.ts index eba52be..eaad2a8 100644 --- a/scanner/rules/access-controls.ts +++ b/scanner/rules/access-controls.ts @@ -33,7 +33,7 @@ const ADMIN_ROUTE_PATTERNS = [ ]; // Patterns that indicate sensitive operations without auth context -const SENSITIVE_OPS = [ +const _SENSITIVE_OPS = [ { pattern: /process\.env\[|process\.env\./g, label: "env-access" }, { pattern: /\.exec\s*\(|child_process|spawn\s*\(/g, label: "command-execution" }, { pattern: /fs\.(?:writeFile|unlink|rmdir|rm)\s*\(/g, label: "filesystem-write" }, @@ -49,7 +49,7 @@ export async function scanAccessControls(ctx: ScanContext): Promise<{ // Uses the non-admin branches endpoint (returns "protected" boolean) // Detailed rules (reviewer count, signed commits) require admin access // so we infer what we can from the basic API - let controls: AccessControls = { + const controls: AccessControls = { branchProtection: null, requiredReviews: null, signedCommits: null, @@ -175,9 +175,7 @@ export async function scanAccessControls(ctx: ScanContext): Promise<{ // Check for admin/sensitive routes for (const { pattern, label } of ADMIN_ROUTE_PATTERNS) { - pattern.lastIndex = 0; - let match; - while ((match = pattern.exec(content)) !== null) { + for (const match of content.matchAll(pattern)) { const method = match[1].toUpperCase(); const route = match[2]; diff --git a/scanner/rules/ai-risk-classifier.test.ts b/scanner/rules/ai-risk-classifier.test.ts new file mode 100644 index 0000000..badfaf9 --- /dev/null +++ b/scanner/rules/ai-risk-classifier.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it } from "vitest"; +import type { AISystem } from "../types.js"; +import { classifyAISystems, applyAISystemOverrides } from "./ai-risk-classifier.js"; + +function sys(overrides: Partial = {}): AISystem { + return { + provider: "OpenAI", + sdk: "openai", + location: "package.json", + category: "inference", + dataFlows: [], + ...overrides, + }; +} + +describe("classifyAISystems", () => { + describe("prohibited keyword matches", () => { + it("flags systems whose location contains social-score as prohibited", () => { + const [result] = classifyAISystems([sys({ location: "src/social-score/rank.ts" })]); + expect(result?.riskTier).toBe("prohibited"); + expect(result?.riskTierSource).toBe("heuristic"); + expect(result?.riskReasoning).toMatch(/Article 5/); + }); + + it("flags usageLocations containing subliminal as prohibited even when location is a manifest", () => { + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: ["src/subliminal/nudge.ts"] }), + ]); + expect(result?.riskTier).toBe("prohibited"); + expect(result?.riskReasoning).toContain("subliminal"); + }); + }); + + describe("high-risk keyword matches", () => { + it.each([ + ["src/hiring/resume-screen.ts", "hiring"], + ["services/credit-score/evaluator.py", "credit-score"], + ["app/medical-dx/recommend.ts", "medical-dx"], + ["modules/biometric-id/match.ts", "biometric-id"], + ])("classifies %s as high-risk (matches %s)", (path, keyword) => { + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: [path] }), + ]); + expect(result?.riskTier).toBe("high"); + expect(result?.riskReasoning).toContain(keyword); + expect(result?.riskReasoning).toMatch(/Annex III/); + }); + }); + + describe("category defaults", () => { + it("training libraries default to limited", () => { + const [result] = classifyAISystems([sys({ category: "training", location: "package.json" })]); + expect(result?.riskTier).toBe("limited"); + }); + + it("vector-db defaults to minimal", () => { + const [result] = classifyAISystems([sys({ category: "vector-db", location: "package.json" })]); + expect(result?.riskTier).toBe("minimal"); + }); + + it("self-hosted defaults to minimal", () => { + const [result] = classifyAISystems([sys({ category: "self-hosted", location: "package.json" })]); + expect(result?.riskTier).toBe("minimal"); + }); + + it("inference in normal src path defaults to limited", () => { + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: ["src/chat/handler.ts"] }), + ]); + expect(result?.riskTier).toBe("limited"); + }); + + it("framework in normal src path defaults to limited", () => { + const [result] = classifyAISystems([ + sys({ category: "framework", location: "package.json", usageLocations: ["src/assistant/index.ts"] }), + ]); + expect(result?.riskTier).toBe("limited"); + }); + }); + + describe("low-signal downgrade for inference/framework", () => { + it("downgrades to minimal when usage is confined to test directories", () => { + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: ["tests/openai-mock.spec.ts", "__tests__/other.test.ts"] }), + ]); + expect(result?.riskTier).toBe("minimal"); + expect(result?.riskReasoning).toMatch(/test|dev tooling/i); + }); + + it("stays limited when usage mixes tests and real source", () => { + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: ["src/chat/handler.ts", "tests/chat.spec.ts"] }), + ]); + expect(result?.riskTier).toBe("limited"); + }); + + it("stays limited when usage is only in scripts (low-signal) but has no substantive src", () => { + // Regression guard: scripts/ alone is still low-signal, so this should + // downgrade. If someone changes LOW_SIGNAL_SEGMENTS later this test + // will catch the behaviour shift. + const [result] = classifyAISystems([ + sys({ location: "package.json", usageLocations: ["scripts/migrate.ts"] }), + ]); + expect(result?.riskTier).toBe("minimal"); + }); + }); + + describe("edge cases", () => { + it("returns unknown when no usage locations and no category default fits", () => { + // Unreachable via the AISystem type's current category union, but the + // classifier defensively handles the case. Cast to any to exercise it. + const input = sys({ category: "inference" }); + // Mutate to a bogus category to drive the default branch. + (input as any).category = "weird-category"; + const [result] = classifyAISystems([input]); + expect(result?.riskTier).toBe("unknown"); + }); + + it("preserves all non-classification fields", () => { + const input = sys({ + provider: "Anthropic", + sdk: "@anthropic-ai/sdk", + location: "requirements.txt", + category: "framework", + dataFlows: ["user-prompt", "context"], + usageLocations: ["src/chat/handler.ts"], + }); + const [result] = classifyAISystems([input]); + expect(result?.provider).toBe("Anthropic"); + expect(result?.sdk).toBe("@anthropic-ai/sdk"); + expect(result?.location).toBe("requirements.txt"); + expect(result?.category).toBe("framework"); + expect(result?.dataFlows).toEqual(["user-prompt", "context"]); + expect(result?.usageLocations).toEqual(["src/chat/handler.ts"]); + }); + }); +}); + +describe("applyAISystemOverrides", () => { + it("applies the euMarket default to every system when no overrides match", () => { + const classified = classifyAISystems([sys({ location: "package.json", usageLocations: ["src/chat/handler.ts"] })]); + const result = applyAISystemOverrides(classified, [], { euMarket: true }); + expect(result[0]?.euMarket).toBe(true); + expect(result[0]?.riskTierSource).toBe("heuristic"); + }); + + it("respects per-system euMarket override and flips riskTierSource to override", () => { + const classified = classifyAISystems([sys({ location: "package.json", usageLocations: ["src/chat/handler.ts"] })]); + const result = applyAISystemOverrides( + classified, + [{ location: "package.json", riskTier: "high", euMarket: false, purpose: "Internal analytics" }], + { euMarket: true }, + ); + expect(result[0]?.riskTier).toBe("high"); + expect(result[0]?.riskTierSource).toBe("override"); + expect(result[0]?.euMarket).toBe(false); + expect(result[0]?.riskReasoning).toContain("Internal analytics"); + }); + + it("matches overrides by location + optional name", () => { + const systems = classifyAISystems([ + sys({ provider: "OpenAI", sdk: "openai", location: "package.json" }), + sys({ provider: "Anthropic", sdk: "@anthropic-ai/sdk", location: "package.json" }), + ]); + const result = applyAISystemOverrides( + systems, + [{ location: "package.json", name: "@anthropic-ai/sdk", riskTier: "minimal" }], + { euMarket: false }, + ); + // Only the Anthropic entry should flip to the override + expect(result[0]?.riskTierSource).toBe("heuristic"); + expect(result[1]?.riskTierSource).toBe("override"); + expect(result[1]?.riskTier).toBe("minimal"); + }); + + it("leaves riskTier unchanged when an override has no riskTier field", () => { + const classified = classifyAISystems([sys({ location: "package.json", usageLocations: ["src/chat/handler.ts"] })]); + const originalTier = classified[0]?.riskTier; + const result = applyAISystemOverrides( + classified, + [{ location: "package.json", euMarket: true }], // riskTier deliberately omitted + { euMarket: false }, + ); + expect(result[0]?.riskTier).toBe(originalTier); + expect(result[0]?.euMarket).toBe(true); + // riskTierSource stays heuristic since no tier change was declared + expect(result[0]?.riskTierSource).toBe("heuristic"); + }); + + it("defaults euMarket to false when no defaults arg is provided", () => { + const classified = classifyAISystems([sys({ location: "package.json", usageLocations: ["src/chat/handler.ts"] })]); + const result = applyAISystemOverrides(classified, []); + expect(result[0]?.euMarket).toBe(false); + }); +}); diff --git a/scanner/rules/ai-risk-classifier.ts b/scanner/rules/ai-risk-classifier.ts index 09c90f6..69d6be1 100644 --- a/scanner/rules/ai-risk-classifier.ts +++ b/scanner/rules/ai-risk-classifier.ts @@ -1,4 +1,4 @@ -import { AISystem, AIRiskTier } from "../types.js"; +import { AISystem, } from "../types.js"; import type { AISystemOverride } from "../config.js"; // EU AI Act Article 5 — prohibited practices. Path keywords that might signal diff --git a/scanner/rules/cookies.ts b/scanner/rules/cookies.ts index 5b83ce7..0f9fe58 100644 --- a/scanner/rules/cookies.ts +++ b/scanner/rules/cookies.ts @@ -1,4 +1,4 @@ -import { extname } from "node:path"; + import { ScanContext, DataCollectionPoint } from "../types.js"; import { walkFiles, readFileContent, relativePath } from "../utils.js"; diff --git a/scanner/rules/endpoints.ts b/scanner/rules/endpoints.ts index a3b3189..3e68d01 100644 --- a/scanner/rules/endpoints.ts +++ b/scanner/rules/endpoints.ts @@ -17,7 +17,7 @@ const DESTRUCTURED_BODY = /const\s*\{([^}]+)\}\s*=\s*req\.body/g; const FLASK_POST = /@\w+\.route\s*\(\s*["']([^"']+)["'][^)]*methods\s*=\s*\[.*?["']POST["']/g; // Generic form data access -const FORM_DATA = /formData|FormData|form_data|request\.form/g; +const _FORM_DATA = /formData|FormData|form_data|request\.form/g; export async function scanEndpoints(ctx: ScanContext): Promise { const files = await walkFiles(ctx.repoPath, CODE_EXTENSIONS); @@ -47,16 +47,14 @@ export async function scanEndpoints(ctx: ScanContext): Promise s.trim().split(":")[0].split("=")[0].trim()); + for (const destructuredMatch of content.matchAll(DESTRUCTURED_BODY)) { + const destructured = destructuredMatch[1].split(",").map(s => s.trim().split(":")[0].split("=")[0].trim()); fields.push(...destructured.filter(f => f.length > 0)); } diff --git a/scanner/rules/forms.ts b/scanner/rules/forms.ts index ada2df4..89a788f 100644 --- a/scanner/rules/forms.ts +++ b/scanner/rules/forms.ts @@ -40,9 +40,7 @@ export async function scanForms(ctx: ScanContext): Promise = new Map(); for (const pattern of [INPUT_PATTERN, INPUT_PATTERN_ALT]) { - let match; - pattern.lastIndex = 0; - while ((match = pattern.exec(content)) !== null) { + for (const match of content.matchAll(pattern)) { const name = pattern === INPUT_PATTERN ? match[1] : match[2]; const type = pattern === INPUT_PATTERN ? match[2] : match[1]; if (name && !["submit", "hidden", "csrf", "_token"].includes(name.toLowerCase())) { @@ -54,9 +52,7 @@ export async function scanForms(ctx: ScanContext): Promise