diff --git a/.github/workflows/agent-deb.yml b/.github/workflows/agent-deb.yml index b467d221..dcaa6b51 100644 --- a/.github/workflows/agent-deb.yml +++ b/.github/workflows/agent-deb.yml @@ -52,7 +52,7 @@ jobs: runner: ubuntu-24.04-arm steps: - name: Checkout main repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -133,7 +133,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout gh-pages (init if missing) - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: gh-pages path: pages diff --git a/.github/workflows/agent-k8s-image.yml b/.github/workflows/agent-k8s-image.yml index 631f8417..fe78b57e 100644 --- a/.github/workflows/agent-k8s-image.yml +++ b/.github/workflows/agent-k8s-image.yml @@ -46,7 +46,7 @@ jobs: sha_short: ${{ steps.tags.outputs.sha_short }} steps: - name: Checkout main repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c26c62a..f90acae4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: name: rust (clippy + test) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt @@ -54,7 +54,7 @@ jobs: run: working-directory: web steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 @@ -64,13 +64,16 @@ jobs: - run: npm run lint - run: npm run typecheck - run: npm test + - name: Production synthetic contract tests + working-directory: scripts + run: node --test production-synthetic.test.mjs journey: name: journey (browser + server + agent) needs: [rust, web] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 @@ -117,7 +120,7 @@ jobs: name: helm (lint + template) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: azure/setup-helm@v5 - name: Lint and render matrix run: bash helm/shellfleet-agent/tests/render_test.sh diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index b1efacd1..3ab1436c 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -28,7 +28,7 @@ jobs: - x86_64-unknown-linux-gnu - aarch64-unknown-linux-gnu steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.event.release.tag_name || inputs.tag }} diff --git a/.github/workflows/container-images.yml b/.github/workflows/container-images.yml index c074fe32..b4972bd4 100644 --- a/.github/workflows/container-images.yml +++ b/.github/workflows/container-images.yml @@ -51,7 +51,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Buildx uses: docker/setup-buildx-action@v4 diff --git a/Cargo.lock b/Cargo.lock index f5c2dac4..70065664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,9 +924,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "combine" @@ -3351,9 +3351,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -3451,9 +3451,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -3513,7 +3513,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3664,9 +3664,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4173,7 +4173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4715,9 +4715,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", "getrandom 0.4.2", @@ -5034,7 +5034,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Dockerfile.web b/Dockerfile.web index a8b780c3..598e7914 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,14 +1,14 @@ -# Pinned to a specific Node 20 LTS digest channel (alpine 3.21) so a +# Pinned to a specific Node 22 LTS digest channel (alpine 3.23) so a # silent upstream Node release can't change the produced bundle # behaviour mid-deploy. Bump deliberately when it's time. -FROM docker.io/library/node:20.19-alpine3.21 AS builder +FROM docker.io/library/node:22.22-alpine3.23 AS builder WORKDIR /app COPY web/package*.json ./ RUN npm ci --no-audit --no-fund COPY web/ ./ RUN npm run build -FROM docker.io/library/node:20.19-alpine3.21 AS runner +FROM docker.io/library/node:22.22-alpine3.23 AS runner WORKDIR /app ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 diff --git a/README.md b/README.md index cb491c66..413168d2 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ place instead of drifting in this file: - **[Quickstart & environment variables](https://shellfleet.sppidy.in/docs.html#quickstart)** — deploy, reverse-proxy routes, the `.env`, and agent pairing. - **[Operator CLI](https://shellfleet.sppidy.in/docs.html#cli)** — device authorization without copying browser cookies or dashboard API keys. - **[Metrics](https://shellfleet.sppidy.in/docs.html#metrics)** — point the dashboard at your Prometheus; YAML panel templates. +- **[Production synthetics](docs/production-monitoring.md)** — continuously verify authenticated REST, SSE, WebSocket, and passkey paths through the public edge. - **[Kubernetes](https://shellfleet.sppidy.in/docs.html#kubernetes)** / **[Helm](https://shellfleet.sppidy.in/docs.html#helm)** — the k8s agent flavor and every chart value. - **[Webhooks](https://shellfleet.sppidy.in/docs.html#webhooks)** and **[Cloudflare](https://shellfleet.sppidy.in/docs.html#cloudflare)** — outbound events and edge setup. - **[Enterprise Edition](https://shellfleet.sppidy.in/ee-docs.html)** — SSO/SCIM, passkeys, ACLs, multi-tenancy, runbooks, recording, drift, multi-source metrics with custom charts, SLA, cost, AI log analysis, Vault. diff --git a/deploy/systemd/shellfleet-production-synthetic-telegram.conf b/deploy/systemd/shellfleet-production-synthetic-telegram.conf new file mode 100644 index 00000000..16e14520 --- /dev/null +++ b/deploy/systemd/shellfleet-production-synthetic-telegram.conf @@ -0,0 +1,3 @@ +[Service] +LoadCredential=telegram-bot-token:/etc/shellfleet/synthetic.telegram-bot-token +LoadCredential=telegram-chat-id:/etc/shellfleet/synthetic.telegram-chat-id diff --git a/deploy/systemd/shellfleet-production-synthetic.service b/deploy/systemd/shellfleet-production-synthetic.service new file mode 100644 index 00000000..77a31127 --- /dev/null +++ b/deploy/systemd/shellfleet-production-synthetic.service @@ -0,0 +1,35 @@ +[Unit] +Description=ShellFleet authenticated production synthetic probe +Documentation=https://github.com/sppidy/shellfleet/blob/main/docs/production-monitoring.md +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +DynamicUser=yes +EnvironmentFile=/etc/shellfleet/synthetic.env +Environment=SHELLFLEET_SYNTHETIC_STATE_FILE=/var/lib/shellfleet-synthetic/status.json +LoadCredential=jwt-secret:/etc/shellfleet/synthetic.jwt-secret +ExecStart=/usr/bin/node /usr/local/libexec/shellfleet-production-synthetic.mjs +StateDirectory=shellfleet-synthetic +UMask=0077 +TimeoutStartSec=90 +NoNewPrivileges=yes +CapabilityBoundingSet= +LockPersonality=yes +MemoryMax=128M +PrivateDevices=yes +PrivateTmp=yes +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +TasksMax=32 diff --git a/deploy/systemd/shellfleet-production-synthetic.timer b/deploy/systemd/shellfleet-production-synthetic.timer new file mode 100644 index 00000000..b997eb55 --- /dev/null +++ b/deploy/systemd/shellfleet-production-synthetic.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Run the ShellFleet production synthetic probe every five minutes + +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +RandomizedDelaySec=30s +Persistent=yes +Unit=shellfleet-production-synthetic.service + +[Install] +WantedBy=timers.target diff --git a/docs/production-monitoring.md b/docs/production-monitoring.md new file mode 100644 index 00000000..321ff250 --- /dev/null +++ b/docs/production-monitoring.md @@ -0,0 +1,92 @@ +# Production synthetic monitoring + +`scripts/production-synthetic.mjs` exercises the public dashboard path instead +of probing the containers directly. Each run creates a 60-second JWT in memory +from the control plane's existing signing secret and checks: + +- authenticated `GET /api/core/v1/fleet`, including an online-host floor and a + fresh system snapshot; +- authenticated `/api/core/v1/events`, waiting for a real fleet event through + the complete SSE stream; +- authenticated `/ui/ws`, including the Cloudflare/ingress upgrade and the + initial live agent list; +- passwordless passkey challenge creation, including the configured RP id. + +The token and signing secret are never printed or stored by the probe. systemd +delivers the signing secret through its protected credentials directory rather +than an environment variable. The service runs as an isolated dynamic user, +records only health state, alerts once when a failure starts, re-alerts hourly +while it persists, and sends a recovery notice. Telegram is used when its two +optional systemd credentials are installed. + +## Install on the control-plane host + +Install the script and units, then provide a root-readable environment file: + +```sh +sudo install -D -m 0755 scripts/production-synthetic.mjs \ + /usr/local/libexec/shellfleet-production-synthetic.mjs +sudo install -D -m 0644 deploy/systemd/shellfleet-production-synthetic.service \ + /etc/systemd/system/shellfleet-production-synthetic.service +sudo install -D -m 0644 deploy/systemd/shellfleet-production-synthetic.timer \ + /etc/systemd/system/shellfleet-production-synthetic.timer +sudo install -d -m 0755 /etc/shellfleet +sudoedit /etc/shellfleet/synthetic.env +sudoedit /etc/shellfleet/synthetic.jwt-secret +``` + +Put only the raw JWT signing-secret value in `synthetic.jwt-secret`, without a +variable name or quotes, and set its mode to `0600`. Required environment +settings are `SHELLFLEET_SYNTHETIC_BASE_URL` and +`SHELLFLEET_SYNTHETIC_LOGIN`. `UI_URL` and the first entry in +`ALLOWED_GITHUB_USERS` are accepted as deployment-compatible fallbacks. + +```dotenv +SHELLFLEET_SYNTHETIC_BASE_URL=https://fleet.example.com/ +SHELLFLEET_SYNTHETIC_LOGIN=monitor +SHELLFLEET_SYNTHETIC_ROLE=viewer +SHELLFLEET_SYNTHETIC_MIN_ONLINE=1 +SHELLFLEET_SYNTHETIC_MAX_SNAPSHOT_AGE_SECS=60 +``` + +The selected login must already be allowed and present in the server database. +Use a viewer where possible. If Enterprise ACLs intentionally hide every agent +from that viewer, the WebSocket transport is still validated; its visible-agent +count is reported but is not used as the fleet availability floor. + +```sh +sudo chmod 0600 /etc/shellfleet/synthetic.env \ + /etc/shellfleet/synthetic.jwt-secret +sudo systemctl daemon-reload +sudo systemctl enable --now shellfleet-production-synthetic.timer +sudo systemctl start shellfleet-production-synthetic.service +sudo systemctl status shellfleet-production-synthetic.service +sudo journalctl -u shellfleet-production-synthetic.service -n 20 --no-pager +``` + +For Telegram alerts, put the raw bot token and chat ID in separate root-only +files, then install the supplied credential drop-in: + +```sh +sudo install -D -m 0644 \ + deploy/systemd/shellfleet-production-synthetic-telegram.conf \ + /etc/systemd/system/shellfleet-production-synthetic.service.d/telegram.conf +``` + +The drop-in contains: + +```ini +# /etc/systemd/system/shellfleet-production-synthetic.service.d/telegram.conf +[Service] +LoadCredential=telegram-bot-token:/etc/shellfleet/synthetic.telegram-bot-token +LoadCredential=telegram-chat-id:/etc/shellfleet/synthetic.telegram-chat-id +``` + +Direct `JWT_SECRET`, `TELEGRAM_BOT_TOKEN`, and `TELEGRAM_CHAT_ID` environment +variables remain supported for one-shot/manual runs, but the shipped systemd +unit intentionally keeps secrets out of its ordinary environment. + +Tune `SHELLFLEET_SYNTHETIC_TIMEOUT_MS` (default `25000`) or +`SHELLFLEET_SYNTHETIC_ALERT_COOLDOWN_SECS` (default `3600`) only when the edge +has known longer latency. A failed run exits non-zero, so the service is also +compatible with systemd or external failure collectors. diff --git a/scripts/production-synthetic.mjs b/scripts/production-synthetic.mjs new file mode 100644 index 00000000..299e0fb1 --- /dev/null +++ b/scripts/production-synthetic.mjs @@ -0,0 +1,439 @@ +#!/usr/bin/env node + +import { createHash, createHmac, randomBytes } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import { dirname } from 'node:path'; +import tls from 'node:tls'; +import { fileURLToPath } from 'node:url'; + +const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +const MAX_STREAM_BUFFER_BYTES = 1024 * 1024; + +function required(value, name) { + if (!value?.trim()) throw new Error(`${name} is required`); + return value.trim(); +} + +function positiveInteger(value, fallback, name) { + if (value == null || value === '') return fallback; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < 0) { + throw new Error(`${name} must be a non-negative integer`); + } + return parsed; +} + +export function mintSessionJwt({ secret, login, role = 'viewer', now = Math.floor(Date.now() / 1000) }) { + if (secret !== 'dev' && secret.length < 32) { + throw new Error('JWT_SECRET must be at least 32 characters'); + } + if (!['admin', 'viewer'].includes(role)) { + throw new Error('SHELLFLEET_SYNTHETIC_ROLE must be admin or viewer'); + } + const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url'); + const unsigned = `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode({ + sub: login, + exp: now + 60, + iat: now, + role, + mfa: true, + cli: false, + })}`; + const signature = createHmac('sha256', secret).update(unsigned).digest('base64url'); + return `${unsigned}.${signature}`; +} + +export function validateFleet(payload, { minOnline = 1, maxSnapshotAgeSecs = 60, now = Math.floor(Date.now() / 1000) } = {}) { + if (!payload || !Array.isArray(payload.hosts)) { + throw new Error('fleet response does not contain a hosts array'); + } + const online = payload.hosts.filter((host) => host?.status === 'online'); + if (online.length < minOnline) { + throw new Error(`fleet has ${online.length} online host(s); expected at least ${minOnline}`); + } + const fresh = online.filter((host) => { + const observedAt = host?.system?.observed_at; + return Number.isFinite(observedAt) && now - observedAt <= maxSnapshotAgeSecs; + }); + if (minOnline > 0 && fresh.length === 0) { + throw new Error(`no online host has a system snapshot newer than ${maxSnapshotAgeSecs}s`); + } + return { online: online.length, freshSnapshots: fresh.length, hosts: payload.hosts.length }; +} + +export function validatePasskeyResponse(payload, expectedRpId) { + const stateId = payload?.state_id; + const publicKey = payload?.options?.publicKey; + if (typeof stateId !== 'string' || stateId.length < 8) { + throw new Error('passkey begin response has no usable state_id'); + } + if (typeof publicKey?.challenge !== 'string' || publicKey.challenge.length < 8) { + throw new Error('passkey begin response has no usable challenge'); + } + if (publicKey.rpId !== expectedRpId) { + throw new Error(`passkey RP id is ${JSON.stringify(publicKey.rpId)}; expected ${expectedRpId}`); + } + return { rpId: publicKey.rpId }; +} + +export function parseSseEvent(block) { + let event = 'message'; + const data = []; + for (const rawLine of block.split(/\r?\n/)) { + const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; + if (line.startsWith(':')) continue; + const separator = line.indexOf(':'); + const field = separator === -1 ? line : line.slice(0, separator); + let value = separator === -1 ? '' : line.slice(separator + 1); + if (value.startsWith(' ')) value = value.slice(1); + if (field === 'event') event = value; + if (field === 'data') data.push(value); + } + if (data.length === 0) return null; + return { event, data: data.join('\n') }; +} + +export function decodeWebSocketFrame(buffer) { + if (buffer.length < 2) return null; + const first = buffer[0]; + const second = buffer[1]; + const fin = (first & 0x80) !== 0; + const opcode = first & 0x0f; + const masked = (second & 0x80) !== 0; + let length = second & 0x7f; + let offset = 2; + if (length === 126) { + if (buffer.length < 4) return null; + length = buffer.readUInt16BE(2); + offset = 4; + } else if (length === 127) { + if (buffer.length < 10) return null; + const wideLength = buffer.readBigUInt64BE(2); + if (wideLength > BigInt(1024 * 1024)) throw new Error('WebSocket frame exceeds 1 MiB'); + length = Number(wideLength); + offset = 10; + } + let mask; + if (masked) { + if (buffer.length < offset + 4) return null; + mask = buffer.subarray(offset, offset + 4); + offset += 4; + } + if (buffer.length < offset + length) return null; + const payload = Buffer.from(buffer.subarray(offset, offset + length)); + if (mask) { + for (let index = 0; index < payload.length; index += 1) { + payload[index] ^= mask[index % 4]; + } + } + return { fin, opcode, payload, consumed: offset + length }; +} + +async function responseError(response, label) { + let detail = ''; + try { + detail = (await response.text()).replace(/\s+/g, ' ').slice(0, 180); + } catch { + // The status is enough when the response body cannot be read. + } + throw new Error(`${label} returned HTTP ${response.status}${detail ? `: ${detail}` : ''}`); +} + +async function fetchWithTimeout(url, options, timeoutMs) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs); + try { + // Never forward the short-lived session cookie or notification credential + // through an unexpected redirect. The configured production origin is the + // only intended destination for authenticated probes. + return await fetch(url, { redirect: 'manual', ...options, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +async function fetchFleet(baseUrl, cookie, timeoutMs) { + const response = await fetchWithTimeout(new URL('/api/core/v1/fleet', baseUrl), { + headers: { Accept: 'application/json', Cookie: cookie }, + }, timeoutMs); + if (!response.ok) await responseError(response, 'fleet REST probe'); + return response.json(); +} + +async function probePasskey(baseUrl, expectedRpId, timeoutMs) { + const response = await fetchWithTimeout(new URL('/api/auth/passkey/login/begin', baseUrl), { + method: 'POST', + headers: { Accept: 'application/json', Origin: baseUrl.origin }, + }, timeoutMs); + if (!response.ok) await responseError(response, 'passkey begin probe'); + return validatePasskeyResponse(await response.json(), expectedRpId); +} + +async function probeSse(baseUrl, cookie, timeoutMs) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new Error(`SSE timed out after ${timeoutMs}ms`)), timeoutMs); + let reader; + try { + const response = await fetch(new URL('/api/core/v1/events', baseUrl), { + headers: { Accept: 'text/event-stream', Cookie: cookie, 'Cache-Control': 'no-cache' }, + redirect: 'manual', + signal: controller.signal, + }); + if (!response.ok) await responseError(response, 'fleet SSE probe'); + if (!response.headers.get('content-type')?.toLowerCase().includes('text/event-stream')) { + throw new Error('fleet SSE probe returned the wrong content type'); + } + if (!response.body) throw new Error('fleet SSE probe returned no response stream'); + reader = response.body.getReader(); + const decoder = new TextDecoder(); + let pending = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) throw new Error('fleet SSE stream closed before a fleet event'); + pending += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n'); + if (Buffer.byteLength(pending) > MAX_STREAM_BUFFER_BYTES) { + throw new Error('fleet SSE event exceeds 1 MiB'); + } + let boundary; + while ((boundary = pending.indexOf('\n\n')) !== -1) { + const parsed = parseSseEvent(pending.slice(0, boundary)); + pending = pending.slice(boundary + 2); + if (!parsed || parsed.event !== 'fleet') continue; + const event = JSON.parse(parsed.data); + if (typeof event?.kind !== 'string' || typeof event?.observed_at !== 'number') { + throw new Error('fleet SSE event has an invalid payload'); + } + return event; + } + } + } finally { + clearTimeout(timer); + controller.abort(); + await reader?.cancel().catch(() => {}); + } +} + +async function probeWebSocket(baseUrl, cookie, timeoutMs) { + const wsUrl = new URL('/ui/ws', baseUrl); + wsUrl.protocol = 'wss:'; + const port = Number(wsUrl.port || 443); + const key = randomBytes(16).toString('base64'); + const expectedAccept = createHash('sha1').update(`${key}${WS_GUID}`).digest('base64'); + + return new Promise((resolve, reject) => { + const socket = tls.connect({ host: wsUrl.hostname, port, servername: wsUrl.hostname }); + let settled = false; + let headersParsed = false; + let pending = Buffer.alloc(0); + const timer = setTimeout(() => finish(new Error(`WebSocket timed out after ${timeoutMs}ms`)), timeoutMs); + + function finish(error, value) { + if (settled) return; + settled = true; + clearTimeout(timer); + socket.destroy(); + if (error) reject(error); + else resolve(value); + } + + socket.once('secureConnect', () => { + const path = `${wsUrl.pathname}${wsUrl.search}`; + socket.write([ + `GET ${path} HTTP/1.1`, + `Host: ${wsUrl.host}`, + 'Connection: Upgrade', + 'Upgrade: websocket', + 'Sec-WebSocket-Version: 13', + `Sec-WebSocket-Key: ${key}`, + `Origin: ${baseUrl.origin}`, + `Cookie: ${cookie}`, + 'User-Agent: shellfleet-production-synthetic/1', + '', + '', + ].join('\r\n')); + }); + socket.on('error', (error) => finish(new Error(`WebSocket transport failed: ${error.message}`))); + socket.on('end', () => finish(new Error('WebSocket closed before the initial agent list'))); + socket.on('data', (chunk) => { + pending = Buffer.concat([pending, chunk]); + if (pending.length > MAX_STREAM_BUFFER_BYTES) { + return finish(new Error('WebSocket response exceeds 1 MiB')); + } + if (!headersParsed) { + const marker = pending.indexOf('\r\n\r\n'); + if (marker === -1) return; + const headerText = pending.subarray(0, marker).toString('latin1'); + pending = pending.subarray(marker + 4); + const lines = headerText.split('\r\n'); + if (!/^HTTP\/1\.[01] 101\b/.test(lines[0])) { + return finish(new Error(`WebSocket upgrade returned ${lines[0] || 'an invalid response'}`)); + } + const headers = new Map(lines.slice(1).map((line) => { + const separator = line.indexOf(':'); + return [line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim()]; + })); + if (headers.get('sec-websocket-accept') !== expectedAccept) { + return finish(new Error('WebSocket upgrade returned an invalid accept key')); + } + headersParsed = true; + } + + while (headersParsed) { + let frame; + try { + frame = decodeWebSocketFrame(pending); + } catch (error) { + return finish(error); + } + if (!frame) return; + pending = pending.subarray(frame.consumed); + if (frame.opcode === 0x8) return finish(new Error('WebSocket closed before the initial agent list')); + if (frame.opcode !== 0x1) continue; + if (!frame.fin) return finish(new Error('fragmented WebSocket response is not supported by the probe')); + let message; + try { + message = JSON.parse(frame.payload.toString('utf8')); + } catch { + return finish(new Error('WebSocket returned non-JSON text')); + } + if (message?.type !== 'ListAgentsResponse' || !Array.isArray(message?.payload?.agents)) { + return finish(new Error('WebSocket did not return the initial agent list')); + } + return finish(null, { agents: message.payload.agents.length }); + } + }); + }); +} + +async function readState(path) { + try { + return JSON.parse(await fs.readFile(path, 'utf8')); + } catch { + return null; + } +} + +async function writeState(path, state) { + const temporary = `${path}.${process.pid}.tmp`; + await fs.mkdir(dirname(path), { recursive: true }); + await fs.writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 }); + await fs.rename(temporary, path); +} + +async function sendTelegram(message, env, timeoutMs) { + const token = env.TELEGRAM_BOT_TOKEN?.trim(); + const chatId = env.TELEGRAM_CHAT_ID?.trim(); + if (!token || !chatId) return false; + const response = await fetchWithTimeout(`https://api.telegram.org/bot${encodeURIComponent(token)}/sendMessage`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chat_id: chatId, text: message, disable_web_page_preview: true }), + }, timeoutMs); + if (!response.ok) throw new Error(`Telegram alert returned HTTP ${response.status}`); + return true; +} + +export async function loadCredentialSecrets(env) { + const resolved = { ...env }; + const credentialsDirectory = env.CREDENTIALS_DIRECTORY?.trim(); + if (!credentialsDirectory) return resolved; + const mappings = [ + ['JWT_SECRET', 'jwt-secret'], + ['TELEGRAM_BOT_TOKEN', 'telegram-bot-token'], + ['TELEGRAM_CHAT_ID', 'telegram-chat-id'], + ]; + for (const [environmentName, credentialName] of mappings) { + if (resolved[environmentName]?.trim()) continue; + try { + resolved[environmentName] = (await fs.readFile(`${credentialsDirectory}/${credentialName}`, 'utf8')).trim(); + } catch (error) { + if (environmentName === 'JWT_SECRET') { + throw new Error(`required systemd credential ${credentialName} is unavailable: ${error.code || error.message}`); + } + } + } + return resolved; +} + +function safeError(error) { + return String(error?.message || error || 'unknown failure').replace(/[\r\n]+/g, ' ').slice(0, 500); +} + +export async function runSynthetic(env = process.env) { + const baseUrl = new URL(required(env.SHELLFLEET_SYNTHETIC_BASE_URL || env.UI_URL, 'SHELLFLEET_SYNTHETIC_BASE_URL or UI_URL')); + if (baseUrl.protocol !== 'https:') throw new Error('production synthetic base URL must use HTTPS'); + baseUrl.pathname = '/'; + baseUrl.search = ''; + baseUrl.hash = ''; + const login = required(env.SHELLFLEET_SYNTHETIC_LOGIN || env.ALLOWED_GITHUB_USERS?.split(',')[0], 'SHELLFLEET_SYNTHETIC_LOGIN or ALLOWED_GITHUB_USERS'); + const secret = required(env.JWT_SECRET, 'JWT_SECRET'); + const role = env.SHELLFLEET_SYNTHETIC_ROLE?.trim() || 'viewer'; + const timeoutMs = positiveInteger(env.SHELLFLEET_SYNTHETIC_TIMEOUT_MS, 25_000, 'SHELLFLEET_SYNTHETIC_TIMEOUT_MS'); + const minOnline = positiveInteger(env.SHELLFLEET_SYNTHETIC_MIN_ONLINE, 1, 'SHELLFLEET_SYNTHETIC_MIN_ONLINE'); + const maxSnapshotAgeSecs = positiveInteger(env.SHELLFLEET_SYNTHETIC_MAX_SNAPSHOT_AGE_SECS, 60, 'SHELLFLEET_SYNTHETIC_MAX_SNAPSHOT_AGE_SECS'); + const expectedRpId = env.SHELLFLEET_SYNTHETIC_RP_ID?.trim() || baseUrl.hostname; + const token = mintSessionJwt({ secret, login, role }); + const cookie = `__Host-auth_token=${token}`; + const startedAt = Date.now(); + + const firstFleet = await fetchFleet(baseUrl, cookie, timeoutMs); + const fleet = validateFleet(firstFleet, { minOnline, maxSnapshotAgeSecs }); + const passkey = await probePasskey(baseUrl, expectedRpId, timeoutMs); + const webSocket = await probeWebSocket(baseUrl, cookie, timeoutMs); + const event = await probeSse(baseUrl, cookie, timeoutMs); + const secondFleet = await fetchFleet(baseUrl, cookie, timeoutMs); + const eventHost = event.agent_id && secondFleet.hosts.find((host) => host.agent_id === event.agent_id); + const firstEventHost = event.agent_id && firstFleet.hosts.find((host) => host.agent_id === event.agent_id); + if (event.kind === 'host_updated' && firstEventHost && eventHost && eventHost.last_seen_at <= firstEventHost.last_seen_at) { + throw new Error(`SSE reported ${event.agent_id} without advancing its durable fleet timestamp`); + } + + return { + baseUrl: baseUrl.origin, + durationMs: Date.now() - startedAt, + fleet, + passkey, + webSocket, + sse: { kind: event.kind, agentId: event.agent_id || null }, + }; +} + +async function main() { + const env = await loadCredentialSecrets(process.env); + const statePath = env.SHELLFLEET_SYNTHETIC_STATE_FILE || '/var/lib/shellfleet-synthetic/status.json'; + const previous = await readState(statePath); + const now = Math.floor(Date.now() / 1000); + const alertCooldown = positiveInteger(env.SHELLFLEET_SYNTHETIC_ALERT_COOLDOWN_SECS, 3600, 'SHELLFLEET_SYNTHETIC_ALERT_COOLDOWN_SECS'); + try { + const result = await runSynthetic(env); + if (previous?.status === 'degraded') { + await sendTelegram(`ShellFleet production probe recovered\n${result.baseUrl}\nREST, SSE, WebSocket, and passkey begin are healthy.`, env, 10_000) + .catch((error) => console.error(`recovery notification failed: ${safeError(error)}`)); + } + await writeState(statePath, { status: 'healthy', checked_at: now }).catch((error) => { + console.error(`state update failed: ${safeError(error)}`); + }); + console.log(JSON.stringify({ ok: true, ...result })); + } catch (error) { + const reason = safeError(error); + const shouldAlert = previous?.status !== 'degraded' || now - Number(previous?.alerted_at || 0) >= alertCooldown; + let alertedAt = Number(previous?.alerted_at || 0); + if (shouldAlert) { + const sent = await sendTelegram(`ShellFleet production probe failed\n${reason}`, env, 10_000) + .catch((alertError) => { + console.error(`failure notification failed: ${safeError(alertError)}`); + return false; + }); + if (sent) alertedAt = now; + } + await writeState(statePath, { status: 'degraded', checked_at: now, alerted_at: alertedAt, reason }).catch((stateError) => { + console.error(`state update failed: ${safeError(stateError)}`); + }); + console.error(JSON.stringify({ ok: false, error: reason })); + process.exitCode = 1; + } +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + await main(); +} diff --git a/scripts/production-synthetic.test.mjs b/scripts/production-synthetic.test.mjs new file mode 100644 index 00000000..6c489585 --- /dev/null +++ b/scripts/production-synthetic.test.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import { createHmac } from 'node:crypto'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { + decodeWebSocketFrame, + loadCredentialSecrets, + mintSessionJwt, + parseSseEvent, + validateFleet, + validatePasskeyResponse, +} from './production-synthetic.mjs'; + +test('loads secrets from systemd credentials without replacing explicit values', async () => { + const directory = await mkdtemp(join(tmpdir(), 'shellfleet-synthetic-')); + try { + await writeFile(join(directory, 'jwt-secret'), `${'c'.repeat(32)}\n`, { mode: 0o600 }); + await writeFile(join(directory, 'telegram-bot-token'), 'credential-token\n', { mode: 0o600 }); + const loaded = await loadCredentialSecrets({ + CREDENTIALS_DIRECTORY: directory, + TELEGRAM_BOT_TOKEN: 'explicit-token', + }); + assert.equal(loaded.JWT_SECRET, 'c'.repeat(32)); + assert.equal(loaded.TELEGRAM_BOT_TOKEN, 'explicit-token'); + assert.equal(loaded.TELEGRAM_CHAT_ID, undefined); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +test('mints a short-lived, MFA-verified browser session JWT', () => { + const secret = 'a'.repeat(32); + const token = mintSessionJwt({ secret, login: 'monitor', role: 'viewer', now: 100 }); + const [header, payload, signature] = token.split('.'); + assert.deepEqual(JSON.parse(Buffer.from(header, 'base64url')), { alg: 'HS256', typ: 'JWT' }); + assert.deepEqual(JSON.parse(Buffer.from(payload, 'base64url')), { + sub: 'monitor', exp: 160, iat: 100, role: 'viewer', mfa: true, cli: false, + }); + assert.equal(signature, createHmac('sha256', secret).update(`${header}.${payload}`).digest('base64url')); +}); + +test('requires fresh snapshots from the configured online fleet floor', () => { + const payload = { + hosts: [{ status: 'online', system: { observed_at: 980 } }, { status: 'offline', system: null }], + }; + assert.deepEqual(validateFleet(payload, { minOnline: 1, maxSnapshotAgeSecs: 30, now: 1_000 }), { + online: 1, freshSnapshots: 1, hosts: 2, + }); + assert.throws(() => validateFleet(payload, { minOnline: 2, now: 1_000 }), /expected at least 2/); + assert.throws(() => validateFleet(payload, { minOnline: 1, maxSnapshotAgeSecs: 10, now: 1_000 }), /no online host/); +}); + +test('validates the passkey RP id and challenge shape', () => { + const payload = { + state_id: 'state-identifier', + options: { publicKey: { challenge: 'challenge-value', rpId: 'fleet.example.com' } }, + }; + assert.deepEqual(validatePasskeyResponse(payload, 'fleet.example.com'), { rpId: 'fleet.example.com' }); + assert.throws(() => validatePasskeyResponse(payload, 'wrong.example.com'), /expected wrong/); +}); + +test('parses fleet SSE messages and ignores comments', () => { + assert.deepEqual(parseSseEvent(': keepalive\nevent: fleet\ndata: {"kind":"host_updated"}'), { + event: 'fleet', data: '{"kind":"host_updated"}', + }); + assert.equal(parseSseEvent(': keepalive'), null); +}); + +test('decodes an extended-length unmasked WebSocket text frame', () => { + const payload = Buffer.from('x'.repeat(130)); + const frame = Buffer.alloc(4 + payload.length); + frame[0] = 0x81; + frame[1] = 126; + frame.writeUInt16BE(payload.length, 2); + payload.copy(frame, 4); + const decoded = decodeWebSocketFrame(frame); + assert.equal(decoded.fin, true); + assert.equal(decoded.opcode, 1); + assert.deepEqual(decoded.payload, payload); + assert.equal(decoded.consumed, frame.length); +}); diff --git a/tests/journey/playwright.config.ts b/tests/journey/playwright.config.ts index 33f224c8..167a0762 100644 --- a/tests/journey/playwright.config.ts +++ b/tests/journey/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; +const systemChromium = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH?.trim(); + export default defineConfig({ testDir: '.', testMatch: 'fleet-read-plane.spec.ts', @@ -15,6 +17,7 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'], baseURL: 'http://127.0.0.1:18080', + launchOptions: systemChromium ? { executablePath: systemChromium } : undefined, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', diff --git a/web/package-lock.json b/web/package-lock.json index dcfa612f..47f941d9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,12 +16,12 @@ "next": "^16.2.6", "qrcode.react": "^4.2.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.8" }, "devDependencies": { "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "@types/node": "^26.0.0", "@types/react": "^19.2.17", @@ -29,10 +29,10 @@ "eslint": "^9.39.4", "eslint-config-next": "^16.2.6", "js-yaml": "^4.2.0", - "jsdom": "^29.1.1", + "jsdom": "^30.0.1", "tailwindcss": "^4", "typescript": "^6", - "vitest": "^4.1.9" + "vitest": "^4.1.10" } }, "node_modules/@adobe/css-tools": { @@ -56,52 +56,58 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", "dev": true, + "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", "dev": true, + "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -365,9 +371,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", "dev": true, "funding": [ { @@ -379,14 +385,15 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "engines": { "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -398,6 +405,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=20.19.0" }, @@ -407,9 +415,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", - "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", "dev": true, "funding": [ { @@ -421,9 +429,10 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -448,6 +457,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=20.19.0" }, @@ -456,9 +466,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", "dev": true, "funding": [ { @@ -470,6 +480,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "peerDependencies": { "css-tree": "^3.2.1" }, @@ -494,6 +505,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=20.19.0" } @@ -1658,10 +1670,11 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } @@ -1682,14 +1695,32 @@ "node": ">=18" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1699,13 +1730,14 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1715,13 +1747,14 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1731,13 +1764,14 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1747,13 +1781,14 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1763,13 +1798,17 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1779,13 +1818,17 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1795,13 +1838,17 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1811,13 +1858,17 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1827,13 +1878,17 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1843,13 +1898,17 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1859,13 +1918,14 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -1874,32 +1934,15 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1909,13 +1952,14 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1928,7 +1972,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -1940,7 +1985,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@swc/helpers": { "version": "0.5.15", @@ -2321,9 +2367,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2335,9 +2381,18 @@ "redent": "^3.0.0" }, "engines": { - "node": ">=14", + "node": ">=22", "npm": ">=6", "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } } }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { @@ -2398,6 +2453,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, + "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -2407,7 +2463,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", @@ -2998,15 +3055,16 @@ ] }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, + "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3015,12 +3073,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3041,10 +3100,11 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" }, @@ -3053,12 +3113,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -3066,13 +3127,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3081,21 +3143,23 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, + "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3358,6 +3422,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -3438,6 +3503,7 @@ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, + "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" } @@ -3585,6 +3651,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -4605,6 +4672,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -4769,6 +4837,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5422,7 +5491,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -5641,38 +5711,39 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", "dev": true, + "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -5681,14 +5752,30 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6609,7 +6696,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -6812,22 +6900,24 @@ ] }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-is": { @@ -6899,6 +6989,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6956,12 +7047,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", "dev": true, + "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6971,21 +7063,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, "node_modules/run-parallel": { @@ -7640,31 +7732,34 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/tldts": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", - "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", "dev": true, + "license": "MIT", "dependencies": { - "tldts-core": "^7.4.3" + "tldts-core": "^7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", - "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", - "dev": true + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -7679,10 +7774,11 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" }, @@ -7894,13 +7990,13 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -7987,15 +8083,16 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, + "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "bin": { @@ -8012,7 +8109,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -8063,11 +8160,285 @@ } } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -8076,18 +8447,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", - "dev": true, - "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8115,12 +8487,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/web/package.json b/web/package.json index e16ebcb0..43c65643 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,7 @@ "next": "^16.2.6", "qrcode.react": "^4.2.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.8" }, "overrides": { "dompurify": "^3.4.11", @@ -29,7 +29,7 @@ "devDependencies": { "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "@types/node": "^26.0.0", "@types/react": "^19.2.17", @@ -37,9 +37,9 @@ "eslint": "^9.39.4", "eslint-config-next": "^16.2.6", "js-yaml": "^4.2.0", - "jsdom": "^29.1.1", + "jsdom": "^30.0.1", "tailwindcss": "^4", "typescript": "^6", - "vitest": "^4.1.9" + "vitest": "^4.1.10" } }