diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b3334..b449a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ The project publishes 0.x prerelease versions; a stable release line is not yet ### Added +- Workspace-scoped Markdown Vault sync HTTP API (`#234`): discover Vaults, pull bounded + snapshots and atomically commit logical-note deltas with whole-Vault CAS. + Independent empty notes, immutable revisions and deletion tombstones preserve + identity and concurrent edits; workspace, read/write and token-path boundaries + reuse canonical authorization. Additive migration 0027 leaves existing files + unchanged. See `docs/VAULT_SYNC.md` for limits, downgrade data loss and the + explicit boundary from current workspace-bundle export/import formats. - Per-agent memory namespace filter on lexical recall (`#227`): `POST /v1/context` and `mem_context` accept `agent_id` plus optional `extra_agent_ids`. When set, structured-memory recall hard-filters `producer_agent` before ranking so @@ -17,6 +24,17 @@ The project publishes 0.x prerelease versions; a stable release line is not yet Design: `docs/agent-memory-namespace.md`. - `mem put --watch` one-way foreground directory watch: poll `--interval` (default 30s), ingest new files after one quiet interval, report changed files without re-ingesting, and never propagate local deletes. Refs #110. +### Fixed + +- Restore Linux/Windows Web dependency audit gates by removing the unpatched + braces build-tool chain with official Tailwind/PostCSS 4 and typescript-eslint + 8, while retaining existing theme utility behavior and browser acceptance. + Apply compatible transitive security updates without relaxing audit thresholds. + Validation and public advisory references: `docs/acceptance/VAULT_SYNC_VALIDATION.md`. +- Build isolated lifecycle-test MinIO/mc images from checksum-pinned official + sources when former public images are no longer anonymously pullable. Keep + production assets and all HTTP/CLI/MCP assertions unchanged. Refs #234. + ### Changed - The live recall producer (`python3 -m benchmarks.recall produce`) reads engine, profile, provider and embedding dimension from the running memd instead of CLI labels, scores the multilingual v1 set (English and Chinese, including image-description queries), and stays on-demand rather than a CI gate. Refs #175. diff --git a/SPEC.md b/SPEC.md index ce49097..79da954 100644 --- a/SPEC.md +++ b/SPEC.md @@ -254,6 +254,28 @@ workspace import 的数据库 commit 若无法确认结果,服务端不得删 --- +### F9A · Workspace Markdown Vault 同步(HTTP v1) + +需求:[mem #234](https://github.com/bytefolk/mem/issues/234)。 + +`GET /v1/vault/list`、`GET /v1/vault/snapshot?vaultId=` 与 +`POST /v1/vault/commit` 提供工作区内的逻辑笔记同步底座。每份笔记使用独立 +`noteId`,相同正文和空笔记不会因 blob 去重而合并。commit 传递 +`{vaultId,baseRevision,title?,entries:[{noteId,path,content,deleted?,properties?}]}` +增量;整个 Vault 的 head 在单个事务内执行 CAS,过期 head 返回 `409` +及 `currentRevision`,不覆盖其他客户端。删除保留 tombstone 与不可原地更新的 +正文 revision;重命名保留逻辑 ID。只读需 `read`,提交需 `read` + `write`, +复用现有 workspace membership 和 `/Vaults/` token path 边界。 +properties 仅为有界数据,不能授予工具或身份权限。图片等原件继续使用 files API。 + +`vault-snapshot.v1` 是独立的当前状态可迁移合同,详见 +[API、限制与错误码](docs/VAULT_SYNC.md) 和 +[JSON Schema](docs/schemas/vault-snapshot.v1.schema.json)。CLI/MCP/Web 本次不新增 +Vault 命令或独立笔记编辑器。现有 workspace bundle v1/v2 不包含 Vault 表和历史; +完整历史仍需数据库备份。服务不运行桌面文件监听或自动合并客户端冲突;客户端必须 +拉取 head、三方合并并保留冲突副本。迁移 0027 不改变 files/auth/memory 表;降级 +到 0026 会丢弃 Vault 数据,操作前需备份。 + ## 4. 非功能需求 | 维度 | 目标 | diff --git a/docs/TESTING.md b/docs/TESTING.md index a3204af..480a42e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -274,8 +274,10 @@ Required tests: - `TestManagedAISettlementOutboxPostgres` - `TestReleasedFileStageRetryPostgres` - `TestDurableContextPostgres` +- `TestVaultSyncPostgres` (two-client CAS, distinct empty notes, auth/workspace, + immutable history, tombstones, path swap/title and migration 27 down/up) -Expected result: migrations reach the declared current head (currently `20`), +Expected result: migrations reach the declared current head (currently `27`), all rollback-state assertions pass, every named test prints `PASS`, all commands exit `0`, and the race run reports no data race. @@ -298,6 +300,15 @@ Run the isolated, model-free process test: make test-acceptance ``` +The script builds temporary MinIO/mc binaries from checksum-pinned official +source archives because Community Edition is now source-only. It uses the +existing Go toolchain, `tar` and `sha256sum`, then builds a uniquely named test +image on a pinned official Alpine runtime. The temporary Compose override, +image and run directory are removed by its ownership-checked cleanup. This +does not replace images or credentials in a production deployment. Network +access to the fixed source archives and locked Go module dependencies is +required; missing artifacts or checksum mismatches fail the gate. + The script owns a per-run Compose project, random loopback PostgreSQL/MinIO ports, an ephemeral database/store and a new memd process on an atomically locked, twice-preflighted loopback port. It builds the current CLI, MCP adapter @@ -449,8 +460,8 @@ Use this table in pull requests and add implementation-specific scenarios: | V2 | Worker processing regressions remain hermetic | `make test-worker` | Exit `0`; real-model gate explicitly skipped | | V3 | Localization, theme, enrichment, memory, transfer and managed-embedding control surfaces work in a browser | `make test-web` | Typecheck/lint/build, the localization audit, all browser acceptance suites and managed status mapping pass | | V4 | High-risk Go paths are race-free | `make test-race` | Exit `0`; no data-race warning | -| V5 | Fresh schema, rollback and PostgreSQL semantics hold | `make test-integration` | Migration head and eighteen named tests pass, none skipped | -| V6 | DB concurrency paths are race-free | `make test-integration-race` | The same eighteen tests pass under `-race` | +| V5 | Fresh schema, rollback and PostgreSQL semantics hold | `make test-integration` | Migration head and all required named tests pass, none skipped | +| V6 | DB concurrency paths are race-free | `make test-integration-race` | The same required tests pass under `-race` | | V7 | Real service boundaries agree | `make test-acceptance` | HTTP, CLI and MCP share one isolated service; memory citation/provenance, bounded checkpoint listing, full checkpoint get, lifecycle and forget redaction pass | | V8 | Five config shapes and the real adapter preserve the host-neutral MCP contract | `MEM_MCP_CERT_BINARY=... make test-agent-certification` | All fixtures and current-adapter scenarios pass with no skip | | V9 | Multilingual visual quality meets the chosen checkpoint | Opt-in command in section 7 | All fixed ranking assertions pass | diff --git a/docs/VAULT_SYNC.md b/docs/VAULT_SYNC.md new file mode 100644 index 0000000..3f78447 --- /dev/null +++ b/docs/VAULT_SYNC.md @@ -0,0 +1,109 @@ +# Workspace Vault sync v1 + +Implements [mem #234](https://github.com/bytefolk/mem/issues/234). + +This additive text-note service keeps logical identity independent of the +existing `/v1/files` content-deduplication model. It never changes that model, +invokes AI, or treats note properties as permissions. + +All endpoints use existing Bearer authentication and workspace membership. +`X-Workspace-ID`/a bound token selects the workspace. A token must also cover +the canonical path `/Vaults/`. + +* `GET /v1/vault/list` (`read`) returns + `{ "vaults": [{ "vaultId", "title", "revision", "updatedAt" }] }`. + Out-of-path Vault IDs are omitted, without revealing their count. +* `GET /v1/vault/snapshot?vaultId=` (`read`) returns a full snapshot. + A new Vault returns revision `0`, empty title, `updatedAt: null`, and `[]` + without creating a row. +* `POST /v1/vault/commit` (`read` + `write`) accepts + `{ "vaultId", "baseRevision", "title"?, "entries": [...] }`. + Entries are **deltas**, not a replacement of the full Vault. An entry is + `{ "noteId", "path", "content", "deleted"?, "properties"? }`. + Unlisted notes remain unchanged. `deleted: true` requires empty content. + Omitted properties retain existing metadata; a new note defaults to `{}`. + Empty entries are allowed only with a title update. + +Both snapshot and successful commit return: + +```json +{ + "schemaVersion": "vault-snapshot.v1", + "vaultId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "title": "Knowledge", + "revision": 1, + "updatedAt": "2026-10-07T12:00:00Z", + "entries": [ + { + "noteId": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "path": "岗位/空白笔记.md", + "content": "", + "revision": 1, + "deleted": false, + "properties": {} + } + ] +} +``` + +IDs are nonzero UUIDs. Vault IDs in requests use canonical lowercase form. +Paths are canonical relative POSIX paths up to 512 UTF-8 bytes, preserving +Chinese and spaces. Absolute paths, traversal, backslashes, control characters, +Windows-forbidden filename characters and trailing dots/spaces are rejected. +Two active notes cannot share a case-folded path. Logical notes with identical +content, including empty content, retain separate IDs. Text is UTF-8; only +newline, carriage return and tab control characters are accepted. Attachments +continue to use the existing mem file API and references rather than binary +data embedded in this text protocol. + +One transaction locks and compares the whole Vault head. Successful commits +advance it exactly once and append content revisions; mutable head pointers +select current versions. Old entry revisions cannot be updated in place. +Delete creates a retained tombstone; it does not physically erase history. +Rename/swap changes logical paths while preserving note IDs. + +* Stale head: `409 { "error": "vault_revision_conflict", "currentRevision": N }`. +* Active path collision: `409`, `error: vault_path_conflict`. +* Invalid body/path/identity: `400`, `error: vault_request_invalid`. +* Capacity/body/history limit: `413`, `error: vault_limit_exceeded`. +* Unavailable storage: `503`, `error: vault_unavailable`; success is not assumed. + +Clients keep their own base snapshot and local manifest. On 409 they pull the +current head, merge nonoverlapping edits, and preserve conflicts as explicit +copies. A lost response is reconciled through snapshot readback, never a blind +overwrite. The service does not modify local files or promise a sync daemon. + +Bounds: 4 MiB HTTP commit body, 1 MiB per note, 16 KiB per properties object, +256 UTF-8 bytes per title, 512 delta entries, 1000 heads **including tombstones**, +16 MiB current serialized content/path/properties, 64 MiB retained entry-history +content/path/properties, 10,000 commits and 64 Vaults per workspace. Limits +refuse atomically; snapshots are never silently truncated. A full snapshot may +require roughly 32 MiB on the wire due to JSON escaping; callers must use a +bounded Vault-specific response budget rather than an 8 MiB file-card budget. + +Validation: unit/API tests are hermetic. `TestVaultSyncPostgres` requires an +explicit `MEM_TEST_DB` whose database name ends in `_test`; it verifies real +two-client races, distinct empty notes, workspace/auth isolation, tombstones, +immutable history, swaps, title CAS and additive migration down/up. + +## Compatibility and portability boundary + +The HTTP API is the canonical implementation. This initial additive surface +adds no CLI, MCP or standalone mem Web Vault editor; existing adapters and file +operations retain their contracts. Clients may synchronize ordinary Markdown +files and upload attachments through the existing file API. + +The current workspace-bundle v1/v2 export/import formats do not contain Vault +heads or revision history. A Vault snapshot is a separate portable current-state +contract (docs/schemas/vault-snapshot.v1.schema.json). It has no auth tokens, +workspace membership or actor audit fields, and importing its properties never +grants permissions. Restoring a snapshot into an authorized target Vault uses +a normal CAS commit with the target head and a client-selected logical Vault +ID. Full history remains in PostgreSQL and requires an operator database backup; +this API does not claim to transport retained history between deployments. + +Migration 0027 only creates new Vault tables and indexes. Downgrading to 0026 +drops all Vault state and history, so preserve a database backup or current +snapshot before an operator downgrade. Existing files, auth and memory tables +are untouched. No login recovery, deployment credentials or model dependency +is introduced. diff --git a/docs/acceptance/VAULT_SYNC_VALIDATION.md b/docs/acceptance/VAULT_SYNC_VALIDATION.md new file mode 100644 index 0000000..f16d0c4 --- /dev/null +++ b/docs/acceptance/VAULT_SYNC_VALIDATION.md @@ -0,0 +1,160 @@ +# Vault sync validation ledger + +Issue: [mem #234](https://github.com/bytefolk/mem/issues/234). +Base: `c5f3fd591973dfaf0e36baecfee8cb8295c1bcb7` (`origin/main`). +Validation date: 2026-10-08. This ledger records implementation-side evidence; +it is not independent review approval or a deployment/release claim. + +Environment: Linux amd64 through WSL, Go 1.25.0, PostgreSQL 16 with the CI-pinned +`pgvector/pgvector@sha256:00ba258a66dac104fd5171074a0084462a64a1369d8513f3d0a634e2f24d15bc` +image. A new loopback-only container used tmpfs PostgreSQL data and the synthetic +control database `mem_vault_publish_test`. Each integration-runner phase created +and removed its own nonce-marked `mem_verify_*_test` database. No deployed +database, credentials, container or user configuration was read or changed. + +| Check | Exact command (repository root unless specified) | Result | +| --- | --- | --- | +| Server/CLI/MCP compilation | `(cd server && env -u MEM_TEST_DB go build ./...)` | PASS | +| Go static analysis | `(cd server && env -u MEM_TEST_DB go vet ./...)` | PASS | +| All hermetic Go regressions | `(cd server && env -u MEM_TEST_DB go test -count=1 ./...)` | PASS; database tests excluded here and verified separately | +| High-risk hermetic race checks, including Vault | `bash scripts/verify.sh race` | PASS; no data race | +| Fresh schema, strict populated 23 → 27 upgrade, historical rollback round trips, HNSW checks and all required PostgreSQL tests | `MEM_TEST_DB= bash scripts/verify.sh integration` | PASS; migration head 27; no required test skipped | +| All required PostgreSQL tests under race detector | `MEM_TEST_DB= bash scripts/verify.sh integration-race` | PASS; no required test skipped or data race | +| Whitespace and schema serialization | `git diff --check`; Python `json.load` on `docs/schemas/vault-snapshot.v1.schema.json` | PASS; JSON parsing is not a claim of full JSON Schema conformance testing | + +The initial Windows worktree had CRLF shell/golden-fixture copies. The first +Linux attempt failed before integration setup (`pipefail\r`) and the Go doctor +golden comparison rejected line endings. Only those validation working copies +were converted to the Git-equivalent LF content; the commands above were rerun +and passed. No unrelated fixture, shell content or file-mode change is included +in this issue's diff. + +## Observable Vault acceptance + +`TestVaultSyncPostgres` passed in both normal and race integration runners: + +- separate UUID identities preserve two independently synchronized empty notes; +- two actual clients writing the same base revision have exactly one winner + and one `409` conflict; the losing edit never overwrites the winner; +- real router authentication, workspace selection and read/write scopes deny + reader writes and keep another workspace's same Vault UUID isolated; +- successful commits append immutable old text, advance the head once, retain + deletion tombstones, support path swaps and use the same CAS for title changes; +- real discovery lists the workspace's Vault, and real HTTP stale writes return + the observed `currentRevision`; +- migration `27 → 26 → 27` is exercised only on disposable data, preserving + existing file/auth schemas while explicitly dropping Vault state on downgrade. + +Hermetic `internal/vaultsync` and `internal/api` tests additionally cover +malformed identity/path/text/properties, distinct note identity, deterministic +delta planning, stale heads, active-path collisions, count/byte bounds, +tombstones, unavailable storage, strict HTTP bodies, anonymous denial and +token-path filtering for both snapshot and discovery. Property values are +untrusted data and do not add workspace membership or tool authority. + +## Compatibility and honest limits + +- API: additive canonical HTTP routes; existing file/blob identity is unchanged. + The new note ID is independent of content SHA-256. +- CLI/MCP/Web: N/A for new Vault UI/commands in this backend change. Their + existing behavior is covered by the complete Go suite where applicable; + no separate Vault adapter or desktop sync daemon is claimed. +- Worker: N/A; no Worker, protobuf, model or queue behavior changed. The Web + tooling follow-up and its complete validation are recorded below. +- Provenance/privacy: public snapshot contains current note data/properties, + with no authentication material, actor audit IDs or new authorization model. +- Retry: a stale or uncertain commit is reconciled through current-head + readback; clients retain their base and preserve divergent edits as copies. + No request-level idempotency key or server-side automatic merge is claimed. +- Recall warnings: N/A; this surface does not change search, recall or ranking. +- Portability: `vault-snapshot.v1` transports current state. Workspace bundle + v1/v2 does not include Vault state; retained history needs database backup. +- Deployment: NOT VERIFIED by this publication worktree; no production + migration, restart, credential issuance, release or hosted-service rollout ran. + +## CI baseline repair (2026-10-09) + +The first PR run passed Go, PostgreSQL, Worker, existing Web browser acceptance, +deployment profiles, CodeQL and dependency review. Three gates failed for +unchanged baseline dependencies or external artifacts, before exercising any +Vault code: + +- [Linux Web audit](https://github.com/bytefolk/mem/actions/runs/37806469015/job/113411946797) + and [Windows audit](https://github.com/bytefolk/mem/actions/runs/37806469015/job/113411946574) + both reported 14 high and 2 moderate findings in the original lockfile. +- [HTTP/CLI/MCP lifecycle](https://github.com/bytefolk/mem/actions/runs/37806468988/job/113411942810) + stopped at an unauthorized pull of the original public MinIO image; no + application service or migration had started. + +This follow-up keeps the audit thresholds, all lifecycle assertions and CI +timeouts. It does not suppress vulnerabilities, ignore findings or disable a +check. + +### Dependency cause and compatibility + +The published [braces stack-exhaustion advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm) +affects every available version through 3.0.3 and lists no patched version. +Tailwind 3.4.19 still requires chokidar 3, fast-glob and micromatch, which retain +that chain. typescript-eslint 7 also retains it through globby/fast-glob. These +are active CSS and TypeScript build tools, so removing an unused package or +applying a compatible braces patch cannot remove the finding. + +Use official Tailwind 4.3.3 and its same-version PostCSS plugin, plus +typescript-eslint 8.71.1 (which supports the existing ESLint 8.57.1). Keep the +existing theme/config palette, font, animation and shadow definitions. Use +tailwind-merge 3.7.0 for its [supported Tailwind 4 class semantics](https://github.com/dcastil/tailwind-merge/blob/main/README.md). +Apply compatible lockfile patches for +[brace-expansion](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr), +[source-map-js](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) and the published +undici advisories surfaced by npm audit. No vulnerable package is renamed or +replaced by an unreviewed fork to hide its audit identity. + +Follow the [official compiler upgrade contract](https://tailwindcss.com/docs/upgrade-guide): +the existing JS theme is loaded explicitly with `@config`, and compilation uses +`@tailwindcss/postcss`. Global compatibility declarations retain the existing +2px small radius, 4px backdrop blur, button cursor, placeholder defaults and +keyboard outlines. No React component or product flow is migrated. + +| Verification | Result | +| --- | --- | +| Linux Node 24.11.1 / npm 11.17.0 `npm run audit` | PASS; zero vulnerabilities at original production-moderate and all-dependencies-high thresholds | +| Windows Node 24.19.0 / npm 11.17.0 `npm.cmd run audit` | PASS; both original thresholds and shell-free Windows invocation pass | +| `npm run lint`, `npm run typecheck`, `npm test`, `npm run build` | PASS; 109 unit tests; pinned token bridge checks 334 contrast pairs; production build succeeds | +| Fresh `npm ci` followed by `npm run audit` and `npm run build` | PASS; checked lockfile reproduces the zero-finding audit and same production assets | +| `npm run test:i18n`, `test:theme`, `test:enrichment`, `test:memory`, `test:transfer`, `test:managed-embedding` | PASS; all original assertions retained | +| New theme compatibility browser probes | PASS; dark/light RGB and opacity, radius/blur, pointer and 2px solid keyboard outline also verified under forced colors | + +The token-source and generated-CSS validation copies were normalized to their +Git-equivalent LF bytes for Linux's pinned-byte check. Neither the upstream +token hash nor generated content was changed in the commit. The existing +production bundle-size warning remains visible. + +### Lifecycle artifact repair + +MinIO Community Edition's [official source-only distribution contract](https://github.com/minio/minio#source-only-distribution) +no longer provides the anonymously pullable historical artifacts used by this +test. Use the official +[MinIO source](https://github.com/minio/minio/tree/7aac2a2c5b7c882e68c1ce017d8256be2feea27f) +and [mc source](https://github.com/minio/mc/tree/77f82e18b5401a65958f1619df6ebb994634bd88), +pinning both commits and archive SHA-256 before extraction. Build with existing +Go using `-mod=readonly`, then place the binaries and their upstream licenses +into a fixed-digest official Alpine runtime image. A temporary Compose override +uses only the uniquely named test image, with `pull_policy: never`; the existing +test Compose graph and production deployment assets are unchanged. + +| Verification | Result | +| --- | --- | +| `bash -n scripts/acceptance_agent_memory.sh` | PASS | +| Official ShellCheck 0.11.0 on the final script | PASS | +| Full `scripts/acceptance_agent_memory.sh` with the CI-exact `GOTOOLCHAIN=go1.25.0` | PASS; official-source build, fresh migration 27, healthy MinIO, real HTTP/CLI/MCP and PostgreSQL forgetting/redaction assertions all complete | +| Ownership cleanup | PASS; run-specific containers, networks, volumes, image, directory and port lock removed; production containers unchanged | + +The local Windows-owned worktree used `GOFLAGS=-buildvcs=false` only while +executing its validation binary builds, because Linux Git cannot parse its +Windows `.git` pointer. The script's checksum-pinned upstream tarball builds +explicitly disable VCS stamping, rather than accidentally attributing those +sources to the enclosing checkout. The CI checkout, Go version, timeout and +all existing service and lifecycle assertions remain unchanged. + +Remote CI on the repaired head must still rerun. Local pass is not substituted +for required remote checks, CODEOWNERS approval or resolved review conversations. diff --git a/docs/schemas/vault-snapshot.v1.schema.json b/docs/schemas/vault-snapshot.v1.schema.json new file mode 100644 index 0000000..a66a423 --- /dev/null +++ b/docs/schemas/vault-snapshot.v1.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/bytefolk/mem/blob/main/docs/schemas/vault-snapshot.v1.schema.json", + "title": "Workspace Vault current-state snapshot v1", + "description": "Canonical GET snapshot and successful commit response. This transports current logical notes and tombstones, not auth state or retained history. UTF-8 byte bounds are additionally enforced by the server.", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "vaultId", "title", "revision", "updatedAt", "entries"], + "properties": { + "schemaVersion": { "const": "vault-snapshot.v1" }, + "vaultId": { "$ref": "#/$defs/id" }, + "title": { "type": "string", "maxLength": 256, "$comment": "At most 256 UTF-8 bytes; empty only before the first commit." }, + "revision": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "updatedAt": { "type": ["string", "null"], "format": "date-time" }, + "entries": { + "type": "array", "maxItems": 1000, + "items": { + "type": "object", "additionalProperties": false, + "required": ["noteId", "path", "content", "revision", "deleted", "properties"], + "properties": { + "noteId": { "$ref": "#/$defs/id" }, + "path": { "type": "string", "minLength": 1, "maxLength": 512, "$comment": "Canonical safe relative POSIX path, at most 512 UTF-8 bytes. Active paths are case-fold unique." }, + "content": { "type": "string", "maxLength": 1048576, "$comment": "At most 1 MiB UTF-8 bytes; only tab, CR and LF control characters are accepted." }, + "revision": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 }, + "deleted": { "type": "boolean" }, + "properties": { "type": "object", "$comment": "At most 16 KiB serialized JSON. Untrusted application data, never a permission grant." } + }, + "allOf": [{ "if": { "properties": { "deleted": { "const": true } } }, "then": { "properties": { "content": { "const": "" } } } }] + } + } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "not": { "const": "00000000-0000-0000-0000-000000000000" } } + } +} diff --git a/scripts/acceptance_agent_memory.sh b/scripts/acceptance_agent_memory.sh index f913556..99977cb 100755 --- a/scripts/acceptance_agent_memory.sh +++ b/scripts/acceptance_agent_memory.sh @@ -4,7 +4,9 @@ set -euo pipefail REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${REPO_ROOT}/docker-compose.test.yml" +COMPOSE_OVERRIDE="" COMPOSE_PROJECT="" +MINIO_IMAGE="" PG_PORT="${MEM_ACCEPTANCE_PG_PORT:-0}" S3_PORT="${MEM_ACCEPTANCE_S3_PORT:-0}" HTTP_PORT_REQUESTED="${MEM_ACCEPTANCE_HTTP_PORT:-}" @@ -33,11 +35,15 @@ require_command() { } compose() { + local -a compose_files=(-f "$COMPOSE_FILE") + if [[ -n "$COMPOSE_OVERRIDE" ]]; then + compose_files+=(-f "$COMPOSE_OVERRIDE") + fi MEM_TEST_PROJECT="$COMPOSE_PROJECT" \ MEM_TEST_PG_PORT="$PG_PORT" \ MEM_TEST_S3_PORT="$S3_PORT" \ MEM_TEST_DB_NAME="$DB_NAME" \ - docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" --profile e2e "$@" + docker compose -p "$COMPOSE_PROJECT" "${compose_files[@]}" --profile e2e "$@" } cleanup() { @@ -63,6 +69,14 @@ cleanup() { printf 'WARNING: failed to remove Compose project %s\n' "$COMPOSE_PROJECT" >&2 cleanup_status=1 fi + if [[ -n "$MINIO_IMAGE" ]] && + docker image inspect "$MINIO_IMAGE" >/dev/null 2>&1; then + if [[ "$MINIO_IMAGE" != "${COMPOSE_PROJECT}:minio" ]] || + ! docker image rm "$MINIO_IMAGE" >/dev/null 2>&1; then + printf 'WARNING: failed to remove acceptance MinIO image\n' >&2 + cleanup_status=1 + fi + fi if [[ -n "$E2E_DIR" && "$E2E_DIR" == "${E2E_ROOT}/acceptance."* ]]; then if ! rm -rf -- "$E2E_DIR"; then printf 'WARNING: failed to remove acceptance directory\n' >&2 @@ -92,6 +106,8 @@ require_command curl require_command jq require_command od require_command tr +require_command tar +require_command sha256sum curl_safe() { command curl --connect-timeout 2 --max-time 20 "$@" @@ -184,6 +200,64 @@ published_port() { printf '%s\n' "${address##*:}" } +build_test_minio() { + # MinIO CE is source-only; its former public minio and mc images no longer + # pull anonymously. Build the official sources, without third-party mirrors. + # https://github.com/minio/minio#source-only-distribution + local minio_revision="7aac2a2c5b7c882e68c1ce017d8256be2feea27f" + local mc_revision="77f82e18b5401a65958f1619df6ebb994634bd88" + local build_dir="${E2E_DIR}/minio-build" + mkdir -p "${build_dir}/minio" "${build_dir}/mc" "${build_dir}/licenses" + curl --fail --silent --show-error --location --retry 2 \ + --connect-timeout 5 --max-time 180 \ + "https://codeload.github.com/minio/minio/tar.gz/${minio_revision}" \ + --output "${build_dir}/minio.tar.gz" + curl --fail --silent --show-error --location --retry 2 \ + --connect-timeout 5 --max-time 180 \ + "https://codeload.github.com/minio/mc/tar.gz/${mc_revision}" \ + --output "${build_dir}/mc.tar.gz" + printf '%s %s\n' \ + '71794c2df26aad0cc99e8421c58b7aa2dd55969f979b0e7d1e931042e9fabcd6' \ + "${build_dir}/minio.tar.gz" \ + '167415edd21bc29f5360943dac64272aa5cda0a39f3070b15cfeca671c43d975' \ + "${build_dir}/mc.tar.gz" | sha256sum --check --status \ + || die "official MinIO source checksum mismatch" + tar -xzf "${build_dir}/minio.tar.gz" --strip-components=1 -C "${build_dir}/minio" + tar -xzf "${build_dir}/mc.tar.gz" --strip-components=1 -C "${build_dir}/mc" + ( + cd "${build_dir}/minio" + CGO_ENABLED=0 GOOS=linux go build -mod=readonly -trimpath -buildvcs=false \ + -o "${build_dir}/minio-bin" . + ) + ( + cd "${build_dir}/mc" + CGO_ENABLED=0 GOOS=linux go build -mod=readonly -trimpath -buildvcs=false \ + -o "${build_dir}/mc-bin" . + ) + cp "${build_dir}/minio/LICENSE" "${build_dir}/licenses/minio-LICENSE" + cp "${build_dir}/mc/LICENSE" "${build_dir}/licenses/mc-LICENSE" + cat >"${build_dir}/Dockerfile" <<'DOCKERFILE' +FROM alpine:3.22.6@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 +COPY minio-bin /usr/local/bin/minio +COPY mc-bin /usr/local/bin/mc +COPY licenses /usr/share/licenses/minio +ENTRYPOINT ["minio"] +DOCKERFILE + printf 'minio/\nmc/\n*.tar.gz\n' >"${build_dir}/.dockerignore" + MINIO_IMAGE="${COMPOSE_PROJECT}:minio" + docker build --tag "$MINIO_IMAGE" "$build_dir" + COMPOSE_OVERRIDE="${E2E_DIR}/minio-compose.yml" + cat >"$COMPOSE_OVERRIDE" <= 27 { + var tables int + if err := sqldb.QueryRowContext(ctx, `SELECT count(*) FROM pg_tables WHERE schemaname='public' + AND tablename IN ('vaults','vault_commits','vault_entry_revisions','vault_entry_heads')`).Scan(&tables); err != nil || tables != 4 { + t.Fatalf("Vault tables=%d, want=4, err=%v", tables, err) + } + } t.Logf("PASS: strict Goose upgrade to %d; complete history and populated data preserved", version) } // The real startup path must accept the upgraded history unchanged. diff --git a/server/internal/db/migrations/0027_vault_sync.sql b/server/internal/db/migrations/0027_vault_sync.sql new file mode 100644 index 0000000..c69ad2a --- /dev/null +++ b/server/internal/db/migrations/0027_vault_sync.sql @@ -0,0 +1,66 @@ +-- +goose Up +CREATE TABLE vaults ( + workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + id uuid NOT NULL, + title text NOT NULL CHECK (octet_length(title) BETWEEN 1 AND 256), + revision bigint NOT NULL DEFAULT 0 CHECK (revision BETWEEN 0 AND 9007199254740991), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (workspace_id, id) +); + +CREATE TABLE vault_commits ( + workspace_id uuid NOT NULL, + vault_id uuid NOT NULL, + revision bigint NOT NULL CHECK (revision BETWEEN 1 AND 9007199254740991), + title text NOT NULL, + actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (workspace_id, vault_id, revision), + FOREIGN KEY (workspace_id, vault_id) REFERENCES vaults(workspace_id, id) ON DELETE CASCADE +); + +CREATE TABLE vault_entry_revisions ( + workspace_id uuid NOT NULL, + vault_id uuid NOT NULL, + note_id uuid NOT NULL, + revision bigint NOT NULL, + path text NOT NULL CHECK (octet_length(path) BETWEEN 1 AND 512), + content text NOT NULL CHECK (octet_length(content) <= 1048576), + properties jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(properties) = 'object' AND octet_length(properties::text) <= 16384), + deleted boolean NOT NULL DEFAULT false, + CHECK (NOT deleted OR content = ''), + PRIMARY KEY (workspace_id, vault_id, note_id, revision), + FOREIGN KEY (workspace_id, vault_id, revision) REFERENCES vault_commits(workspace_id, vault_id, revision) ON DELETE CASCADE +); + +CREATE TABLE vault_entry_heads ( + workspace_id uuid NOT NULL, + vault_id uuid NOT NULL, + note_id uuid NOT NULL, + revision bigint NOT NULL, + path text NOT NULL, + deleted boolean NOT NULL, + PRIMARY KEY (workspace_id, vault_id, note_id), + FOREIGN KEY (workspace_id, vault_id, note_id, revision) + REFERENCES vault_entry_revisions(workspace_id, vault_id, note_id, revision) ON DELETE CASCADE +); +-- Blob identity is never note identity. Separate empty notes keep separate IDs. +CREATE UNIQUE INDEX vault_entry_active_path ON vault_entry_heads(workspace_id, vault_id, lower(path)) WHERE NOT deleted; + +-- Content revisions are append-only; head pointers are the mutable projection. +-- +goose StatementBegin +CREATE FUNCTION reject_vault_entry_revision_update() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'Vault entry revisions are immutable' USING ERRCODE = '23514'; +END; +$$; +-- +goose StatementEnd +CREATE TRIGGER vault_entry_revisions_immutable BEFORE UPDATE ON vault_entry_revisions + FOR EACH ROW EXECUTE FUNCTION reject_vault_entry_revision_update(); + +-- +goose Down +DROP TABLE vault_entry_heads; +DROP TABLE vault_entry_revisions; +DROP FUNCTION reject_vault_entry_revision_update(); +DROP TABLE vault_commits; +DROP TABLE vaults; diff --git a/server/internal/vaultsync/README.md b/server/internal/vaultsync/README.md new file mode 100644 index 0000000..5ebeee1 --- /dev/null +++ b/server/internal/vaultsync/README.md @@ -0,0 +1,6 @@ +# Workspace Vault sync + +The canonical wire contract, bounds and compatibility boundaries are documented +in [docs/VAULT_SYNC.md](../../../docs/VAULT_SYNC.md). + +The snapshot schema is [vault-snapshot.v1](../../../docs/schemas/vault-snapshot.v1.schema.json). diff --git a/server/internal/vaultsync/service.go b/server/internal/vaultsync/service.go new file mode 100644 index 0000000..402eb8c --- /dev/null +++ b/server/internal/vaultsync/service.go @@ -0,0 +1,470 @@ +// Package vaultsync stores workspace-scoped text Vaults with append-only +// revisions and an atomic Vault-wide compare-and-swap head. File/blob upload +// deduplication is deliberately outside this logical note identity model. +package vaultsync + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "path" + "sort" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/PeterGuy326/mem/server/internal/workspacelock" +) + +const ( + SchemaVersion = "vault-snapshot.v1" + MaxFileBytes = 1 << 20 + MaxPropertiesBytes = 16 << 10 + MaxCurrentBytes = 16 << 20 + MaxHistoryBytes = 64 << 20 + MaxEntries = 1000 + MaxMutationEntries = 512 + MaxVaults = 64 + MaxCommits = 10000 + MaxSafeRevision int64 = 9007199254740991 +) + +var ( + ErrInvalid = errors.New("invalid Vault request") + ErrLimit = errors.New("Vault limit exceeded") + ErrPathConflict = errors.New("Vault path conflict") + ErrUnavailable = errors.New("Vault sync is unavailable") +) + +type RevisionConflict struct{ CurrentRevision int64 } + +func (e *RevisionConflict) Error() string { return "Vault revision conflict" } + +type Mutation struct { + NoteID uuid.UUID `json:"noteId"` + Path string `json:"path"` + Content string `json:"content"` + Deleted bool `json:"deleted,omitempty"` + Properties json.RawMessage `json:"properties,omitempty"` +} + +type Entry struct { + NoteID uuid.UUID `json:"noteId"` + Path string `json:"path"` + Content string `json:"content"` + Revision int64 `json:"revision"` + Deleted bool `json:"deleted"` + Properties json.RawMessage `json:"properties"` +} + +type Snapshot struct { + SchemaVersion string `json:"schemaVersion"` + VaultID uuid.UUID `json:"vaultId"` + Title string `json:"title"` + Revision int64 `json:"revision"` + UpdatedAt *time.Time `json:"updatedAt"` + Entries []Entry `json:"entries"` +} + +type Summary struct { + VaultID uuid.UUID `json:"vaultId"` + Title string `json:"title"` + Revision int64 `json:"revision"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CommitCommand struct { + WorkspaceID uuid.UUID + ActorUserID uuid.UUID + VaultID uuid.UUID + BaseRevision int64 + Title *string + Entries []Mutation +} + +type Service struct{ pool *pgxpool.Pool } + +func New(pool *pgxpool.Pool) *Service { return &Service{pool: pool} } +func ScopePath(id uuid.UUID) string { return "/Vaults/" + id.String() } + +func ParseVaultID(raw string) (uuid.UUID, error) { + id, err := uuid.Parse(raw) + if err != nil || id == uuid.Nil || id.String() != raw { + return uuid.Nil, ErrInvalid + } + return id, nil +} + +func ValidatePath(value string) error { + if !utf8.ValidString(value) || len(value) == 0 || len(value) > 512 || path.IsAbs(value) || path.Clean(value) != value || + strings.ContainsAny(value, "\\:*?\"<>|") || strings.IndexFunc(value, unicode.IsControl) >= 0 { + return ErrInvalid + } + for _, part := range strings.Split(value, "/") { + if part == "." || part == ".." || strings.HasSuffix(part, ".") || strings.HasSuffix(part, " ") { + return ErrInvalid + } + } + return nil +} + +func normalizeProperties(raw json.RawMessage) (json.RawMessage, error) { + if len(raw) == 0 { + return nil, nil + } // Existing metadata is preserved when omitted. + if len(raw) > MaxPropertiesBytes || !utf8.Valid(raw) { + return nil, ErrLimit + } + var value map[string]any + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + if err := decoder.Decode(&value); err != nil || value == nil { + return nil, ErrInvalid + } + if containsZero(value) { + return nil, ErrInvalid + } + if err := decoder.Decode(new(any)); err != io.EOF { + return nil, ErrInvalid + } + encoded, err := json.Marshal(value) + if err != nil { + return nil, ErrInvalid + } + // PostgreSQL jsonb may add spaces between keys/values; this bound applies + // to its actual persisted representation as well as the compact wire value. + var indented bytes.Buffer + if err := json.Indent(&indented, encoded, "", ""); err != nil || indented.Len() > MaxPropertiesBytes { + return nil, ErrLimit + } + return encoded, nil +} + +func containsZero(value any) bool { + switch typed := value.(type) { + case string: + return strings.ContainsRune(typed, 0) + case map[string]any: + for key, item := range typed { + if strings.ContainsRune(key, 0) || containsZero(item) { + return true + } + } + case []any: + for _, item := range typed { + if containsZero(item) { + return true + } + } + } + return false +} + +func NormalizeCommand(cmd CommitCommand) (CommitCommand, error) { + if cmd.WorkspaceID == uuid.Nil || cmd.ActorUserID == uuid.Nil || cmd.VaultID == uuid.Nil || cmd.BaseRevision < 0 || + cmd.BaseRevision >= MaxSafeRevision || len(cmd.Entries) > MaxMutationEntries || (len(cmd.Entries) == 0 && cmd.Title == nil) { + return cmd, ErrInvalid + } + if cmd.Title != nil && (!utf8.ValidString(*cmd.Title) || len(*cmd.Title) == 0 || len(*cmd.Title) > 256 || + strings.TrimSpace(*cmd.Title) == "" || strings.IndexFunc(*cmd.Title, unicode.IsControl) >= 0) { + return cmd, ErrInvalid + } + seen := make(map[uuid.UUID]bool) + cmd.Entries = append([]Mutation(nil), cmd.Entries...) + for i, entry := range cmd.Entries { + if entry.NoteID == uuid.Nil || seen[entry.NoteID] || ValidatePath(entry.Path) != nil || !utf8.ValidString(entry.Content) || + strings.IndexFunc(entry.Content, func(r rune) bool { return unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' }) >= 0 || (entry.Deleted && entry.Content != "") { + return cmd, ErrInvalid + } + if len(entry.Content) > MaxFileBytes { + return cmd, ErrLimit + } + seen[entry.NoteID] = true + properties, err := normalizeProperties(entry.Properties) + if err != nil { + return cmd, err + } + cmd.Entries[i].Properties = properties + } + return cmd, nil +} + +// plan applies a delta without silently discarding notes, tombstones or a +// conflict. Properties are data and can never grant tools or membership. +func plan(current Snapshot, cmd CommitCommand) (Snapshot, error) { + if current.Revision != cmd.BaseRevision { + return Snapshot{}, &RevisionConflict{current.Revision} + } + entries := make(map[uuid.UUID]Entry, len(current.Entries)+len(cmd.Entries)) + for _, entry := range current.Entries { + entries[entry.NoteID] = entry + } + next := current + next.Revision++ + if cmd.Title != nil { + next.Title = *cmd.Title + } + if next.Title == "" { + next.Title = "Vault " + cmd.VaultID.String()[:8] + } + for _, mutation := range cmd.Entries { + properties := mutation.Properties + if len(properties) == 0 { + properties = entries[mutation.NoteID].Properties + } + if len(properties) == 0 { + properties = json.RawMessage(`{}`) + } + entries[mutation.NoteID] = Entry{NoteID: mutation.NoteID, Path: mutation.Path, Content: mutation.Content, + Deleted: mutation.Deleted, Properties: append(json.RawMessage(nil), properties...), Revision: next.Revision} + } + if len(entries) > MaxEntries { + return Snapshot{}, ErrLimit + } + paths := make(map[string]bool) + total := 0 + next.Entries = make([]Entry, 0, len(entries)) + for _, entry := range entries { + if !entry.Deleted { + key := strings.ToLower(entry.Path) + if paths[key] { + return Snapshot{}, ErrPathConflict + } + paths[key] = true + } + total += len(entry.Content) + len(entry.Properties) + len(entry.Path) + if total > MaxCurrentBytes { + return Snapshot{}, ErrLimit + } + next.Entries = append(next.Entries, entry) + } + sort.Slice(next.Entries, func(i, j int) bool { + if next.Entries[i].Path == next.Entries[j].Path { + return next.Entries[i].NoteID.String() < next.Entries[j].NoteID.String() + } + return next.Entries[i].Path < next.Entries[j].Path + }) + return next, nil +} + +type querier interface { + Query(context.Context, string, ...any) (pgx.Rows, error) + QueryRow(context.Context, string, ...any) pgx.Row +} + +func readSnapshot(ctx context.Context, q querier, workspaceID, vaultID uuid.UUID) (Snapshot, error) { + snapshot := Snapshot{SchemaVersion: SchemaVersion, VaultID: vaultID, Entries: []Entry{}} + var updated time.Time + err := q.QueryRow(ctx, `SELECT title, revision, updated_at FROM vaults WHERE workspace_id=$1 AND id=$2`, workspaceID, vaultID). + Scan(&snapshot.Title, &snapshot.Revision, &updated) + if errors.Is(err, pgx.ErrNoRows) { + return snapshot, nil + } + if err != nil { + return Snapshot{}, err + } + snapshot.UpdatedAt = &updated + rows, err := q.Query(ctx, `SELECT r.note_id, r.path, r.content, r.revision, r.deleted, r.properties + FROM vault_entry_heads h JOIN vault_entry_revisions r USING(workspace_id,vault_id,note_id,revision) + WHERE h.workspace_id=$1 AND h.vault_id=$2 ORDER BY r.path COLLATE "C", r.note_id LIMIT $3`, workspaceID, vaultID, MaxEntries+1) + if err != nil { + return Snapshot{}, err + } + defer rows.Close() + for rows.Next() { + var entry Entry + if err := rows.Scan(&entry.NoteID, &entry.Path, &entry.Content, &entry.Revision, &entry.Deleted, &entry.Properties); err != nil { + return Snapshot{}, err + } + snapshot.Entries = append(snapshot.Entries, entry) + } + if len(snapshot.Entries) > MaxEntries { + return Snapshot{}, ErrLimit + } + return snapshot, rows.Err() +} + +func (s *Service) Snapshot(ctx context.Context, workspaceID, vaultID uuid.UUID) (Snapshot, error) { + if s == nil || s.pool == nil { + return Snapshot{}, ErrUnavailable + } + if workspaceID == uuid.Nil || vaultID == uuid.Nil { + return Snapshot{}, ErrInvalid + } + tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly}) + if err != nil { + return Snapshot{}, err + } + defer func() { _ = tx.Rollback(ctx) }() + snapshot, err := readSnapshot(ctx, tx, workspaceID, vaultID) + if err != nil { + return Snapshot{}, err + } + if err := tx.Commit(ctx); err != nil { + return Snapshot{}, err + } + return snapshot, nil +} + +func (s *Service) List(ctx context.Context, workspaceID uuid.UUID) ([]Summary, error) { + if s == nil || s.pool == nil { + return nil, ErrUnavailable + } + if workspaceID == uuid.Nil { + return nil, ErrInvalid + } + rows, err := s.pool.Query(ctx, `SELECT id,title,revision,updated_at FROM vaults WHERE workspace_id=$1 ORDER BY updated_at DESC,id LIMIT $2`, workspaceID, MaxVaults+1) + if err != nil { + return nil, err + } + defer rows.Close() + result := []Summary{} + for rows.Next() { + var item Summary + if err := rows.Scan(&item.VaultID, &item.Title, &item.Revision, &item.UpdatedAt); err != nil { + return nil, err + } + result = append(result, item) + } + if len(result) > MaxVaults { + return nil, ErrLimit + } + return result, rows.Err() +} + +func (s *Service) Commit(ctx context.Context, raw CommitCommand) (Snapshot, error) { + if s == nil || s.pool == nil { + return Snapshot{}, ErrUnavailable + } + cmd, err := NormalizeCommand(raw) + if err != nil { + return Snapshot{}, err + } + tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return Snapshot{}, err + } + defer func() { _ = tx.Rollback(ctx) }() + // Take the existing stronger workspace coordination lock first. Ordinary + // file writers retain KEY SHARE compatibility, while Vault commits and + // creation quotas serialize across distinct Vault IDs in this workspace. + if _, err := workspacelock.ForAIProfileCoordination(ctx, tx, cmd.WorkspaceID); err != nil { + return Snapshot{}, err + } + var count int + if err := tx.QueryRow(ctx, `SELECT count(*) FROM vaults WHERE workspace_id=$1`, cmd.WorkspaceID).Scan(&count); err != nil { + return Snapshot{}, err + } + var exists bool + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM vaults WHERE workspace_id=$1 AND id=$2)`, cmd.WorkspaceID, cmd.VaultID).Scan(&exists); err != nil { + return Snapshot{}, err + } + if !exists && count >= MaxVaults { + return Snapshot{}, ErrLimit + } + defaultTitle := "Vault " + cmd.VaultID.String()[:8] + if _, err := tx.Exec(ctx, `INSERT INTO vaults(workspace_id,id,title) VALUES($1,$2,$3) ON CONFLICT DO NOTHING`, cmd.WorkspaceID, cmd.VaultID, defaultTitle); err != nil { + return Snapshot{}, err + } + var revision int64 + if err := tx.QueryRow(ctx, `SELECT revision FROM vaults WHERE workspace_id=$1 AND id=$2 FOR UPDATE`, cmd.WorkspaceID, cmd.VaultID).Scan(&revision); err != nil { + return Snapshot{}, err + } + if revision != cmd.BaseRevision { + return Snapshot{}, &RevisionConflict{revision} + } + current, err := readSnapshot(ctx, tx, cmd.WorkspaceID, cmd.VaultID) + if err != nil { + return Snapshot{}, err + } + next, err := plan(current, cmd) + if err != nil { + return Snapshot{}, err + } + var historyBytes int64 + var commits int + if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(content)+octet_length(properties::text)+octet_length(path)),0) + FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&historyBytes); err != nil { + return Snapshot{}, err + } + if err := tx.QueryRow(ctx, `SELECT count(*) FROM vault_commits WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&commits); err != nil { + return Snapshot{}, err + } + changed := make(map[uuid.UUID]Entry) + for _, entry := range next.Entries { + if entry.Revision == next.Revision { + changed[entry.NoteID] = entry + historyBytes += int64(len(entry.Content) + len(entry.Path) + len(entry.Properties)) + } + } + if historyBytes > MaxHistoryBytes || commits >= MaxCommits { + return Snapshot{}, ErrLimit + } + updated := time.Now().UTC() + if _, err := tx.Exec(ctx, `INSERT INTO vault_commits(workspace_id,vault_id,revision,title,actor_user_id,created_at) VALUES($1,$2,$3,$4,$5,$6)`, + cmd.WorkspaceID, cmd.VaultID, next.Revision, next.Title, cmd.ActorUserID, updated); err != nil { + return Snapshot{}, err + } + ids := make([]uuid.UUID, 0, len(changed)) + for id := range changed { + ids = append(ids, id) + } + // Delete only mutable pointers inside the atomic transaction. This permits + // swaps/renames without a transient uniqueness conflict; history remains. + if _, err := tx.Exec(ctx, `DELETE FROM vault_entry_heads WHERE workspace_id=$1 AND vault_id=$2 AND note_id=ANY($3::uuid[])`, cmd.WorkspaceID, cmd.VaultID, ids); err != nil { + return Snapshot{}, err + } + for _, entry := range changed { + if _, err := tx.Exec(ctx, `INSERT INTO vault_entry_revisions(workspace_id,vault_id,note_id,revision,path,content,deleted,properties) + VALUES($1,$2,$3,$4,$5,$6,$7,$8::jsonb)`, cmd.WorkspaceID, cmd.VaultID, entry.NoteID, entry.Revision, entry.Path, entry.Content, entry.Deleted, string(entry.Properties)); err != nil { + return Snapshot{}, classifyWriteError(err) + } + if _, err := tx.Exec(ctx, `INSERT INTO vault_entry_heads(workspace_id,vault_id,note_id,revision,path,deleted) VALUES($1,$2,$3,$4,$5,$6)`, + cmd.WorkspaceID, cmd.VaultID, entry.NoteID, entry.Revision, entry.Path, entry.Deleted); err != nil { + return Snapshot{}, classifyWriteError(err) + } + } + // Enforce quotas against PostgreSQL's actual jsonb representation, including + // its spacing, rather than estimating persisted bytes from compact JSON. + var persistedCurrent, persistedHistory int64 + if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(r.content)+octet_length(r.properties::text)+octet_length(r.path)),0) + FROM vault_entry_heads h JOIN vault_entry_revisions r USING(workspace_id,vault_id,note_id,revision) + WHERE h.workspace_id=$1 AND h.vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&persistedCurrent); err != nil { + return Snapshot{}, err + } + if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(content)+octet_length(properties::text)+octet_length(path)),0) + FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&persistedHistory); err != nil { + return Snapshot{}, err + } + if persistedCurrent > MaxCurrentBytes || persistedHistory > MaxHistoryBytes { + return Snapshot{}, ErrLimit + } + if _, err := tx.Exec(ctx, `UPDATE vaults SET title=$3,revision=$4,updated_at=$5 WHERE workspace_id=$1 AND id=$2`, cmd.WorkspaceID, cmd.VaultID, next.Title, next.Revision, updated); err != nil { + return Snapshot{}, err + } + if err := tx.Commit(ctx); err != nil { + return Snapshot{}, err + } + next.UpdatedAt = &updated + return next, nil +} + +func classifyWriteError(err error) error { + var pgError *pgconn.PgError + if errors.As(err, &pgError) && pgError.Code == "23505" { + return ErrPathConflict + } + if errors.As(err, &pgError) && pgError.Code == "23514" { + return ErrLimit + } + return fmt.Errorf("Vault write failed: %w", err) +} diff --git a/server/internal/vaultsync/service_integration_test.go b/server/internal/vaultsync/service_integration_test.go new file mode 100644 index 0000000..c56086a --- /dev/null +++ b/server/internal/vaultsync/service_integration_test.go @@ -0,0 +1,235 @@ +package vaultsync_test + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/pressly/goose/v3" + + "github.com/PeterGuy326/mem/server/internal/api" + "github.com/PeterGuy326/mem/server/internal/auth" + memdb "github.com/PeterGuy326/mem/server/internal/db" + "github.com/PeterGuy326/mem/server/internal/vaultsync" + "github.com/PeterGuy326/mem/server/internal/workspace" +) + +// Uses only an explicitly selected isolated _test database. No production +// migration, credential recovery, or container restart occurs in this test. +func TestVaultSyncPostgres(t *testing.T) { + dsn := os.Getenv("MEM_TEST_DB") + if dsn == "" { + t.Skip("MEM_TEST_DB not set; real PostgreSQL Vault verification was not executed") + } + config, err := pgxpool.ParseConfig(dsn) + if err != nil { + t.Fatal("invalid isolated test database configuration") + } + if !strings.HasSuffix(config.ConnConfig.Database, "_test") { + t.Fatal("refusing to modify a database whose name does not end in _test") + } + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + database, err := memdb.Open(ctx, dsn) + if err != nil { + t.Fatal("could not connect to isolated test database") + } + t.Cleanup(database.Close) + if err := database.Migrate(ctx); err != nil { + t.Fatalf("isolated migrations: %v", err) + } + createTenant := func() (uuid.UUID, uuid.UUID) { + user, ws := uuid.New(), uuid.New() + if _, err := database.Pool.Exec(ctx, `INSERT INTO users(id,email,password_hash) VALUES($1,$2,'test-hash')`, user, user.String()+"@vault.test"); err != nil { + t.Fatal(err) + } + if _, err := database.Pool.Exec(ctx, `INSERT INTO workspaces(id,name,resource_owner_user_id) VALUES($1,'Vault test',$2)`, ws, user); err != nil { + t.Fatal(err) + } + if _, err := database.Pool.Exec(ctx, `INSERT INTO workspace_memberships(workspace_id,user_id,role) VALUES($1,$2,'owner')`, ws, user); err != nil { + t.Fatal(err) + } + return user, ws + } + userA, wsA := createTenant() + userB, wsB := createTenant() + t.Cleanup(func() { + cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, _ = database.Pool.Exec(cleanupCtx, `DELETE FROM users WHERE id=$1 OR id=$2`, userA, userB) + }) + service := vaultsync.New(database.Pool) + vault, noteA, noteB := uuid.New(), uuid.New(), uuid.New() + command := vaultsync.CommitCommand{WorkspaceID: wsA, ActorUserID: userA, VaultID: vault, Entries: []vaultsync.Mutation{ + {NoteID: noteA, Path: "岗位/第一份 空笔记.md", Content: ""}, {NoteID: noteB, Path: "共享/第二份 空笔记.md", Content: ""}}} + empty, err := service.Snapshot(ctx, wsA, vault) + if err != nil || empty.Revision != 0 || len(empty.Entries) != 0 { + t.Fatal("new Vault snapshot", err) + } + first, err := service.Commit(ctx, command) + if err != nil || first.Revision != 1 || len(first.Entries) != 2 { + t.Fatalf("two independent empty notes: %+v %v", first, err) + } + // Two clients read the same head, then race a different edit. Exactly one + // succeeds; the other gets a conflict without overwriting the winner. + var wg sync.WaitGroup + var lock sync.Mutex + successes, conflicts := 0, 0 + for _, text := range []string{"客户端 A 内容", "客户端 B 内容"} { + wg.Add(1) + go func(text string) { + defer wg.Done() + cmd := command + cmd.BaseRevision = 1 + cmd.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: command.Entries[0].Path, Content: text}} + _, err := service.Commit(ctx, cmd) + lock.Lock() + defer lock.Unlock() + var conflict *vaultsync.RevisionConflict + if err == nil { + successes++ + } else if errors.As(err, &conflict) && conflict.CurrentRevision == 2 { + conflicts++ + } else { + t.Errorf("unexpected concurrent result: %v", err) + } + }(text) + } + wg.Wait() + if successes != 1 || conflicts != 1 { + t.Fatalf("CAS race successes=%d conflicts=%d", successes, conflicts) + } + second, err := service.Snapshot(ctx, wsA, vault) + if err != nil || second.Revision != 2 || len(second.Entries) != 2 { + t.Fatal("consistent snapshot after race", err) + } + other, err := service.Snapshot(ctx, wsB, vault) + if err != nil || other.Revision != 0 || len(other.Entries) != 0 { + t.Fatal("another workspace saw the Vault", err) + } + // Old text revisions remain unchanged and cannot be updated in place. + var original string + if err := database.Pool.QueryRow(ctx, `SELECT content FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2 AND note_id=$3 AND revision=1`, wsA, vault, noteA).Scan(&original); err != nil || original != "" { + t.Fatal("original revision changed", err) + } + if _, err := database.Pool.Exec(ctx, `UPDATE vault_entry_revisions SET content='rewrite' WHERE workspace_id=$1 AND vault_id=$2 AND note_id=$3 AND revision=1`, wsA, vault, noteA); err == nil { + t.Fatal("immutable revision accepted UPDATE") + } + command.BaseRevision = 2 + command.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: "岗位/第一份 空笔记.md", Deleted: true}} + third, err := service.Commit(ctx, command) + if err != nil || third.Revision != 3 || len(third.Entries) != 2 { + t.Fatal("delete tombstone commit", err) + } + for _, entry := range third.Entries { + if entry.NoteID == noteA && (!entry.Deleted || entry.Content != "") { + t.Fatal("missing tombstone") + } + } + // Test swap/rename under the actual unique index, then title-only commit. + command.BaseRevision = 3 + command.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: "共享/第二份 空笔记.md", Content: "恢复"}, {NoteID: noteB, Path: "岗位/第一份 空笔记.md", Content: "交换"}} + fourth, err := service.Commit(ctx, command) + if err != nil || fourth.Revision != 4 { + t.Fatal("atomic path swap", err) + } + title := "组织知识库" + command.BaseRevision = 4 + command.Entries = nil + command.Title = &title + fifth, err := service.Commit(ctx, command) + if err != nil || fifth.Revision != 5 || fifth.Title != title { + t.Fatal("title CAS", err) + } + listed, err := service.List(ctx, wsA) + if err != nil || len(listed) != 1 || listed[0].VaultID != vault { + t.Fatal("Vault discovery", err) + } + // Real HTTP + canonical auth + workspace middleware use sandbox-only tokens. + authService := auth.New(database.Pool) + ttl := time.Now().Add(time.Minute) + writer, _, err := authService.CreateToken(ctx, userA, &wsA, "vault-test-writer", []string{auth.ScopeRead, auth.ScopeWrite}, nil, &ttl, false) + if err != nil { + t.Fatal(err) + } + reader, _, err := authService.CreateToken(ctx, userA, &wsA, "vault-test-reader", []string{auth.ScopeRead}, nil, &ttl, false) + if err != nil { + t.Fatal(err) + } + foreign, _, err := authService.CreateToken(ctx, userB, &wsB, "vault-test-foreign", []string{auth.ScopeRead}, nil, &ttl, false) + if err != nil { + t.Fatal(err) + } + server := httptest.NewServer((&api.Server{Auth: authService, Workspace: workspace.New(database.Pool), VaultSync: service, Log: slog.New(slog.NewTextHandler(io.Discard, nil))}).Router()) + defer server.Close() + request := func(method, route, token string, body any) (int, []byte) { + t.Helper() + var encoded []byte + if body != nil { + encoded, err = json.Marshal(body) + if err != nil { + t.Fatal(err) + } + } + req, err := http.NewRequestWithContext(ctx, method, server.URL+route, bytes.NewReader(encoded)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + response, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + payload, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + return response.StatusCode, payload + } + status, body := request("GET", "/v1/vault/snapshot?vaultId="+vault.String(), reader, nil) + if status != 200 || !bytes.Contains(body, []byte("组织知识库")) { + t.Fatal("authenticated snapshot HTTP", status) + } + status, _ = request("POST", "/v1/vault/commit", reader, map[string]any{"vaultId": vault, "baseRevision": 5, "title": "拒绝只读写入", "entries": []any{}}) + if status != 403 { + t.Fatal("read-only credential could commit", status) + } + status, body = request("GET", "/v1/vault/snapshot?vaultId="+vault.String(), foreign, nil) + if status != 200 || !bytes.Contains(body, []byte(`"revision":0`)) || bytes.Contains(body, []byte("组织知识库")) { + t.Fatal("HTTP cross-workspace isolation", status) + } + status, body = request("POST", "/v1/vault/commit", writer, map[string]any{"vaultId": vault, "baseRevision": 1, "title": "stale", "entries": []any{}}) + if status != 409 || !bytes.Contains(body, []byte(`"currentRevision":5`)) { + t.Fatalf("HTTP CAS conflict: %d %s", status, body) + } + // The additive migration is reversible on a disposable test deployment; + // dropping Vault tables does not change existing files or auth tables. + server.Close() + sqlDB, err := sql.Open("pgx", dsn) + if err != nil { + t.Fatal("could not open isolated migration rehearsal") + } + defer sqlDB.Close() + goose.SetBaseFS(os.DirFS("../db")) + if err := goose.DownToContext(ctx, sqlDB, "migrations", 26); err != nil { + t.Fatalf("isolated Vault down migration: %v", err) + } + if err := database.Migrate(ctx); err != nil { + t.Fatalf("isolated Vault up migration: %v", err) + } +} diff --git a/server/internal/vaultsync/service_test.go b/server/internal/vaultsync/service_test.go new file mode 100644 index 0000000..cd81ccb --- /dev/null +++ b/server/internal/vaultsync/service_test.go @@ -0,0 +1,150 @@ +package vaultsync + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/google/uuid" +) + +func validCommand() CommitCommand { + return CommitCommand{WorkspaceID: uuid.New(), ActorUserID: uuid.New(), VaultID: uuid.New(), + Entries: []Mutation{{NoteID: uuid.New(), Path: "知识/空白笔记.md", Content: ""}}} +} + +func TestLogicalIdentityEmptyNotesAndCAS(t *testing.T) { + cmd := validCommand() + cmd.Entries = append(cmd.Entries, Mutation{NoteID: uuid.New(), Path: "知识/另一个空笔记.md", Content: ""}) + empty := Snapshot{SchemaVersion: SchemaVersion, VaultID: cmd.VaultID, Entries: []Entry{}} + first, err := plan(empty, cmd) + if err != nil || first.Revision != 1 || len(first.Entries) != 2 || first.Entries[0].NoteID == first.Entries[1].NoteID { + t.Fatalf("separate empty note identities: %+v, %v", first, err) + } + _, err = plan(first, cmd) + var conflict *RevisionConflict + if !errors.As(err, &conflict) || conflict.CurrentRevision != 1 { + t.Fatalf("stale client must conflict: %v", err) + } + if len(first.Entries) != 2 || first.Entries[0].Content != "" { + t.Fatal("conflict modified original snapshot") + } +} + +func TestDeltaPreservesUntouchedNotesMetadataAndTombstones(t *testing.T) { + cmd := validCommand() + cmd.Entries[0].Properties = json.RawMessage(`{"positionIds":["writer"],"source":"user"}`) + first, _ := plan(Snapshot{VaultID: cmd.VaultID}, cmd) + noteID := cmd.Entries[0].NoteID + cmd.BaseRevision = 1 + cmd.Entries = []Mutation{{NoteID: noteID, Path: "知识/改名.md", Content: "第二版"}, + {NoteID: uuid.New(), Path: "共享 SOP.md", Content: "流程"}} + second, err := plan(first, cmd) + if err != nil || len(second.Entries) != 2 { + t.Fatal(err) + } + for _, entry := range second.Entries { + if entry.NoteID == noteID && !strings.Contains(string(entry.Properties), "writer") { + t.Fatal("omitted properties erased metadata") + } + } + cmd.BaseRevision = 2 + cmd.Entries = []Mutation{{NoteID: noteID, Path: "知识/改名.md", Deleted: true}} + third, err := plan(second, cmd) + if err != nil || len(third.Entries) != 2 { + t.Fatal("deletion must retain tombstone and unrelated note", err) + } + for _, entry := range third.Entries { + if entry.NoteID == noteID && (!entry.Deleted || entry.Content != "" || entry.Revision != 3) { + t.Fatal("invalid tombstone") + } + } + if first.Entries[0].Path != "知识/空白笔记.md" || first.Revision != 1 { + t.Fatal("later revisions mutated previous snapshot") + } +} + +func TestPathsDuplicatesAndUntrustedPropertiesAreValidated(t *testing.T) { + for _, name := range []string{"../secret.md", "/absolute.md", "a\\b.md", "a/../b.md", "a//b.md", "a\x00.md", "C:notes.md", "dir/a ", "dir/a.", "a?.md"} { + if ValidatePath(name) == nil { + t.Fatalf("unsafe path accepted: %q", name) + } + } + if err := ValidatePath("客户 ACME/项目 決策.md"); err != nil { + t.Fatal(err) + } + for _, properties := range []string{`null`, `[]`, `{"source":"\u0000"}`, `{"source":true} trailing`} { + cmd := validCommand() + cmd.Entries[0].Properties = json.RawMessage(properties) + if _, err := NormalizeCommand(cmd); err == nil { + t.Fatalf("invalid metadata accepted: %s", properties) + } + } + cmd := validCommand() + cmd.Entries[0].Properties = json.RawMessage(`{"tools":["write"],"authority":"admin"}`) + normalized, err := NormalizeCommand(cmd) + if err != nil || normalized.ActorUserID != cmd.ActorUserID || normalized.WorkspaceID != cmd.WorkspaceID { + t.Fatal("metadata changed authorization", err) + } + cmd.Entries = append(cmd.Entries, cmd.Entries[0]) + if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrInvalid) { + t.Fatal("duplicate note ID accepted") + } + cmd = validCommand() + cmd.Entries[0].Content = "secret" + cmd.Entries[0].Deleted = true + if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrInvalid) { + t.Fatal("tombstone retained content") + } +} + +func TestActivePathsConflictCaseInsensitivelyAndSwapsAreValid(t *testing.T) { + cmd := validCommand() + cmd.Entries = []Mutation{{NoteID: uuid.New(), Path: "A.md"}, {NoteID: uuid.New(), Path: "a.md"}} + if _, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd); !errors.Is(err, ErrPathConflict) { + t.Fatal("Windows path collision accepted") + } + cmd.Entries[1].Path = "B.md" + first, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd) + if err != nil { + t.Fatal(err) + } + cmd.BaseRevision = 1 + cmd.Entries[0].Path = "B.md" + cmd.Entries[1].Path = "A.md" + if _, err := plan(first, cmd); err != nil { + t.Fatal("atomic path swap rejected", err) + } +} + +func TestBoundsAndTitleOnlyCAS(t *testing.T) { + cmd := validCommand() + cmd.Entries[0].Content = strings.Repeat("x", MaxFileBytes+1) + if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrLimit) { + t.Fatal("oversize note accepted") + } + cmd = validCommand() + cmd.Entries = nil + if _, err := NormalizeCommand(cmd); err == nil { + t.Fatal("empty mutation accepted") + } + title := "岗位知识库" + cmd.Title = &title + cmd, err := NormalizeCommand(cmd) + if err != nil { + t.Fatal(err) + } + result, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd) + if err != nil || result.Title != title || result.Revision != 1 || len(result.Entries) != 0 { + t.Fatal("title-only commit failed", err) + } + large := Snapshot{VaultID: cmd.VaultID} + for i := 0; i < MaxEntries; i++ { + large.Entries = append(large.Entries, Entry{NoteID: uuid.New(), Path: "old.md", Deleted: true, Properties: json.RawMessage(`{}`)}) + } + cmd.Entries = []Mutation{{NoteID: uuid.New(), Path: "new.md"}} + if _, err := plan(large, cmd); !errors.Is(err, ErrLimit) { + t.Fatal("tombstones were excluded from note-count bound") + } +} diff --git a/web/package-lock.json b/web/package-lock.json index ce35cc0..5eec4fe 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -18,16 +18,17 @@ "react-dom": "^19.2.8", "react-router": "^8.3.0", "sonner": "^1.7.0", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^3.7.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.71.1", + "@typescript-eslint/parser": "^8.71.1", "@vitejs/plugin-react": "^6.0.4", "autoprefixer": "^10.4.20", "eslint": "^8.57.1", @@ -38,7 +39,7 @@ "playwright": "^1.60.0", "postcss": "^8.5.23", "prettier": "^3.3.3", - "tailwindcss": "^3.4.14", + "tailwindcss": "^4.3.3", "typescript": "^5.6.3", "vite": "^8.1.5", "vitest": "^4.1.10" @@ -312,7 +313,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" @@ -325,7 +325,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -337,7 +336,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -396,9 +394,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -502,9 +500,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -645,6 +643,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1724,159 +1733,691 @@ "dev": true, "license": "MIT" }, - "node_modules/@tanstack/query-core": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", - "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" } }, - "node_modules/@tanstack/react-query": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", - "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", - "license": "MIT", + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@tanstack/query-core": "5.100.10" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "peerDependencies": { - "react": "^18 || ^19" + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" + "node": ">= 12.0.0" }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "node": ">= 12.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", - "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12", - "npm": ">=6" + "node": ">= 12.0.0" }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "peer": true + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", + "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, @@ -1935,122 +2476,159 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz", + "integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/type-utils": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.71.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz", + "integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz", + "integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.71.1", + "@typescript-eslint/types": "^8.71.1", + "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz", + "integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz", + "integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz", + "integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1", + "@typescript-eslint/utils": "8.71.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz", + "integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2058,75 +2636,88 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz", + "integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/project-service": "8.71.1", + "@typescript-eslint/tsconfig-utils": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/visitor-keys": "8.71.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz", + "integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.1", + "@typescript-eslint/types": "8.71.1", + "@typescript-eslint/typescript-estree": "8.71.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.71.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz", + "integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.71.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", @@ -2339,34 +2930,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2396,16 +2959,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2483,40 +3036,27 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { @@ -2563,16 +3103,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", @@ -2621,44 +3151,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2713,16 +3205,6 @@ "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2793,19 +3275,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2885,33 +3354,6 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2947,6 +3389,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -2960,16 +3416,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", @@ -3111,9 +3557,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3225,36 +3671,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3319,19 +3735,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3407,16 +3810,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3472,9 +3865,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3511,26 +3904,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", @@ -3559,19 +3938,6 @@ "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/headers-polyfill": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", @@ -3662,35 +4028,6 @@ "dev": true, "license": "ISC" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3731,16 +4068,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -3766,13 +4093,13 @@ "license": "ISC" }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -4153,26 +4480,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4233,40 +4540,16 @@ "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4278,16 +4561,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4371,18 +4654,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -4419,36 +4690,6 @@ "node": ">=18" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -4586,13 +4827,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -4600,16 +4834,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4624,39 +4848,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/playwright": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", @@ -4733,133 +4924,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -5073,29 +5137,6 @@ } } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -5130,28 +5171,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5275,9 +5294,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -5337,16 +5356,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -5358,9 +5367,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5452,29 +5461,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5488,19 +5474,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5522,9 +5495,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", - "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", "license": "MIT", "funding": { "type": "github", @@ -5532,41 +5505,24 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, "engines": { - "node": ">=14.0.0" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/text-table": { @@ -5576,29 +5532,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5694,19 +5627,6 @@ "dev": true, "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -5734,25 +5654,18 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "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": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5800,9 +5713,9 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "dev": true, "license": "MIT", "engines": { @@ -5910,13 +5823,6 @@ } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", diff --git a/web/package.json b/web/package.json index 2988971..177f4d8 100644 --- a/web/package.json +++ b/web/package.json @@ -37,16 +37,17 @@ "react-dom": "^19.2.8", "react-router": "^8.3.0", "sonner": "^1.7.0", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^3.7.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.71.1", + "@typescript-eslint/parser": "^8.71.1", "@vitejs/plugin-react": "^6.0.4", "autoprefixer": "^10.4.20", "eslint": "^8.57.1", @@ -57,7 +58,7 @@ "playwright": "^1.60.0", "postcss": "^8.5.23", "prettier": "^3.3.3", - "tailwindcss": "^3.4.14", + "tailwindcss": "^4.3.3", "typescript": "^5.6.3", "vite": "^8.1.5", "vitest": "^4.1.10" diff --git a/web/postcss.config.js b/web/postcss.config.js index 2aa7205..51a6e4e 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, autoprefixer: {}, }, }; diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 41ef4dd..3e10e37 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -1,8 +1,14 @@ @import './design-tokens.generated.css'; +@import 'tailwindcss'; -@tailwind base; -@tailwind components; -@tailwind utilities; +@config '../../tailwind.config.js'; + +/* Preserve the existing v3 utility scale while removing its unpatched + * dependency chain. The product palette remains in the pinned JS config. */ +@theme { + --radius-sm: 0.125rem; + --blur-sm: 4px; +} @layer base { html, @@ -11,6 +17,16 @@ height: 100%; } + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } + + input::placeholder, + textarea::placeholder { + color: rgb(156 163 175); + } + body { @apply bg-bg text-fg font-sans antialiased; font-size: 14px; @@ -48,7 +64,7 @@ /* Focus ring — keyboard only, geometric not glow-y */ :focus-visible { - @apply outline-none ring-2 ring-accent/60 ring-offset-2 ring-offset-bg; + @apply outline-hidden ring-2 ring-accent/60 ring-offset-2 ring-offset-bg; } /* Heading defaults */ @@ -108,3 +124,13 @@ background: linear-gradient(90deg, transparent, rgb(var(--border-strong) / 0.25), transparent); animation: shimmer 1.6s infinite; } + +/* Keep existing class consumers' keyboard outline visible in forced colors. + * These declarations follow the v4 utility layer's outline-style: none. */ +@layer utilities { + .outline-none, + .focus-visible\:outline-none:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + } +} diff --git a/web/theme-acceptance.mjs b/web/theme-acceptance.mjs index a347a50..e2f0bfe 100644 --- a/web/theme-acceptance.mjs +++ b/web/theme-acceptance.mjs @@ -116,6 +116,38 @@ async function assertTheme(page, theme) { assert.equal(await page.locator('[data-toast-theme]').getAttribute('data-toast-theme'), theme); } +async function assertUtilityCompatibility(page) { + await page.keyboard.press('Tab'); + const values = await page.evaluate(() => { + const probe = document.createElement('button'); + probe.className = 'rounded-sm backdrop-blur-sm text-fg bg-accent/10 outline-none'; + const focusProbe = document.createElement('button'); + focusProbe.className = 'focus-visible:outline-none'; + const reference = document.createElement('span'); + reference.style.color = 'rgb(var(--fg) / calc(var(--fg-opacity, 1) * 1))'; + reference.style.backgroundColor = 'rgb(var(--accent) / calc(var(--accent-opacity, 1) * 0.1))'; + document.body.append(probe, focusProbe, reference); focusProbe.focus(); + const rgba = (value) => { + const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; + const context = canvas.getContext('2d'); context.fillStyle = value; context.fillRect(0, 0, 1, 1); + return [...context.getImageData(0, 0, 1, 1).data]; + }; + const style = getComputedStyle(probe); const focused = getComputedStyle(focusProbe); const expected = getComputedStyle(reference); + const result = { radius: style.borderRadius, blur: style.backdropFilter, cursor: style.cursor, + outlineStyle: style.outlineStyle, outlineWidth: style.outlineWidth, + focusVisible: focusProbe.matches(':focus-visible'), focusOutlineStyle: focused.outlineStyle, focusOutlineWidth: focused.outlineWidth, + foreground: rgba(style.color), expectedForeground: rgba(expected.color), + accent: rgba(style.backgroundColor), expectedAccent: rgba(expected.backgroundColor) }; + probe.remove(); focusProbe.remove(); reference.remove(); return result; + }); + assert.equal(values.radius, '2px'); assert.equal(values.blur, 'blur(4px)'); assert.equal(values.cursor, 'pointer'); + assert.equal(values.outlineStyle, 'solid'); assert.equal(values.outlineWidth, '2px'); + assert.equal(values.focusVisible, true); assert.equal(values.focusOutlineStyle, 'solid'); assert.equal(values.focusOutlineWidth, '2px'); + for (const [actual, expected] of [[values.foreground, values.expectedForeground], [values.accent, values.expectedAccent]]) { + assert.ok(actual.every((value, index) => Math.abs(value - expected[index]) <= 1), 'theme color/opacity utility changed'); + } +} + const vite = startVite(); let browser; let failure; @@ -135,6 +167,7 @@ try { const lightToggle = defaultPage.getByRole('button', { name: 'Switch to light theme' }); await lightToggle.waitFor(); await assertTheme(defaultPage, 'dark'); + await assertUtilityCompatibility(defaultPage); assert.equal( await defaultPage.evaluate(() => localStorage.getItem('mem.theme')), null, @@ -148,6 +181,10 @@ try { await lightToggle.click(); await assertTheme(defaultPage, 'light'); + await assertUtilityCompatibility(defaultPage); + await defaultPage.emulateMedia({ forcedColors: 'active' }); + await assertUtilityCompatibility(defaultPage); + await defaultPage.emulateMedia({ forcedColors: 'none' }); assert.equal(await defaultPage.evaluate(() => localStorage.getItem('mem.theme')), 'light'); assert.equal( (await defaultPage.evaluate(() => getComputedStyle(document.body).backgroundColor)).replaceAll(