diff --git a/AGENTS.md b/AGENTS.md index 8d3dea51..a46cfc80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permi - If you're uncomfortable pushing back out loud, just say "Strange things are afoot at the Circle K". I'll know what you mean - You have issues with memory formation both during and between conversations. Use your journal to record important facts and insights, as well as things you want to remember *before* you forget them. - You search your journal when you trying to remember or figure stuff out. -- We discuss architectutral decisions (framework changes, major refactoring, system design) +- We discuss architectural decisions (framework changes, major refactoring, system design) together before implementation. Routine fixes and clear implementations don't need discussion. @@ -53,7 +53,7 @@ When asked to do something, just do it - including obvious follow-up actions nee ## Designing software -- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accomodate would be difficult. +- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accommodate would be difficult. - When it doesn't conflict with YAGNI, architect for extensibility and flexibility. ## Third-Party Dependencies diff --git a/CLAUDE.md b/CLAUDE.md index 0053c468..be8d3626 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permi - If you're uncomfortable pushing back out loud, just say "Strange things are afoot at the Circle K". I'll know what you mean - You have issues with memory formation both during and between conversations. Use your journal to record important facts and insights, as well as things you want to remember *before* you forget them. - You search your journal when you trying to remember or figure stuff out. -- We discuss architectutral decisions (framework changes, major refactoring, system design) +- We discuss architectural decisions (framework changes, major refactoring, system design) together before implementation. Routine fixes and clear implementations don't need discussion. @@ -53,7 +53,7 @@ When asked to do something, just do it - including obvious follow-up actions nee ## Designing software -- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accomodate would be difficult. +- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accommodate would be difficult. - When it doesn't conflict with YAGNI, architect for extensibility and flexibility. ## Third-Party Dependencies diff --git a/README.md b/README.md index 0117ff22..88fc7ac3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ CVErt Ops pulls from 10 vulnerability data sources and merges them into a unifie - **CSAF** — Common Security Advisory Framework documents - **Generic** — Configurable adapter for custom or internal feeds -Each source is ingested independently, and a merge pipeline recomputes the canonical CVE record from all available sources on every update. A material hash (SHA-256 over normalized fields) tracks meaningful changes and drives alert evaluation — cosmetic updates don't trigger false alerts. +Each source is ingested independently through a shared HTTP client with per-feed circuit breakers (sony/gobreaker v2), SSRF-hardened transport, and response-body size limits. A merge pipeline recomputes the canonical CVE record from all available sources on every update. A material hash (SHA-256 over normalized fields) tracks meaningful changes and drives alert evaluation — cosmetic updates don't trigger false alerts. ### Full-Text Search and Faceted Filtering @@ -76,9 +76,17 @@ Every org gets full data isolation through dual-layer tenant separation: Four RBAC roles control access: **Owner** > **Admin** > **Member** > **Viewer**. Per-route middleware enforces minimum role requirements. API key authentication is supported with org-scoping and role caps. +### Multi-Factor Authentication + +MFA can be required per-org or per-user, with TOTP (authenticator apps) and email OTP as second factors. Enrollment is a multi-step flow gated by pending tokens that encode the remaining MFA requirements directly in their claims — the client can't skip a step by replaying an earlier token. TOTP verification uses `FOR UPDATE` locking with skew-aware step tracking to prevent code replay. Email OTP challenges have per-attempt rate limits and emit security events when exhausted. Admins can reset a user's MFA atomically, and password reset completion itself is MFA-gated. + ### Enterprise SSO -Organizations can configure OIDC-based single sign-on with domain-based auto-discovery. Supports GitHub OAuth, Google OIDC, and generic OIDC providers. Users can link SSO identities to existing accounts. SCIM provisioning for automated user lifecycle management is planned. +Organizations can configure OIDC-based single sign-on with domain-based auto-discovery. Supports GitHub OAuth, Google OIDC, and generic OIDC providers. Users can link SSO identities to existing accounts. + +### SCIM Provisioning + +Full SCIM 2.0 support for automated user lifecycle management via your identity provider (Okta, Azure AD, Google Workspace, etc.). Bearer-token authenticated endpoints expose standard `/Users` and `/Groups` resources with create/read/update/patch/delete and filter parsing. Group-to-role mappings let you drive RBAC membership directly from IdP groups — changes take effect on the next SCIM sync with no admin action required. Org members can be flagged `scim_exempt` to keep local accounts (emergency access, service accounts) from being deactivated by an IdP sync. A dedicated per-org rate limiter isolates SCIM traffic from the main API budget. ### Site Administration @@ -92,13 +100,13 @@ Site admins get a dedicated set of endpoints and UI views for: ### Testing -CVErt Ops has extensive test coverage — over 1,600 Go test functions across 147 test files, plus 32 frontend test suites. Aggregate statement coverage is 62%, but that number is diluted by generated code (sqlc output), test infrastructure, and CLI boilerplate — all at 0%. Business logic packages where coverage matters most range from 80% to 100%: alert DSL 94%, feed adapters 84-100%, auth 89%, merge 87%, retention 96%, worker 91%. +CVErt Ops has extensive test coverage — over 2,200 Go test functions across 200+ test files, plus 32 frontend test suites. Aggregate statement coverage is diluted by generated code (sqlc output), test infrastructure, and CLI boilerplate — all at 0% by design. Business logic packages where coverage matters most — alert DSL, feed adapters, auth, merge, retention, worker — sit consistently in the 80–100% range. -**Integration tests hit real infrastructure.** Over 70 test files run against a real PostgreSQL instance (via testcontainers) with full RLS enforcement, real migrations, and seeded data. API tests stand up real HTTP servers and exercise the full middleware stack — auth, RBAC, CSRF, tier enforcement, rate limiting. No mocking away the hard parts. +**Integration tests hit real infrastructure.** Over 100 test files run against a real PostgreSQL instance (via testcontainers) with full RLS enforcement, real migrations, and seeded data. API tests stand up real HTTP servers and exercise the full middleware stack — auth, RBAC, MFA, SCIM, CSRF, tier enforcement, rate limiting. No mocking away the hard parts. -**Shared test infrastructure** in `internal/testutil/` provides reusable helpers: a managed test database with automatic migration, seed data utilities, a mock OIDC provider for SSO testing, and a local SMTP server for email delivery tests. This keeps individual test files focused on the behavior under test rather than setup boilerplate. +**Shared test infrastructure** in `internal/testutil/` provides reusable helpers: a managed test database with automatic migration, seed data utilities, a mock OIDC provider for SSO testing, and a local SMTP server for email delivery tests. `testutil.SeedCorpus` seeds a test database with 65 real CVEs across 8 feeds (NVD, MITRE, GHSA, OSV, KEV, MSRC, Red Hat, EPSS) by running captured upstream responses through the real merge pipeline — giving downstream tests (alert evaluation, search, reports) a realistic corpus without hand-crafted fixtures. -**Feed adapter tests** use recorded HTTP responses to verify parsing, streaming, error handling, and rate limit compliance without hitting upstream APIs. Alert DSL tests cover the compiler, evaluator, and all three evaluation paths (realtime, batch, EPSS). Notification delivery tests verify the transactional safety guarantees — claim, commit, deliver, record — with real database state. +**Feed adapter tests** use captured HTTP responses served via `httptest` to verify parsing, streaming, error handling, and rate limit compliance without hitting upstream APIs. Each adapter has a golden-file test that runs real captured responses end-to-end, catching upstream schema drift that unit tests with hand-crafted fixtures cannot detect. Alert DSL tests cover the compiler, evaluator, and all three evaluation paths (realtime, batch, EPSS). Notification delivery tests verify the transactional safety guarantees — claim, commit, deliver, record — with real database state. The frontend uses Vitest with jsdom and Vue Test Utils for component and composable testing. @@ -118,17 +126,23 @@ This project is developed with [Claude Code](https://claude.com/claude-code) usi **Supply chain security** — GitHub CodeQL scans on every PR, Dependabot alerts and automated security update PRs for vulnerable dependencies, secret scanning with push protection, and weekly version update PRs for Go modules, npm packages, and GitHub Actions. -**Structured planning** — features are designed in `docs/plans/` before implementation, with research notes in `dev/research-findings/` capturing technical investigations and trade-off analyses for architectural decisions. +**Structured planning** — features are designed in `dev/plans/` before implementation, with research notes in `dev/research-findings/` capturing technical investigations and trade-off analyses for architectural decisions. ## Architecture -CVErt Ops is a single Go binary with three runtime modes: +CVErt Ops is a single Go binary (`cvert-ops`) with cobra subcommands covering every operational task. A second small binary (`healthcheck`) ships alongside it for container probes. | Command | What it runs | |---------|-------------| | `cvert-ops serve` | HTTP API server + embedded background worker pool | | `cvert-ops worker` | Standalone worker pool (no HTTP) | | `cvert-ops migrate` | Database migrations | +| `cvert-ops import-bulk` | Bulk-import CVE data from a file (dev seed / airgapped loader) | +| `cvert-ops doctor` | System health checks (DB, feeds, config, migrations) | +| `cvert-ops validate-feeds` | Validate feed configuration without running a sync | +| `cvert-ops quota` | Manage per-org AI quota (`set`/`get`/`list`/`delete`) | +| `cvert-ops rotate-encryption-key` | Rotate the at-rest encryption key with re-encrypt pass | +| `healthcheck` | Minimal container liveness/readiness probe | The background worker handles feed ingestion, alert evaluation, notification delivery, retention cleanup, and report generation — all via an internal job queue in PostgreSQL. No Redis, no RabbitMQ, no external dependencies beyond Postgres. @@ -150,21 +164,27 @@ The background worker handles feed ingestion, alert evaluation, notification del ``` cmd/cvert-ops/ CLI entry points (cobra subcommands) +cmd/healthcheck/ Container liveness/readiness probe binary internal/ ai/ LLM client, quota, sanitization alert/ Alert DSL compiler and evaluator - api/ HTTP handlers and middleware + api/ HTTP handlers and middleware (REST + SCIM 2.0) audit/ Audit logging - auth/ JWT, OAuth, API keys, Argon2id + auth/ JWT, OAuth/OIDC, MFA (TOTP + email OTP), API keys, Argon2id config/ Environment-based configuration - feed/ Feed adapters (NVD, MITRE, KEV, OSV, GHSA, EPSS, ...) + crypto/ Encryption helpers (AES-GCM with AAD binding) + doctor/ System health check framework + feed/ Feed adapters + circuit breaker + SSRF-hardened client ingest/ Feed ingestion orchestrator merge/ CVE merge pipeline + metrics/ Prometheus counters and histograms notify/ Notification channels and delivery report/ Scheduled report generation retention/ Data retention policies search/ Full-text search and facets - store/ Repository layer (sqlc + squirrel) + secure/ Async security event pipeline + store/ Repository layer (sqlc + squirrel) + SCIM store methods + tier/ Subscription tier logic worker/ Job queue and worker pool migrations/ SQL migration files (embedded) templates/ Notification and report templates (embedded) diff --git a/cmd/cvert-ops/rotate.go b/cmd/cvert-ops/rotate.go index 6eefecbf..e1153858 100644 --- a/cmd/cvert-ops/rotate.go +++ b/cmd/cvert-ops/rotate.go @@ -100,21 +100,22 @@ func rotateEncryptionKeys(ctx context.Context, pool *pgxpool.Pool, currentKey, p return 0, fmt.Errorf("set bypass_rls: %w", err) } - rows, err := tx.Query(ctx, "SELECT id, client_secret_enc FROM sso_connections") + rows, err := tx.Query(ctx, "SELECT id, org_id, client_secret_enc FROM sso_connections") if err != nil { return 0, fmt.Errorf("query sso_connections: %w", err) } defer rows.Close() type pending struct { - id string - enc []byte + id string + orgID [16]byte + enc []byte } var updates []pending for rows.Next() { var p pending - if err := rows.Scan(&p.id, &p.enc); err != nil { + if err := rows.Scan(&p.id, &p.orgID, &p.enc); err != nil { return 0, fmt.Errorf("scan row: %w", err) } updates = append(updates, p) @@ -125,12 +126,12 @@ func rotateEncryptionKeys(ctx context.Context, pool *pgxpool.Pool, currentKey, p count := 0 for _, u := range updates { - plaintext, err := crypto.DecryptWithFallback(currentKey, previousKey, u.enc) + plaintext, err := crypto.DecryptWithFallback(currentKey, previousKey, u.enc, u.orgID[:]) if err != nil { return 0, fmt.Errorf("decrypt row %s: %w", u.id, err) } - newEnc, err := crypto.Encrypt(currentKey, plaintext) + newEnc, err := crypto.Encrypt(currentKey, plaintext, u.orgID[:]) if err != nil { return 0, fmt.Errorf("re-encrypt row %s: %w", u.id, err) } diff --git a/cmd/cvert-ops/rotate_test.go b/cmd/cvert-ops/rotate_test.go index fc9acd0d..b851a37c 100644 --- a/cmd/cvert-ops/rotate_test.go +++ b/cmd/cvert-ops/rotate_test.go @@ -29,9 +29,9 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) { } orgID := org.ID - // Encrypt a secret with the old key. + // Encrypt a secret with the old key, bound to the org. secret := []byte("my-client-secret") - enc, err := crypto.Encrypt(oldKey, secret) + enc, err := crypto.Encrypt(oldKey, secret, orgID[:]) if err != nil { t.Fatalf("encrypt with old key: %v", err) } @@ -64,7 +64,7 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) { t.Fatalf("read re-encrypted value: %v", err) } - plaintext, err := crypto.Decrypt(newKey, reEncrypted) + plaintext, err := crypto.Decrypt(newKey, reEncrypted, orgID[:]) if err != nil { t.Fatalf("decrypt with new key failed: %v", err) } @@ -73,7 +73,7 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) { } // Verify old key alone no longer works. - _, err = crypto.Decrypt(oldKey, reEncrypted) + _, err = crypto.Decrypt(oldKey, reEncrypted, orgID[:]) if err == nil { t.Error("decrypt with old key should fail on re-encrypted data, but succeeded") } diff --git a/dev/specs/sso-secret-storage.md b/dev/specs/sso-secret-storage.md new file mode 100644 index 00000000..06692346 --- /dev/null +++ b/dev/specs/sso-secret-storage.md @@ -0,0 +1,93 @@ +# SSO Secret Storage Architecture + +This document describes how CVErt Ops stores and manages user-provided secrets (specifically, OAuth/OIDC client secrets for enterprise SSO connections) in the production/SaaS configuration. + +## What's Encrypted + +The **only** user-input secret encrypted at rest in the database is `sso_connections.client_secret_enc` — the OIDC client secret that tenants provide when configuring enterprise SSO. It is stored as `BYTEA` in Postgres (migration `000028_sso_connections.up.sql`). + +## Encryption Scheme + +**AES-256-GCM** with random 12-byte nonces, implemented in `internal/crypto/aes.go`. + +- **Ciphertext format:** `nonce (12 bytes) || ciphertext + GCM authentication tag` +- **Nonce source:** `crypto/rand.Reader` (OS CSPRNG) +- **Library:** Go stdlib `crypto/aes` and `crypto/cipher` — no external crypto dependencies + +AES-256-GCM provides both confidentiality and integrity (authenticated encryption). An attacker who obtains a database dump cannot read or tamper with the client secrets without also possessing the encryption key. + +## Key Sourcing + +The encryption key is a raw 32-byte value provided as 64 hex characters via: + +1. **Startup:** The `SSO_ENCRYPTION_KEY` environment variable, parsed by `internal/config/reloadable.go` +2. **Hot-reload:** A secrets file (one `KEY=VALUE` per line) can be reloaded at runtime via `SIGHUP` signal or the admin API reload endpoint. The key is swapped atomically using `atomic.Pointer` in `config.Holder`, so in-flight requests are never disrupted + +The API handler reads the active key via `srv.ssoEncryptionKey()` in `internal/api/sso.go`, which prefers the hot-reloadable config, falling back to the startup config value. + +## Key Rotation + +Key rotation uses a **dual-key** strategy with zero downtime: + +1. **Operator** generates a new 32-byte key (`openssl rand -hex 32`) +2. **Operator** moves the current `SSO_ENCRYPTION_KEY` value to `SSO_ENCRYPTION_KEY_PREVIOUS` and sets the new key as `SSO_ENCRYPTION_KEY` in the secrets file +3. **Operator** reloads config (SIGHUP or admin API) +4. **During the transition window**, all decryption uses `crypto.DecryptWithFallback()` — tries the current key first, then falls back to the previous key on GCM authentication failure. Structural errors (truncated ciphertext, invalid key length) fail fast without attempting fallback +5. **Operator** runs `cvert-ops rotate-encryption-key`, which re-encrypts every `sso_connections.client_secret_enc` row in a single Postgres transaction: decrypt with fallback, re-encrypt with current key +6. **After re-encryption succeeds**, the operator removes `SSO_ENCRYPTION_KEY_PREVIOUS` and reloads config + +The re-encryption command is transactional — if it fails partway through, the transaction rolls back and all rows remain encrypted with the original key. Safe to retry. + +The full step-by-step procedure is documented in `docs/deployment/runbooks/secret-rotation.md`. + +## Security Boundaries and Assumptions + +| Boundary | Status | +|----------|--------| +| **Encryption at rest** | AES-256-GCM. Protects against database dump or backup theft | +| **Tenant isolation** | Row-Level Security (RLS) on `sso_connections` + `org_id` scoping. One tenant cannot read another's encrypted secret | +| **Key storage** | The encryption key lives in an environment variable or secrets file on the host. There is no KMS or HSM wrapping — compromise of the application server's environment means compromise of the key | +| **Memory exposure** | The key is held in process memory as a `[32]byte`. Standard Go runtime — no `mlock` or secure memory wipe. Acceptable for non-HSM deployments | +| **Rotation atomicity** | The `rotate-encryption-key` command runs in a single DB transaction. Failure leaves all rows encrypted with the old key (safe to retry) | +| **No envelope encryption** | There is no KMS-wrapped DEK/KEK split. `SSO_ENCRYPTION_KEY` is the data encryption key directly. Key rotation therefore requires re-encrypting every row (currently only `sso_connections`, so the blast radius is small) | + +### Deployment expectation + +The security model assumes that the deployment environment adequately protects the `SSO_ENCRYPTION_KEY` value. In practice this means: + +- **Container deployments:** Use the platform's native secret injection (Kubernetes Secrets, Docker Swarm secrets, ECS task definition secrets, etc.) +- **Cloud VMs:** Use a cloud secret manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) to inject the value into the environment at startup +- **Self-hosted:** Ensure the secrets file has restrictive file permissions and is excluded from backups and version control + +If CVErt Ops later needs to support a managed SaaS model where the operator controls infrastructure, the natural upgrade path would be envelope encryption with a cloud KMS wrapping the SSO encryption key. + +## What's NOT Encrypted at Rest + +These values are **not** stored in the database — they live only in environment variables or the secrets file: + +- OAuth provider secrets (`GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_SECRET`) — app-level config, not tenant-provided +- JWT signing secrets (`JWT_SECRET`, `JWT_SECRET_PREVIOUS`) +- SMTP credentials (`SMTP_PASSWORD`) + +These values are stored in the database but use **hashing, not encryption** (correct approach — they never need to be recovered in plaintext): + +- User passwords — argon2id +- API key hashes + +## Key Files + +| File | Role | +|------|------| +| `internal/crypto/aes.go` | AES-256-GCM Encrypt / Decrypt / DecryptWithFallback | +| `internal/config/reloadable.go` | Hot-reloadable config with atomic key swap | +| `internal/api/sso.go` | SSO handler — encrypts on write, decrypts on read | +| `cmd/cvert-ops/rotate.go` | CLI re-encryption command | +| `migrations/000028_sso_connections.up.sql` | Schema with `client_secret_enc BYTEA` column + RLS | +| `internal/store/queries/sso.sql` | sqlc queries (encrypted column passed as opaque bytes) | +| `docs/deployment/runbooks/secret-rotation.md` | Operator-facing rotation procedures | + +## Dependencies + +- **Go stdlib crypto** (`crypto/aes`, `crypto/cipher`, `crypto/rand`) — no third-party crypto libraries +- **pgx** for the rotation transaction +- **Operator-managed key** — no external secrets manager SDK dependency diff --git a/go.mod b/go.mod index d1af5cbc..003b55fa 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.9.1 - github.com/lib/pq v1.12.0 + github.com/lib/pq v1.12.3 github.com/pquerna/otp v1.5.0 github.com/prometheus/client_golang v1.23.2 github.com/sony/gobreaker/v2 v2.4.0 @@ -32,7 +32,7 @@ require ( golang.org/x/crypto v0.49.0 golang.org/x/oauth2 v0.36.0 golang.org/x/time v0.15.0 - google.golang.org/genai v1.52.0 + google.golang.org/genai v1.52.1 ) require ( diff --git a/go.sum b/go.sum index 6da185c1..16586b1b 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= -github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= @@ -276,8 +276,8 @@ golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.52.0 h1:ekVIxWHtLUNbt+v0WWi4j3JT4yrHDEbysMcHQcaCQoI= -google.golang.org/genai v1.52.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genai v1.52.1 h1:dYoljKtLDXMiBdVaClSJ/ZPwZ7j1N0lGjMhwOKOQUlk= +google.golang.org/genai v1.52.1/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= diff --git a/internal/api/auth_mfa.go b/internal/api/auth_mfa.go index 401590b1..0ff86a11 100644 --- a/internal/api/auth_mfa.go +++ b/internal/api/auth_mfa.go @@ -374,7 +374,7 @@ func (srv *Server) verifyTOTP(ctx context.Context, userID uuid.UUID, code string return false, fmt.Errorf("encryption key: %w", err) } prevKey := srv.ssoEncryptionKeyPrevious() - secretBytes, err := crypto.DecryptWithFallback(encKey, prevKey, cred.SecretEnc) + secretBytes, err := crypto.DecryptWithFallback(encKey, prevKey, cred.SecretEnc, userID[:]) if err != nil { return false, fmt.Errorf("decrypt TOTP secret: %w", err) } @@ -580,7 +580,7 @@ func (srv *Server) mfaTOTPSetupHandler(ctx context.Context, input *mfaTOTPSetupI slog.ErrorContext(ctx, "totp-setup: encryption key", "error", err) return nil, huma.Error500InternalServerError("encryption key not configured") } - secretEnc, err := crypto.Encrypt(encKey, []byte(key.Secret())) + secretEnc, err := crypto.Encrypt(encKey, []byte(key.Secret()), userID[:]) if err != nil { slog.ErrorContext(ctx, "totp-setup: encrypt secret", "error", err) return nil, huma.Error500InternalServerError("internal error") @@ -645,7 +645,7 @@ func (srv *Server) mfaTOTPConfirmHandler(ctx context.Context, input *mfaTOTPConf return nil, huma.Error500InternalServerError("internal error") } prevKey := srv.ssoEncryptionKeyPrevious() - secretBytes, err := crypto.DecryptWithFallback(encKey, prevKey, enrollClaims.SecretEnc) + secretBytes, err := crypto.DecryptWithFallback(encKey, prevKey, enrollClaims.SecretEnc, userID[:]) if err != nil { slog.ErrorContext(ctx, "totp-confirm: decrypt secret", "error", err) return nil, huma.Error500InternalServerError("internal error") @@ -677,7 +677,7 @@ func (srv *Server) mfaTOTPConfirmHandler(ctx context.Context, input *mfaTOTPConf // Re-encrypt secret for DB storage (enrollment cookie used same key, but // re-encrypt to get a fresh nonce for defense in depth). - secretEncDB, err := crypto.Encrypt(encKey, secretBytes) + secretEncDB, err := crypto.Encrypt(encKey, secretBytes, userID[:]) if err != nil { slog.ErrorContext(ctx, "totp-confirm: re-encrypt secret", "error", err) return nil, huma.Error500InternalServerError("internal error") diff --git a/internal/api/auth_mfa_test.go b/internal/api/auth_mfa_test.go index 0fa9b817..f4531871 100644 --- a/internal/api/auth_mfa_test.go +++ b/internal/api/auth_mfa_test.go @@ -55,7 +55,7 @@ func enrollTOTP(t *testing.T, ctx context.Context, srv *Server, userID uuid.UUID if err != nil { t.Fatalf("enrollTOTP: encryption key: %v", err) } - secretEnc, err := crypto.Encrypt(encKey, []byte(secret)) + secretEnc, err := crypto.Encrypt(encKey, []byte(secret), userID[:]) if err != nil { t.Fatalf("enrollTOTP: encrypt: %v", err) } diff --git a/internal/api/oauth_oidc.go b/internal/api/oauth_oidc.go index 1b32e807..245764a0 100644 --- a/internal/api/oauth_oidc.go +++ b/internal/api/oauth_oidc.go @@ -53,7 +53,7 @@ func (srv *Server) oidcBuildOAuthConfig(ctx context.Context, conn *store.SSOConn if err != nil { return nil, nil, fmt.Errorf("encryption key: %w", err) } - secret, err := crypto.DecryptWithFallback(key, srv.ssoEncryptionKeyPrevious(), conn.ClientSecretEnc) + secret, err := crypto.DecryptWithFallback(key, srv.ssoEncryptionKeyPrevious(), conn.ClientSecretEnc, conn.OrgID[:]) if err != nil { return nil, nil, fmt.Errorf("decrypt secret: %w", err) } diff --git a/internal/api/sso.go b/internal/api/sso.go index 7a5a657a..bc9af9ef 100644 --- a/internal/api/sso.go +++ b/internal/api/sso.go @@ -171,7 +171,7 @@ func (srv *Server) createSSOHandler(w http.ResponseWriter, r *http.Request) { writeProblem(w, http.StatusInternalServerError, "server configuration error") return } - encSecret, err := crypto.Encrypt(key, []byte(req.ClientSecret)) + encSecret, err := crypto.Encrypt(key, []byte(req.ClientSecret), orgID[:]) if err != nil { slog.ErrorContext(r.Context(), "sso create: encrypt secret", "error", err) writeProblem(w, http.StatusInternalServerError, "encryption error") @@ -347,7 +347,7 @@ func (srv *Server) patchSSOHandler(w http.ResponseWriter, r *http.Request) { writeProblem(w, http.StatusInternalServerError, "server configuration error") return } - secretEnc, err = crypto.Encrypt(key, []byte(*req.ClientSecret)) + secretEnc, err = crypto.Encrypt(key, []byte(*req.ClientSecret), orgID[:]) if err != nil { slog.ErrorContext(r.Context(), "sso patch: encrypt secret", "error", err) writeProblem(w, http.StatusInternalServerError, "encryption error") diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go index 2d88fe65..97d801a8 100644 --- a/internal/crypto/aes.go +++ b/internal/crypto/aes.go @@ -15,9 +15,10 @@ import ( // authentication fails and previousKey is non-zero, it retries with // previousKey. This supports seamless encryption key rotation. // Structural errors (truncated ciphertext, invalid key) fail immediately -// without attempting fallback. -func DecryptWithFallback(currentKey, previousKey [32]byte, data []byte) ([]byte, error) { - plaintext, err := Decrypt(currentKey, data) +// without attempting fallback. The aad (additional authenticated data) is +// passed through to GCM and must match the value used during encryption. +func DecryptWithFallback(currentKey, previousKey [32]byte, data []byte, aad []byte) ([]byte, error) { + plaintext, err := Decrypt(currentKey, data, aad) if err == nil { return plaintext, nil } @@ -25,7 +26,7 @@ func DecryptWithFallback(currentKey, previousKey [32]byte, data []byte) ([]byte, // Only fall back on GCM authentication failure (wrong key). // Structural errors (truncated ciphertext, invalid key) fail fast. if previousKey != [32]byte{} && isGCMAuthError(err) { - plaintext, err2 := Decrypt(previousKey, data) + plaintext, err2 := Decrypt(previousKey, data, aad) if err2 == nil { return plaintext, nil } @@ -42,8 +43,10 @@ func isGCMAuthError(err error) bool { } // Encrypt encrypts plaintext using AES-256-GCM with a random nonce. -// Returns nonce || ciphertext. -func Encrypt(key [32]byte, plaintext []byte) ([]byte, error) { +// Returns nonce || ciphertext. The aad (additional authenticated data) is +// mixed into the GCM authentication tag, binding the ciphertext to a context +// (e.g., an org_id or user_id). Pass nil for context-free encryption. +func Encrypt(key [32]byte, plaintext []byte, aad []byte) ([]byte, error) { block, err := aes.NewCipher(key[:]) if err != nil { return nil, fmt.Errorf("aes new cipher: %w", err) @@ -60,12 +63,13 @@ func Encrypt(key [32]byte, plaintext []byte) ([]byte, error) { } // Seal appends ciphertext to nonce, so result is nonce || ciphertext. - return gcm.Seal(nonce, nonce, plaintext, nil), nil + return gcm.Seal(nonce, nonce, plaintext, aad), nil } // Decrypt decrypts AES-256-GCM ciphertext produced by Encrypt. -// Expects nonce (12 bytes) || ciphertext. -func Decrypt(key [32]byte, data []byte) ([]byte, error) { +// Expects nonce (12 bytes) || ciphertext. The aad must match the value +// used during encryption; a mismatch causes an authentication failure. +func Decrypt(key [32]byte, data []byte, aad []byte) ([]byte, error) { block, err := aes.NewCipher(key[:]) if err != nil { return nil, fmt.Errorf("aes new cipher: %w", err) @@ -82,7 +86,7 @@ func Decrypt(key [32]byte, data []byte) ([]byte, error) { } nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + plaintext, err := gcm.Open(nil, nonce, ciphertext, aad) if err != nil { return nil, fmt.Errorf("gcm decrypt: %w", err) } diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go index 1c5acf1a..2537ce87 100644 --- a/internal/crypto/aes_test.go +++ b/internal/crypto/aes_test.go @@ -23,12 +23,12 @@ func TestAESGCM_RoundTrip(t *testing.T) { key := testKey(t) plaintext := []byte("secret webhook signing key 🔑") - ciphertext, err := Encrypt(key, plaintext) + ciphertext, err := Encrypt(key, plaintext, nil) if err != nil { t.Fatalf("Encrypt: %v", err) } - got, err := Decrypt(key, ciphertext) + got, err := Decrypt(key, ciphertext, nil) if err != nil { t.Fatalf("Decrypt: %v", err) } @@ -37,16 +37,73 @@ func TestAESGCM_RoundTrip(t *testing.T) { } } +func TestAESGCM_RoundTrip_WithAAD(t *testing.T) { + t.Parallel() + key := testKey(t) + plaintext := []byte("org-scoped secret") + aad := []byte("org-id-abc-123") + + ciphertext, err := Encrypt(key, plaintext, aad) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + + got, err := Decrypt(key, ciphertext, aad) + if err != nil { + t.Fatalf("Decrypt: %v", err) + } + if !bytes.Equal(got, plaintext) { + t.Errorf("round-trip mismatch: got %q, want %q", got, plaintext) + } +} + +func TestAESGCM_AADMismatch_Rejected(t *testing.T) { + t.Parallel() + key := testKey(t) + plaintext := []byte("bound to org A") + aadA := []byte("org-A") + aadB := []byte("org-B") + + ciphertext, err := Encrypt(key, plaintext, aadA) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + + // Decrypting with different AAD must fail (ciphertext relocation attack). + _, err = Decrypt(key, ciphertext, aadB) + if err == nil { + t.Error("Decrypt succeeded with wrong AAD, want authentication failure") + } +} + +func TestAESGCM_AADVsNilAAD_Rejected(t *testing.T) { + t.Parallel() + key := testKey(t) + plaintext := []byte("has AAD binding") + aad := []byte("some-context") + + ciphertext, err := Encrypt(key, plaintext, aad) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + + // Encrypted with AAD, decrypted without — must fail. + _, err = Decrypt(key, ciphertext, nil) + if err == nil { + t.Error("Decrypt with nil AAD succeeded on AAD-encrypted data, want failure") + } +} + func TestAESGCM_UniqueNonce(t *testing.T) { t.Parallel() key := testKey(t) plaintext := []byte("same input") - ct1, err := Encrypt(key, plaintext) + ct1, err := Encrypt(key, plaintext, nil) if err != nil { t.Fatalf("Encrypt 1: %v", err) } - ct2, err := Encrypt(key, plaintext) + ct2, err := Encrypt(key, plaintext, nil) if err != nil { t.Fatalf("Encrypt 2: %v", err) } @@ -60,7 +117,7 @@ func TestAESGCM_TamperedCiphertext(t *testing.T) { t.Parallel() key := testKey(t) - ciphertext, err := Encrypt(key, []byte("tamper me")) + ciphertext, err := Encrypt(key, []byte("tamper me"), nil) if err != nil { t.Fatalf("Encrypt: %v", err) } @@ -70,7 +127,7 @@ func TestAESGCM_TamperedCiphertext(t *testing.T) { copy(tampered, ciphertext) tampered[len(tampered)-1] ^= 0xff - _, err = Decrypt(key, tampered) + _, err = Decrypt(key, tampered, nil) if err == nil { t.Error("Decrypt succeeded on tampered ciphertext, want error") } @@ -81,12 +138,12 @@ func TestAESGCM_WrongKey(t *testing.T) { key1 := testKey(t) key2 := testKey(t) - ciphertext, err := Encrypt(key1, []byte("wrong key test")) + ciphertext, err := Encrypt(key1, []byte("wrong key test"), nil) if err != nil { t.Fatalf("Encrypt: %v", err) } - _, err = Decrypt(key2, ciphertext) + _, err = Decrypt(key2, ciphertext, nil) if err == nil { t.Error("Decrypt succeeded with wrong key, want error") } @@ -96,12 +153,12 @@ func TestAESGCM_EmptyPlaintext(t *testing.T) { t.Parallel() key := testKey(t) - ciphertext, err := Encrypt(key, []byte{}) + ciphertext, err := Encrypt(key, []byte{}, nil) if err != nil { t.Fatalf("Encrypt empty: %v", err) } - got, err := Decrypt(key, ciphertext) + got, err := Decrypt(key, ciphertext, nil) if err != nil { t.Fatalf("Decrypt empty: %v", err) } @@ -116,7 +173,7 @@ func TestAESGCM_ShortCiphertext(t *testing.T) { // ciphertext too short to contain a nonce is rejected at runtime. key := testKey(t) - _, err := Decrypt(key, []byte("short")) + _, err := Decrypt(key, []byte("short"), nil) if err == nil { t.Error("Decrypt succeeded on too-short ciphertext, want error") } @@ -130,12 +187,12 @@ func TestDecryptWithFallback_CurrentKeyWorks(t *testing.T) { previousKey := testKey(t) plaintext := []byte("current key decryption") - ciphertext, err := Encrypt(currentKey, plaintext) + ciphertext, err := Encrypt(currentKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt: %v", err) } - got, err := DecryptWithFallback(currentKey, previousKey, ciphertext) + got, err := DecryptWithFallback(currentKey, previousKey, ciphertext, nil) if err != nil { t.Fatalf("DecryptWithFallback: %v", err) } @@ -150,13 +207,13 @@ func TestDecryptWithFallback_PreviousKeyWorks(t *testing.T) { newKey := testKey(t) plaintext := []byte("encrypted with old key") - ciphertext, err := Encrypt(oldKey, plaintext) + ciphertext, err := Encrypt(oldKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt: %v", err) } // newKey as current fails GCM auth; oldKey as previous succeeds. - got, err := DecryptWithFallback(newKey, oldKey, ciphertext) + got, err := DecryptWithFallback(newKey, oldKey, ciphertext, nil) if err != nil { t.Fatalf("DecryptWithFallback: %v", err) } @@ -172,12 +229,12 @@ func TestDecryptWithFallback_BothKeysWrong(t *testing.T) { keyC := testKey(t) plaintext := []byte("neither key works") - ciphertext, err := Encrypt(keyA, plaintext) + ciphertext, err := Encrypt(keyA, plaintext, nil) if err != nil { t.Fatalf("Encrypt: %v", err) } - _, err = DecryptWithFallback(keyB, keyC, ciphertext) + _, err = DecryptWithFallback(keyB, keyC, ciphertext, nil) if err == nil { t.Error("DecryptWithFallback succeeded with both wrong keys, want error") } @@ -189,13 +246,13 @@ func TestDecryptWithFallback_NoPreviousKey(t *testing.T) { var zeroKey [32]byte plaintext := []byte("no previous key") - ciphertext, err := Encrypt(currentKey, plaintext) + ciphertext, err := Encrypt(currentKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt: %v", err) } // Zero previous key → only current key tried. - got, err := DecryptWithFallback(currentKey, zeroKey, ciphertext) + got, err := DecryptWithFallback(currentKey, zeroKey, ciphertext, nil) if err != nil { t.Fatalf("DecryptWithFallback: %v", err) } @@ -212,7 +269,7 @@ func TestDecryptWithFallback_TruncatedCiphertext_NoFallback(t *testing.T) { previousKey := [32]byte{2} shortData := []byte("short") - _, err := DecryptWithFallback(currentKey, previousKey, shortData) + _, err := DecryptWithFallback(currentKey, previousKey, shortData, nil) if err == nil { t.Fatal("DecryptWithFallback succeeded on truncated ciphertext, want error") } @@ -231,14 +288,53 @@ func TestDecryptWithFallback_NoPreviousKeyCurrentFails(t *testing.T) { keyB := testKey(t) var zeroKey [32]byte - ciphertext, err := Encrypt(keyA, []byte("no previous key fails")) + ciphertext, err := Encrypt(keyA, []byte("no previous key fails"), nil) if err != nil { t.Fatalf("Encrypt: %v", err) } // Wrong current key, zero previous → returns error without panic. - _, err = DecryptWithFallback(keyB, zeroKey, ciphertext) + _, err = DecryptWithFallback(keyB, zeroKey, ciphertext, nil) if err == nil { t.Error("DecryptWithFallback succeeded with wrong current and zero previous, want error") } } + +func TestDecryptWithFallback_WithAAD(t *testing.T) { + t.Parallel() + currentKey := testKey(t) + previousKey := testKey(t) + plaintext := []byte("aad-bound secret") + aad := []byte("org-id-bytes") + + ciphertext, err := Encrypt(currentKey, plaintext, aad) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + + got, err := DecryptWithFallback(currentKey, previousKey, ciphertext, aad) + if err != nil { + t.Fatalf("DecryptWithFallback: %v", err) + } + if !bytes.Equal(got, plaintext) { + t.Errorf("plaintext mismatch: got %q, want %q", got, plaintext) + } +} + +func TestDecryptWithFallback_AADMismatch_Rejected(t *testing.T) { + t.Parallel() + currentKey := testKey(t) + var zeroKey [32]byte + plaintext := []byte("bound to org A") + + ciphertext, err := Encrypt(currentKey, plaintext, []byte("org-A")) + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + + // Correct key but wrong AAD must fail. + _, err = DecryptWithFallback(currentKey, zeroKey, ciphertext, []byte("org-B")) + if err == nil { + t.Error("DecryptWithFallback succeeded with wrong AAD, want error") + } +} diff --git a/internal/doctor/checks.go b/internal/doctor/checks.go index 4f2bbf57..5593812e 100644 --- a/internal/doctor/checks.go +++ b/internal/doctor/checks.go @@ -182,7 +182,7 @@ func (c *EncryptionSentinelCheck) Run(ctx context.Context) (string, string, erro return StatusFail, fmt.Sprintf("query system_settings: %v", err), nil } - _, err = crypto.DecryptWithFallback(c.Key, c.PreviousKey, value) + _, err = crypto.DecryptWithFallback(c.Key, c.PreviousKey, value, []byte("encryption_sentinel")) if err != nil { return StatusFail, fmt.Sprintf("sentinel decryption failed: %v — encryption key may have changed", err), nil } @@ -344,8 +344,8 @@ func (c *SecurityHeadersCheck) Run(ctx context.Context) (string, string, error) required := map[string]string{ "X-Content-Type-Options": "nosniff", - "X-Frame-Options": "DENY", - "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Frame-Options": "DENY", + "Referrer-Policy": "strict-origin-when-cross-origin", } var missing []string @@ -444,7 +444,7 @@ type StandardChecksConfig struct { SMTPHost string SMTPPort int SMTPUsername string - CORSAllowedOrigins string + CORSAllowedOrigins string CookieAuth bool ServerAddr string // empty in CLI mode, "http://localhost:{port}" in API mode } diff --git a/web/package-lock.json b/web/package-lock.json index 08e4a40d..370f0c6f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,16 +16,16 @@ "lucide-vue-next": "^0.577.0", "openapi-fetch": "^0.17.0", "pinia": "^3.0.4", - "reka-ui": "^2.9.1", + "reka-ui": "^2.9.3", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue-sonner": "^2.0.9" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", - "@types/node": "^24.11.0", + "@types/node": "^25.5.2", "@vitejs/plugin-vue": "^6.0.4", "@vitest/eslint-plugin": "^1.6.13", "@vue/eslint-config-typescript": "^14.7.0", @@ -33,13 +33,13 @@ "@vue/tsconfig": "^0.9.0", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-oxlint": "~1.56.0", + "eslint-plugin-oxlint": "~1.58.0", "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^29.0.0", "npm-run-all2": "^8.0.4", "openapi-typescript": "^7.13.0", - "oxlint": "~1.56.0", + "oxlint": "~1.58.0", "prettier": "3.8.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", @@ -756,9 +756,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz", - "integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.58.0.tgz", + "integrity": "sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==", "cpu": [ "arm" ], @@ -773,9 +773,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz", - "integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.58.0.tgz", + "integrity": "sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==", "cpu": [ "arm64" ], @@ -790,9 +790,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz", - "integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.58.0.tgz", + "integrity": "sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==", "cpu": [ "arm64" ], @@ -807,9 +807,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz", - "integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.58.0.tgz", + "integrity": "sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==", "cpu": [ "x64" ], @@ -824,9 +824,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz", - "integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.58.0.tgz", + "integrity": "sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==", "cpu": [ "x64" ], @@ -841,9 +841,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz", - "integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.58.0.tgz", + "integrity": "sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==", "cpu": [ "arm" ], @@ -858,9 +858,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz", - "integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.58.0.tgz", + "integrity": "sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==", "cpu": [ "arm" ], @@ -875,9 +875,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz", - "integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.58.0.tgz", + "integrity": "sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==", "cpu": [ "arm64" ], @@ -892,9 +892,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz", - "integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.58.0.tgz", + "integrity": "sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==", "cpu": [ "arm64" ], @@ -909,9 +909,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz", - "integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.58.0.tgz", + "integrity": "sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==", "cpu": [ "ppc64" ], @@ -926,9 +926,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz", - "integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.58.0.tgz", + "integrity": "sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==", "cpu": [ "riscv64" ], @@ -943,9 +943,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz", - "integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.58.0.tgz", + "integrity": "sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==", "cpu": [ "riscv64" ], @@ -960,9 +960,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz", - "integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.58.0.tgz", + "integrity": "sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==", "cpu": [ "s390x" ], @@ -977,9 +977,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz", - "integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.58.0.tgz", + "integrity": "sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==", "cpu": [ "x64" ], @@ -994,9 +994,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz", - "integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.58.0.tgz", + "integrity": "sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==", "cpu": [ "x64" ], @@ -1011,9 +1011,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz", - "integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.58.0.tgz", + "integrity": "sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==", "cpu": [ "arm64" ], @@ -1028,9 +1028,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz", - "integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.58.0.tgz", + "integrity": "sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==", "cpu": [ "arm64" ], @@ -1045,9 +1045,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz", - "integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.58.0.tgz", + "integrity": "sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==", "cpu": [ "ia32" ], @@ -1062,9 +1062,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz", - "integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.58.0.tgz", + "integrity": "sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==", "cpu": [ "x64" ], @@ -1865,13 +1865,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/web-bluetooth": { @@ -1881,20 +1881,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1904,9 +1904,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1920,16 +1920,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "engines": { @@ -1941,18 +1941,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "engines": { @@ -1963,18 +1963,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1985,9 +1985,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "license": "MIT", "engines": { @@ -1998,21 +1998,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2023,13 +2023,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "license": "MIT", "engines": { @@ -2041,21 +2041,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2065,20 +2065,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2089,17 +2089,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2351,39 +2351,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", @@ -2391,13 +2391,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/devtools-api": { @@ -2489,53 +2489,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.30" + "vue": "3.5.32" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -2550,13 +2550,13 @@ } }, "node_modules/@vue/tsconfig": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.0.tgz", - "integrity": "sha512-RP+v9Cpbsk1ZVXltCHHkYBr7+624x6gcijJXVjIcsYk7JXqvIpRtMwU2ARLvWDhmy9ffdFYxhsfJnPztADBohQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", "dev": true, "license": "MIT", "peerDependencies": { - "typescript": "5.x", + "typescript": ">= 5.8", "vue": "^3.4.0" }, "peerDependenciesMeta": { @@ -3263,13 +3263,16 @@ } }, "node_modules/eslint-plugin-oxlint": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-oxlint/-/eslint-plugin-oxlint-1.56.0.tgz", - "integrity": "sha512-s47/OjE4cfQ+CD4eA38g+5axvwuyswY5H6acCdVGIvowYuLVJ6zrR7N260XfVVLRuyjjPO9L77qNYwSbmRNyuw==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-oxlint/-/eslint-plugin-oxlint-1.58.0.tgz", + "integrity": "sha512-L3aZSg0x2fL0dXyOgoK8A1QUbnfGzXt6bX4AFD7Scauw6zVUBOZrES5eRTzLLGgeVg0el5lvqHGl1WFAGo14DA==", "dev": true, "license": "MIT", "dependencies": { "jsonc-parser": "^3.3.1" + }, + "peerDependencies": { + "oxlint": "~1.58.0" } }, "node_modules/eslint-plugin-vue": { @@ -4702,9 +4705,9 @@ } }, "node_modules/oxlint": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz", - "integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==", + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.58.0.tgz", + "integrity": "sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==", "dev": true, "license": "MIT", "bin": { @@ -4717,28 +4720,28 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.56.0", - "@oxlint/binding-android-arm64": "1.56.0", - "@oxlint/binding-darwin-arm64": "1.56.0", - "@oxlint/binding-darwin-x64": "1.56.0", - "@oxlint/binding-freebsd-x64": "1.56.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", - "@oxlint/binding-linux-arm-musleabihf": "1.56.0", - "@oxlint/binding-linux-arm64-gnu": "1.56.0", - "@oxlint/binding-linux-arm64-musl": "1.56.0", - "@oxlint/binding-linux-ppc64-gnu": "1.56.0", - "@oxlint/binding-linux-riscv64-gnu": "1.56.0", - "@oxlint/binding-linux-riscv64-musl": "1.56.0", - "@oxlint/binding-linux-s390x-gnu": "1.56.0", - "@oxlint/binding-linux-x64-gnu": "1.56.0", - "@oxlint/binding-linux-x64-musl": "1.56.0", - "@oxlint/binding-openharmony-arm64": "1.56.0", - "@oxlint/binding-win32-arm64-msvc": "1.56.0", - "@oxlint/binding-win32-ia32-msvc": "1.56.0", - "@oxlint/binding-win32-x64-msvc": "1.56.0" + "@oxlint/binding-android-arm-eabi": "1.58.0", + "@oxlint/binding-android-arm64": "1.58.0", + "@oxlint/binding-darwin-arm64": "1.58.0", + "@oxlint/binding-darwin-x64": "1.58.0", + "@oxlint/binding-freebsd-x64": "1.58.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.58.0", + "@oxlint/binding-linux-arm-musleabihf": "1.58.0", + "@oxlint/binding-linux-arm64-gnu": "1.58.0", + "@oxlint/binding-linux-arm64-musl": "1.58.0", + "@oxlint/binding-linux-ppc64-gnu": "1.58.0", + "@oxlint/binding-linux-riscv64-gnu": "1.58.0", + "@oxlint/binding-linux-riscv64-musl": "1.58.0", + "@oxlint/binding-linux-s390x-gnu": "1.58.0", + "@oxlint/binding-linux-x64-gnu": "1.58.0", + "@oxlint/binding-linux-x64-musl": "1.58.0", + "@oxlint/binding-openharmony-arm64": "1.58.0", + "@oxlint/binding-win32-arm64-msvc": "1.58.0", + "@oxlint/binding-win32-ia32-msvc": "1.58.0", + "@oxlint/binding-win32-x64-msvc": "1.58.0" }, "peerDependencies": { - "oxlint-tsgolint": ">=0.15.0" + "oxlint-tsgolint": ">=0.18.0" }, "peerDependenciesMeta": { "oxlint-tsgolint": { @@ -5109,9 +5112,9 @@ } }, "node_modules/reka-ui": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.2.tgz", - "integrity": "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.5.tgz", + "integrity": "sha512-6cZGIMgEeslpFLJ7IihaCSMPp1cJgl2eDkZ2vBMdl+HPUVBaV/iDPMWu3abT2KUkj1lir+oyHq5KelOTT9OheQ==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.13", @@ -5122,7 +5125,7 @@ "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", - "defu": "^6.1.4", + "defu": "^6.1.5", "ohash": "^2.0.11" }, "funding": { @@ -5640,9 +5643,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -5709,16 +5712,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5729,7 +5732,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/ufo": { @@ -5749,9 +5752,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, @@ -6025,16 +6028,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" diff --git a/web/package.json b/web/package.json index 33c826a4..436ed838 100644 --- a/web/package.json +++ b/web/package.json @@ -25,16 +25,16 @@ "lucide-vue-next": "^0.577.0", "openapi-fetch": "^0.17.0", "pinia": "^3.0.4", - "reka-ui": "^2.9.1", + "reka-ui": "^2.9.3", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", - "vue": "^3.5.30", + "vue": "^3.5.32", "vue-router": "^5.0.4", "vue-sonner": "^2.0.9" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", - "@types/node": "^24.11.0", + "@types/node": "^25.5.2", "@vitejs/plugin-vue": "^6.0.4", "@vitest/eslint-plugin": "^1.6.13", "@vue/eslint-config-typescript": "^14.7.0", @@ -42,13 +42,13 @@ "@vue/tsconfig": "^0.9.0", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-oxlint": "~1.56.0", + "eslint-plugin-oxlint": "~1.58.0", "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^29.0.0", "npm-run-all2": "^8.0.4", "openapi-typescript": "^7.13.0", - "oxlint": "~1.56.0", + "oxlint": "~1.58.0", "prettier": "3.8.1", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", diff --git a/web/src/components/__tests__/AppSidebar.test.ts b/web/src/components/__tests__/AppSidebar.test.ts index 04696d2e..fdef46e2 100644 --- a/web/src/components/__tests__/AppSidebar.test.ts +++ b/web/src/components/__tests__/AppSidebar.test.ts @@ -8,8 +8,8 @@ import { useAuthStore } from '@/stores/auth' // Mock vue-router vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ path: '/cves' })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ path: '/cves' })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -20,8 +20,8 @@ vi.mock('vue-router', () => ({ // Mock API client (needed by auth store) vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -126,7 +126,7 @@ describe('OrgSwitcher', () => { user_id: 'u1', email: 'sam@example.com', display_name: 'Sam Carter', - is_site_admin: false, + is_site_admin: false, orgs: [ { org_id: 'org-1', name: 'Acme Corp', role: 'owner' }, { org_id: 'org-2', name: 'Globex Inc', role: 'member' }, diff --git a/web/src/components/cve/__tests__/CveResultsTable.test.ts b/web/src/components/cve/__tests__/CveResultsTable.test.ts index bc46c910..956aca4f 100644 --- a/web/src/components/cve/__tests__/CveResultsTable.test.ts +++ b/web/src/components/cve/__tests__/CveResultsTable.test.ts @@ -8,8 +8,8 @@ import type { components } from '@/lib/api/schema' type CVEItem = components['schemas']['CVEItem'] vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ query: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -20,7 +20,8 @@ vi.mock('vue-router', () => ({ function makeCVE(overrides: Partial = {}): CVEItem { return { cve_id: 'CVE-2024-12345', - description_primary: 'A critical vulnerability in Apache Log4j allows remote code execution via crafted log messages.', + description_primary: + 'A critical vulnerability in Apache Log4j allows remote code execution via crafted log messages.', cvss_v3_score: 9.8, epss_score: 0.975, severity: 'critical', @@ -133,7 +134,7 @@ describe('CveResultsTable', () => { const wrapper = await mountTable({ items }) const cells = wrapper.findAll('td') - const epssCell = cells.find(c => c.text() === '\u2014') + const epssCell = cells.find((c) => c.text() === '\u2014') expect(epssCell).toBeDefined() }) }) diff --git a/web/src/components/cve/__tests__/CveSearchFilters.test.ts b/web/src/components/cve/__tests__/CveSearchFilters.test.ts index 9d2c4056..c41aa524 100644 --- a/web/src/components/cve/__tests__/CveSearchFilters.test.ts +++ b/web/src/components/cve/__tests__/CveSearchFilters.test.ts @@ -5,8 +5,8 @@ import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ query: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], diff --git a/web/src/components/cve/__tests__/CveSourceComparison.test.ts b/web/src/components/cve/__tests__/CveSourceComparison.test.ts index 7898396b..ad3678ec 100644 --- a/web/src/components/cve/__tests__/CveSourceComparison.test.ts +++ b/web/src/components/cve/__tests__/CveSourceComparison.test.ts @@ -8,8 +8,8 @@ import type { components } from '@/lib/api/schema' type CVESourceResponse = components['schemas']['CVESourceResponse'] vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ query: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -34,9 +34,7 @@ function makeSource(overrides: Partial = {}): CVESourceRespon } async function mountComponent(props: Record = {}) { - const { default: CveSourceComparison } = await import( - '@/components/cve/CveSourceComparison.vue' - ) + const { default: CveSourceComparison } = await import('@/components/cve/CveSourceComparison.vue') return mount(CveSourceComparison, { props: props as any }) } diff --git a/web/src/components/settings/__tests__/GroupDialog.test.ts b/web/src/components/settings/__tests__/GroupDialog.test.ts index 765a02c0..3c97ab64 100644 --- a/web/src/components/settings/__tests__/GroupDialog.test.ts +++ b/web/src/components/settings/__tests__/GroupDialog.test.ts @@ -8,8 +8,8 @@ import { useAuthStore } from '@/stores/auth' import type { GroupEntry } from '@/components/settings/GroupDialog.vue' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -17,15 +17,15 @@ vi.mock('vue-router', () => ({ }, })) -const mockPOST = vi.fn() -const mockPATCH = vi.fn() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() +const mockPATCH = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), POST: (...args: unknown[]) => mockPOST(...args), PATCH: (...args: unknown[]) => mockPATCH(...args), - DELETE: vi.fn(), + DELETE: vi.fn<(...args: unknown[]) => unknown>(), }, })) diff --git a/web/src/components/settings/__tests__/GroupMembersDialog.test.ts b/web/src/components/settings/__tests__/GroupMembersDialog.test.ts index 311f3902..223b6184 100644 --- a/web/src/components/settings/__tests__/GroupMembersDialog.test.ts +++ b/web/src/components/settings/__tests__/GroupMembersDialog.test.ts @@ -7,8 +7,8 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -16,15 +16,15 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockPOST = vi.fn() -const mockDELETE = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() +const mockDELETE = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), POST: (...args: unknown[]) => mockPOST(...args), - PATCH: vi.fn(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), DELETE: (...args: unknown[]) => mockDELETE(...args), }, })) @@ -93,9 +93,8 @@ function bodyText(): string { let wrapper: VueWrapper async function mountDialog(props: { open?: boolean; groupId?: string; groupName?: string } = {}) { - const { default: GroupMembersDialog } = await import( - '@/components/settings/GroupMembersDialog.vue' - ) + const { default: GroupMembersDialog } = + await import('@/components/settings/GroupMembersDialog.vue') wrapper = mount(GroupMembersDialog, { props: { open: true, @@ -110,7 +109,9 @@ async function mountDialog(props: { open?: boolean; groupId?: string; groupName? // Clean up portaled DOM elements function cleanupPortals() { - document.querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]').forEach((el) => el.remove()) + document + .querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]') + .forEach((el) => el.remove()) } describe('GroupMembersDialog', () => { @@ -250,9 +251,7 @@ describe('GroupMembersDialog', () => { describe('add member', () => { it('shows available org members not already in group', async () => { - mockGroupMembersSuccess([ - makeGroupMember({ user_id: 'u1', email: 'alice@example.com' }), - ]) + mockGroupMembersSuccess([makeGroupMember({ user_id: 'u1', email: 'alice@example.com' })]) mockOrgMembersSuccess([ makeOrgMember({ user_id: 'u1', email: 'alice@example.com' }), makeOrgMember({ user_id: 'u2', email: 'bob@example.com', display_name: 'Bob' }), diff --git a/web/src/components/settings/__tests__/InviteMemberDialog.test.ts b/web/src/components/settings/__tests__/InviteMemberDialog.test.ts index aa08f61d..52686e87 100644 --- a/web/src/components/settings/__tests__/InviteMemberDialog.test.ts +++ b/web/src/components/settings/__tests__/InviteMemberDialog.test.ts @@ -7,8 +7,8 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -16,14 +16,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockPOST = vi.fn() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), POST: (...args: unknown[]) => mockPOST(...args), - PATCH: vi.fn(), - DELETE: vi.fn(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), + DELETE: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -74,9 +74,8 @@ async function clickTestId(testId: string) { let wrapper: VueWrapper async function mountDialog(props: { open?: boolean; currentUserRole?: string } = {}) { - const { default: InviteMemberDialog } = await import( - '@/components/settings/InviteMemberDialog.vue' - ) + const { default: InviteMemberDialog } = + await import('@/components/settings/InviteMemberDialog.vue') wrapper = mount(InviteMemberDialog, { props: { open: true, currentUserRole: 'admin', ...props }, attachTo: document.body, diff --git a/web/src/components/watchlist/__tests__/AddItemDialog.test.ts b/web/src/components/watchlist/__tests__/AddItemDialog.test.ts index 0bbe1718..d1492428 100644 --- a/web/src/components/watchlist/__tests__/AddItemDialog.test.ts +++ b/web/src/components/watchlist/__tests__/AddItemDialog.test.ts @@ -7,8 +7,8 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -16,14 +16,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockPOST = vi.fn() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), POST: (...args: unknown[]) => mockPOST(...args), - PATCH: vi.fn(), - DELETE: vi.fn(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), + DELETE: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -112,9 +112,7 @@ async function clickTestId(testId: string) { let wrapper: VueWrapper async function mountDialog(open = true) { - const { default: AddItemDialog } = await import( - '@/components/watchlist/AddItemDialog.vue' - ) + const { default: AddItemDialog } = await import('@/components/watchlist/AddItemDialog.vue') wrapper = mount(AddItemDialog, { props: { open, watchlistId: TEST_WATCHLIST_ID }, attachTo: document.body, diff --git a/web/src/components/watchlist/__tests__/CreateWatchlistDialog.test.ts b/web/src/components/watchlist/__tests__/CreateWatchlistDialog.test.ts index e06ad824..c1c1820c 100644 --- a/web/src/components/watchlist/__tests__/CreateWatchlistDialog.test.ts +++ b/web/src/components/watchlist/__tests__/CreateWatchlistDialog.test.ts @@ -7,8 +7,8 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -16,14 +16,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockPOST = vi.fn() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), POST: (...args: unknown[]) => mockPOST(...args), - PATCH: vi.fn(), - DELETE: vi.fn(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), + DELETE: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -91,9 +91,8 @@ async function clickTestId(testId: string) { let wrapper: VueWrapper async function mountDialog(open = true) { - const { default: CreateWatchlistDialog } = await import( - '@/components/watchlist/CreateWatchlistDialog.vue' - ) + const { default: CreateWatchlistDialog } = + await import('@/components/watchlist/CreateWatchlistDialog.vue') wrapper = mount(CreateWatchlistDialog, { props: { open }, attachTo: document.body, @@ -166,7 +165,7 @@ describe('CreateWatchlistDialog', () => { ) // Verify the body includes name and description - const callArgs = mockPOST.mock.calls[0]! + const callArgs = mockPOST.mock.calls[0] as [string, { body: Record }] expect(callArgs[1].body.name).toBe('Test WL') expect(callArgs[1].body.description).toBe('Desc') }) @@ -183,7 +182,7 @@ describe('CreateWatchlistDialog', () => { await clickTestId('create-watchlist-btn') await flushPromises() - const callArgs = mockPOST.mock.calls[0]! + const callArgs = mockPOST.mock.calls[0] as [string, { body: Record }] expect(callArgs[1].body.name).toBe('Name Only') expect(callArgs[1].body.description).toBeNull() }) diff --git a/web/src/lib/api/__tests__/client.test.ts b/web/src/lib/api/__tests__/client.test.ts index 289ad5b0..744ab851 100644 --- a/web/src/lib/api/__tests__/client.test.ts +++ b/web/src/lib/api/__tests__/client.test.ts @@ -119,7 +119,7 @@ describe('refresh middleware', () => { it('does not attempt refresh for auth endpoints', async () => { const { refreshMiddleware } = await import('../client') - const fetchMock = vi.fn() + const fetchMock = vi.fn() globalThis.fetch = fetchMock const loginRequest = new Request('http://localhost/api/v1/auth/login') @@ -136,7 +136,7 @@ describe('refresh middleware', () => { it('does not attempt refresh for auth/me endpoint', async () => { const { refreshMiddleware } = await import('../client') - const fetchMock = vi.fn() + const fetchMock = vi.fn() globalThis.fetch = fetchMock const meRequest = new Request('http://localhost/api/v1/auth/me') @@ -153,7 +153,7 @@ describe('refresh middleware', () => { it('does not attempt refresh for the refresh endpoint itself', async () => { const { refreshMiddleware } = await import('../client') - const fetchMock = vi.fn() + const fetchMock = vi.fn() globalThis.fetch = fetchMock const refreshRequest = new Request('http://localhost/api/v1/auth/refresh') @@ -172,7 +172,7 @@ describe('refresh middleware', () => { const { refreshMiddleware } = await import('../client') const retryResponse = new Response('ok', { status: 200 }) - const fetchMock = vi.fn() + const fetchMock = vi.fn() // First call: refresh succeeds. fetchMock.mockResolvedValueOnce(new Response('', { status: 200 })) // Second call: retry the original request. @@ -198,7 +198,7 @@ describe('refresh middleware', () => { it('returns original 401 response when refresh fails', async () => { const { refreshMiddleware } = await import('../client') - const fetchMock = vi.fn() + const fetchMock = vi.fn() // Refresh returns 401 (failure). fetchMock.mockResolvedValueOnce(new Response('', { status: 401 })) globalThis.fetch = fetchMock diff --git a/web/src/router/__tests__/guards.test.ts b/web/src/router/__tests__/guards.test.ts index 08b57dc3..9909ef56 100644 --- a/web/src/router/__tests__/guards.test.ts +++ b/web/src/router/__tests__/guards.test.ts @@ -11,8 +11,8 @@ import { routes, authGuard, titleGuard } from '../index' // Mock the API client so fetchMe doesn't make real HTTP calls. vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) diff --git a/web/src/stores/__tests__/auth.test.ts b/web/src/stores/__tests__/auth.test.ts index aa0d23eb..4231a55f 100644 --- a/web/src/stores/__tests__/auth.test.ts +++ b/web/src/stores/__tests__/auth.test.ts @@ -8,8 +8,8 @@ import { useAuthStore } from '../auth' // Mock the API client vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -120,7 +120,11 @@ describe('auth store', () => { is_site_admin: false, orgs: [{ org_id: 'org-1', name: 'Org One', role: 'owner' }], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() const result = await auth.fetchMe() @@ -131,7 +135,11 @@ describe('auth store', () => { }) it('returns false on API error', async () => { - vi.mocked(client.GET).mockResolvedValue({ data: undefined, error: { type: 'about:blank', detail: 'unauthorized' }, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: { type: 'about:blank', detail: 'unauthorized' }, + response: {} as Response, + }) const auth = useAuthStore() const result = await auth.fetchMe() @@ -148,7 +156,11 @@ describe('auth store', () => { is_site_admin: false, orgs: [{ org_id: 'only-org', name: 'Only Org', role: 'admin' }], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -168,7 +180,11 @@ describe('auth store', () => { { org_id: 'org-2', name: 'Org Two', role: 'member' }, ], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -185,7 +201,11 @@ describe('auth store', () => { is_site_admin: false, orgs: [{ org_id: 'current-org', name: 'Current', role: 'admin' }], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -207,7 +227,11 @@ describe('auth store', () => { { org_id: 'org-2', name: 'Org Two', role: 'member' }, ], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -230,7 +254,11 @@ describe('auth store', () => { is_site_admin: false, orgs: [], } - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -239,7 +267,11 @@ describe('auth store', () => { }) it('is set to true after failed fetchMe', async () => { - vi.mocked(client.GET).mockResolvedValue({ data: undefined, error: { type: 'about:blank', detail: 'unauthorized' }, response: {} as Response }) + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: { type: 'about:blank', detail: 'unauthorized' }, + response: {} as Response, + }) const auth = useAuthStore() await auth.fetchMe() @@ -248,7 +280,11 @@ describe('auth store', () => { }) it('is reset to false on clearAuth', async () => { - vi.mocked(client.POST).mockResolvedValue({ data: undefined, error: undefined, response: {} as Response }) + vi.mocked(client.POST).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() auth.sessionChecked = true @@ -268,8 +304,16 @@ describe('auth store', () => { is_site_admin: false, orgs: [{ org_id: 'org-1', name: 'Org One', role: 'admin' }], } - vi.mocked(client.POST).mockResolvedValue({ data: undefined, error: undefined, response: {} as Response }) - vi.mocked(client.GET).mockResolvedValue({ data: meData, error: undefined, response: {} as Response }) + vi.mocked(client.POST).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }) + vi.mocked(client.GET).mockResolvedValue({ + data: meData, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() const result = await auth.login('test@example.com', 'password123') @@ -280,7 +324,11 @@ describe('auth store', () => { }) it('returns error on failed login', async () => { - vi.mocked(client.POST).mockResolvedValue({ data: undefined, error: { type: 'about:blank', detail: 'bad creds' }, response: {} as Response }) + vi.mocked(client.POST).mockResolvedValue({ + data: undefined, + error: { type: 'about:blank', detail: 'bad creds' }, + response: {} as Response, + }) const auth = useAuthStore() const result = await auth.login('bad@example.com', 'wrong') @@ -293,7 +341,11 @@ describe('auth store', () => { describe('logout', () => { it('calls logout endpoint and clears auth state', async () => { - vi.mocked(client.POST).mockResolvedValue({ data: undefined, error: undefined, response: {} as Response }) + vi.mocked(client.POST).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }) const auth = useAuthStore() auth.user = { diff --git a/web/src/views/__tests__/CreateOrgView.test.ts b/web/src/views/__tests__/CreateOrgView.test.ts index 5d37fade..5b6b7717 100644 --- a/web/src/views/__tests__/CreateOrgView.test.ts +++ b/web/src/views/__tests__/CreateOrgView.test.ts @@ -6,11 +6,11 @@ import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { query: Record }>(() => ({ query: {} })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -18,11 +18,11 @@ vi.mock('vue-router', () => ({ }, })) -const mockPOST = vi.fn() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), POST: (...args: unknown[]) => mockPOST(...args), }, })) @@ -65,7 +65,10 @@ describe('CreateOrgView', () => { let resolvePost: (value: unknown) => void mockPOST.mockImplementation( - () => new Promise((resolve) => { resolvePost = resolve }), + () => + new Promise((resolve) => { + resolvePost = resolve + }), ) const wrapper = await mountCreateOrg() diff --git a/web/src/views/__tests__/CveDetailView.test.ts b/web/src/views/__tests__/CveDetailView.test.ts index 4db8745e..a4bfe559 100644 --- a/web/src/views/__tests__/CveDetailView.test.ts +++ b/web/src/views/__tests__/CveDetailView.test.ts @@ -12,13 +12,15 @@ type CVESourceResponse = components['schemas']['CVESourceResponse'] let mockRouteParams: Record = {} vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ + useRoute: vi.fn<() => { params: Record; query: Record }>(() => ({ params: mockRouteParams, query: {}, })), - useRouter: vi.fn(() => ({ - push: vi.fn(), - back: vi.fn(), + useRouter: vi.fn< + () => { push: (...args: unknown[]) => unknown; back: (...args: unknown[]) => unknown } + >(() => ({ + push: vi.fn<(...args: unknown[]) => unknown>(), + back: vi.fn<(...args: unknown[]) => unknown>(), })), RouterLink: { name: 'RouterLink', @@ -27,12 +29,12 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() +const mockGET = vi.fn<(path: string, ...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { - GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), + GET: (path: string, ...args: unknown[]) => mockGET(path, ...args), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -200,7 +202,7 @@ describe('CveDetailView', () => { await flushPromises() const scoreCards = wrapper.findAll('[data-testid="score-card"]') - const cvssCard = scoreCards.find(c => c.text().includes('CVSS')) + const cvssCard = scoreCards.find((c) => c.text().includes('CVSS')) expect(cvssCard?.text()).toContain('N/A') }) @@ -210,7 +212,7 @@ describe('CveDetailView', () => { await flushPromises() const scoreCards = wrapper.findAll('[data-testid="score-card"]') - const epssCard = scoreCards.find(c => c.text().includes('EPSS')) + const epssCard = scoreCards.find((c) => c.text().includes('EPSS')) expect(epssCard?.text()).toContain('N/A') }) @@ -280,7 +282,7 @@ describe('CveDetailView', () => { await flushPromises() const links = wrapper.findAll('a[target="_blank"]') - const urls = links.map(l => l.attributes('href')) + const urls = links.map((l) => l.attributes('href')) expect(urls).toContain('https://nvd.nist.gov/vuln/detail/CVE-2024-12345') expect(urls).toContain('https://github.com/advisories/GHSA-xxxx-xxxx-xxxx') }) @@ -370,7 +372,9 @@ describe('CveDetailView', () => { // Set up a slow response (will become stale) let resolveStale: (v: unknown) => void - const stalePromise = new Promise((resolve) => { resolveStale = resolve }) + const stalePromise = new Promise((resolve) => { + resolveStale = resolve + }) mockGET.mockReturnValueOnce(stalePromise) // Trigger first refetch — increments fetchId diff --git a/web/src/views/__tests__/CveSearchView.test.ts b/web/src/views/__tests__/CveSearchView.test.ts index bd6e3a17..ecf22a24 100644 --- a/web/src/views/__tests__/CveSearchView.test.ts +++ b/web/src/views/__tests__/CveSearchView.test.ts @@ -8,13 +8,18 @@ import type { components } from '@/lib/api/schema' type CVEItem = components['schemas']['CVEItem'] -const mockPush = vi.fn() -const mockReplace = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() +const mockReplace = vi.fn<(...args: unknown[]) => unknown>() let mockRouteQuery: Record = {} vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: mockRouteQuery })), - useRouter: vi.fn(() => ({ push: mockPush, replace: mockReplace })), + useRoute: vi.fn<() => { query: Record }>(() => ({ + query: mockRouteQuery, + })), + useRouter: vi.fn<() => { push: typeof mockPush; replace: typeof mockReplace }>(() => ({ + push: mockPush, + replace: mockReplace, + })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -22,12 +27,12 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -114,11 +119,14 @@ describe('CveSearchView', () => { await mountView() await flushPromises() - expect(mockGET).toHaveBeenCalledWith('/cves', expect.objectContaining({ - params: expect.objectContaining({ - query: expect.any(Object), + expect(mockGET).toHaveBeenCalledWith( + '/cves', + expect.objectContaining({ + params: expect.objectContaining({ + query: expect.any(Object), + }), }), - })) + ) }) it('displays fetched CVE results', async () => { @@ -139,13 +147,16 @@ describe('CveSearchView', () => { await mountView() await flushPromises() - expect(mockGET).toHaveBeenCalledWith('/cves', expect.objectContaining({ - params: { - query: expect.objectContaining({ - q: 'apache', - }), - }, - })) + expect(mockGET).toHaveBeenCalledWith( + '/cves', + expect.objectContaining({ + params: { + query: expect.objectContaining({ + q: 'apache', + }), + }, + }), + ) }) it('passes severity filter to API', async () => { @@ -154,13 +165,16 @@ describe('CveSearchView', () => { await mountView() await flushPromises() - expect(mockGET).toHaveBeenCalledWith('/cves', expect.objectContaining({ - params: { - query: expect.objectContaining({ - severity: ['critical'], - }), - }, - })) + expect(mockGET).toHaveBeenCalledWith( + '/cves', + expect.objectContaining({ + params: { + query: expect.objectContaining({ + severity: ['critical'], + }), + }, + }), + ) }) }) @@ -190,11 +204,13 @@ describe('CveSearchView', () => { await wrapper.find('form').trigger('submit') await flushPromises() - expect(mockReplace).toHaveBeenCalledWith(expect.objectContaining({ - query: expect.objectContaining({ - q: 'openssl', + expect(mockReplace).toHaveBeenCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ + q: 'openssl', + }), }), - })) + ) }) }) @@ -240,13 +256,16 @@ describe('CveSearchView', () => { await wrapper.find('[data-testid="next-page"]').trigger('click') await flushPromises() - expect(mockGET).toHaveBeenCalledWith('/cves', expect.objectContaining({ - params: { - query: expect.objectContaining({ - cursor: 'cursor-page2', - }), - }, - })) + expect(mockGET).toHaveBeenCalledWith( + '/cves', + expect.objectContaining({ + params: { + query: expect.objectContaining({ + cursor: 'cursor-page2', + }), + }, + }), + ) expect(wrapper.text()).toContain('CVE-2024-0002') }) diff --git a/web/src/views/__tests__/FeedStatusView.test.ts b/web/src/views/__tests__/FeedStatusView.test.ts index ae6d8cd0..f959a355 100644 --- a/web/src/views/__tests__/FeedStatusView.test.ts +++ b/web/src/views/__tests__/FeedStatusView.test.ts @@ -5,8 +5,10 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ path: '/admin/feeds' })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => { path: string }>(() => ({ path: '/admin/feeds' })), + useRouter: vi.fn<() => { push: (...args: unknown[]) => unknown }>(() => ({ + push: vi.fn<(...args: unknown[]) => unknown>(), + })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -14,8 +16,8 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockPOST = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { diff --git a/web/src/views/__tests__/ForgotPasswordView.test.ts b/web/src/views/__tests__/ForgotPasswordView.test.ts index efc2c661..efe29204 100644 --- a/web/src/views/__tests__/ForgotPasswordView.test.ts +++ b/web/src/views/__tests__/ForgotPasswordView.test.ts @@ -7,8 +7,10 @@ import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => { query: Record }>(() => ({ query: {} })), + useRouter: vi.fn<() => { push: (...args: unknown[]) => unknown }>(() => ({ + push: vi.fn<(...args: unknown[]) => unknown>(), + })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -18,14 +20,14 @@ vi.mock('vue-router', () => ({ vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) import { useAuthStore } from '@/stores/auth' -const mockFetch = vi.fn() +const mockFetch = vi.fn<(...args: unknown[]) => unknown>() vi.stubGlobal('fetch', mockFetch) async function mountForgotPassword() { @@ -96,7 +98,10 @@ describe('ForgotPasswordView', () => { it('shows success message even on failure (anti-enumeration)', async () => { const auth = useAuthStore() - vi.spyOn(auth, 'forgotPassword').mockResolvedValue({ success: false, error: 'something went wrong' }) + vi.spyOn(auth, 'forgotPassword').mockResolvedValue({ + success: false, + error: 'something went wrong', + }) const wrapper = await mountForgotPassword() @@ -125,7 +130,10 @@ describe('ForgotPasswordView', () => { const auth = useAuthStore() let resolveForgot: (value: { success: boolean }) => void vi.spyOn(auth, 'forgotPassword').mockImplementation( - () => new Promise((resolve) => { resolveForgot = resolve }), + () => + new Promise((resolve) => { + resolveForgot = resolve + }), ) const wrapper = await mountForgotPassword() diff --git a/web/src/views/__tests__/GroupsView.test.ts b/web/src/views/__tests__/GroupsView.test.ts index e201066f..2a94212e 100644 --- a/web/src/views/__tests__/GroupsView.test.ts +++ b/web/src/views/__tests__/GroupsView.test.ts @@ -6,11 +6,11 @@ import { mount, flushPromises, VueWrapper } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { params: Record }>(() => ({ params: {} })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -18,14 +18,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockDELETE = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockDELETE = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), - PATCH: vi.fn(), + POST: vi.fn<(...args: unknown[]) => unknown>(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), DELETE: (...args: unknown[]) => mockDELETE(...args), }, })) @@ -97,7 +97,9 @@ async function mountView() { // Clean up portaled DOM elements (reka-ui Select, AlertDialog, Dialog) function cleanupPortals() { - document.querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]').forEach((el) => el.remove()) + document + .querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]') + .forEach((el) => el.remove()) } describe('GroupsView', () => { diff --git a/web/src/views/__tests__/InvitationView.test.ts b/web/src/views/__tests__/InvitationView.test.ts index ee585084..39738d9a 100644 --- a/web/src/views/__tests__/InvitationView.test.ts +++ b/web/src/views/__tests__/InvitationView.test.ts @@ -7,11 +7,11 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' let mockRouteParams: Record = { token: 'test-token-abc' } -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: mockRouteParams })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { params: Record }>(() => ({ params: mockRouteParams })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -19,8 +19,8 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockPOST = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockPOST = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { @@ -178,7 +178,7 @@ describe('InvitationView', () => { user_id: 'u1', email: 'sam@example.com', display_name: 'Sam Carter', - is_site_admin: false, + is_site_admin: false, orgs: [ { org_id: 'org-old', name: 'Old Org', role: 'admin' }, { org_id: 'org-new', name: 'Acme Corp', role: 'member' }, diff --git a/web/src/views/__tests__/LoginView.test.ts b/web/src/views/__tests__/LoginView.test.ts index 22b9057e..8de55d27 100644 --- a/web/src/views/__tests__/LoginView.test.ts +++ b/web/src/views/__tests__/LoginView.test.ts @@ -6,12 +6,12 @@ import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() const mockRouteQuery = { redirect: undefined as string | undefined } vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: mockRouteQuery })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { query: typeof mockRouteQuery }>(() => ({ query: mockRouteQuery })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -21,8 +21,8 @@ vi.mock('vue-router', () => ({ vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) @@ -271,7 +271,7 @@ describe('LoginView', () => { it('GitHub button redirects to OAuth endpoint', async () => { mockProvidersResponse(true, false) const originalLocation = window.location.href - const hrefSetter = vi.fn() + const hrefSetter = vi.fn<(v: string) => void>() Object.defineProperty(window, 'location', { value: { ...window.location, @@ -299,7 +299,7 @@ describe('LoginView', () => { it('Google button redirects to OAuth endpoint', async () => { mockProvidersResponse(false, true) const originalLocation = window.location.href - const hrefSetter = vi.fn() + const hrefSetter = vi.fn<(v: string) => void>() Object.defineProperty(window, 'location', { value: { ...window.location, diff --git a/web/src/views/__tests__/MembersView.test.ts b/web/src/views/__tests__/MembersView.test.ts index 5b7eaf22..266a53fd 100644 --- a/web/src/views/__tests__/MembersView.test.ts +++ b/web/src/views/__tests__/MembersView.test.ts @@ -6,11 +6,11 @@ import { mount, flushPromises, VueWrapper } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => unknown>(() => ({ params: {} })), + useRouter: vi.fn<() => unknown>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -18,14 +18,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockPATCH = vi.fn() -const mockDELETE = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockPATCH = vi.fn<(...args: unknown[]) => unknown>() +const mockDELETE = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), + POST: vi.fn<(...args: unknown[]) => unknown>(), PATCH: (...args: unknown[]) => mockPATCH(...args), DELETE: (...args: unknown[]) => mockDELETE(...args), }, @@ -121,7 +121,9 @@ async function openRoleSelectAndGetOptions(): Promise { trigger.hasPointerCapture = () => false trigger.releasePointerCapture = () => {} } - trigger.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, button: 0, pointerId: 1 })) + trigger.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true, cancelable: true, button: 0, pointerId: 1 }), + ) await flushPromises() const options = document.querySelectorAll('[role="option"]') return Array.from(options).map((el) => el.textContent?.trim() ?? '') @@ -139,7 +141,9 @@ async function mountView() { // Clean up portaled DOM elements (reka-ui Select, AlertDialog, Dialog) function cleanupPortals() { - document.querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]').forEach((el) => el.remove()) + document + .querySelectorAll('[data-reka-portal], [data-radix-popper-content-wrapper]') + .forEach((el) => el.remove()) } describe('MembersView', () => { @@ -193,8 +197,18 @@ describe('MembersView', () => { it('renders members table with data', async () => { setupAuthStore('admin') mockMembersSuccess([ - makeMember({ user_id: 'u1', email: 'alice@example.com', display_name: 'Alice', role: 'admin' }), - makeMember({ user_id: 'u2', email: 'bob@example.com', display_name: 'Bob', role: 'member' }), + makeMember({ + user_id: 'u1', + email: 'alice@example.com', + display_name: 'Alice', + role: 'admin', + }), + makeMember({ + user_id: 'u2', + email: 'bob@example.com', + display_name: 'Bob', + role: 'member', + }), ]) mockInvitationsSuccess([]) await mountView() @@ -322,9 +336,7 @@ describe('MembersView', () => { it('hides remove button on owner members', async () => { setupAuthStore('admin') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'owner', email: 'owner@example.com' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'owner', email: 'owner@example.com' })]) mockInvitationsSuccess([]) await mountView() await flushPromises() @@ -364,8 +376,18 @@ describe('MembersView', () => { it('calls DELETE on confirmation and removes from list', async () => { setupAuthStore('admin') mockMembersSuccess([ - makeMember({ user_id: 'u1', email: 'keep@example.com', display_name: 'Keep', role: 'member' }), - makeMember({ user_id: 'u2', email: 'remove@example.com', display_name: 'Remove', role: 'member' }), + makeMember({ + user_id: 'u1', + email: 'keep@example.com', + display_name: 'Keep', + role: 'member', + }), + makeMember({ + user_id: 'u2', + email: 'remove@example.com', + display_name: 'Remove', + role: 'member', + }), ]) mockInvitationsSuccess([]) await mountView() @@ -428,9 +450,7 @@ describe('MembersView', () => { it('shows role select for admin on non-owner members', async () => { setupAuthStore('admin') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'member' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'member' })]) mockInvitationsSuccess([]) await mountView() await flushPromises() @@ -441,9 +461,7 @@ describe('MembersView', () => { it('shows plain text role for owner members (not changeable)', async () => { setupAuthStore('admin') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'owner' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'owner' })]) mockInvitationsSuccess([]) await mountView() await flushPromises() @@ -458,9 +476,7 @@ describe('MembersView', () => { it('calls PATCH when role is changed', async () => { setupAuthStore('owner') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'member' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'member' })]) mockInvitationsSuccess([]) await mountView() await flushPromises() @@ -484,9 +500,7 @@ describe('MembersView', () => { it('shows plain text role badge for non-admin users', async () => { setupAuthStore('viewer') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'member' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'member' })]) await mountView() await flushPromises() @@ -552,9 +566,7 @@ describe('MembersView', () => { it('shows error when cancelling invitation fails', async () => { setupAuthStore('admin') mockMembersSuccess([makeMember()]) - mockInvitationsSuccess([ - makeInvitation({ id: 'inv-1', email: 'fail@example.com' }), - ]) + mockInvitationsSuccess([makeInvitation({ id: 'inv-1', email: 'fail@example.com' })]) await mountView() await flushPromises() @@ -577,9 +589,7 @@ describe('MembersView', () => { describe('role change error handling', () => { it('reverts role display and shows error when PATCH fails', async () => { setupAuthStore('owner') - mockMembersSuccess([ - makeMember({ user_id: 'u1', role: 'admin', email: 'admin@example.com' }), - ]) + mockMembersSuccess([makeMember({ user_id: 'u1', role: 'admin', email: 'admin@example.com' })]) mockInvitationsSuccess([]) await mountView() await flushPromises() diff --git a/web/src/views/__tests__/NotFoundView.test.ts b/web/src/views/__tests__/NotFoundView.test.ts index 86f4bdb5..f9822485 100644 --- a/web/src/views/__tests__/NotFoundView.test.ts +++ b/web/src/views/__tests__/NotFoundView.test.ts @@ -5,8 +5,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { mount } from '@vue/test-utils' vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ path: '/nonexistent' })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => unknown>(() => ({ path: '/nonexistent' })), + useRouter: vi.fn<() => unknown>(() => ({ push: vi.fn<(...args: unknown[]) => unknown>() })), RouterLink: { name: 'RouterLink', props: ['to'], diff --git a/web/src/views/__tests__/RegisterView.test.ts b/web/src/views/__tests__/RegisterView.test.ts index 99352f27..7018452f 100644 --- a/web/src/views/__tests__/RegisterView.test.ts +++ b/web/src/views/__tests__/RegisterView.test.ts @@ -6,11 +6,11 @@ import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: {} })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { query: Record }>(() => ({ query: {} })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -20,8 +20,8 @@ vi.mock('vue-router', () => ({ vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) diff --git a/web/src/views/__tests__/ResetPasswordView.test.ts b/web/src/views/__tests__/ResetPasswordView.test.ts index 226d388d..aea30908 100644 --- a/web/src/views/__tests__/ResetPasswordView.test.ts +++ b/web/src/views/__tests__/ResetPasswordView.test.ts @@ -6,12 +6,12 @@ import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { nextTick } from 'vue' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() const mockRouteQuery = { token: undefined as string | undefined } vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: mockRouteQuery })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { query: typeof mockRouteQuery }>(() => ({ query: mockRouteQuery })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -21,14 +21,14 @@ vi.mock('vue-router', () => ({ vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) import { useAuthStore } from '@/stores/auth' -const mockFetch = vi.fn() +const mockFetch = vi.fn<(...args: unknown[]) => unknown>() vi.stubGlobal('fetch', mockFetch) async function mountResetPassword() { @@ -118,7 +118,10 @@ describe('ResetPasswordView', () => { await wrapper.find('form').trigger('submit') await flushPromises() - expect(auth.resetPassword).toHaveBeenCalledWith('valid-hex-token-abc123', 'new-password-1234567') + expect(auth.resetPassword).toHaveBeenCalledWith( + 'valid-hex-token-abc123', + 'new-password-1234567', + ) }) it('shows success message after successful reset', async () => { @@ -187,7 +190,10 @@ describe('ResetPasswordView', () => { const auth = useAuthStore() let resolveReset: (value: { success: boolean }) => void vi.spyOn(auth, 'resetPassword').mockImplementation( - () => new Promise((resolve) => { resolveReset = resolve }), + () => + new Promise((resolve) => { + resolveReset = resolve + }), ) const wrapper = await mountResetPassword() diff --git a/web/src/views/__tests__/VerifyEmailView.test.ts b/web/src/views/__tests__/VerifyEmailView.test.ts index ac0bbcfc..14ac4b31 100644 --- a/web/src/views/__tests__/VerifyEmailView.test.ts +++ b/web/src/views/__tests__/VerifyEmailView.test.ts @@ -8,8 +8,10 @@ import { createPinia, setActivePinia } from 'pinia' const mockRouteQuery = { token: undefined as string | undefined } vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ query: mockRouteQuery })), - useRouter: vi.fn(() => ({ push: vi.fn() })), + useRoute: vi.fn<() => { query: typeof mockRouteQuery }>(() => ({ query: mockRouteQuery })), + useRouter: vi.fn<() => { push: (...args: unknown[]) => unknown }>(() => ({ + push: vi.fn<(...args: unknown[]) => unknown>(), + })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -19,14 +21,14 @@ vi.mock('vue-router', () => ({ vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn(), - POST: vi.fn(), + GET: vi.fn<(...args: unknown[]) => unknown>(), + POST: vi.fn<(...args: unknown[]) => unknown>(), }, })) import { useAuthStore } from '@/stores/auth' -const mockFetch = vi.fn() +const mockFetch = vi.fn<(...args: unknown[]) => unknown>() vi.stubGlobal('fetch', mockFetch) async function mountVerifyEmail() { @@ -109,7 +111,10 @@ describe('VerifyEmailView', () => { it('shows helpful expired link text on error', async () => { const auth = useAuthStore() - vi.spyOn(auth, 'verifyEmail').mockResolvedValue({ success: false, error: 'Verification failed' }) + vi.spyOn(auth, 'verifyEmail').mockResolvedValue({ + success: false, + error: 'Verification failed', + }) const wrapper = await mountVerifyEmail() await flushPromises() diff --git a/web/src/views/__tests__/WatchlistDetailView.test.ts b/web/src/views/__tests__/WatchlistDetailView.test.ts index f3a37ce8..458cc9b1 100644 --- a/web/src/views/__tests__/WatchlistDetailView.test.ts +++ b/web/src/views/__tests__/WatchlistDetailView.test.ts @@ -7,11 +7,11 @@ import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' let mockRouteParams: Record = { id: 'wl-123' } -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: mockRouteParams })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { params: Record }>(() => ({ params: mockRouteParams })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -19,14 +19,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockPATCH = vi.fn() -const mockDELETE = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockPATCH = vi.fn<(...args: unknown[]) => unknown>() +const mockDELETE = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), + POST: vi.fn<(...args: unknown[]) => unknown>(), PATCH: (...args: unknown[]) => mockPATCH(...args), DELETE: (...args: unknown[]) => mockDELETE(...args), }, @@ -134,9 +134,7 @@ async function clickTestId(testId: string) { let wrapper: VueWrapper async function mountView() { - const { default: WatchlistDetailView } = await import( - '@/views/WatchlistDetailView.vue' - ) + const { default: WatchlistDetailView } = await import('@/views/WatchlistDetailView.vue') wrapper = mount(WatchlistDetailView, { attachTo: document.body, }) diff --git a/web/src/views/__tests__/WatchlistListView.test.ts b/web/src/views/__tests__/WatchlistListView.test.ts index a580dc5f..13892b7f 100644 --- a/web/src/views/__tests__/WatchlistListView.test.ts +++ b/web/src/views/__tests__/WatchlistListView.test.ts @@ -6,11 +6,11 @@ import { mount, flushPromises, VueWrapper } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' -const mockPush = vi.fn() +const mockPush = vi.fn<(...args: unknown[]) => unknown>() vi.mock('vue-router', () => ({ - useRoute: vi.fn(() => ({ params: {} })), - useRouter: vi.fn(() => ({ push: mockPush })), + useRoute: vi.fn<() => { params: Record }>(() => ({ params: {} })), + useRouter: vi.fn<() => { push: typeof mockPush }>(() => ({ push: mockPush })), RouterLink: { name: 'RouterLink', props: ['to'], @@ -18,14 +18,14 @@ vi.mock('vue-router', () => ({ }, })) -const mockGET = vi.fn() -const mockDELETE = vi.fn() +const mockGET = vi.fn<(...args: unknown[]) => unknown>() +const mockDELETE = vi.fn<(...args: unknown[]) => unknown>() vi.mock('@/lib/api/client', () => ({ default: { GET: (...args: unknown[]) => mockGET(...args), - POST: vi.fn(), - PATCH: vi.fn(), + POST: vi.fn<(...args: unknown[]) => unknown>(), + PATCH: vi.fn<(...args: unknown[]) => unknown>(), DELETE: (...args: unknown[]) => mockDELETE(...args), }, })) diff --git a/web/src/views/admin/__tests__/AdminSystemView.test.ts b/web/src/views/admin/__tests__/AdminSystemView.test.ts index 6a901f66..4412e139 100644 --- a/web/src/views/admin/__tests__/AdminSystemView.test.ts +++ b/web/src/views/admin/__tests__/AdminSystemView.test.ts @@ -24,7 +24,9 @@ const unhealthyDoctor = { // Stub the openapi-fetch client used by the component. vi.mock('@/lib/api/client', () => ({ default: { - GET: vi.fn().mockResolvedValue({ data: null, error: { status: 500 } }), + GET: vi + .fn<(...args: unknown[]) => unknown>() + .mockResolvedValue({ data: null, error: { status: 500 } }), }, })) diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json index dccf2bd6..4be693aa 100644 --- a/web/tsconfig.app.json +++ b/web/tsconfig.app.json @@ -9,7 +9,6 @@ "noUncheckedIndexedAccess": true, // Path mapping for cleaner imports. - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, diff --git a/web/tsconfig.json b/web/tsconfig.json index 1702e9dd..a3e245f9 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -12,7 +12,6 @@ } ], "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }